diff options
294 files changed, 8273 insertions, 1763 deletions
diff --git a/Android.bp b/Android.bp index cd0b9c99c..5ca551216 100644 --- a/Android.bp +++ b/Android.bp @@ -37,7 +37,7 @@ aconfig_declarations { java_aconfig_library { name: "docsui-flags-aconfig-java-lib", aconfig_declarations: "docsui-flags-aconfig", - min_sdk_version: "29", + min_sdk_version: "30", sdk_version: "system_current", } @@ -45,7 +45,7 @@ java_library { name: "docsui-change-ids", srcs: ["src/com/android/documentsui/ChangeIds.java"], libs: ["app-compat-annotations"], - min_sdk_version: "29", + min_sdk_version: "30", sdk_version: "system_current", } @@ -77,7 +77,7 @@ java_defaults { }, sdk_version: "system_current", - min_sdk_version: "29", + min_sdk_version: "30", } platform_compat_config { @@ -133,7 +133,7 @@ android_library { sdk_version: "system_current", target_sdk_version: "33", - min_sdk_version: "29", + min_sdk_version: "30", lint: { baseline_filename: "lint-baseline.xml", }, @@ -144,13 +144,14 @@ android_library { defaults: ["documentsui_defaults"], manifest: "AndroidManifest.xml", + flags_packages: ["docsui-flags-aconfig"], resource_dirs: [], libs: ["DocumentsUI-lib"], sdk_version: "system_current", target_sdk_version: "33", - min_sdk_version: "29", + min_sdk_version: "30", } android_app { @@ -163,6 +164,8 @@ android_app { static_libs: ["DocumentsUI-lib"], resource_dirs: [], + flags_packages: ["docsui-flags-aconfig"], + licenses: [ "Android-Apache-2.0", "packages_apps_DocumentsUI_res_drawable_pd_license", @@ -170,6 +173,6 @@ android_app { required: ["privapp_whitelist_com.android.documentsui"], - min_sdk_version: "29", + min_sdk_version: "30", updatable: true, } diff --git a/AndroidManifest.xml b/AndroidManifest.xml index d948b605c..944b27471 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -19,7 +19,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.documentsui"> - <uses-sdk android:minSdkVersion="29"/> + <uses-sdk android:minSdkVersion="30"/> <uses-permission android:name="android.permission.MANAGE_DOCUMENTS" /> <uses-permission android:name="android.permission.REMOVE_TASKS" /> @@ -52,6 +52,7 @@ android:allowBackup="true" android:backupAgent=".prefs.BackupAgent" android:fullBackupOnly="false" + android:enableOnBackInvokedCallback="false" android:crossProfile="true"> <meta-data @@ -59,29 +60,67 @@ android:value="AEdPqrEAAAAInBA8ued0O_ZyYUsVhwinUF-x50NIe9K0GzBW4A" /> <activity + android:name=".picker.TrampolineActivity" + android:exported="true" + android:theme="@android:style/Theme.NoDisplay" + android:featureFlag="com.android.documentsui.flags.redirect_get_content_ro" + android:visibleToInstantApps="true"> + <intent-filter android:priority="120"> + <action android:name="android.intent.action.OPEN_DOCUMENT" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.OPENABLE" /> + <data android:mimeType="*/*" /> + </intent-filter> + <intent-filter android:priority="120"> + <action android:name="android.intent.action.CREATE_DOCUMENT" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.OPENABLE" /> + <data android:mimeType="*/*" /> + </intent-filter> + <intent-filter android:priority="120"> + <action android:name="android.intent.action.GET_CONTENT" /> + <category android:name="android.intent.category.DEFAULT" /> + <category android:name="android.intent.category.OPENABLE" /> + <data android:mimeType="*/*" /> + </intent-filter> + <intent-filter android:priority="120"> + <action android:name="android.intent.action.OPEN_DOCUMENT_TREE" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + </activity> + + <activity android:name=".picker.PickActivity" android:exported="true" android:theme="@style/LauncherTheme" android:visibleToInstantApps="true"> - <intent-filter android:priority="100"> + <intent-filter + android:featureFlag="!com.android.documentsui.flags.redirect_get_content_ro" + android:priority="100"> <action android:name="android.intent.action.OPEN_DOCUMENT" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.OPENABLE" /> <data android:mimeType="*/*" /> </intent-filter> - <intent-filter android:priority="100"> + <intent-filter + android:featureFlag="!com.android.documentsui.flags.redirect_get_content_ro" + android:priority="100"> <action android:name="android.intent.action.CREATE_DOCUMENT" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.OPENABLE" /> <data android:mimeType="*/*" /> </intent-filter> - <intent-filter android:priority="100"> + <intent-filter + android:featureFlag="!com.android.documentsui.flags.redirect_get_content_ro" + android:priority="100"> <action android:name="android.intent.action.GET_CONTENT" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.OPENABLE" /> <data android:mimeType="*/*" /> </intent-filter> - <intent-filter android:priority="100"> + <intent-filter + android:featureFlag="!com.android.documentsui.flags.redirect_get_content_ro" + android:priority="100"> <action android:name="android.intent.action.OPEN_DOCUMENT_TREE" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> diff --git a/AndroidManifestLib.xml b/AndroidManifestLib.xml index 993d4409a..b7e54192b 100644 --- a/AndroidManifestLib.xml +++ b/AndroidManifestLib.xml @@ -18,7 +18,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.documentsui"> - <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29" /> + <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30" /> <uses-permission android:name="android.permission.MANAGE_DOCUMENTS" /> <uses-permission android:name="android.permission.REMOVE_TASKS" /> diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg index ebc1264c7..edf1ee5a3 100644 --- a/PREUPLOAD.cfg +++ b/PREUPLOAD.cfg @@ -1,4 +1,4 @@ [Hook Scripts] checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT} - ktlint_hook = ${REPO_ROOT}/prebuilts/ktlint/ktlint.py -f ${PREUPLOAD_FILES} + diff --git a/TEST_MAPPING b/TEST_MAPPING index 8036110a0..6de67146a 100644 --- a/TEST_MAPPING +++ b/TEST_MAPPING @@ -7,5 +7,13 @@ { "name": "DocumentsUIUnitTests" } + ], + "desktop-postsubmit": [ { + "name": "DocumentsUIGoogleTests", + "keywords": ["primary-device"] + }, + { + "name": "DocumentsUIUnitTests" + } ] } diff --git a/compose/Android.bp b/compose/Android.bp index f00bf4c20..385dac69e 100644 --- a/compose/Android.bp +++ b/compose/Android.bp @@ -46,7 +46,7 @@ android_library { sdk_version: "system_current", target_sdk_version: "33", - min_sdk_version: "29", + min_sdk_version: "30", } android_app { @@ -58,5 +58,5 @@ android_app { certificate: "platform", sdk_version: "system_current", - min_sdk_version: "29", + min_sdk_version: "30", } diff --git a/compose/AndroidManifest.xml b/compose/AndroidManifest.xml index dbed77310..9b943e086 100644 --- a/compose/AndroidManifest.xml +++ b/compose/AndroidManifest.xml @@ -19,7 +19,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.documentsui.compose"> - <uses-sdk android:minSdkVersion="29"/> + <uses-sdk android:minSdkVersion="30"/> <!-- Permissions copied from com.android.documentsui AndroidManifest.xml --> <uses-permission android:name="android.permission.MANAGE_DOCUMENTS" /> diff --git a/flags.aconfig b/flags.aconfig index 6646e196b..21fc9567c 100644 --- a/flags.aconfig +++ b/flags.aconfig @@ -10,10 +10,56 @@ flag { } flag { - name: "use_search_v2" + name: "use_search_v2_rw" namespace: "documentsui" - description: "Enables the next generation search functionality." - bug: "378590312" + description: "Read/write flag that enables the next generation search functionality." + bug: "383412640" +} + +flag { + name: "zip_ng_ro" + namespace: "documentsui" + description: "Enables the next generation ZIP functionality." + bug: "382550591" + is_fixed_read_only: true +} + +flag { + name: "desktop_file_handling_ro" + namespace: "documentsui" + description: "Enables desktop file handling." + bug: "381778967" + is_fixed_read_only: true +} + +flag { + name: "visual_signals_ro" + namespace: "documentsui" + description: "Enables in-app progress display of file operations" + bug: "378011512" is_fixed_read_only: true } +flag { + name: "hide_roots_on_desktop_ro" + namespace: "documentsui" + description: "Enables the hiding of the Images/Videos/Audio/Documents roots on desktop." + bug: "381959330" + is_fixed_read_only: true +} + +flag { + name: "redirect_get_content_ro" + namespace: "documentsui" + description: "Redirects GET_CONTENT requests to Photopicker when appropriate" + bug: "377771195" + is_fixed_read_only: true +} + +flag { + name: "use_peek_preview_ro" + namespace: "documentsui" + description: "Enables the Peek previewing capability as a substitute for the Inspector." + bug: "373242058" + is_fixed_read_only: true +} diff --git a/proguard.flags b/proguard.flags index a805e4ad8..76449d4e9 100644 --- a/proguard.flags +++ b/proguard.flags @@ -87,11 +87,13 @@ int cross_profile; int cross_profile_content; int cross_profile_progress; + int dir_menu_browse; int dir_menu_copy_to_clipboard; int dir_menu_create_dir; int dir_menu_cut_to_clipboard; int dir_menu_delete; int dir_menu_deselect_all; + int dir_menu_extract_here; int dir_menu_inspect; int dir_menu_open; int dir_menu_open_in_new_window; @@ -106,6 +108,7 @@ int inspector_details_view; int option_menu_create_dir; int option_menu_debug; + int option_menu_extract_all; int option_menu_inspect; int option_menu_launcher; int option_menu_new_window; @@ -203,3 +206,8 @@ # Keep Apache Commons Compress classes -keep class org.apache.commons.compress.** { *; } + +# This is used in the unit test +-keep class com.google.android.material.chip.Chip { + public android.graphics.drawable.Drawable getChipIcon(); +}
\ No newline at end of file diff --git a/res/menu/action_mode_menu.xml b/res/flag(!com.android.documentsui.flags.use_material3)/menu/action_mode_menu.xml index 49a9ade70..49a9ade70 100644 --- a/res/menu/action_mode_menu.xml +++ b/res/flag(!com.android.documentsui.flags.use_material3)/menu/action_mode_menu.xml diff --git a/res/menu/activity.xml b/res/flag(!com.android.documentsui.flags.use_material3)/menu/activity.xml index 39be106ca..9c3516aeb 100644 --- a/res/menu/activity.xml +++ b/res/flag(!com.android.documentsui.flags.use_material3)/menu/activity.xml @@ -67,6 +67,13 @@ android:visible="false" app:showAsAction="never"/> <item + android:id="@+id/option_menu_extract_all" + android:title="@string/menu_extract_all" + android:icon="@drawable/ic_menu_extract" + android:enabled="false" + android:visible="false" + app:showAsAction="always"/> + <item android:id="@+id/option_menu_settings" android:title="@string/menu_settings" android:visible="false" diff --git a/res/menu/container_context_menu.xml b/res/flag(!com.android.documentsui.flags.use_material3)/menu/container_context_menu.xml index bd8b6c35e..bd8b6c35e 100644 --- a/res/menu/container_context_menu.xml +++ b/res/flag(!com.android.documentsui.flags.use_material3)/menu/container_context_menu.xml diff --git a/res/menu/dir_context_menu.xml b/res/flag(!com.android.documentsui.flags.use_material3)/menu/dir_context_menu.xml index 232753b9d..232753b9d 100644 --- a/res/menu/dir_context_menu.xml +++ b/res/flag(!com.android.documentsui.flags.use_material3)/menu/dir_context_menu.xml diff --git a/res/menu/file_context_menu.xml b/res/flag(!com.android.documentsui.flags.use_material3)/menu/file_context_menu.xml index 02b0e87e1..02b0e87e1 100644 --- a/res/menu/file_context_menu.xml +++ b/res/flag(!com.android.documentsui.flags.use_material3)/menu/file_context_menu.xml diff --git a/res/menu/mixed_context_menu.xml b/res/flag(!com.android.documentsui.flags.use_material3)/menu/mixed_context_menu.xml index 128b130d5..128b130d5 100644 --- a/res/menu/mixed_context_menu.xml +++ b/res/flag(!com.android.documentsui.flags.use_material3)/menu/mixed_context_menu.xml diff --git a/res/menu/root_context_menu.xml b/res/flag(!com.android.documentsui.flags.use_material3)/menu/root_context_menu.xml index 1ff818b3c..1ff818b3c 100644 --- a/res/menu/root_context_menu.xml +++ b/res/flag(!com.android.documentsui.flags.use_material3)/menu/root_context_menu.xml diff --git a/res/menu/sub_menu.xml b/res/flag(!com.android.documentsui.flags.use_material3)/menu/sub_menu.xml index 73a97dc97..73a97dc97 100644 --- a/res/menu/sub_menu.xml +++ b/res/flag(!com.android.documentsui.flags.use_material3)/menu/sub_menu.xml diff --git a/res/values-sw720dp/config.xml b/res/flag(!com.android.documentsui.flags.use_material3)/values-sw720dp/config.xml index b6444d8b6..b6444d8b6 100644 --- a/res/values-sw720dp/config.xml +++ b/res/flag(!com.android.documentsui.flags.use_material3)/values-sw720dp/config.xml diff --git a/res/flag(com.android.documentsui.flags.use_material3)/color/doc_list_item_badge_icon_color.xml b/res/flag(com.android.documentsui.flags.use_material3)/color/doc_list_item_badge_icon_color.xml new file mode 100644 index 000000000..de2849840 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/color/doc_list_item_badge_icon_color.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_selected="true" android:color="?attr/colorOnPrimary" /> + <item android:color="?attr/colorSecondary" /> +</selector> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/color/doc_list_item_label_color.xml b/res/flag(com.android.documentsui.flags.use_material3)/color/doc_list_item_label_color.xml new file mode 100644 index 000000000..909bbf05e --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/color/doc_list_item_label_color.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:color="?attr/colorOnPrimaryContainer" + android:state_selected="true" /> + <item android:color="?attr/colorOnSurface" /> +</selector> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/color/doc_list_item_subtitle_color.xml b/res/flag(com.android.documentsui.flags.use_material3)/color/doc_list_item_subtitle_color.xml index d9f27e60a..c8843089f 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/color/doc_list_item_subtitle_color.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/color/doc_list_item_subtitle_color.xml @@ -16,7 +16,8 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_enabled="false" - android:color="@color/doc_list_item_subtitle_disabled" /> - <item android:color="@color/doc_list_item_subtitle_enabled" /> + <item + android:color="?attr/colorOnPrimaryContainer" + android:state_selected="true" /> + <item android:color="?attr/colorOnSurfaceVariant" /> </selector> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/color/item_action_icon.xml b/res/flag(com.android.documentsui.flags.use_material3)/color/item_action_icon.xml index 4487795c1..0ef672d95 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/color/item_action_icon.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/color/item_action_icon.xml @@ -14,6 +14,7 @@ limitations under the License. --> +<!-- TODO(b/379776735): remove this file after use_material3 flag is launched. --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_enabled="false" android:alpha="@dimen/root_icon_disabled_alpha" diff --git a/res/flag(com.android.documentsui.flags.use_material3)/color/item_root_icon.xml b/res/flag(com.android.documentsui.flags.use_material3)/color/item_root_icon.xml index 142d85e6f..e30080679 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/color/item_root_icon.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/color/item_root_icon.xml @@ -16,9 +16,8 @@ <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item - android:state_activated="false" - android:color="?android:colorControlNormal" /> - <item android:state_activated="true" - android:color="?android:colorControlActivated" /> + android:color="?attr/colorOnSecondaryContainer" /> + <item + android:color="?attr/colorOnSurfaceVariant" /> </selector> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/color/item_root_primary_text.xml b/res/flag(com.android.documentsui.flags.use_material3)/color/item_root_primary_text.xml index 337c1a2c1..c0175ca1b 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/color/item_root_primary_text.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/color/item_root_primary_text.xml @@ -15,8 +15,6 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_focused="true" android:state_activated="true" android:color="?android:colorControlActivated" /> - <item android:state_focused="false" android:state_activated="true" android:color="?android:colorControlActivated" /> - <item android:state_enabled="false" android:alpha="0.5" android:color="?android:textColorPrimary" /> - <item android:color="?android:textColorPrimary" /> + <item android:state_activated="true" android:color="?attr/colorOnSecondaryContainer" /> + <item android:color="?attr/colorOnSurfaceVariant" /> </selector> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/color/item_root_ripple_color.xml b/res/flag(com.android.documentsui.flags.use_material3)/color/item_root_ripple_color.xml new file mode 100644 index 000000000..8c974f1f0 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/color/item_root_ripple_color.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2024 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 + + https://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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_activated="true" + android:alpha="@dimen/ripple_overlay_alpha" android:color="?attr/colorOnSecondaryContainer"/> + <item android:alpha="@dimen/ripple_overlay_alpha" android:color="?attr/colorOnSurface"/> +</selector>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/color/item_root_secondary_text.xml b/res/flag(com.android.documentsui.flags.use_material3)/color/item_root_secondary_text.xml index b6149ff13..50932a8c9 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/color/item_root_secondary_text.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/color/item_root_secondary_text.xml @@ -16,10 +16,6 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_focused="true" android:state_activated="true" - android:color="?android:colorControlActivated" /> - <item android:state_focused="false" android:state_activated="true" - android:color="?android:colorControlActivated" /> - <item android:state_enabled="false" android:alpha="0.5" android:color="?android:textColorSecondary" /> - <item android:color="?android:textColorSecondary" /> + <item android:state_activated="true" android:color="?attr/colorOnSecondaryContainer" /> + <item android:color="?attr/colorOnSurfaceVariant" /> </selector> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/color/list_item_ripple_color.xml b/res/flag(com.android.documentsui.flags.use_material3)/color/list_item_ripple_color.xml new file mode 100644 index 000000000..6c2d0714e --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/color/list_item_ripple_color.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 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 + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_enabled="false" android:color="@android:color/transparent" /> + <item android:state_selected="true" android:alpha="@dimen/ripple_overlay_alpha" + android:color="?attr/colorOnPrimaryContainer" /> + <item android:alpha="@dimen/ripple_overlay_alpha" + android:color="?attr/colorOnSurface" /> +</selector> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/color/menu_item_ripple_color.xml b/res/flag(com.android.documentsui.flags.use_material3)/color/menu_item_ripple_color.xml new file mode 100644 index 000000000..a5f5ad8b7 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/color/menu_item_ripple_color.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2025 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 + + https://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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:alpha="@dimen/ripple_overlay_alpha" android:color="?attr/colorOnSurface" /> +</selector>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/color/nav_rail_burger_icon_ripple_color.xml b/res/flag(com.android.documentsui.flags.use_material3)/color/nav_rail_burger_icon_ripple_color.xml new file mode 100644 index 000000000..19c657b29 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/color/nav_rail_burger_icon_ripple_color.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 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 + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:alpha="@dimen/ripple_overlay_alpha" android:color="?attr/colorOnSurfaceVariant" /> +</selector> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/color/nav_rail_item_text_color.xml b/res/flag(com.android.documentsui.flags.use_material3)/color/nav_rail_item_text_color.xml new file mode 100644 index 000000000..ec5aecb33 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/color/nav_rail_item_text_color.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2024 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 + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_activated="true" android:color="?attr/colorSecondary" /> + <item android:color="?attr/colorOnSurfaceVariant" /> +</selector> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/color/profile_tab_ripple_color.xml b/res/flag(com.android.documentsui.flags.use_material3)/color/profile_tab_ripple_color.xml new file mode 100644 index 000000000..de985a40f --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/color/profile_tab_ripple_color.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright 2024 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 + + https://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. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_activated="true" + android:alpha="@dimen/ripple_overlay_alpha" android:color="?attr/colorOnPrimaryContainer"/> + <item android:alpha="@dimen/ripple_overlay_alpha" android:color="?attr/colorOnSurfaceVariant"/> +</selector>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/color/search_chip_ripple_color.xml b/res/flag(com.android.documentsui.flags.use_material3)/color/search_chip_ripple_color.xml index b4aa0b8a7..09677a81a 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/color/search_chip_ripple_color.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/color/search_chip_ripple_color.xml @@ -15,7 +15,7 @@ limitations under the License. --> -<!-- TODO(b/379776735): remove this file after M3 uplift --> +<!-- TODO(b/379776735): remove this file after use_material3 flag is launched. --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <!-- Selected. --> <item android:state_pressed="true" android:state_selected="true" diff --git a/res/flag(com.android.documentsui.flags.use_material3)/color/search_chip_text_color.xml b/res/flag(com.android.documentsui.flags.use_material3)/color/search_chip_text_color.xml index be0cc404b..6935cef57 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/color/search_chip_text_color.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/color/search_chip_text_color.xml @@ -15,7 +15,7 @@ limitations under the License. --> -<!-- TODO(b/379776735): remove this file after M3 uplift --> +<!-- TODO(b/379776735): remove this file after use_material3 flag is launched. --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> <item android:state_selected="true" android:color="@color/search_chip_text_selected_color"/> <item android:state_enabled="true" android:color="?android:textColorSecondary"/> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/band_select_overlay.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/band_select_overlay.xml index 53f969284..c75b36f21 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/band_select_overlay.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/band_select_overlay.xml @@ -16,7 +16,7 @@ --> <shape xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="rectangle"> - <solid android:color="@color/band_select_background" /> + android:shape="rectangle" android:tint="@color/band_select_background"> + <solid android:color="#52000000" /> <!-- 32% alpha --> <stroke android:width="1dp" android:color="@color/band_select_border" /> </shape> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/bottom_sheet_dialog_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/bottom_sheet_dialog_background.xml index eba36783f..4574b96f4 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/bottom_sheet_dialog_background.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/bottom_sheet_dialog_background.xml @@ -17,9 +17,10 @@ <solid android:color="?android:attr/colorBackground" /> - <corners android:topLeftRadius="@dimen/grid_item_radius" - android:topRightRadius="@dimen/grid_item_radius" - android:bottomLeftRadius="0dp" - android:bottomRightRadius="0dp"/> + <corners + android:topLeftRadius="@dimen/bottom_sheet_dialog_radius" + android:topRightRadius="@dimen/bottom_sheet_dialog_radius" + android:bottomLeftRadius="0dp" + android:bottomRightRadius="0dp"/> </shape> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/color/profile_tab_selector.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/grid_nameplate_background.xml index a163185af..4a25b9a02 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/color/profile_tab_selector.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/grid_nameplate_background.xml @@ -15,9 +15,11 @@ --> <selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item - android:state_selected="true" - android:color="@color/profile_tab_selected_color"/> - <item - android:color="@color/profile_tab_default_color"/> -</selector> + <!-- selected --> + <item android:state_selected="true"> + <shape> + <corners android:radius="@dimen/grid_item_nameplate_radius" /> + <solid android:color="?attr/colorPrimaryContainer" /> + </shape> + </item> +</selector>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/grid_thumbnail_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/grid_thumbnail_background.xml new file mode 100644 index 000000000..aad18471b --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/grid_thumbnail_background.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2024 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- selected --> + <item android:state_selected="true"> + <shape> + <corners android:radius="@dimen/grid_item_thumbnail_radius" /> + <solid android:color="?attr/colorPrimaryContainer" /> + </shape> + </item> +</selector>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_arrow_upward.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_arrow_upward.xml index 96fa93ade..dbee1c363 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_arrow_upward.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_arrow_upward.xml @@ -15,14 +15,12 @@ limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24" - android:viewportHeight="24"> + android:width="28dp" + android:height="32dp" + android:viewportHeight="32" + android:viewportWidth="28"> <path - android:pathData="M0 0h24v24H0V0z" /> - <path - android:fillColor="?android:textColorSecondary" - android:pathData="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z" /> + android:fillColor="?attr/colorOnSecondaryContainer" + android:pathData="M13.25 19.125V10.75c0-.208.07-.382.208-.52A.74.74 0 0 1 14 10c.208 0 .382.076.52.23.154.138.23.312.23.52v8.375l3.667-3.667a.718.718 0 0 1 .52-.229c.209 0 .39.077.542.23.153.152.23.333.23.541a.718.718 0 0 1-.23.52l-4.958 4.96a.786.786 0 0 1-.25.166.85.85 0 0 1-.271.041c-.097 0-.194-.013-.292-.041a.878.878 0 0 1-.229-.167l-4.958-4.958A.718.718 0 0 1 8.29 16a.822.822 0 0 1 .25-.542.718.718 0 0 1 .521-.229.74.74 0 0 1 .542.23l3.646 3.666Z" /> </vector>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_cancel.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_cancel.xml new file mode 100644 index 000000000..f47cd4f6b --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_cancel.xml @@ -0,0 +1,24 @@ +<!-- + Copyright (C) 2025 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="24dp" + android:height="24dp" + android:viewportHeight="960" + android:viewportWidth="960"> + <path + android:fillColor="?attr/colorOnSurface" + android:pathData="M256,760L200,704L424,480L200,256L256,200L480,424L704,200L760,256L536,480L760,704L704,760L480,536L256,760Z" /> +</vector> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_check_circle.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_check_circle.xml index 88b784183..71ad135b1 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_check_circle.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_check_circle.xml @@ -14,11 +14,12 @@ Copyright (C) 2024 The Android Open Source Project limitations under the License. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" - android:width="24dp" - android:height="24dp" - android:viewportWidth="24.0" - android:viewportHeight="24.0"> + android:width="20dp" + android:height="20dp" + android:viewportWidth="960" + android:viewportHeight="960"> <path - android:fillColor="?android:attr/colorAccent" - android:pathData="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10,-4.48 10,-10S17.52 2 12 2zm-2 15l-5,-5 1.41,-1.41L10 14.17l7.59,-7.59L19 8l-9 9z"/> + android:fillColor="?attr/colorOnPrimaryContainer" + android:pathData="M428.28,628.78L669.87,388.2L612.41,330.5L428.28,513.63L346.15,432.5L288.7,490.2L428.28,628.78ZM480,872.13Q399.09,872.13 327.66,841.51Q256.22,810.89 202.66,757.34Q149.11,703.78 118.49,632.34Q87.87,560.91 87.87,480Q87.87,398.09 118.49,327.16Q149.11,256.22 202.66,202.66Q256.22,149.11 327.66,118.49Q399.09,87.87 480,87.87Q561.91,87.87 632.84,118.49Q703.78,149.11 757.34,202.66Q810.89,256.22 841.51,327.16Q872.13,398.09 872.13,480Q872.13,560.91 841.51,632.34Q810.89,703.78 757.34,757.34Q703.78,810.89 632.84,841.51Q561.91,872.13 480,872.13Z"/> </vector> + diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_audio.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_audio.xml new file mode 100644 index 000000000..ad9707b68 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_audio.xml @@ -0,0 +1,24 @@ +<!-- +Copyright (C) 2025 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="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960"> + <path + android:fillColor="?attr/colorPrimary" + android:pathData="M400,840Q334,840 287,793Q240,746 240,680Q240,614 287,567Q334,520 400,520Q423,520 442.5,525.5Q462,531 480,542L480,120L720,120L720,280L560,280L560,680Q560,746 513,793Q466,840 400,840Z"/> +</vector>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_document.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_document.xml new file mode 100644 index 000000000..45b8afe1f --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_document.xml @@ -0,0 +1,25 @@ +<!-- +Copyright (C) 2025 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="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960" + android:autoMirrored="true"> + <path + android:fillColor="?attr/colorPrimary" + android:pathData="M240,880Q207,880 183.5,856.5Q160,833 160,800L160,160Q160,127 183.5,103.5Q207,80 240,80L560,80L800,320L800,800Q800,833 776.5,856.5Q753,880 720,880L240,880ZM520,360L520,160L240,160Q240,160 240,160Q240,160 240,160L240,800Q240,800 240,800Q240,800 240,800L720,800Q720,800 720,800Q720,800 720,800L720,360L520,360ZM240,160L240,160L240,360L240,360L240,160L240,360L240,360L240,800Q240,800 240,800Q240,800 240,800L240,800Q240,800 240,800Q240,800 240,800L240,160Q240,160 240,160Q240,160 240,160Z"/> +</vector>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_from_this_week.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_from_this_week.xml index dca3b19a0..269a2cab5 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_from_this_week.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_from_this_week.xml @@ -20,6 +20,6 @@ android:viewportWidth="24" android:viewportHeight="24"> <path - android:fillColor="#5F6368" + android:fillColor="?attr/colorPrimary" android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l4,3.99L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.25,2.52 0.77,-1.28 -3.52,-2.09L13.5,8z"/> </vector>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_image.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_image.xml new file mode 100644 index 000000000..8a85244b2 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_image.xml @@ -0,0 +1,24 @@ +<!-- +Copyright (C) 2025 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="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960"> + <path + android:fillColor="?attr/colorPrimary" + android:pathData="M200,840Q167,840 143.5,816.5Q120,793 120,760L120,200Q120,167 143.5,143.5Q167,120 200,120L760,120Q793,120 816.5,143.5Q840,167 840,200L840,760Q840,793 816.5,816.5Q793,840 760,840L200,840ZM200,760L760,760Q760,760 760,760Q760,760 760,760L760,200Q760,200 760,200Q760,200 760,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760ZM240,680L720,680L570,480L450,640L360,520L240,680ZM200,760Q200,760 200,760Q200,760 200,760L200,200Q200,200 200,200Q200,200 200,200L200,200Q200,200 200,200Q200,200 200,200L200,760Q200,760 200,760Q200,760 200,760Z"/> +</vector>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_large_files.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_large_files.xml index d0fe55090..39c09c97a 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_large_files.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_large_files.xml @@ -20,6 +20,6 @@ android:viewportWidth="24" android:viewportHeight="24"> <path - android:fillColor="#5F6368" + android:fillColor="?attr/colorPrimary" android:pathData="M21.41,11.58l-9,-9C12.05,2.22 11.55,2 11,2H4c-1.1,0 -2,0.9 -2,2v7c0,0.55 0.22,1.05 0.59,1.42l9,9c0.36,0.36 0.86,0.58 1.41,0.58s1.05,-0.22 1.41,-0.59l7,-7c0.37,-0.36 0.59,-0.86 0.59,-1.41s-0.23,-1.06 -0.59,-1.42zM13,20.01L4,11V4h7v-0.01l9,9 -7,7.02zM8,6.5C8,7.33 7.33,8 6.5,8S5,7.33 5,6.5 5.67,5 6.5,5 8,5.67 8,6.5z"/> </vector>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_video.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_video.xml new file mode 100644 index 000000000..e22e2fc8a --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_video.xml @@ -0,0 +1,24 @@ +<!-- +Copyright (C) 2025 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="24dp" + android:height="24dp" + android:viewportWidth="960" + android:viewportHeight="960"> + <path + android:fillColor="?attr/colorPrimary" + android:pathData="M160,160L240,320L360,320L280,160L360,160L440,320L560,320L480,160L560,160L640,320L760,320L680,160L800,160Q833,160 856.5,183.5Q880,207 880,240L880,720Q880,753 856.5,776.5Q833,800 800,800L160,800Q127,800 103.5,776.5Q80,753 80,720L80,240Q80,207 103.5,183.5Q127,160 160,160ZM160,400L160,720Q160,720 160,720Q160,720 160,720L800,720Q800,720 800,720Q800,720 800,720L800,400L160,400ZM160,400L160,400L160,720Q160,720 160,720Q160,720 160,720L160,720Q160,720 160,720Q160,720 160,720L160,400Z"/> +</vector>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_hamburger.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_hamburger.xml index 1d3990887..5a3e42d5e 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_hamburger.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_hamburger.xml @@ -20,6 +20,6 @@ android:viewportWidth="24" android:viewportHeight="24"> <path - android:fillColor="?android:attr/colorControlNormal" + android:fillColor="?attr/colorOnSurface" android:pathData="M3,18h18v-2H3V18zM3,13h18v-2H3V13zM3,6v2h18V6H3z"/> </vector>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_sort_arrow.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_sort_arrow.xml index e54ee3158..ec23152ab 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_sort_arrow.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_sort_arrow.xml @@ -16,7 +16,7 @@ --> <rotate xmlns:android="http://schemas.android.com/apk/res/android" - android:drawable="@drawable/ic_arrow_upward" + android:drawable="@drawable/ic_sort_icon" android:fromDegrees="0" android:toDegrees="180" android:pivotX="50%" diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_sort_icon.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_sort_icon.xml new file mode 100644 index 000000000..52a265729 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_sort_icon.xml @@ -0,0 +1,98 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 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. + --> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:state_focused="true" + android:state_hovered="true"> + <layer-list> + <item> + <shape + android:background="?attr/colorSurfaceBright" + android:shape="rectangle" + android:tint="?attr/colorSecondaryContainer" + android:tintMode="multiply"> + <corners android:radius="14dp" /> + <size + android:width="28dp" + android:height="32dp" /> + <solid android:color="?attr/colorSecondaryContainer" /> + <stroke + android:width="@dimen/focus_ring_width" + android:color="?attr/colorSecondary" /> + </shape> + </item> + <item android:drawable="@drawable/ic_arrow_upward" /> + </layer-list> + </item> + <item + android:state_focused="false" + android:state_hovered="true"> + <layer-list> + <item> + <shape + android:background="?attr/colorSurfaceBright" + android:shape="rectangle" + android:tint="?attr/colorSecondaryContainer" + android:tintMode="multiply"> + <corners android:radius="14dp" /> + <size + android:width="28dp" + android:height="32dp" /> + <solid android:color="?attr/colorSecondaryContainer" /> + </shape> + </item> + <item android:drawable="@drawable/ic_arrow_upward" /> + </layer-list> + </item> + <item + android:state_focused="true" + android:state_hovered="false"> + <layer-list> + <item> + <shape + android:background="?attr/colorSurfaceBright" + android:shape="rectangle"> + <corners android:radius="14dp" /> + <size + android:width="28dp" + android:height="32dp" /> + <solid android:color="?attr/colorSecondaryContainer" /> + <stroke + android:width="@dimen/focus_ring_width" + android:color="?attr/colorSecondary" /> + </shape> + </item> + <item android:drawable="@drawable/ic_arrow_upward" /> + </layer-list> + </item> + <item> + <layer-list> + <item> + <shape + android:background="?attr/colorSurfaceBright" + android:shape="rectangle"> + <corners android:radius="14dp" /> + <size + android:width="28dp" + android:height="32dp" /> + <solid android:color="?attr/colorSecondaryContainer" /> + </shape> + </item> + <item android:drawable="@drawable/ic_arrow_upward" /> + </layer-list> + </item> +</selector>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/list_item_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/list_item_background.xml index 126788c6d..31bbec1f2 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/list_item_background.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/list_item_background.xml @@ -14,18 +14,127 @@ limitations under the License. --> -<selector xmlns:android="http://schemas.android.com/apk/res/android"> - <item android:state_focused="true" > - <color android:color="@color/list_item_selected_background_color"/> - </item> - <item android:state_selected="true"> - <color android:color="@color/list_item_selected_background_color"/> - </item> - <item android:state_drag_hovered="true"> - <color android:color="?android:strokeColor"/> - </item> - <item android:state_selected="false" - android:state_focused="false"> - <color android:color="?android:attr/colorBackground"/> +<!-- Use @color/list_item_selected_background_color instead of the "?attr/colorPrimaryContainer" + because the variable is exposed in overlayable.xml. --> +<ripple xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/list_item_ripple_color"> + + <!-- The mask below only works for the ripple itself, doesn't work for other <item>s, we + need to explicitly apply the drawable if the other items also need this mask. --> + <item android:id="@android:id/mask" android:drawable="@drawable/list_item_mask"/> + + <item> + <selector> + <!-- Selected --> + <item android:state_selected="true" android:state_drag_hovered="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/list_item_height" /> + <solid android:color="@color/list_item_selected_background_color" /> + </shape> + </item> + <item> + <shape android:tint="?attr/colorOnPrimaryContainer"> + <corners android:radius="@dimen/list_item_height" /> + <solid android:color="@color/overlay_hover_color_percentage" /> + </shape> + </item> + </layer-list> + </item> + <item android:state_selected="true" android:state_pressed="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/list_item_height" /> + <solid android:color="@color/list_item_selected_background_color" /> + </shape> + </item> + <item> + <shape android:tint="?attr/colorOnPrimaryContainer"> + <corners android:radius="@dimen/list_item_height" /> + <solid android:color="@color/overlay_hover_color_percentage" /> + </shape> + </item> + </layer-list> + </item> + <item android:state_selected="true" android:state_focused="true"> + <layer-list> + <item + android:bottom="@dimen/focus_ring_gap" + android:left="@dimen/focus_ring_gap" + android:right="@dimen/focus_ring_gap" + android:top="@dimen/focus_ring_gap"> + <shape> + <corners android:radius="@dimen/list_item_height" /> + <solid android:color="@color/list_item_selected_background_color" /> + </shape> + </item> + <item> + <shape> + <corners android:radius="@dimen/list_item_height" /> + <stroke + android:width="@dimen/focus_ring_width" + android:color="?attr/colorSecondary" /> + </shape> + </item> + </layer-list> + </item> + <item android:state_selected="true" android:state_hovered="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/list_item_height" /> + <solid android:color="@color/list_item_selected_background_color" /> + </shape> + </item> + <item> + <shape android:tint="?attr/colorOnPrimaryContainer"> + <corners android:radius="@dimen/list_item_height" /> + <solid android:color="@color/overlay_hover_color_percentage" /> + </shape> + </item> + </layer-list> + </item> + <item android:state_selected="true"> + <shape> + <corners android:radius="@dimen/list_item_height" /> + <solid android:color="@color/list_item_selected_background_color" /> + </shape> + </item> + + <!-- Unselected --> + <item android:state_drag_hovered="true"> + <shape android:tint="?attr/colorOnSurface"> + <corners android:radius="@dimen/list_item_height" /> + <solid android:color="@color/overlay_hover_color_percentage" /> + </shape> + </item> + <item android:state_pressed="true"> + <shape android:tint="?attr/colorOnSurface"> + <corners android:radius="@dimen/list_item_height" /> + <solid android:color="@color/overlay_hover_color_percentage" /> + </shape> + </item> + <item android:state_focused="true"> + <shape> + <corners android:radius="@dimen/list_item_height" /> + <stroke + android:width="@dimen/focus_ring_width" + android:color="?attr/colorSecondary" /> + </shape> + </item> + <item android:state_hovered="true"> + <shape android:tint="?attr/colorOnSurface"> + <corners android:radius="@dimen/list_item_height" /> + <solid android:color="@color/overlay_hover_color_percentage" /> + </shape> + </item> + + <!-- Default: use the container background. --> + <item> + <color android:color="@android:color/transparent"/> + </item> + </selector> </item> -</selector>
\ No newline at end of file +</ripple>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/list_item_mask.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/list_item_mask.xml new file mode 100644 index 000000000..d1b7a3a94 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/list_item_mask.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2025 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. +--> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="@dimen/list_item_height" /> + <!-- The color here doesn't matter, it's just being used as a mask. --> + <solid android:color="@android:color/white" /> +</shape>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_bottom_section_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_bottom_section_background.xml new file mode 100644 index 000000000..04f425e16 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_bottom_section_background.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="?attr/colorSurfaceBright" /> + <corners + android:topLeftRadius="@dimen/main_container_corner_radius_small" + android:topRightRadius="@dimen/main_container_corner_radius_small" + android:bottomLeftRadius="@dimen/main_container_corner_radius_large" + android:bottomRightRadius="@dimen/main_container_corner_radius_large" /> +</shape>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_middle_section_background.xml index 151a7ba77..8b0b42bee 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_background.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_middle_section_background.xml @@ -16,5 +16,5 @@ <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <solid android:color="?attr/colorSurfaceBright" /> - <corners android:radius="16dp" /> + <corners android:radius="@dimen/main_container_corner_radius_small" /> </shape>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_top_section_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_top_section_background.xml new file mode 100644 index 000000000..b35ed4060 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_top_section_background.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="?attr/colorSurfaceBright" /> + <corners + android:topLeftRadius="@dimen/main_container_corner_radius_large" + android:topRightRadius="@dimen/main_container_corner_radius_large" + android:bottomLeftRadius="@dimen/main_container_corner_radius_small" + android:bottomRightRadius="@dimen/main_container_corner_radius_small" /> +</shape>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/menu_dropdown_panel.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/menu_dropdown_panel.xml index 43dd62e2c..f1a72ad6e 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/menu_dropdown_panel.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/menu_dropdown_panel.xml @@ -14,6 +14,7 @@ limitations under the License. --> +<!-- TODO(b/379776735): remove this file after use_material3 flag is launched. --> <layer-list xmlns:android="http://schemas.android.com/apk/res/android" > <!-- Panel shadow --> <item> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/menu_item_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/menu_item_background.xml new file mode 100644 index 000000000..e1e59b900 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/menu_item_background.xml @@ -0,0 +1,36 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2025 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. +--> + +<ripple + xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/menu_item_ripple_color"> + + <item> + <selector> + <item android:state_pressed="true"> + <shape android:tint="?attr/colorOnSurface"> + <solid android:color="@color/overlay_hover_color_percentage"/> + </shape> + </item> + <!-- This is the actually the focused state (with keyboard navigation). --> + <item android:state_selected="true"> + <shape> + <stroke android:width="@dimen/focus_ring_width" android:color="?attr/colorSecondary" /> + </shape> + </item> + </selector> + </item> +</ripple>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/nav_rail_burger_icon_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/nav_rail_burger_icon_background.xml new file mode 100644 index 000000000..0957d36bb --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/nav_rail_burger_icon_background.xml @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 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 + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="true" + android:color="?attr/colorOnSurfaceVariant" + android:alpha="@dimen/hover_overlay_alpha" /> + <item android:state_hovered="true" + android:color="?attr/colorOnSurfaceVariant" + android:alpha="@dimen/hover_overlay_alpha" /> + <item android:color="@android:color/transparent" /> +</selector> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/nav_rail_item_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/nav_rail_item_background.xml new file mode 100644 index 000000000..7809766cd --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/nav_rail_item_background.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> + +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <!-- By default the nav rail item has a grey background when it's focused, but we need the + background to be put on the icon inside, so we override the focus background color to be + transparent here. + --> + <item android:state_focused="true" android:drawable="@android:color/transparent" /> +</selector>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/nav_rail_item_icon_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/nav_rail_item_icon_background.xml new file mode 100644 index 000000000..1c7bc8c0b --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/nav_rail_item_icon_background.xml @@ -0,0 +1,145 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> + +<ripple + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:color="@color/item_root_ripple_color"> + + <!-- The mask below only works for the ripple itself, doesn't work for other <item>s, we + need to explicitly apply the drawable if the other items also need this mask. --> + <item + android:id="@android:id/mask" + android:drawable="@drawable/nav_rail_item_icon_mask"/> + + <item> + <selector> + <!-- Selected (activated). --> + <!-- Highlight: when dragging files over the item. --> + <item + android:state_activated="true" + app:state_highlighted="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/nav_rail_item_icon_bg_radius" /> + <solid android:color="?attr/colorSecondaryContainer" /> + </shape> + </item> + <item> + <shape android:tint="?attr/colorOnSecondaryContainer"> + <corners android:radius="@dimen/nav_rail_item_icon_bg_radius" /> + <solid android:color="@color/overlay_hover_color_percentage" /> + </shape> + </item> + </layer-list> + </item> + <item + android:state_activated="true" + android:state_pressed="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/nav_rail_item_icon_bg_radius" /> + <solid android:color="?attr/colorSecondaryContainer" /> + </shape> + </item> + <item> + <shape android:tint="?attr/colorOnSecondaryContainer"> + <corners android:radius="@dimen/nav_rail_item_icon_bg_radius" /> + <solid android:color="@color/overlay_hover_color_percentage" /> + </shape> + </item> + </layer-list> + </item> + <item + android:state_activated="true" + android:state_focused="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/nav_rail_item_icon_bg_radius" /> + <solid android:color="?attr/colorSecondaryContainer" /> + </shape> + </item> + <item> + <shape> + <corners android:radius="@dimen/nav_rail_item_icon_bg_radius" /> + <stroke + android:width="@dimen/focus_ring_width" + android:color="?attr/colorSecondary" /> + </shape> + </item> + </layer-list> + </item> + <item + android:state_activated="true" + android:state_hovered="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/nav_rail_item_icon_bg_radius" /> + <solid android:color="?attr/colorSecondaryContainer" /> + </shape> + </item> + <item> + <shape android:tint="?attr/colorOnSecondaryContainer"> + <corners android:radius="@dimen/nav_rail_item_icon_bg_radius" /> + <solid android:color="@color/overlay_hover_color_percentage" /> + </shape> + </item> + </layer-list> + </item> + <item android:state_activated="true"> + <shape> + <corners android:radius="@dimen/nav_rail_item_icon_bg_radius" /> + <solid android:color="?attr/colorSecondaryContainer" /> + </shape> + </item> + + <!-- Unselected. --> + <item app:state_highlighted="true"> + <shape android:tint="?attr/colorOnSurface"> + <corners android:radius="@dimen/nav_rail_item_icon_bg_radius" /> + <solid android:color="@color/overlay_hover_color_percentage" /> + </shape> + </item> + <item android:state_pressed="true"> + <shape android:tint="?attr/colorOnSurface"> + <corners android:radius="@dimen/nav_rail_item_icon_bg_radius" /> + <solid android:color="@color/overlay_hover_color_percentage" /> + </shape> + </item> + <item android:state_focused="true"> + <shape> + <corners android:radius="@dimen/nav_rail_item_icon_bg_radius" /> + <stroke + android:width="@dimen/focus_ring_width" + android:color="?attr/colorSecondary" /> + </shape> + </item> + <item android:state_hovered="true"> + <shape android:tint="?attr/colorOnSurface"> + <corners android:radius="@dimen/nav_rail_item_icon_bg_radius" /> + <solid android:color="@color/overlay_hover_color_percentage" /> + </shape> + </item> + + <!-- Default: use the container background. --> + <item android:drawable="@android:color/transparent" /> + </selector> + </item> +</ripple>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/nav_rail_item_icon_mask.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/nav_rail_item_icon_mask.xml new file mode 100644 index 000000000..fd2a14b8d --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/nav_rail_item_icon_mask.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="@dimen/nav_rail_item_icon_bg_radius"/> + <!-- The color here doesn't matter, it's just being used as a mask. --> + <solid android:color="@android:color/white" /> +</shape>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/profile_tab_mask.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/profile_tab_mask.xml new file mode 100644 index 000000000..3c1e33b0e --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/profile_tab_mask.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> + +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="@dimen/profile_tab_radius"/> + <!-- The color here doesn't matter, it's just being used as a mask in tab_border_rounded. --> + <solid android:color="@android:color/white"/> +</shape> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/progress_indeterminate_horizontal_material_trimmed.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/progress_indeterminate_horizontal_material_trimmed.xml index 1200ab00b..7a0a5e555 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/progress_indeterminate_horizontal_material_trimmed.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/progress_indeterminate_horizontal_material_trimmed.xml @@ -14,6 +14,7 @@ limitations under the License. --> +<!-- TODO(b/379776735): remove this file after use_material3 flag is launched. --> <!-- Variant of progress_indeterminate_horizontal_material in frameworks/base/core/res, which draws the whole height of the progress bar instead having blank space above and below the bar. --> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/root_item_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/root_item_background.xml index 544d23beb..236b92179 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/root_item_background.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/root_item_background.xml @@ -17,27 +17,113 @@ <ripple xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" - android:color="?android:attr/colorControlHighlight"> + android:color="@color/item_root_ripple_color"> + + <!-- The mask below only works for the ripple itself, doesn't work for other <item>s, we + need to explicitly apply the drawable if the other items also need this mask. --> <item android:id="@android:id/mask" android:drawable="@drawable/root_list_selector"/> <item> <selector> + <!-- Selected (activated). --> + <!-- Highlight: when dragging files over the item. --> + <item android:state_activated="true" app:state_highlighted="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/drawer_item_height"/> + <solid android:color="?attr/colorSecondaryContainer"/> + </shape> + </item> + <item> + <shape android:tint="?attr/colorOnSecondaryContainer"> + <corners android:radius="@dimen/drawer_item_height"/> + <solid android:color="@color/overlay_hover_color_percentage"/> + </shape> + </item> + </layer-list> + </item> + <item android:state_activated="true" android:state_pressed="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/drawer_item_height"/> + <solid android:color="?attr/colorSecondaryContainer"/> + </shape> + </item> + <item> + <shape android:tint="?attr/colorOnSecondaryContainer"> + <corners android:radius="@dimen/drawer_item_height"/> + <solid android:color="@color/overlay_hover_color_percentage"/> + </shape> + </item> + </layer-list> + </item> + <item android:state_activated="true" android:state_focused="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/drawer_item_height"/> + <solid android:color="?attr/colorSecondaryContainer"/> + </shape> + </item> + <item> + <shape> + <corners android:radius="@dimen/drawer_item_height"/> + <stroke android:width="@dimen/focus_ring_width" android:color="?attr/colorSecondary"/> + </shape> + </item> + </layer-list> + </item> + <item android:state_activated="true" android:state_hovered="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/drawer_item_height"/> + <solid android:color="?attr/colorSecondaryContainer"/> + </shape> + </item> + <item> + <shape android:tint="?attr/colorOnSecondaryContainer"> + <corners android:radius="@dimen/drawer_item_height"/> + <solid android:color="@color/overlay_hover_color_percentage"/> + </shape> + </item> + </layer-list> + </item> + <item android:state_activated="true" android:drawable="@drawable/root_list_selector"/> + + <!-- Unselected. --> <item app:state_highlighted="true"> - <color android:color="?android:attr/colorControlHighlight"/> + <shape android:tint="?attr/colorOnSurface"> + <corners android:radius="@dimen/drawer_item_height"/> + <solid android:color="@color/overlay_hover_color_percentage"/> + </shape> + </item> + <item android:state_pressed="true"> + <shape android:tint="?attr/colorOnSurface"> + <corners android:radius="@dimen/drawer_item_height"/> + <solid android:color="@color/overlay_hover_color_percentage"/> + </shape> + </item> + <item android:state_focused="true"> + <shape> + <corners android:radius="@dimen/drawer_item_height"/> + <stroke android:width="@dimen/focus_ring_width" android:color="?attr/colorSecondary"/> + </shape> + </item> + <item android:state_hovered="true"> + <shape android:tint="?attr/colorOnSurface"> + <corners android:radius="@dimen/drawer_item_height"/> + <solid android:color="@color/overlay_hover_color_percentage"/> + </shape> </item> - <item - app:state_highlighted="false" - android:drawable="@android:color/transparent"/> - </selector> - </item> - <item> - <selector> + <!-- Default: use the container background. --> <item - android:state_activated="true" - android:drawable="@drawable/root_list_selector"/> + android:drawable="@android:color/transparent"/> </selector> </item> </ripple>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/root_list_selector.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/root_list_selector.xml index 11d28a70f..5bb3d6afe 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/root_list_selector.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/root_list_selector.xml @@ -14,17 +14,8 @@ limitations under the License. --> -<inset xmlns:android="http://schemas.android.com/apk/res/android" - android:inset="8dp"> - <shape - xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="rectangle"> - <corners - android:topLeftRadius="2dp" - android:topRightRadius="2dp" - android:bottomLeftRadius="2dp" - android:bottomRightRadius="2dp"/> - <solid - android:color="?android:attr/colorSecondary"/> - </shape> -</inset>
\ No newline at end of file +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="@dimen/drawer_item_height"/> + <!-- The color here is used as activated color in root_item_background.xml. --> + <solid android:color="?attr/colorSecondaryContainer"/> +</shape>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/sort_widget_background.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/sort_widget_background.xml index 212dab765..b18bc4eb3 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/sort_widget_background.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/sort_widget_background.xml @@ -15,6 +15,7 @@ limitations under the License. --> +<!-- TODO(b/379776735): remove this file after use_material3 flag is launched. --> <layer-list xmlns:android="http://schemas.android.com/apk/res/android"> <item android:top="-1dp" diff --git a/res/flag(com.android.documentsui.flags.use_material3)/drawable/tab_border_rounded.xml b/res/flag(com.android.documentsui.flags.use_material3)/drawable/tab_border_rounded.xml index 96b7e6d49..08d515301 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/drawable/tab_border_rounded.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/drawable/tab_border_rounded.xml @@ -14,10 +14,135 @@ limitations under the License. --> -<shape xmlns:android="http://schemas.android.com/apk/res/android" - android:shape="rectangle"> +<ripple + xmlns:android="http://schemas.android.com/apk/res/android" + android:color="@color/profile_tab_ripple_color"> - <solid - android:color="@color/profile_tab_selector"/> - <corners android:radius="12dp"/> -</shape> + <!-- The mask below only works for the ripple itself, doesn't work for other <item>s, we + need to explicitly apply the drawable if the other items also need this mask. --> + <item + android:id="@android:id/mask" + android:drawable="@drawable/profile_tab_mask"/> + + <item> + <selector> + <!-- Selected (activated). --> + <item android:state_activated="true" android:state_pressed="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/profile_tab_radius" /> + <solid android:color="?attr/colorPrimaryContainer" /> + </shape> + </item> + <item> + <shape android:tint="?attr/colorOnPrimaryContainer"> + <corners android:radius="@dimen/profile_tab_radius" /> + <solid android:color="@color/overlay_hover_color_percentage" /> + </shape> + </item> + </layer-list> + </item> + <item android:state_activated="true" android:state_focused="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/profile_tab_radius" /> + <solid android:color="?attr/colorPrimaryContainer" /> + </shape> + </item> + <item> + <shape> + <corners android:radius="@dimen/profile_tab_radius" /> + <stroke + android:width="@dimen/focus_ring_width" + android:color="?attr/colorSecondary" /> + </shape> + </item> + </layer-list> + </item> + <item android:state_activated="true" android:state_hovered="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/profile_tab_radius" /> + <solid android:color="?attr/colorPrimaryContainer" /> + </shape> + </item> + <item> + <shape android:tint="?attr/colorOnPrimaryContainer"> + <corners android:radius="@dimen/profile_tab_radius" /> + <solid android:color="@color/overlay_hover_color_percentage" /> + </shape> + </item> + </layer-list> + </item> + <item android:state_activated="true"> + <shape> + <corners android:radius="@dimen/profile_tab_radius" /> + <solid android:color="?attr/colorPrimaryContainer" /> + </shape> + </item> + + <!-- Unselected. --> + <item android:state_pressed="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/profile_tab_radius" /> + <solid android:color="?attr/colorSurfaceContainerHighest" /> + </shape> + </item> + <item> + <shape android:tint="?attr/colorOnSurface"> + <corners android:radius="@dimen/profile_tab_radius" /> + <solid android:color="@color/overlay_hover_color_percentage" /> + </shape> + </item> + </layer-list> + </item> + <item android:state_focused="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/profile_tab_radius" /> + <solid android:color="?attr/colorSurfaceContainerHighest" /> + </shape> + </item> + <item> + <shape> + <corners android:radius="@dimen/profile_tab_radius" /> + <stroke + android:width="@dimen/focus_ring_width" + android:color="?attr/colorSecondary" /> + </shape> + </item> + </layer-list> + </item> + <item android:state_hovered="true"> + <layer-list> + <item> + <shape> + <corners android:radius="@dimen/profile_tab_radius" /> + <solid android:color="?attr/colorSurfaceContainerHighest" /> + </shape> + </item> + <item> + <shape android:tint="?attr/colorOnSurface"> + <corners android:radius="@dimen/profile_tab_radius" /> + <solid android:color="@color/overlay_hover_color_percentage" /> + </shape> + </item> + </layer-list> + </item> + + <!-- Default --> + <item> + <shape> + <corners android:radius="@dimen/profile_tab_radius" /> + <solid android:color="?attr/colorSurfaceContainerHighest" /> + </shape> + </item> + </selector> + </item> +</ripple> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout-w720dp/directory_app_bar.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout-w720dp/directory_app_bar.xml deleted file mode 100644 index 177aeba4f..000000000 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout-w720dp/directory_app_bar.xml +++ /dev/null @@ -1,55 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<!-- Copyright (C) 2024 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. ---> - -<com.google.android.material.appbar.AppBarLayout - xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - android:id="@+id/app_bar" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="?android:attr/colorBackground"> - - <androidx.appcompat.widget.Toolbar - android:id="@+id/toolbar" - android:layout_width="match_parent" - android:layout_height="?android:attr/actionBarSize" - android:layout_margin="@dimen/search_bar_margin" - android:background="?android:attr/colorBackground" - android:theme="?actionBarTheme" - android:popupTheme="?actionBarPopupTheme" - android:elevation="3dp" - app:collapseContentDescription="@string/button_back" - app:titleTextAppearance="@style/ToolbarTitle" - app:layout_collapseMode="pin"> - - <TextView - android:id="@+id/searchbar_title" - android:layout_width="match_parent" - android:layout_height="?android:attr/actionBarSize" - android:layout_marginStart="@dimen/search_bar_text_margin_start" - android:layout_marginEnd="@dimen/search_bar_text_margin_end" - android:paddingStart="@dimen/search_bar_icon_padding" - android:gravity="center_vertical" - android:text="@string/search_bar_hint" - android:textAppearance="@style/SearchBarTitle" - android:drawableStart="@drawable/ic_menu_search" - android:drawablePadding="@dimen/search_bar_icon_padding"/> - - </androidx.appcompat.widget.Toolbar> - - <include layout="@layout/directory_header"/> - -</com.google.android.material.appbar.AppBarLayout>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout-w720dp/column_headers.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout-w900dp/column_headers.xml index f24a28241..c692b4124 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout-w720dp/column_headers.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout-w900dp/column_headers.xml @@ -15,13 +15,19 @@ limitations under the License. --> +<!-- The 2 placeholder views here are for vertical alignment purpose, in the file row it uses + ratio-based layout for Name/Type/Size/Date columns but excluding the thumbnail icon and + the preview icon. In order to make the header and row are vertically aligned, we need to + use placeholder for thumbnail icon and preview icon and then do the ratio-based layout + for table headers. + --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/table_header" android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="@dimen/doc_header_height" - android:background="@drawable/sort_widget_background" + android:paddingHorizontal="@dimen/list_container_padding" android:visibility="gone"> <LinearLayout @@ -30,15 +36,15 @@ android:baselineAligned="false" android:gravity="center_vertical" android:minHeight="@dimen/list_item_height" - android:paddingStart="@dimen/list_item_padding" - android:paddingEnd="@dimen/list_item_width" + android:paddingStart="@dimen/list_item_padding_start" + android:paddingEnd="@dimen/list_item_padding_end" android:orientation="horizontal"> - <!-- Placeholder for icon --> + <!-- Placeholder for MIME/thumbnail icon --> <View - android:layout_width="@dimen/list_item_thumbnail_size" - android:layout_height="@dimen/list_item_thumbnail_size" + android:layout_width="@dimen/list_item_icon_size" + android:layout_height="@dimen/list_item_icon_size" android:layout_gravity="center_vertical" - android:layout_marginEnd="16dp" + android:layout_marginEnd="@dimen/list_item_icon_margin_end" android:layout_marginStart="0dp"/> <!-- Column headers --> @@ -54,7 +60,8 @@ android:layout_height="match_parent" android:layout_weight="0.4" android:layout_marginEnd="12dp" - android:focusable="true" + android:clickable="true" + android:focusable="false" android:gravity="center_vertical" android:orientation="horizontal" android:animateLayoutChanges="true"> @@ -68,7 +75,8 @@ android:layout_height="match_parent" android:layout_weight="0" android:layout_marginEnd="0dp" - android:focusable="true" + android:clickable="true" + android:focusable="false" android:gravity="center_vertical" android:orientation="horizontal" android:animateLayoutChanges="true"> @@ -81,8 +89,8 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="0.2" - android:layout_marginEnd="12dp" - android:focusable="true" + android:clickable="true" + android:focusable="false" android:gravity="center_vertical" android:orientation="horizontal" android:animateLayoutChanges="true"> @@ -95,8 +103,8 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="0.2" - android:layout_marginEnd="12dp" - android:focusable="true" + android:clickable="true" + android:focusable="false" android:gravity="center_vertical" android:orientation="horizontal" android:animateLayoutChanges="true"> @@ -109,8 +117,8 @@ android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="0.2" - android:layout_marginEnd="12dp" - android:focusable="true" + android:clickable="true" + android:focusable="false" android:gravity="center_vertical" android:orientation="horizontal" android:animateLayoutChanges="true"> @@ -118,5 +126,12 @@ <include layout="@layout/shared_cell_content" /> </com.android.documentsui.sorting.HeaderCell> </LinearLayout> + + <!-- Placeholder for preview icon in picker mode --> + <View + android:id="@+id/preview_icon_placeholder" + android:layout_width="@dimen/list_item_icon_size" + android:layout_height="@dimen/list_item_icon_size" + android:layout_marginEnd="@dimen/list_item_icon_margin_end" /> </LinearLayout> </LinearLayout>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout-w720dp/item_doc_list.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout-w900dp/item_doc_list.xml index d9b0ab6f4..09657d01a 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout-w720dp/item_doc_list.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout-w900dp/item_doc_list.xml @@ -20,7 +20,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/list_item_background" - android:foreground="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true" android:orientation="horizontal" > @@ -31,23 +30,27 @@ android:baselineAligned="false" android:gravity="center_vertical" android:minHeight="@dimen/list_item_height" - android:orientation="horizontal" > + android:orientation="horizontal" + android:paddingStart="@dimen/list_item_padding_start" + android:paddingEnd="@dimen/list_item_padding_end" + android:paddingVertical="@dimen/list_item_padding_vertical"> <FrameLayout android:id="@+id/icon" android:pointerIcon="hand" - android:layout_width="@dimen/list_item_width" - android:layout_height="@dimen/list_item_height" - android:paddingBottom="@dimen/list_item_icon_padding" - android:paddingTop="@dimen/list_item_icon_padding" - android:paddingEnd="16dp" - android:paddingStart="@dimen/list_item_padding" > + android:layout_width="@dimen/list_item_icon_size" + android:layout_height="@dimen/list_item_icon_size" + android:layout_marginEnd="@dimen/list_item_icon_margin_end"> + <!-- stroke width will be controlled dynamically in the code. --> <com.google.android.material.card.MaterialCardView + android:id="@+id/icon_wrapper" app:cardElevation="0dp" - app:cardBackgroundColor="@android:color/transparent" android:layout_width="match_parent" - android:layout_height="match_parent"> + android:layout_height="match_parent" + app:cardBackgroundColor="?attr/colorSurfaceContainerLowest" + app:strokeColor="?attr/colorSecondaryContainer" + app:strokeWidth="0dp"> <ImageView android:id="@+id/icon_mime" @@ -101,83 +104,58 @@ android:layout_marginEnd="@dimen/briefcase_icon_margin" android:layout_gravity="center_vertical" android:src="@drawable/ic_briefcase" - android:tint="?android:attr/colorAccent" + android:tint="@color/doc_list_item_badge_icon_color" android:contentDescription="@string/a11y_work"/> <TextView android:id="@android:id/title" android:layout_width="match_parent" android:layout_height="wrap_content" - android:ellipsize="middle" + android:ellipsize="end" android:singleLine="true" android:textAlignment="viewStart" - android:textAppearance="@style/Subhead" - android:textColor="?android:attr/textColorPrimary"/> + android:textAppearance="@style/FileItemLabelText"/> </LinearLayout> <TextView android:id="@+id/file_type" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginEnd="12dp" android:layout_weight="0.2" - android:ellipsize="end" - android:singleLine="true" android:textAlignment="viewStart" - android:textAppearance="@style/Body1" - android:textColor="?android:attr/textColorSecondary" /> + style="@style/FileItemLabelStyle"/> <TextView android:id="@+id/size" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginEnd="12dp" android:layout_weight="0.2" - android:ellipsize="end" - android:minWidth="70dp" - android:singleLine="true" - android:textAlignment="viewEnd" - android:textAppearance="@style/Body1" - android:textColor="?android:attr/textColorSecondary" /> + android:textAlignment="viewStart" + style="@style/FileItemLabelStyle"/> <TextView android:id="@+id/date" android:layout_width="0dp" android:layout_height="wrap_content" - android:layout_marginEnd="12dp" android:layout_weight="0.2" - android:ellipsize="end" - android:minWidth="70dp" - android:singleLine="true" - android:textAlignment="viewEnd" - android:textAppearance="@style/Body1" - android:textColor="?android:attr/textColorSecondary" /> + android:textAlignment="viewStart" + style="@style/FileItemLabelStyle"/> </LinearLayout> <FrameLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content"> - - <FrameLayout - android:id="@+id/preview_icon" - android:layout_width="@dimen/list_item_width" - android:layout_height="@dimen/list_item_height" - android:padding="@dimen/list_item_icon_padding" - android:focusable="true"> - - <ImageView - android:layout_width="@dimen/check_icon_size" - android:layout_height="@dimen/check_icon_size" - android:layout_gravity="center" - android:scaleType="fitCenter" - android:tint="?android:attr/textColorPrimary" - android:src="@drawable/ic_zoom_out"/> - - </FrameLayout> - - <android.widget.Space - android:layout_width="@dimen/list_item_width" - android:layout_height="@dimen/list_item_height"/> + android:id="@+id/preview_icon" + android:layout_width="@dimen/list_item_icon_size" + android:layout_height="@dimen/list_item_icon_size" + android:layout_marginEnd="@dimen/list_item_icon_margin_end" + android:focusable="true"> + + <ImageView + android:layout_width="@dimen/check_icon_size" + android:layout_height="@dimen/check_icon_size" + android:layout_gravity="center" + android:scaleType="fitCenter" + android:tint="@color/doc_list_item_badge_icon_color" + android:src="@drawable/ic_zoom_out"/> </FrameLayout> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout-w720dp/shared_cell_content.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout-w900dp/shared_cell_content.xml index 4702eada3..f269afdbe 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout-w720dp/shared_cell_content.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout-w900dp/shared_cell_content.xml @@ -23,8 +23,8 @@ android:ellipsize="end" android:singleLine="true" android:textAlignment="viewStart" - android:textAppearance="@style/Subhead" - android:textColor="?android:attr/textColorSecondary"/> + android:textAppearance="@style/ListTableHeaderText" + android:textColor="?attr/colorOnSurface"/> <ImageView android:id="@+id/sort_arrow" @@ -32,6 +32,9 @@ android:layout_width="@dimen/doc_header_sort_icon_size" android:layout_marginStart="3dp" android:visibility="gone" + android:focusable="true" + android:clickable="true" + android:background="?attr/colorSurfaceBright" android:src="@drawable/ic_sort_arrow" android:contentDescription="@null"/> </merge>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/apps_row.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/apps_row.xml index 8b6471265..249a6d3f1 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout/apps_row.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/apps_row.xml @@ -14,6 +14,9 @@ limitations under the License. --> +<!-- TODO(b/379776735): Remove this after use_material3 flag is launched. + Currently it's being referenced in AppsRowManager. +--> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/apps_row" diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/directory_app_bar.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/directory_app_bar.xml index 1f8aa7b3c..54a6a7cb2 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout/directory_app_bar.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/directory_app_bar.xml @@ -14,13 +14,14 @@ limitations under the License. --> +<!-- This is only used in DrawerLayout (compact screen) right now. --> <com.google.android.material.appbar.AppBarLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/app_bar" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="?android:attr/colorBackground"> + android:touchscreenBlocksFocus="false"> <com.google.android.material.appbar.CollapsingToolbarLayout android:id="@+id/collapsing_toolbar" @@ -35,20 +36,17 @@ <include layout="@layout/directory_header" /> + <!-- column headers are empty on small screens, in portrait or in grid mode. --> + <include layout="@layout/column_headers"/> + </androidx.core.widget.NestedScrollView> - <androidx.appcompat.widget.Toolbar + <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" - android:layout_height="?android:attr/actionBarSize" - android:layout_margin="@dimen/search_bar_margin" - android:background="?android:attr/colorBackground" - android:theme="?actionBarTheme" - android:popupTheme="?actionBarPopupTheme" - android:elevation="@dimen/search_bar_elevation" - app:collapseContentDescription="@string/button_back" - app:titleTextAppearance="@style/ToolbarTitle" - app:layout_collapseMode="pin"> + android:layout_height="?attr/actionBarSize" + app:layout_collapseMode="pin" + android:touchscreenBlocksFocus="false"> <TextView android:id="@+id/searchbar_title" @@ -58,8 +56,8 @@ android:text="@string/search_bar_hint" android:textAppearance="@style/SearchBarTitle" /> - </androidx.appcompat.widget.Toolbar> + </com.google.android.material.appbar.MaterialToolbar> </com.google.android.material.appbar.CollapsingToolbarLayout> -</com.google.android.material.appbar.AppBarLayout>
\ No newline at end of file +</com.google.android.material.appbar.AppBarLayout> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/directory_header.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/directory_header.xml index 8d8bd7f8a..171c5882a 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout/directory_header.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/directory_header.xml @@ -20,76 +20,36 @@ android:layout_height="wrap_content" android:orientation="vertical"> - <com.android.documentsui.HorizontalBreadcrumb - android:id="@+id/horizontal_breadcrumb" - android:layout_width="match_parent" - android:layout_height="wrap_content" /> - <!-- used for search chip. --> <include layout="@layout/search_chip_row"/> <LinearLayout android:id="@+id/tabs_container" - android:theme="@style/TabTheme" android:clipToPadding="true" android:clipChildren="true" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingLeft="@dimen/profile_tab_padding" - android:paddingRight="@dimen/profile_tab_padding" + android:paddingStart="@dimen/main_container_padding_start" + android:paddingEnd="@dimen/main_container_padding_end" + android:paddingBottom="@dimen/space_extra_small_6" android:orientation="vertical"> <com.google.android.material.tabs.TabLayout android:id="@+id/tabs" - android:background="@android:color/transparent" android:layout_width="match_parent" android:layout_height="wrap_content" app:tabMaxWidth="0dp" app:tabGravity="fill" app:tabMode="fixed" - app:tabIndicatorColor="?android:attr/colorAccent" - app:tabIndicatorHeight="@dimen/tab_selector_indicator_height" - app:tabSelectedTextColor="@color/tab_selected_text_color" - app:tabTextAppearance="@style/TabTextAppearance" - app:tabTextColor="@color/tab_unselected_text_color"/> + style="@style/ProfileTabStyle"/> <View android:id="@+id/tab_separator" android:layout_width="match_parent" android:layout_height="1dp" - android:background="?android:attr/listDivider"/> + android:background="?attr/colorOutlineVariant"/> </LinearLayout> <!-- used for apps row. --> <include layout="@layout/apps_row"/> - <LinearLayout - android:id="@+id/header_container" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_marginStart="@dimen/root_info_header_horizontal_padding" - android:layout_marginEnd="@dimen/root_info_header_horizontal_padding" - android:minHeight="@dimen/root_info_header_height" - android:accessibilityHeading="true"> - - <TextView - android:id="@+id/header_title" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:layout_weight="1" - android:textAppearance="@style/SectionHeader" - android:maxLines="1" - android:ellipsize="end" - android:gravity="start|center_vertical"/> - - <androidx.appcompat.widget.ActionMenuView - android:id="@+id/sub_menu" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="end|center_vertical"/> - - </LinearLayout> - - <!-- column headers are empty on small screens, in portrait or in grid mode. --> - <include layout="@layout/column_headers"/> - </LinearLayout>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/drawer_layout.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/drawer_layout.xml index 58ef57f58..de6b3d8a9 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout/drawer_layout.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/drawer_layout.xml @@ -28,14 +28,19 @@ android:layout_width="match_parent" android:layout_height="match_parent"> + <!-- Main container --> <androidx.coordinatorlayout.widget.CoordinatorLayout android:layout_width="match_parent" android:layout_height="match_parent" - android:orientation="vertical"> + android:orientation="vertical" + android:paddingTop="@dimen/main_container_padding_top" + android:background="?attr/colorSurfaceBright"> + <!-- Main list area (file list/grid or search results), full height --> <FrameLayout android:layout_width="match_parent" android:layout_height="match_parent" + android:paddingBottom="@dimen/file_area_padding_bottom" app:layout_behavior="@string/scrolling_behavior"> <FrameLayout @@ -60,35 +65,35 @@ android:layout_height="match_parent"/> </FrameLayout> + <!-- Footer of right hand side: Breadcrumbs and Picker footer. --> + <com.android.documentsui.HorizontalBreadcrumb + android:id="@+id/horizontal_breadcrumb" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="bottom" + android:background="?attr/colorSurfaceBright" + /> + <androidx.coordinatorlayout.widget.CoordinatorLayout android:id="@+id/container_save" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="bottom|center_horizontal" android:background="?android:attr/colorBackgroundFloating" - android:elevation="8dp" /> + /> + <!-- Top section: toolbar, search chips, profile tab --> <include layout="@layout/directory_app_bar"/> </androidx.coordinatorlayout.widget.CoordinatorLayout> + <!-- Drawer section --> <LinearLayout android:id="@+id/drawer_roots" android:layout_width="256dp" android:layout_height="match_parent" android:layout_gravity="start" - android:orientation="vertical" - android:elevation="0dp" - android:background="?android:attr/colorBackground"> - - <androidx.appcompat.widget.Toolbar - android:id="@+id/roots_toolbar" - android:layout_width="match_parent" - android:layout_height="?android:attr/actionBarSize" - android:background="?android:attr/colorBackground" - android:elevation="0dp" - app:titleTextAppearance="@style/DrawerMenuTitle" - app:titleTextColor="?android:colorAccent"/> + android:orientation="vertical"> <FrameLayout android:id="@+id/container_roots" diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/fixed_layout.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/fixed_layout.xml index 682edcb7d..feaa34e6d 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout/fixed_layout.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/fixed_layout.xml @@ -18,7 +18,6 @@ floating action buttons) to operate correctly. --> <androidx.coordinatorlayout.widget.CoordinatorLayout 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" android:id="@+id/coordinator_layout" @@ -27,59 +26,66 @@ <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" + android:orientation="horizontal" + android:baselineAligned="false" android:background="?attr/colorSurfaceContainer" - android:orientation="vertical"> - + android:paddingTop="@dimen/layout_padding_top" + android:paddingBottom="@dimen/layout_padding_bottom" + android:paddingEnd="@dimen/layout_padding_end"> + + <!-- Navigation: left hand side. --> + <FrameLayout + android:id="@+id/container_roots" + android:layout_width="256dp" + android:layout_height="match_parent" + /> + + <!-- Main container for the right hand side. --> <LinearLayout android:layout_width="match_parent" - android:layout_height="0dp" - android:layout_weight="1" - android:orientation="horizontal" - android:baselineAligned="false"> - - <FrameLayout - android:id="@+id/container_roots" - android:layout_width="256dp" - android:layout_height="match_parent" - android:layout_marginTop="@dimen/space_medium_1" - /> + android:layout_height="match_parent" + android:orientation="vertical"> + <!-- Top section: toolbar, search chips, profile tab --> <LinearLayout android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_height="wrap_content" android:orientation="vertical" - android:background="@drawable/main_container_background" - android:layout_margin="@dimen/space_small_1" - android:layout_marginStart="0dp"> + android:paddingTop="@dimen/main_container_padding_top" + android:background="@drawable/main_container_top_section_background"> - <androidx.appcompat.widget.Toolbar + <com.google.android.material.appbar.MaterialToolbar android:id="@+id/toolbar" android:layout_width="match_parent" - android:layout_height="?android:attr/actionBarSize" - android:layout_margin="@dimen/search_bar_margin" - android:elevation="3dp" - android:popupTheme="?actionBarPopupTheme" - android:theme="?actionBarTheme" - app:collapseContentDescription="@string/button_back" - app:titleTextAppearance="@style/ToolbarTitle"> + android:layout_height="?attr/actionBarSize" + android:layout_marginTop="@dimen/action_bar_margin" + android:touchscreenBlocksFocus="false"> <TextView android:id="@+id/searchbar_title" android:layout_width="match_parent" android:layout_height="?android:attr/actionBarSize" - android:layout_marginEnd="@dimen/search_bar_text_margin_end" - android:layout_marginStart="@dimen/search_bar_text_margin_start" - android:drawablePadding="@dimen/search_bar_icon_padding" - android:drawableStart="@drawable/ic_menu_search" android:gravity="center_vertical" - android:paddingStart="@dimen/search_bar_icon_padding" android:text="@string/search_bar_hint" android:textAppearance="@style/SearchBarTitle" /> - </androidx.appcompat.widget.Toolbar> + </com.google.android.material.appbar.MaterialToolbar> <include layout="@layout/directory_header" /> + </LinearLayout> + + <!-- Main list area (file list/grid or search results). --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:orientation="vertical" + android:layout_marginTop="@dimen/main_container_section_gap" + android:background="@drawable/main_container_middle_section_background"> + + <include layout="@layout/column_headers"/> + <FrameLayout android:layout_width="match_parent" android:layout_height="0dp" @@ -98,16 +104,29 @@ android:layout_height="match_parent" /> </FrameLayout> + </LinearLayout> - <androidx.coordinatorlayout.widget.CoordinatorLayout - android:id="@+id/container_save" + <!-- Footer of right hand side: Breadcrumbs and Picker footer. --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/main_container_section_gap" + android:background="@drawable/main_container_bottom_section_background"> + + <com.android.documentsui.HorizontalBreadcrumb + android:id="@+id/horizontal_breadcrumb" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="?android:attr/colorBackgroundFloating" - android:elevation="8dp" /> + android:layout_height="wrap_content" /> </LinearLayout> + <androidx.coordinatorlayout.widget.CoordinatorLayout + android:id="@+id/container_save" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:attr/colorBackgroundFloating" + android:elevation="8dp" /> + </LinearLayout> </LinearLayout> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/fragment_directory.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/fragment_directory.xml index f424e407d..a7ee033f1 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout/fragment_directory.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/fragment_directory.xml @@ -21,13 +21,13 @@ android:layout_height="match_parent" android:orientation="vertical"> - <ProgressBar + <com.google.android.material.progressindicator.LinearProgressIndicator android:id="@+id/progressbar" android:layout_width="match_parent" - android:layout_height="@dimen/progress_bar_height" + android:layout_height="wrap_content" android:indeterminate="true" - style="@style/TrimmedHorizontalProgressBar" - android:visibility="gone"/> + app:trackColor="?attr/colorSecondaryContainer" + android:visibility="gone" /> <com.android.documentsui.dirlist.DocumentsSwipeRefreshLayout android:id="@+id/refresh_layout" diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/fragment_nav_rail_roots.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/fragment_nav_rail_roots.xml new file mode 100644 index 000000000..7848dea0b --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/fragment_nav_rail_roots.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> + +<com.android.documentsui.sidebar.RootsList xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/roots_list" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:keyboardNavigationCluster="true" + android:divider="@null" + android:focusable="false" + android:descendantFocusability="afterDescendants" + style="@style/NavRailStyle"/> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/fragment_roots.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/fragment_roots.xml index 97363208b..0728c6279 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout/fragment_roots.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/fragment_roots.xml @@ -18,6 +18,8 @@ android:id="@+id/roots_list" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingTop="8dp" android:keyboardNavigationCluster="true" - android:divider="@null"/> + android:divider="@null" + android:focusable="false" + android:descendantFocusability="afterDescendants" + style="@style/DrawerStyle"/> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/item_doc_grid.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/item_doc_grid.xml index 32596f2f4..574978c74 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout/item_doc_grid.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/item_doc_grid.xml @@ -14,196 +14,171 @@ limitations under the License. --> -<!-- FYI: This layout has an extra top level container view that was previously used - to allow for the insertion of debug info. The debug info is now gone, but the - container remains because there is a high likelihood of UI regression relating - to focus and selection states, some of which are specific to keyboard - when touch mode is not enable. So, if you, heroic engineer of the future, - decide to rip these out, please be sure to check out focus and keyboards. --> <com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:id="@+id/item_root" - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:layout_margin="4dp" - android:foreground="?android:attr/selectableItemBackground" + android:layout_width="@dimen/grid_item_width" + android:layout_height="@dimen/grid_item_height" + android:layout_margin="@dimen/grid_item_layout_margin" android:clickable="true" android:focusable="true" - app:cardElevation="0dp"> + app:cardBackgroundColor="@android:color/transparent" + app:cardElevation="0dp" + app:strokeWidth="0dp"> - <com.google.android.material.card.MaterialCardView + <RelativeLayout + android:id="@+id/grid_item_layout" android:layout_width="match_parent" android:layout_height="wrap_content" - android:elevation="0dp" - android:duplicateParentState="true" - app:cardElevation="0dp" - app:strokeWidth="1dp" - app:strokeColor="?android:strokeColor"> - - <RelativeLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:duplicateParentState="true"> - - <!-- Main item thumbnail. Comprised of two overlapping images, the - visibility of which is controlled by code in - DirectoryFragment.java. --> - - <FrameLayout - android:id="@+id/thumbnail" - android:background="?attr/gridItemTint" - android:layout_width="match_parent" - android:layout_height="wrap_content"> + android:layout_marginStart="@dimen/grid_item_layout_marginStart" + android:layout_marginEnd="@dimen/grid_item_layout_marginEnd" + android:layout_marginTop="@dimen/grid_item_layout_marginTop"> + + <!-- Main item thumbnail. Comprised of two overlapping images, the + visibility of which is controlled by code in + DirectoryFragment.java. --> + + <FrameLayout + android:id="@+id/thumbnail" + android:layout_width="@dimen/grid_item_thumbnail_width" + android:layout_height="@dimen/grid_item_thumbnail_height" + android:layout_centerHorizontal="true" + android:background="@drawable/grid_thumbnail_background"> + + <!-- stroke width will be controlled dynamically in the code. --> + <com.google.android.material.card.MaterialCardView + android:id="@+id/icon_wrapper" + android:layout_width="@dimen/grid_item_icon_width" + android:layout_height="@dimen/grid_item_icon_height" + android:layout_gravity="center" + app:cardBackgroundColor="?attr/colorSurfaceContainerLowest" + app:cardElevation="0dp" + app:strokeColor="?attr/colorSecondaryContainer" + app:strokeWidth="0dp"> <com.android.documentsui.GridItemThumbnail android:id="@+id/icon_thumb" android:layout_width="match_parent" - android:layout_height="wrap_content" - android:scaleType="centerCrop" + android:layout_height="match_parent" android:contentDescription="@null" + android:scaleType="centerCrop" android:tint="?attr/gridItemTint" - android:tintMode="src_over"/> + android:tintMode="src_over" /> <com.android.documentsui.GridItemThumbnail android:id="@+id/icon_mime_lg" android:layout_width="@dimen/icon_size" android:layout_height="@dimen/icon_size" android:layout_gravity="center" - android:scaleType="fitCenter" - android:contentDescription="@null"/> - - </FrameLayout> - - <FrameLayout - android:id="@+id/preview_icon" - android:layout_width="@dimen/button_touch_size" - android:layout_height="@dimen/button_touch_size" - android:layout_alignParentTop="true" - android:layout_alignParentEnd="true" - android:pointerIcon="hand" - android:focusable="true" - android:clickable="true"> + android:contentDescription="@null" + android:scaleType="fitCenter" /> + + </com.google.android.material.card.MaterialCardView> + + </FrameLayout> + + <FrameLayout + android:id="@+id/preview_icon" + android:layout_width="@dimen/button_touch_size" + android:layout_height="@dimen/button_touch_size" + android:layout_alignParentEnd="true" + android:layout_alignParentTop="true" + android:clickable="true" + android:focusable="true" + android:pointerIcon="hand"> + + <ImageView + android:layout_width="@dimen/zoom_icon_size" + android:layout_height="@dimen/zoom_icon_size" + android:layout_gravity="center" + android:background="@drawable/circle_button_background" + android:padding="2dp" + android:scaleType="fitCenter" + android:src="@drawable/ic_zoom_out" /> + + </FrameLayout> + + <!-- Item nameplate. Has some text fields (title, size, mod-time, etc). --> + + <LinearLayout + android:id="@+id/nameplate" + android:layout_width="@dimen/grid_item_nameplate_width" + android:layout_height="@dimen/grid_item_nameplate_height" + android:layout_below="@id/thumbnail" + android:layout_marginTop="@dimen/grid_item_nameplate_marginTop" + android:background="@drawable/grid_nameplate_background" + android:orientation="vertical" + android:padding="@dimen/grid_item_nameplate_padding"> + + <!-- Top row. --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:orientation="horizontal"> <ImageView - android:layout_width="@dimen/zoom_icon_size" - android:layout_height="@dimen/zoom_icon_size" - android:padding="2dp" - android:layout_gravity="center" - android:background="@drawable/circle_button_background" - android:scaleType="fitCenter" - android:src="@drawable/ic_zoom_out"/> - - </FrameLayout> + android:id="@+id/icon_profile_badge" + android:layout_width="@dimen/briefcase_icon_size" + android:layout_height="@dimen/briefcase_icon_size" + android:layout_marginEnd="@dimen/briefcase_icon_margin" + android:contentDescription="@string/a11y_work" + android:gravity="center_vertical" + android:src="@drawable/ic_briefcase" + android:tint="?android:attr/colorAccent" /> + + <TextView + android:id="@android:id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="end" + android:singleLine="true" + android:textAlignment="center" + android:textAppearance="@style/FileItemLabelText" /> - <!-- Item nameplate. Has a mime-type icon and some text fields (title, - size, mod-time, etc). --> + </LinearLayout> + <!-- Bottom row. --> <LinearLayout - android:id="@+id/nameplate" - android:background="?android:attr/colorBackground" - android:orientation="horizontal" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_below="@id/thumbnail"> + android:gravity="center" + android:orientation="horizontal"> - <FrameLayout - android:id="@+id/icon" + <TextView + android:id="@+id/details" android:layout_width="wrap_content" - android:layout_height="match_parent" - android:layout_centerVertical="true" - android:pointerIcon="hand" - android:paddingTop="8dp" - android:paddingBottom="8dp" - android:paddingStart="12dp" - android:paddingEnd="8dp"> - - <ImageView - android:id="@+id/icon_mime_sm" - android:layout_width="@dimen/grid_item_icon_size" - android:layout_height="@dimen/grid_item_icon_size" - android:layout_gravity="center" - android:scaleType="center" - android:contentDescription="@null"/> - - <ImageView - android:id="@+id/icon_check" - android:src="@drawable/ic_check_circle" - android:alpha="0" - android:layout_width="@dimen/check_icon_size" - android:layout_height="@dimen/check_icon_size" - android:layout_gravity="center" - android:scaleType="fitCenter" - android:contentDescription="@null"/> - - </FrameLayout> - - <RelativeLayout - android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingBottom="8dp" - android:paddingTop="8dp" - android:paddingEnd="12dp"> - - <ImageView - android:id="@+id/icon_profile_badge" - android:layout_height="@dimen/briefcase_icon_size" - android:layout_width="@dimen/briefcase_icon_size" - android:layout_marginEnd="@dimen/briefcase_icon_margin" - android:layout_alignTop="@android:id/title" - android:layout_alignBottom="@android:id/title" - android:gravity="center_vertical" - android:src="@drawable/ic_briefcase" - android:tint="?android:attr/colorAccent" - android:contentDescription="@string/a11y_work"/> - - <TextView - android:id="@android:id/title" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_alignParentTop="true" - android:layout_toEndOf="@+id/icon_profile_badge" - android:singleLine="true" - android:ellipsize="end" - android:textAlignment="viewStart" - android:textAppearance="@style/CardPrimaryText"/> - - <TextView - android:id="@+id/details" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_below="@android:id/title" - android:layout_marginEnd="4dp" - android:singleLine="true" - android:ellipsize="end" - android:textAlignment="viewStart" - android:textAppearance="@style/ItemCaptionText" /> - - <TextView - android:id="@+id/date" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_below="@android:id/title" - android:layout_toEndOf="@id/details" - android:singleLine="true" - android:ellipsize="end" - android:textAlignment="viewStart" - android:textAppearance="@style/ItemCaptionText" /> - - </RelativeLayout> + android:layout_marginEnd="4dp" + android:ellipsize="end" + android:singleLine="true" + android:textAlignment="viewStart" + android:textAppearance="@style/ItemCaptionText" /> + + <TextView + android:id="@+id/bullet" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginEnd="4dp" + android:singleLine="true" + android:text="@string/bullet" + android:textAlignment="viewStart" + android:textAppearance="@style/ItemCaptionText" /> + + <TextView + android:id="@+id/date" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:ellipsize="end" + android:singleLine="true" + android:textAlignment="viewStart" + android:textAppearance="@style/ItemCaptionText" /> </LinearLayout> - </RelativeLayout> - - </com.google.android.material.card.MaterialCardView> + </LinearLayout> - <!-- An overlay that draws the item border when it is focused. --> - <View - android:layout_width="match_parent" - android:layout_height="match_parent" - android:background="@drawable/item_doc_grid_border_rounded" - android:contentDescription="@null" - android:duplicateParentState="true"/> + </RelativeLayout> </com.google.android.material.card.MaterialCardView> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/item_doc_list.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/item_doc_list.xml index 055c1b203..b3ab315c5 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout/item_doc_list.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/item_doc_list.xml @@ -20,7 +20,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/list_item_background" - android:foreground="?android:attr/selectableItemBackground" android:clickable="true" android:focusable="true" android:orientation="vertical"> @@ -31,23 +30,27 @@ android:baselineAligned="false" android:gravity="center_vertical" android:minHeight="@dimen/list_item_height" - android:orientation="horizontal"> + android:orientation="horizontal" + android:paddingStart="@dimen/list_item_padding_start" + android:paddingEnd="@dimen/list_item_padding_end" + android:paddingVertical="@dimen/list_item_padding_vertical"> <FrameLayout android:id="@+id/icon" android:pointerIcon="hand" - android:layout_width="@dimen/list_item_width" - android:layout_height="@dimen/list_item_height" - android:paddingBottom="@dimen/list_item_icon_padding" - android:paddingTop="@dimen/list_item_icon_padding" - android:paddingEnd="16dp" - android:paddingStart="@dimen/list_item_padding"> + android:layout_width="@dimen/list_item_icon_size" + android:layout_height="@dimen/list_item_icon_size" + android:layout_marginEnd="@dimen/list_item_icon_margin_end"> + <!-- stroke width will be controlled dynamically in the code. --> <com.google.android.material.card.MaterialCardView + android:id="@+id/icon_wrapper" android:layout_width="match_parent" android:layout_height="match_parent" - app:cardBackgroundColor="@android:color/transparent" - app:cardElevation="0dp"> + app:cardElevation="0dp" + app:cardBackgroundColor="?attr/colorSurfaceContainerLowest" + app:strokeColor="?attr/colorSecondaryContainer" + app:strokeWidth="0dp"> <ImageView android:id="@+id/icon_mime" @@ -84,7 +87,7 @@ android:layout_weight="1" android:orientation="vertical" android:layout_gravity="center_vertical" - android:layout_marginEnd="@dimen/list_item_padding"> + android:layout_marginEnd="@dimen/list_item_icon_size"> <LinearLayout android:layout_width="wrap_content" @@ -98,7 +101,7 @@ android:layout_marginEnd="@dimen/briefcase_icon_margin" android:layout_gravity="center_vertical" android:src="@drawable/ic_briefcase" - android:tint="?android:attr/colorAccent" + android:tint="@color/doc_list_item_badge_icon_color" android:contentDescription="@string/a11y_work" /> <TextView @@ -108,7 +111,7 @@ android:ellipsize="end" android:singleLine="true" android:textAlignment="viewStart" - android:textAppearance="?android:attr/textAppearanceListItem" /> + android:textAppearance="@style/FileItemLabelText" /> </LinearLayout> @@ -117,7 +120,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:baselineAligned="false" - android:layout_marginTop="4dp" android:gravity="center_vertical" android:orientation="horizontal"> @@ -135,9 +137,8 @@ <FrameLayout android:id="@+id/preview_icon" - android:layout_width="@dimen/list_item_width" - android:layout_height="@dimen/list_item_height" - android:padding="@dimen/list_item_icon_padding" + android:layout_width="@dimen/list_item_icon_size" + android:layout_height="@dimen/list_item_icon_size" android:focusable="true" android:clickable="true"> @@ -146,18 +147,11 @@ android:layout_height="@dimen/check_icon_size" android:layout_gravity="center" android:scaleType="fitCenter" - android:tint="?android:attr/colorControlNormal" + android:tint="@color/doc_list_item_badge_icon_color" android:src="@drawable/ic_zoom_out" /> </FrameLayout> </LinearLayout> - <View - android:layout_width="match_parent" - android:layout_height="1dp" - android:layout_marginStart="72dp" - android:layout_marginEnd="8dp" - android:background="?android:strokeColor" /> - </LinearLayout>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/item_root.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/item_root.xml index 14751c014..599fab108 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout/item_root.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/item_root.xml @@ -16,14 +16,16 @@ <com.android.documentsui.sidebar.RootItemView 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="wrap_content" - android:minHeight="52dp" - android:paddingStart="24dp" + android:minHeight="@dimen/drawer_item_height" android:gravity="center_vertical" android:orientation="horizontal" android:baselineAligned="false" - android:background="@drawable/root_item_background"> + android:clickable="true" + android:focusable="true" + style="@style/DrawerItemStyle"> <FrameLayout android:layout_width="wrap_content" @@ -43,9 +45,7 @@ <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingStart="16dp" - android:paddingTop="8dp" - android:paddingBottom="8dp" + android:layout_marginStart="@dimen/drawer_item_text_margin_start" android:orientation="vertical" android:layout_weight="1"> @@ -69,24 +69,11 @@ </LinearLayout> - <include layout="@layout/root_vertical_divider" /> - - <FrameLayout - android:id="@+id/action_icon_area" - android:layout_width="@dimen/button_touch_size" - android:layout_height="@dimen/button_touch_size" - android:paddingEnd="@dimen/grid_padding_horiz" - android:duplicateParentState="true" - android:visibility="gone"> - - <ImageView - android:id="@+id/action_icon" - android:focusable="false" - android:layout_width="@dimen/root_action_icon_size" - android:layout_height="match_parent" - android:layout_gravity="center" - android:scaleType="centerInside"/> - - </FrameLayout> + <com.google.android.material.button.MaterialButton + android:id="@+id/action_icon" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + style="@style/DrawerItemActionIconStyle" + android:visibility="gone"/> </com.android.documentsui.sidebar.RootItemView> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/item_root_spacer.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/item_root_spacer.xml index 83dcd818c..dab92dbf0 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout/item_root_spacer.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/item_root_spacer.xml @@ -17,13 +17,12 @@ <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingStart="@dimen/root_spacer_padding" - android:paddingTop="12dp" - android:paddingBottom="12dp"> + android:focusable="false" + android:paddingHorizontal="@dimen/drawer_divider_padding_horizontal"> <View android:layout_width="match_parent" - android:layout_height="wrap_content" - android:background="?android:attr/listDivider" /> + android:layout_height="1dp" + android:background="?attr/colorOutlineVariant" /> </FrameLayout> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/nav_rail_item_root.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/nav_rail_item_root.xml new file mode 100644 index 000000000..461027c73 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/nav_rail_item_root.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> + +<com.android.documentsui.sidebar.RootItemView + xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="@dimen/nav_rail_item_height" + android:gravity="center_horizontal" + android:orientation="vertical" + android:baselineAligned="false" + android:clickable="true" + android:focusable="true" + style="@style/NavRailItemStyle"> + + <LinearLayout + android:layout_width="@dimen/nav_rail_item_icon_bg_width" + android:layout_height="@dimen/nav_rail_item_icon_bg_height" + android:gravity="center" + android:duplicateParentState="true" + android:background="@drawable/nav_rail_item_icon_background"> + + <ImageView + android:id="@android:id/icon" + android:layout_width="@dimen/root_icon_size" + android:layout_height="@dimen/root_icon_size" + android:scaleType="centerInside" + android:contentDescription="@null" /> + + </LinearLayout> + + <TextView + android:id="@android:id/title" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:singleLine="true" + android:ellipsize="end" + android:textAlignment="center" + android:duplicateParentState="true" + style="@style/NavRailItemTextStyle" /> + +</com.android.documentsui.sidebar.RootItemView> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/nav_rail_layout.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/nav_rail_layout.xml new file mode 100644 index 000000000..a014d8866 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/nav_rail_layout.xml @@ -0,0 +1,181 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> + +<!-- CoordinatorLayout is necessary for various components (e.g. Snackbars, and + floating action buttons) to operate correctly. --> +<androidx.coordinatorlayout.widget.CoordinatorLayout + 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" + android:id="@+id/coordinator_layout"> + + <androidx.drawerlayout.widget.DrawerLayout + android:id="@+id/drawer_layout" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <!-- Main section --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="horizontal" + android:baselineAligned="false" + android:paddingTop="@dimen/layout_padding_top" + android:paddingBottom="@dimen/layout_padding_bottom" + android:paddingEnd="@dimen/layout_padding_end" + android:background="?attr/colorSurfaceContainer"> + + <!-- Navigation rail: left hand side. --> + <LinearLayout + android:id="@+id/nav_rail_container" + android:layout_width="144dp" + android:layout_height="match_parent" + android:orientation="vertical" + android:gravity="center_horizontal"> + + <com.google.android.material.button.MaterialButton + style="?attr/materialIconButtonStyle" + android:id="@+id/nav_rail_burger_menu" + android:layout_width="@dimen/nav_rail_burger_icon_size" + android:layout_height="@dimen/nav_rail_burger_icon_size" + android:layout_marginBottom="24dp" + app:iconPadding="0dp" + app:iconGravity="textStart" + app:iconTint="?attr/colorOnSurfaceVariant" + app:backgroundTint="@drawable/nav_rail_burger_icon_background" + app:rippleColor="@color/nav_rail_burger_icon_ripple_color" + app:strokeColor="?attr/colorPrimary" + android:contentDescription="@string/drawer_open" + app:icon="@drawable/ic_hamburger" /> + + <FrameLayout + android:id="@+id/nav_rail_container_roots" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" /> + + </LinearLayout> + + <!-- Main container for the right hand side. --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <!-- Top section: toolbar, search chips, profile tab --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingTop="@dimen/main_container_padding_top" + android:background="@drawable/main_container_top_section_background"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + android:layout_marginTop="@dimen/action_bar_margin" + android:touchscreenBlocksFocus="false"> + + <TextView + android:id="@+id/searchbar_title" + android:layout_width="match_parent" + android:layout_height="?android:attr/actionBarSize" + android:gravity="center_vertical" + android:text="@string/search_bar_hint" + android:textAppearance="@style/SearchBarTitle" /> + + </com.google.android.material.appbar.MaterialToolbar> + + <include layout="@layout/directory_header" /> + + </LinearLayout> + + <!-- Main list area (file list/grid or search results). --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" + android:orientation="vertical" + android:layout_marginTop="@dimen/main_container_section_gap" + android:background="@drawable/main_container_middle_section_background"> + + <include layout="@layout/column_headers"/> + + <FrameLayout + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1"> + + <FrameLayout + android:id="@+id/container_directory" + android:clipToPadding="false" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + <FrameLayout + android:id="@+id/container_search_fragment" + android:clipToPadding="false" + android:layout_width="match_parent" + android:layout_height="match_parent" /> + + </FrameLayout> + + </LinearLayout> + + <!-- Footer of right hand side: Breadcrumbs and Picker footer. --> + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_marginTop="@dimen/main_container_section_gap" + android:background="@drawable/main_container_bottom_section_background"> + + <com.android.documentsui.HorizontalBreadcrumb + android:id="@+id/horizontal_breadcrumb" + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + </LinearLayout> + + <androidx.coordinatorlayout.widget.CoordinatorLayout + android:id="@+id/container_save" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:background="?android:attr/colorBackgroundFloating" + android:elevation="8dp" /> + + </LinearLayout> + + </LinearLayout> + + <!-- Drawer section --> + <LinearLayout + android:id="@+id/drawer_roots" + android:layout_width="256dp" + android:layout_height="match_parent" + android:layout_gravity="start" + android:orientation="vertical"> + + <FrameLayout + android:id="@+id/container_roots" + android:layout_width="match_parent" + android:layout_height="0dp" + android:layout_weight="1" /> + + </LinearLayout> + + </androidx.drawerlayout.widget.DrawerLayout> +</androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/root_vertical_divider.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/root_vertical_divider.xml index 74316598d..3197d9dce 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout/root_vertical_divider.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/root_vertical_divider.xml @@ -15,6 +15,7 @@ limitations under the License. --> +<!-- TODO(b/379776735): remove this file after use_material3 flag is launched. --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/vertical_divider" diff --git a/res/flag(com.android.documentsui.flags.use_material3)/layout/search_chip_row.xml b/res/flag(com.android.documentsui.flags.use_material3)/layout/search_chip_row.xml index ef14bcdb2..559ae3188 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/layout/search_chip_row.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/layout/search_chip_row.xml @@ -24,6 +24,6 @@ android:id="@+id/search_chip_group" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingHorizontal="@dimen/search_chip_group_margin_horizontal" - android:paddingVertical="@dimen/search_chip_group_margin_vertical"/> -</HorizontalScrollView>
\ No newline at end of file + android:paddingHorizontal="@dimen/main_container_padding_start" + android:paddingVertical="@dimen/search_chip_group_padding_vertical" /> +</HorizontalScrollView> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/menu/action_mode_menu.xml b/res/flag(com.android.documentsui.flags.use_material3)/menu/action_mode_menu.xml new file mode 100644 index 000000000..c9c139cc9 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/menu/action_mode_menu.xml @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> + +<menu + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + <item + android:id="@+id/action_menu_open_with" + android:title="@string/menu_open_with" + android:showAsAction="never" + app:showAsAction="never" /> + <item + android:id="@+id/action_menu_share" + android:icon="@drawable/ic_menu_share" + android:title="@string/menu_share" + android:showAsAction="always" + app:showAsAction="always" /> + <item + android:id="@+id/action_menu_delete" + android:icon="@drawable/ic_menu_delete" + android:title="@string/menu_delete" + android:showAsAction="always" + app:showAsAction="always" /> + <item + android:id="@+id/action_menu_sort" + android:icon="@drawable/ic_sort" + android:title="@string/menu_sort" + android:showAsAction="never" + app:showAsAction="never" /> + <item + android:id="@+id/action_menu_select" + android:title="@string/menu_select" + android:showAsAction="always" + app:showAsAction="always" /> + <item + android:id="@+id/action_menu_select_all" + android:title="@string/menu_select_all" + android:showAsAction="never" + app:showAsAction="never" /> + <item + android:id="@+id/action_menu_deselect_all" + android:title="@string/menu_deselect_all" + android:showAsAction="never" + app:showAsAction="never" /> + <item + android:id="@+id/action_menu_copy_to" + android:title="@string/menu_copy" + android:showAsAction="never" + android:visible="false" + app:showAsAction="never" /> + <item + android:id="@+id/action_menu_extract_to" + android:title="@string/menu_extract" + android:icon="@drawable/ic_menu_extract" + android:showAsAction="always" + android:visible="false" + app:showAsAction="always" /> + <item + android:id="@+id/action_menu_move_to" + android:title="@string/menu_move" + android:showAsAction="never" + android:visible="false" + app:showAsAction="never" /> + <item + android:id="@+id/action_menu_compress" + android:title="@string/menu_compress" + android:showAsAction="never" + android:visible="false" + app:showAsAction="never" /> + <item + android:id="@+id/action_menu_rename" + android:title="@string/menu_rename" + android:showAsAction="never" + android:visible="false" + app:showAsAction="never" /> + <item + android:id="@+id/action_menu_inspect" + android:title="@string/menu_inspect" + android:showAsAction="never" + android:visible="false" + app:showAsAction="never" /> + <item + android:id="@+id/action_menu_view_in_owner" + android:title="@string/menu_view_in_owner" + android:showAsAction="never" + android:visible="false" + app:showAsAction="never" /> +</menu> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/menu/activity.xml b/res/flag(com.android.documentsui.flags.use_material3)/menu/activity.xml new file mode 100644 index 000000000..0e636a18c --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/menu/activity.xml @@ -0,0 +1,106 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> + +<menu + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> +<!-- showAsAction flag impacts the behavior of SearchView. + When set to collapseActionView, collapsing SearchView to icon is the + default behavior. It would fit UX, however after expanding SearchView is + shown on the left site of the toolbar (replacing title). Since no way to + prevent this behavior was found, the flag is set to always. SearchView is + always visible by default and it is being collapse manually by calling + setIconified() method +--> + <item + android:id="@+id/option_menu_search" + android:title="@string/menu_search" + android:icon="@drawable/ic_menu_search" + android:imeOptions="actionSearch" + android:visible="false" + app:showAsAction="always|collapseActionView" + app:actionViewClass="androidx.appcompat.widget.SearchView"/> + <item + android:id="@+id/sub_menu_grid" + android:title="@string/menu_grid" + android:icon="@drawable/ic_menu_view_grid" + app:showAsAction="always" /> + <item + android:id="@+id/sub_menu_list" + android:title="@string/menu_list" + android:icon="@drawable/ic_menu_view_list" + app:showAsAction="always" /> +<!-- This group is being hidden when searching is in full bar mode--> + <group android:id="@+id/group_hide_when_searching"> + <item + android:id="@+id/option_menu_debug" + android:title="Debug" + android:icon="@drawable/ic_debug_menu" + android:visible="false" + app:showAsAction="always"/> + <item + android:id="@+id/option_menu_new_window" + android:title="@string/menu_new_window" + android:alphabeticShortcut="n" + android:visible="false" + app:showAsAction="never"/> + <item + android:id="@+id/option_menu_create_dir" + android:title="@string/menu_create_dir" + android:icon="@drawable/ic_create_new_folder" + android:alphabeticShortcut="e" + android:visible="false" + app:showAsAction="never"/> + <item + android:id="@+id/option_menu_sort" + android:title="@string/menu_sort" + android:icon="@drawable/ic_sort" + android:showAsAction="never" + android:visible="false" /> + <item + android:id="@+id/option_menu_select_all" + android:title="@string/menu_select_all" + android:alphabeticShortcut="a" + android:visible="false" + app:showAsAction="never"/> + <item + android:id="@+id/option_menu_extract_all" + android:title="@string/menu_extract_all" + android:icon="@drawable/ic_menu_extract" + android:enabled="false" + android:visible="false" + app:showAsAction="always"/> + <item + android:id="@+id/option_menu_settings" + android:title="@string/menu_settings" + android:visible="false" + app:showAsAction="never"/> + <item + android:id="@+id/option_menu_inspect" + android:title="@string/menu_inspect" + android:visible="false" + app:showAsAction="never"/> + <item + android:id="@+id/option_menu_show_hidden_files" + android:title="@string/menu_show_hidden_files" + android:visible="false" + app:showAsAction="never"/> + <item + android:id="@+id/option_menu_launcher" + android:visible="false" + app:showAsAction="never"/> + </group> +</menu> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/menu/container_context_menu.xml b/res/flag(com.android.documentsui.flags.use_material3)/menu/container_context_menu.xml new file mode 100644 index 000000000..b004edeb1 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/menu/container_context_menu.xml @@ -0,0 +1,48 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 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. + --> + +<!-- Context menu used when right clicks on empty area of recycler view or empty view. --> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <group + android:id="@+id/menu_clipboard_group"> + <item + android:id="@+id/dir_menu_paste_from_clipboard" + android:title="@string/menu_paste_from_clipboard" /> + </group> + + <group + android:id="@+id/menu_modifier_group"> + <item + android:id="@+id/dir_menu_create_dir" + android:title="@string/menu_create_dir" /> + + <item + android:id="@+id/dir_menu_select_all" + android:title="@string/menu_select_all" /> + + <item + android:id="@+id/dir_menu_deselect_all" + android:title="@string/menu_deselect_all" /> + </group> + <group + android:id="@+id/menu_extras_group"> + <item + android:id="@+id/dir_menu_inspect" + android:title="@string/menu_inspect" /> + </group> +</menu>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/menu/dir_context_menu.xml b/res/flag(com.android.documentsui.flags.use_material3)/menu/dir_context_menu.xml new file mode 100644 index 000000000..1364b9ec7 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/menu/dir_context_menu.xml @@ -0,0 +1,60 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 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. + --> + +<!-- Context menu used when user right clicks on a folder with a selection that doesn't have files. + Selection may be empty. --> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <group + android:id="@+id/menu_open_group"> + <item + android:id="@+id/dir_menu_open_in_new_window" + android:title="@string/menu_open_in_new_window" /> + </group> + + <group + android:id="@+id/menu_clipboard_group"> + <item + android:id="@+id/dir_menu_cut_to_clipboard" + android:title="@string/menu_cut_to_clipboard" /> + <item + android:id="@+id/dir_menu_copy_to_clipboard" + android:title="@string/menu_copy_to_clipboard" /> + <item + android:id="@+id/dir_menu_compress" + android:title="@string/menu_compress" /> + <item + android:id="@+id/dir_menu_paste_into_folder" + android:title="@string/menu_paste_into_folder" /> + </group> + + <group + android:id="@+id/menu_modifier_group"> + <item + android:id="@+id/dir_menu_rename" + android:title="@string/menu_rename" /> + <item + android:id="@+id/dir_menu_delete" + android:title="@string/menu_delete" /> + </group> + + <group + android:id="@+id/menu_extras_group"> + <item + android:id="@+id/dir_menu_inspect" + android:title="@string/menu_inspect" /> + </group> +</menu> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/menu/file_context_menu.xml b/res/flag(com.android.documentsui.flags.use_material3)/menu/file_context_menu.xml new file mode 100644 index 000000000..70d6fd1bf --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/menu/file_context_menu.xml @@ -0,0 +1,74 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> + +<!-- Context menu used when user right clicks on a file with a selection that doesn't have folders. + The selection may be empty. --> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/dir_menu_extract_here" + android:title="@string/menu_extract_here" + android:visible="false" /> + <item + android:id="@+id/dir_menu_browse" + android:title="@string/menu_browse" + android:visible="false" /> + + <group + android:id="@+id/menu_open_group"> + <item + android:id="@+id/dir_menu_share" + android:title="@string/menu_share" /> + <item + android:id="@+id/dir_menu_open" + android:title="@string/menu_open" /> + <item + android:id="@+id/dir_menu_open_with" + android:title="@string/menu_open_with" /> + </group> + + <group + android:id="@+id/menu_clipboard_group"> + <item + android:id="@+id/dir_menu_cut_to_clipboard" + android:title="@string/menu_cut_to_clipboard" /> + <item + android:id="@+id/dir_menu_copy_to_clipboard" + android:title="@string/menu_copy_to_clipboard" /> + <item + android:id="@+id/dir_menu_compress" + android:title="@string/menu_compress" /> + </group> + + <group + android:id="@+id/menu_modifier_group"> + <item + android:id="@+id/dir_menu_rename" + android:title="@string/menu_rename" /> + <item + android:id="@+id/dir_menu_delete" + android:title="@string/menu_delete" /> + </group> + + <group + android:id="@+id/menu_extras_group"> + <item + android:id="@+id/dir_menu_inspect" + android:title="@string/menu_inspect" /> + <item + android:id="@+id/dir_menu_view_in_owner" + android:title="@string/menu_view_in_owner" /> + </group> +</menu> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/menu/mixed_context_menu.xml b/res/flag(com.android.documentsui.flags.use_material3)/menu/mixed_context_menu.xml new file mode 100644 index 000000000..076aeda0d --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/menu/mixed_context_menu.xml @@ -0,0 +1,47 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2024 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. + --> + +<!-- Context menu used when user right clicks with a selection mixed with both folders and docs. --> +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + + <group + android:id="@+id/menu_clipboard_group"> + <item + android:id="@+id/dir_menu_cut_to_clipboard" + android:title="@string/menu_cut_to_clipboard" /> + <item + android:id="@+id/dir_menu_copy_to_clipboard" + android:title="@string/menu_copy_to_clipboard" /> + <item + android:id="@+id/dir_menu_compress" + android:title="@string/menu_compress" /> + </group> + + <group + android:id="@+id/menu_modifier_group"> + <item + android:id="@+id/dir_menu_delete" + android:title="@string/menu_delete" /> + </group> + + <group + android:id="@+id/menu_extras_group"> + <item + android:id="@+id/dir_menu_inspect" + android:title="@string/menu_inspect" /> + </group> +</menu>
\ No newline at end of file diff --git a/res/flag(com.android.documentsui.flags.use_material3)/menu/root_context_menu.xml b/res/flag(com.android.documentsui.flags.use_material3)/menu/root_context_menu.xml new file mode 100644 index 000000000..20b8a6914 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/menu/root_context_menu.xml @@ -0,0 +1,30 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> + +<menu xmlns:android="http://schemas.android.com/apk/res/android"> + <item + android:id="@+id/root_menu_eject_root" + android:title="@string/menu_eject_root" /> + <item + android:id="@+id/root_menu_open_in_new_window" + android:title="@string/menu_open_in_new_window" /> + <item + android:id="@+id/root_menu_paste_into_folder" + android:title="@string/menu_paste_into_folder" /> + <item + android:id="@+id/root_menu_settings" + android:title="@string/menu_settings" /> +</menu> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values-night-v31/colors.xml b/res/flag(com.android.documentsui.flags.use_material3)/values-night-v31/colors.xml index ab154150b..2105936ab 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/values-night-v31/colors.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/values-night-v31/colors.xml @@ -14,7 +14,6 @@ --> <resources> - <color name="tab_selected_text_color">@android:color/black</color> <color name="work_profile_button_stroke_color"> @*android:color/system_accent1_200 </color> <!-- accent 200 --> @@ -24,15 +23,6 @@ <color name="empty_state_message_text_color"> @*android:color/system_neutral2_200 </color> - <!-- neutral variant 200 --> - <color name="tab_unselected_text_color">@*android:color/system_neutral2_200 - </color> - <!-- neutral variant 200 --> - <color name="profile_tab_default_color">@*android:color/system_neutral1_800 - </color> - <!-- neutral 800 --> - <color name="profile_tab_selected_color">@*android:color/system_neutral2_100 - </color> <!-- neutral variant 100 --> <color name="fragment_pick_inactive_button_color"> @*android:color/system_neutral1_800 diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values-night/colors.xml b/res/flag(com.android.documentsui.flags.use_material3)/values-night/colors.xml index f9c58172a..ef4f5a902 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/values-night/colors.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/values-night/colors.xml @@ -18,7 +18,6 @@ <color name="background_floating">#3C4043</color> <color name="nav_bar_translucent">#52000000</color> - <color name="primary">#8AB4F8</color> <color name="secondary">#3D8AB4F8</color> <color name="hairline">#5F6368</color> @@ -27,15 +26,12 @@ <color name="edge_effect">@android:color/white</color> - <!-- AppCompat.textColorSecondary --> - <color name="doc_list_item_subtitle_enabled">#b3ffffff</color> - <color name="doc_list_item_subtitle_disabled">#36ffffff</color> - <color name="list_divider_color">#9aa0a6</color> - <color name="list_item_selected_background_color">?android:colorSecondary</color> + + <color name="color_surface_header">@color/m3_ref_palette_dynamic_neutral_variant17</color> <color name="fragment_pick_active_text_color">#202124</color> <!-- Grey 900 --> - <!-- TODO(b/379776735): remove this after M3 uplift --> + <!-- TODO(b/379776735): remove this after use_material3 flag is launched. --> <color name="search_chip_text_selected_color">@android:color/black</color> </resources> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values-night/themes.xml b/res/flag(com.android.documentsui.flags.use_material3)/values-night/themes.xml index 12b9577c6..e8e801793 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/values-night/themes.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/values-night/themes.xml @@ -22,7 +22,6 @@ <!-- Toolbar --> <item name="android:actionModeBackground">?android:attr/colorBackground</item> - <item name="android:actionBarSize">@dimen/action_bar_size</item> <!-- Color section --> <item name="android:colorAccent">@color/primary</item> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values-v31/colors.xml b/res/flag(com.android.documentsui.flags.use_material3)/values-v31/colors.xml index 44feff32a..3096f8a3e 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/values-v31/colors.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/values-v31/colors.xml @@ -14,11 +14,6 @@ --> <resources> - <color name="tab_selected_text_color">@*android:color/system_neutral1_900 - </color> - <!-- neutral 900 --> - <color name="tab_unselected_text_color">@*android:color/system_neutral2_700 - </color> <!-- neutral variant 700--> <color name="work_profile_button_stroke_color"> @*android:color/system_accent1_600 @@ -29,12 +24,6 @@ <color name="empty_state_message_text_color"> @*android:color/system_neutral2_700 </color> - <!-- neutral variant 700 --> - <color name="profile_tab_selected_color">@*android:color/system_accent1_100 - </color> - <!-- accent 100 --> - <color name="profile_tab_default_color">@*android:color/system_neutral1_10 - </color> <!-- neutral 10 --> <color name="fragment_pick_inactive_button_color"> @*android:color/system_neutral1_100 diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values-v31/dimens.xml b/res/flag(com.android.documentsui.flags.use_material3)/values-v31/dimens.xml index 006b99173..07630714f 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/values-v31/dimens.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/values-v31/dimens.xml @@ -18,11 +18,10 @@ <dimen name="action_bar_margin">0dp</dimen> <dimen name="button_corner_radius">20dp</dimen> <dimen name="tab_selector_indicator_height">0dp</dimen> - <dimen name="tab_height">48dp</dimen> + <dimen name="tab_height">36dp</dimen> <dimen name="tab_container_height">48dp</dimen> - <dimen name="profile_tab_padding">20dp</dimen> - <dimen name="profile_tab_margin_top">16dp</dimen> - <dimen name="profile_tab_margin_side">4dp</dimen> + <dimen name="profile_tab_margin_top">0dp</dimen> + <dimen name="profile_tab_margin_side">@dimen/space_extra_small_4</dimen> <dimen name="cross_profile_button_corner_radius">30dp</dimen> <dimen name="cross_profile_button_stroke_width">1dp</dimen> <dimen name="cross_profile_button_message_margin_top">16dp</dimen> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values-v31/styles.xml b/res/flag(com.android.documentsui.flags.use_material3)/values-v31/styles.xml index 5be3cd9dc..4f459253a 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/values-v31/styles.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/values-v31/styles.xml @@ -42,11 +42,4 @@ <item name="android:textAppearance">@style/EmptyStateButtonTextAppearance </item> </style> - - <style name="DialogTextButton" parent="@style/Widget.Material3.Button.TextButton.Dialog"> - <item name="android:textAppearance">@style/MaterialButtonTextAppearance - </item> - <item name="android:textColor">?android:attr/colorAccent</item> - <item name="android:backgroundTint">@android:color/transparent</item> - </style> </resources> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values-w600dp/dimens.xml b/res/flag(com.android.documentsui.flags.use_material3)/values-w600dp/dimens.xml index 9884577cf..4707991f6 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/values-w600dp/dimens.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/values-w600dp/dimens.xml @@ -13,8 +13,18 @@ See the License for the specific language governing permissions and limitations under the License. --> - +<!-- Dimensions/sizes for size Medium (>=600dp && <900dp). --> <resources> - <dimen name="search_chip_group_margin_horizontal">@dimen/space_medium_1</dimen> - <dimen name="search_chip_group_margin_vertical">@dimen/space_small_1</dimen> + <dimen name="main_container_padding_start">@dimen/space_medium_1</dimen> + <dimen name="main_container_padding_end">@dimen/space_medium_1</dimen> + <dimen name="main_container_padding_top">@dimen/space_extra_small_4</dimen> + <!-- Main margin is set by main_container_padding_start for the menu button, here is for + the space between the button the text/title, but since on this layout we don't have the button, + we zero here, to avoid pushing the title further. --> + <dimen name="search_bar_text_margin_start">0dp</dimen> + + <dimen name="toolbar_padding_start">@dimen/main_container_padding_start</dimen> + + <dimen name="list_container_padding">@dimen/space_extra_small_6</dimen> </resources> + diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values-w600dp/layouts.xml b/res/flag(com.android.documentsui.flags.use_material3)/values-w600dp/layouts.xml new file mode 100644 index 000000000..4b0634d54 --- /dev/null +++ b/res/flag(com.android.documentsui.flags.use_material3)/values-w600dp/layouts.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources> + <item name="documents_activity" type="layout">@layout/nav_rail_layout</item> + <item name="files_activity" type="layout">@layout/nav_rail_layout</item> +</resources> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values-w720dp/colors.xml b/res/flag(com.android.documentsui.flags.use_material3)/values-w900dp/colors.xml index ec2e1ff1c..ec2e1ff1c 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/values-w720dp/colors.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/values-w900dp/colors.xml diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values-w720dp/dimens.xml b/res/flag(com.android.documentsui.flags.use_material3)/values-w900dp/config.xml index a3232c9c2..21ce0acff 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/values-w720dp/dimens.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/values-w900dp/config.xml @@ -15,17 +15,8 @@ --> <resources> - <dimen name="grid_padding_horiz">16dp</dimen> - <dimen name="grid_padding_vert">16dp</dimen> + <!-- Indicates if search view is taking the whole toolbar space --> + <bool name="full_bar_search_view">false</bool> - <dimen name="list_item_padding">24dp</dimen> - <dimen name="list_item_width">80dp</dimen> - - <dimen name="max_drawer_width">320dp</dimen> - - <dimen name="search_bar_background_margin_start">120dp</dimen> - <dimen name="search_bar_background_margin_end">120dp</dimen> - <dimen name="search_bar_text_margin_start">55dp</dimen> - <dimen name="search_bar_text_margin_end">24dp</dimen> - <dimen name="search_bar_icon_padding">16dp</dimen> + <string name="scrolling_behavior" translatable="false">@string/appbar_scrolling_view_behavior</string> </resources> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values-w900dp/dimens.xml b/res/flag(com.android.documentsui.flags.use_material3)/values-w900dp/dimens.xml index 30c551c21..d37f3af68 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/values-w900dp/dimens.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/values-w900dp/dimens.xml @@ -13,8 +13,27 @@ See the License for the specific language governing permissions and limitations under the License. --> - +<!-- Dimensions/sizes for size Expanded (>=900dp). --> <resources> - <dimen name="search_chip_group_margin_horizontal">@dimen/space_medium_5</dimen> - <dimen name="search_chip_group_margin_vertical">@dimen/space_small_1</dimen> + <dimen name="grid_padding_horiz">16dp</dimen> + <dimen name="grid_padding_vert">16dp</dimen> + + <dimen name="list_item_height">48dp</dimen> + <dimen name="list_item_padding_start">20dp</dimen> + <dimen name="list_item_padding_end">0dp</dimen> + <dimen name="list_item_icon_margin_end">@dimen/space_extra_small_4</dimen> + + <dimen name="max_drawer_width">320dp</dimen> + + <dimen name="search_bar_background_margin_start">120dp</dimen> + <dimen name="search_bar_background_margin_end">120dp</dimen> + <dimen name="search_bar_text_margin_end">24dp</dimen> + <dimen name="search_bar_icon_padding">16dp</dimen> + + <dimen name="main_container_padding_top">@dimen/space_extra_small_6</dimen> + + <dimen name="toolbar_padding_start">@dimen/main_container_padding_start</dimen> + <dimen name="toolbar_padding_end">@dimen/space_small_3</dimen> + + <dimen name="drawer_padding_top">@dimen/space_small_1</dimen> </resources> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values-w720dp/layouts.xml b/res/flag(com.android.documentsui.flags.use_material3)/values-w900dp/layouts.xml index 9e4109a45..9e4109a45 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/values-w720dp/layouts.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/values-w900dp/layouts.xml diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values/colors.xml b/res/flag(com.android.documentsui.flags.use_material3)/values/colors.xml index 76bd5dc18..77a41868f 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/values/colors.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/values/colors.xml @@ -22,11 +22,11 @@ <color name="background_floating">@android:color/white</color> <color name="nav_bar_translucent">#99FFFFFF</color> - <color name="primary">#1E88E5</color> <!-- Blue 600 --> + <color name="primary">?attr/colorPrimary</color> <color name="secondary">#E3F2FD</color> <!-- Blue 50 --> <color name="hairline">#E0E0E0</color> <!-- Gray 300 --> - <!-- TODO(b/379776735): remove this after M3 uplift --> + <!-- TODO(b/379776735): remove this after use_material3 flag is launched. --> <color name="chip_background_disable_color">#fff1f3f4</color> <color name="menu_search_background">@android:color/transparent</color> <color name="item_breadcrumb_background_hovered">#1affffff</color> @@ -36,8 +36,8 @@ </color> <color name="tool_bar_gradient_max">#7f000000</color> - <color name="band_select_background">#88ffffff</color> - <color name="band_select_border">#44000000</color> + <color name="band_select_background">?attr/colorPrimaryInverse</color> + <color name="band_select_border">?attr/colorPrimaryContainer</color> <color name="downloads_icon_background">#ff4688f2</color> <color name="app_icon_background">#ff4688f2</color> @@ -49,26 +49,31 @@ <color name="edge_effect">@android:color/black</color> - <color name="doc_list_item_subtitle_enabled">#5F6368</color> <!-- Gray 700 --> - <color name="doc_list_item_subtitle_disabled">#613c4043 - </color> <!-- 38% Grey800 --> - <color name="list_divider_color">#1f000000</color> - <color name="list_item_selected_background_color">?android:colorSecondary - </color> - <color name="color_surface_header">@color/app_background_color</color> + <color name="list_item_selected_background_color">?attr/colorPrimaryContainer</color> + <!-- This is used when the app bar is in pinned mode inside the CollapsingToolbarLayout. + The code in NavigationViewManager assume the value should be a plain color value so we can't + use the theme attribute "?attr/colorSurfaceContainerHigh" (which is a reference) here, hence + using the mapped system color in both here and dark mode. + --> + <color name="color_surface_header">@color/m3_ref_palette_dynamic_neutral_variant92</color> - <color name="tab_selected_text_color">@color/primary</color> <color name="work_profile_button_stroke_color">@color/primary</color> - <color name="profile_tab_selected_color">?android:attr/colorAccent</color> - <color name="profile_tab_default_color">#E0E0E0</color> - <color name="tab_unselected_text_color">#5F6368</color> <!-- Gray 700 --> <color name="fragment_pick_inactive_button_color">#E0E0E0</color> <color name="fragment_pick_inactive_text_color">#5F6368</color> <color name="fragment_pick_active_button_color">@color/primary</color> <color name="fragment_pick_active_text_color">@android:color/white</color> - <!-- TODO(b/379776735): remove this after M3 uplift --> + <!-- TODO(b/379776735): remove this after use_material3 flag is launched. --> <color name="search_chip_text_selected_color">@android:color/white</color> + + <!-- Use this when we need to set alpha channel on top of a theme attribute color in the + color selector list, e.g. to set colorOnSecondaryContainer with a hover overlay alpha, use: + + <shape android:tint="?attr/colorOnSecondaryContainer"> + <solid android:color="@color/overlay_hover_color_percentage"/> + </shape> + --> + <color name="overlay_hover_color_percentage">#14000000</color> <!-- 8% --> </resources> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values/dimens.xml b/res/flag(com.android.documentsui.flags.use_material3)/values/dimens.xml index 681de06f4..65740f15c 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/values/dimens.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/values/dimens.xml @@ -14,6 +14,7 @@ limitations under the License. --> +<!-- Dimensions/sizes for size Compact (<=600dp). --> <resources> <!-- Material design rounded radius --> <dimen name="material_round_radius">2dp</dimen> @@ -21,18 +22,26 @@ <dimen name="tab_selector_indicator_height">2dp</dimen> <dimen name="profile_tab_padding">0dp</dimen> <dimen name="grid_container_padding">20dp</dimen> - <dimen name="list_container_padding">20dp</dimen> + <dimen name="list_container_padding">@dimen/space_extra_small_4</dimen> + <!-- For compact screen, file area occupies the whole screen height, when use_material3 flag is + ON we show breadcrumb at the bottom, so we need to add padding (breadcrumb height) at the + bottom to make sure breadcrumb won't over-shadow the file area. --> + <dimen name="file_area_padding_bottom">48dp</dimen> <dimen name="icon_size">40dp</dimen> <dimen name="button_touch_size">48dp</dimen> <dimen name="root_icon_size">24dp</dimen> + <!-- TODO(b/379776735): remove this block after use_material3 flag is launched. --> <dimen name="root_icon_margin">0dp</dimen> <dimen name="root_spacer_padding">0dp</dimen> - <dimen name="root_action_icon_size">18dp</dimen> + <!-- block end --> + <dimen name="root_action_icon_size">24dp</dimen> + <!-- TODO(b/379776735): remove this after use_material3 flag is launched. --> <dimen name="root_icon_disabled_alpha">?android:attr/disabledAlpha</dimen> - <dimen name="check_icon_size">30dp</dimen> + <dimen name="check_icon_size">20dp</dimen> <dimen name="zoom_icon_size">24dp</dimen> <dimen name="list_item_thumbnail_size">40dp</dimen> <dimen name="grid_item_icon_size">30dp</dimen> + <!-- TODO(b/379776735): remove this after use_material3 flag is launched. --> <dimen name="progress_bar_height">4dp</dimen> <fraction name="grid_scale_min">85%</fraction> <fraction name="grid_scale_max">200%</fraction> @@ -41,31 +50,71 @@ <dimen name="grid_item_margin">6dp</dimen> <dimen name="grid_padding_horiz">4dp</dimen> <dimen name="grid_padding_vert">4dp</dimen> + <dimen name="list_item_height">56dp</dimen> + <dimen name="list_item_padding_start">16dp</dimen> + <dimen name="list_item_padding_end">8dp</dimen> + <dimen name="list_item_padding_vertical">4dp</dimen> + <dimen name="list_item_icon_margin_end">16dp</dimen> + <dimen name="list_item_icon_size">32dp</dimen> + <!-- TODO(b/379776735): remove this block after use_material3 flag is launched. --> <dimen name="list_item_width">72dp</dimen> - <dimen name="list_item_height">72dp</dimen> <dimen name="list_item_padding">16dp</dimen> <dimen name="list_item_icon_padding">16dp</dimen> + <dimen name="list_divider_inset">72dp</dimen> + <!-- block end --> <dimen name="breadcrumb_item_padding">8dp</dimen> <dimen name="breadcrumb_item_height">36dp</dimen> - <dimen name="list_divider_inset">72dp</dimen> <dimen name="dir_elevation">8dp</dimen> <dimen name="drag_shadow_size">120dp</dimen> + <dimen name="grid_item_width">150dp</dimen> + <dimen name="grid_item_height">132dp</dimen> + <dimen name="grid_item_layout_marginStart">@dimen/space_extra_small_2</dimen> + <dimen name="grid_item_layout_marginEnd">@dimen/space_extra_small_2</dimen> + <dimen name="grid_item_layout_marginTop">@dimen/space_extra_small_2</dimen> + <dimen name="grid_item_thumbnail_width">80dp</dimen> + <dimen name="grid_item_thumbnail_height">80dp</dimen> + <dimen name="grid_item_thumbnail_radius">12dp</dimen> + <dimen name="grid_item_icon_width">64dp</dimen> + <dimen name="grid_item_icon_height">64dp</dimen> + <dimen name="grid_item_layout_margin">@dimen/space_small_1</dimen> + <dimen name="grid_item_nameplate_width">142dp</dimen> + <dimen name="grid_item_nameplate_height">44dp</dimen> + <dimen name="grid_item_nameplate_padding">4dp</dimen> + <dimen name="grid_item_nameplate_marginTop">@dimen/space_extra_small_2</dimen> + <dimen name="grid_item_nameplate_radius">8dp</dimen> <dimen name="grid_item_elevation">2dp</dimen> - <dimen name="grid_item_radius">2dp</dimen> + <dimen name="grid_item_radius">12dp</dimen> <dimen name="max_drawer_width">280dp</dimen> <dimen name="briefcase_icon_margin">8dp</dimen> <dimen name="briefcase_icon_size">14dp</dimen> <dimen name="briefcase_icon_size_photo">24dp</dimen> <dimen name="button_corner_radius">2dp</dimen> + <dimen name="bottom_sheet_dialog_radius">28dp</dimen> + <dimen name="drawer_edge_width">12dp</dimen> + <dimen name="drawer_padding_horizontal">@dimen/space_extra_small_6</dimen> + <dimen name="drawer_padding_top">@dimen/space_small_3</dimen> + <dimen name="drawer_padding_bottom">@dimen/space_small_1</dimen> + <dimen name="drawer_divider_padding_horizontal">16dp</dimen> + <dimen name="drawer_divider_padding_vertical">@dimen/space_extra_small_4</dimen> + <dimen name="drawer_item_height">56dp</dimen> + <dimen name="drawer_item_vertical_margin">10dp</dimen> + <dimen name="drawer_item_text_margin_start">12dp</dimen> + <dimen name="drawer_item_action_icon_margin_start">4dp</dimen> + + <dimen name="nav_rail_item_height">64dp</dimen> + <dimen name="nav_rail_item_icon_bg_radius">16dp</dimen> + <dimen name="nav_rail_item_icon_bg_width">56dp</dimen> + <dimen name="nav_rail_item_icon_bg_height">32dp</dimen> + <dimen name="nav_rail_burger_icon_size">56dp</dimen> <dimen name="drag_shadow_width">176dp</dimen> <dimen name="drag_shadow_height">64dp</dimen> <dimen name="drag_shadow_radius">4dp</dimen> <dimen name="drag_shadow_padding">8dp</dimen> - <dimen name="doc_header_sort_icon_size">16dp</dimen> + <dimen name="doc_header_sort_icon_size">32dp</dimen> <dimen name="doc_header_height">60dp</dimen> <dimen name="dropdown_sort_widget_margin">12dp</dimen> @@ -92,15 +141,24 @@ <dimen name="root_info_header_height">60dp</dimen> <dimen name="root_info_header_horizontal_padding">24dp</dimen> - <!-- TODO(b/379776735): remove this block after M3 uplift --> + <!-- TODO(b/379776735): remove this block after use_material3 flag is launched. --> <dimen name="search_chip_group_margin">20dp</dimen> <dimen name="search_chip_spacing">8dp</dimen> <dimen name="search_chip_half_spacing">4dp</dimen> <dimen name="search_chip_icon_padding">4dp</dimen> <!-- block end --> + <dimen name="search_chip_radius">8dp</dimen> - <dimen name="search_chip_group_margin_horizontal">@dimen/space_small_4</dimen> - <dimen name="search_chip_group_margin_vertical">@dimen/space_small_1</dimen> + <dimen name="search_chip_group_padding_vertical">@dimen/space_extra_small_4</dimen> + <dimen name="main_container_padding_start">@dimen/space_small_4</dimen> + <dimen name="main_container_padding_end">@dimen/space_small_4</dimen> + <dimen name="main_container_padding_top">0dp</dimen> + <dimen name="main_container_section_gap">2dp</dimen> + <dimen name="main_container_corner_radius_large">16dp</dimen> + <dimen name="main_container_corner_radius_small">4dp</dimen> + <dimen name="layout_padding_top">@dimen/space_small_1</dimen> + <dimen name="layout_padding_bottom">@dimen/space_small_1</dimen> + <dimen name="layout_padding_end">@dimen/space_small_1</dimen> <dimen name="dialog_content_padding_top">18dp</dimen> <dimen name="dialog_content_padding_bottom">24dp</dimen> @@ -118,16 +176,25 @@ <dimen name="apps_row_exit_icon_margin_bottom">6dp</dimen> <dimen name="apps_row_item_text_margin_horizontal">8dp</dimen> - <dimen name="search_bar_elevation">3dp</dimen> + <dimen name="profile_tab_radius">12dp</dimen> + + <dimen name="search_bar_elevation">0dp</dimen> <dimen name="search_bar_radius">8dp</dimen> <dimen name="search_bar_background_margin_start">0dp</dimen> <dimen name="search_bar_background_margin_end">0dp</dimen> - <dimen name="search_bar_margin">@dimen/space_extra_small_6</dimen> + <dimen name="search_bar_margin">0dp</dimen> <dimen name="search_bar_text_size">16dp</dimen> - <dimen name="action_bar_elevation">3dp</dimen> - <dimen name="action_bar_margin">1dp</dimen> - <dimen name="action_bar_size">48dp</dimen> + <!-- Main margin is set by main_container_padding_start for the menu button, here is for + the space between the button the text/title. --> + <dimen name="search_bar_text_margin_start">@dimen/space_extra_small_6</dimen> + <!-- The main margin is controlled above on paddingStart, zeroing toolbar_content_insets to + avoid pushing the title or button further. --> + <dimen name="toolbar_content_inset_start">0dp</dimen> + <dimen name="toolbar_padding_start">@dimen/space_extra_small_6</dimen> + <dimen name="toolbar_padding_end">@dimen/space_extra_small_6</dimen> + <dimen name="action_bar_elevation">0dp</dimen> + <dimen name="action_bar_margin">0dp</dimen> <dimen name="action_mode_text_size">18sp</dimen> <dimen name="refresh_icon_range">64dp</dimen> @@ -138,4 +205,8 @@ <dimen name="cross_profile_button_message_margin_top">4dp</dimen> <dimen name="focus_ring_width">3dp</dimen> + <!-- 3dp focus ring width + 2dp gap between the ring and the content --> + <dimen name="focus_ring_gap">5dp</dimen> + <dimen name="hover_overlay_alpha">0.08</dimen> + <dimen name="ripple_overlay_alpha">0.10</dimen> </resources> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values/styles.xml b/res/flag(com.android.documentsui.flags.use_material3)/values/styles.xml index 8aba2dfa3..693679cf9 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/values/styles.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/values/styles.xml @@ -29,9 +29,21 @@ <!-- This gets overridden for specific platform versions and/or configs --> <style name="ActionBarTheme" parent="@style/ActionBarThemeCommon"/> + <style name="HamburgerMenuButtonStyle" parent="@style/Widget.Material3.Search.Toolbar.Button.Navigation"> + <item name="android:maxWidth">@dimen/icon_size_headline_large</item> + <item name="android:maxHeight">@dimen/icon_size_headline_large</item> + </style> + + <style name="ToolbarStyles" parent="@style/Widget.Material3.Toolbar"> + <item name="android:paddingStart">@dimen/toolbar_padding_start</item> + <item name="android:paddingEnd">@dimen/toolbar_padding_end</item> + <item name="contentInsetStart">@dimen/toolbar_content_inset_start</item> + <item name="contentInsetStartWithNavigation">@dimen/toolbar_content_inset_start</item> + <item name="titleMarginStart">@dimen/search_bar_text_margin_start</item> + <item name="titleTextAppearance">@style/ToolbarTitle</item> + </style> + <style name="ActionModeStyle" parent="Widget.AppCompat.ActionMode"> - <!-- attr "height" was used by support lib should not in overlay scope --> - <item name="height">@dimen/action_bar_size</item> <item name="titleTextStyle">@style/ActionModeTitle</item> <item name="android:layout_margin">@dimen/search_bar_margin</item> </style> @@ -43,12 +55,6 @@ <item name="cardElevation">@dimen/grid_item_elevation</item> </style> - <style name="TrimmedHorizontalProgressBar" parent="android:Widget.Material.ProgressBar.Horizontal"> - <item name="android:indeterminateDrawable">@drawable/progress_indeterminate_horizontal_material_trimmed</item> - <item name="android:minHeight">3dp</item> - <item name="android:maxHeight">3dp</item> - </style> - <style name="SnackbarButtonStyle" parent="@style/Widget.AppCompat.Button.Borderless"> <item name="android:textColor">?android:colorPrimary</item> </style> @@ -68,20 +74,15 @@ <item name="android:background">@drawable/bottom_sheet_dialog_background</item> </style> - <style name="OverflowButtonStyle" parent="@style/Widget.AppCompat.ActionButton.Overflow"> - <item name="android:tint">?android:colorControlNormal</item> + <style name="OverflowButtonStyle" parent="@style/Widget.Material3.Search.ActionButton.Overflow"> <item name="android:minWidth">@dimen/button_touch_size</item> </style> - <style name="OverflowMenuStyle" parent="@style/Widget.AppCompat.PopupMenu.Overflow"> - <item name="android:popupBackground">@drawable/menu_dropdown_panel</item> - <item name="android:dropDownWidth">wrap_content</item> + <style name="OverflowMenuStyle" parent="@style/Widget.Material3.PopupMenu.Overflow"> <item name="android:overlapAnchor">false</item> </style> <style name="MaterialAlertDialogTitleStyle" parent="@style/MaterialAlertDialog.Material3.Title.Text.CenterStacked"> - <item name="android:textColor">?attr/colorOnSurface</item> - <item name="android:textSize">20sp</item> <item name="fontFamily">@string/config_fontFamilyMedium</item> </style> @@ -95,7 +96,6 @@ <style name="DialogTextButton" parent="@style/Widget.Material3.Button.TextButton.Dialog"> <item name="android:textAppearance">@style/MaterialButtonTextAppearance</item> - <item name="android:textColor">?android:attr/colorAccent</item> </style> <style name="EmptyStateButton" parent="@style/Widget.Material3.Button.TextButton"> @@ -107,14 +107,7 @@ <item name="buttonBarNegativeButtonStyle">@style/DialogTextButton</item> </style> - <style name="MaterialAlertDialogStyle" parent="@style/MaterialAlertDialog.Material3"> - <item name="backgroundInsetTop">12dp</item> - <item name="backgroundInsetBottom">12dp</item> - </style> - <style name="MaterialAlertDialogTheme" parent="@style/ThemeOverlay.Material3.MaterialAlertDialog.Centered"> - <item name="android:dialogCornerRadius">@dimen/grid_item_radius</item> - <item name="alertDialogStyle">@style/MaterialAlertDialogStyle</item> <item name="buttonBarPositiveButtonStyle">@style/DialogTextButton</item> <item name="buttonBarNegativeButtonStyle">@style/DialogTextButton</item> <item name="materialAlertDialogTitleTextStyle">@style/MaterialAlertDialogTitleStyle</item> @@ -126,4 +119,66 @@ <item name="chipStrokeColor">@color/search_chip_stroke_color</item> <item name="chipCornerRadius">@dimen/search_chip_radius</item> </style> + + <style name="DrawerStyle" parent=""> + <item name="android:background">?attr/colorSurfaceContainer</item> + <!-- Use padding together with the "outsideOverlay" scrollbar style to to make sure the + scrollbar appears on the edge of the container, and scrollbar trigger area doesn't + affect the hover effect of the views (e.g. action icon) on the edge. + --> + <item name="android:paddingHorizontal">@dimen/drawer_padding_horizontal</item> + <item name="android:paddingTop">@dimen/drawer_padding_top</item> + <item name="android:paddingBottom">@dimen/drawer_padding_bottom</item> + <item name="android:dividerHeight">@dimen/drawer_item_vertical_margin</item> + <item name="android:scrollbarStyle">outsideOverlay</item> + <item name="android:clipToPadding">false</item> + </style> + + <style name="DrawerItemStyle" parent=""> + <item name="android:paddingStart">16dp</item> + <item name="android:paddingEnd">4dp</item> + <item name="android:background">@drawable/root_item_background</item> + </style> + + <style name="DrawerItemActionIconStyle" parent="@style/Widget.Material3.Button.IconButton"> + <item name="android:layout_marginStart">@dimen/drawer_item_action_icon_margin_start</item> + <item name="strokeColor">?attr/colorSecondary</item> + </style> + + <style name="ProfileTabStyle" parent="@style/Widget.Material3.TabLayout"> + <!-- Use transparent bg color to hide the underline for tab layout. --> + <item name="android:background">@android:color/transparent</item> + <item name="tabIndicatorColor">?attr/colorPrimary</item> + <item name="tabIndicatorHeight">@dimen/tab_selector_indicator_height</item> + <item name="tabTextColor">?attr/colorOnSurfaceVariant</item> + <item name="tabSelectedTextColor">?attr/colorOnPrimaryContainer</item> + <item name="tabTextAppearance">@style/TabTextAppearance</item> + </style> + + <style name="FileItemLabelStyle" parent=""> + <item name="android:ellipsize">end</item> + <item name="android:minWidth">70dp</item> + <item name="android:singleLine">true</item> + <item name="android:textAppearance">@style/FileItemLabelText</item> + </style> + + <style name="NavRailStyle" parent=""> + <item name="android:background">?attr/colorSurfaceContainer</item> + <item name="android:paddingHorizontal">@dimen/space_small_3</item> + <item name="android:paddingTop">@dimen/space_extra_small_6</item> + <item name="android:paddingBottom">@dimen/space_small_1</item> + <item name="android:scrollbarStyle">outsideOverlay</item> + <item name="android:clipToPadding">false</item> + </style> + + <style name="NavRailItemStyle" parent=""> + <item name="android:background">@drawable/nav_rail_item_background</item> + <item name="android:paddingVertical">6dp</item> + </style> + + <style name="NavRailItemTextStyle" parent=""> + <item name="android:layout_marginTop">4dp</item> + <item name="android:textColor">@color/nav_rail_item_text_color</item> + <item name="android:textAppearance">@style/NavRailItemTextAppearance</item> + </style> </resources> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values/styles_text.xml b/res/flag(com.android.documentsui.flags.use_material3)/values/styles_text.xml index c8cf16cbf..82b498569 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/values/styles_text.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/values/styles_text.xml @@ -70,11 +70,6 @@ <item name="fontFamily">@string/config_fontFamilyMedium</item> </style> - <style name="DrawerMenuTitle" parent="@style/TextAppearance.Material3.TitleLarge"> - <item name="android:textSize">24sp</item> - <item name="fontFamily">@string/config_fontFamilyMedium</item> - </style> - <style name="DrawerMenuHeader" parent="@style/TextAppearance.Material3.BodyLarge"> <item name="android:textColor">?android:attr/textColorSecondary</item> <item name="android:textAllCaps">true</item> @@ -82,14 +77,12 @@ <item name="fontFamily">@string/config_fontFamilyMedium</item> </style> - <style name="DrawerMenuPrimary" parent="@style/TextAppearance.Material3.BodyMedium"> - <item name="android:textSize">14sp</item> + <style name="DrawerMenuPrimary" parent="@style/TextAppearance.Material3.LabelLarge"> <item name="android:textColor">@color/item_root_primary_text</item> <item name="fontFamily">@string/config_fontFamilyMedium</item> </style> - <style name="DrawerMenuSecondary" parent="@style/TextAppearance.Material3.BodyMedium"> - <item name="android:textSize">12sp</item> + <style name="DrawerMenuSecondary" parent="@style/TextAppearance.Material3.BodySmall"> <item name="android:textColor">@color/item_root_secondary_text</item> <item name="fontFamily">@string/config_fontFamily</item> </style> @@ -126,18 +119,22 @@ <item name="fontFamily">@string/config_fontFamilyMedium</item> </style> - <style name="TabTextAppearance" parent="@style/TextAppearance.Material3.TitleMedium"> - <item name="android:textSize">14sp</item> + <style name="TabTextAppearance" parent="@style/TextAppearance.Material3.TitleSmall"> <item name="fontFamily">@string/config_fontFamilyMedium</item> </style> - <style name="ItemCaptionText" parent="@style/TextAppearance.Material3.BodySmall"> + <style name="FileItemLabelText" parent="@style/TextAppearance.Material3.TitleSmall"> + <item name="android:textColor">@color/doc_list_item_label_color</item> + <item name="fontFamily">@string/config_fontFamily</item> + </style> + + <style name="ItemCaptionText" parent="@style/TextAppearance.Material3.LabelSmall"> <item name="android:textColor">@color/doc_list_item_subtitle_color</item> <item name="fontFamily">@string/config_fontFamily</item> </style> - <style name="MenuItemTextAppearance" parent="@style/TextAppearance.Material3.BodyMedium"> - <item name="android:textSize">14sp</item> + <style name="MenuItemTextAppearance" parent="@style/TextAppearance.Material3.LabelLarge"> + <item name="android:textColor">?attr/colorOnSurface</item> <item name="fontFamily">@string/config_fontFamily</item> </style> @@ -145,8 +142,12 @@ <item name="fontFamily">@string/config_fontFamily</item> </style> - <style name="Body1" parent="@style/TextAppearance.Material3.BodyMedium"> + <style name="NavRailItemTextAppearance" parent="@style/TextAppearance.Material3.LabelMedium"> + <item name="fontFamily">@string/config_fontFamily</item> + </style> + + <style name="ListTableHeaderText" parent="@style/TextAppearance.Material3.TitleSmall.Emphasized"> <item name="fontFamily">@string/config_fontFamily</item> </style> -</resources>
\ No newline at end of file +</resources> diff --git a/res/flag(com.android.documentsui.flags.use_material3)/values/themes.xml b/res/flag(com.android.documentsui.flags.use_material3)/values/themes.xml index 8f145fd38..314022eea 100644 --- a/res/flag(com.android.documentsui.flags.use_material3)/values/themes.xml +++ b/res/flag(com.android.documentsui.flags.use_material3)/values/themes.xml @@ -21,7 +21,6 @@ <!-- DocumentsTheme is allow customize by run time overlay --> <style name="DocumentsTheme" parent="@android:style/Theme.DeviceDefault.DocumentsUI"> - <item name="android:actionBarSize">@dimen/action_bar_size</item> <item name="android:actionModeBackground">?android:attr/colorBackground</item> <!-- Color section --> @@ -62,22 +61,23 @@ <item name="gridItemTint">@color/item_doc_grid_tint</item> <item name="actionBarTheme">@style/ActionBarTheme</item> + <item name="toolbarStyle">@style/ToolbarStyles</item> + <item name="toolbarNavigationButtonStyle">@style/HamburgerMenuButtonStyle</item> <item name="actionModeStyle">@style/ActionModeStyle</item> <item name="actionOverflowButtonStyle">@style/OverflowButtonStyle</item> - <item name="actionOverflowMenuStyle">@style/OverflowMenuStyle</item> <item name="alertDialogTheme">@style/AlertDialogTheme</item> <item name="autoCompleteTextViewStyle">@style/AutoCompleteTextViewStyle</item> <item name="bottomSheetDialogTheme">@style/BottomSheetDialogStyle</item> - <item name="materialButtonStyle">@style/MaterialButton</item> - <item name="materialButtonOutlinedStyle">@style/MaterialOutlinedButton</item> <item name="materialCardViewStyle">@style/CardViewStyle</item> <item name="materialAlertDialogTheme">@style/MaterialAlertDialogTheme</item> <item name="queryBackground">@color/menu_search_background</item> <item name="snackbarButtonStyle">@style/SnackbarButtonStyle</item> - <item name="android:itemTextAppearance">@style/MenuItemTextAppearance</item> - </style> - <style name="TabTheme" parent="@android:style/Theme.DeviceDefault.DayNight"> - <item name="colorPrimary">@color/edge_effect</item> + <!-- Menus --> + <item name="actionOverflowMenuStyle">@style/OverflowMenuStyle</item> + <item name="android:itemBackground">@drawable/menu_item_background</item> + + <!-- Menu text appearance --> + <item name="android:itemTextAppearance">@style/MenuItemTextAppearance</item> </style> </resources> diff --git a/res/values-af/strings.xml b/res/values-af/strings.xml index 74f2c90eb..e935ed1a3 100644 --- a/res/values-af/strings.xml +++ b/res/values-af/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Skuif na …"</string> <string name="menu_compress" msgid="37539111904724188">"Pers saam"</string> <string name="menu_extract" msgid="8171946945982532262">"Onttrek na …"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Uittreksel van alles …"</string> <string name="menu_rename" msgid="1883113442688817554">"Hernoem"</string> <string name="menu_inspect" msgid="7279855349299446224">"Kry inligting"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Wys versteekte lêers"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Jy kan nie lêers uit \'n ander app skuif nie."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Wys tans in roostermodus."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Wys tans in lysmodus."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-am/strings.xml b/res/values-am/strings.xml index 428783c41..23c0f4ab3 100644 --- a/res/values-am/strings.xml +++ b/res/values-am/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"ውሰድ ወደ…"</string> <string name="menu_compress" msgid="37539111904724188">"ጭመቅ"</string> <string name="menu_extract" msgid="8171946945982532262">"አውጣ ወደ…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"ሁሉንም አውጣ…"</string> <string name="menu_rename" msgid="1883113442688817554">"ዳግም ሰይም"</string> <string name="menu_inspect" msgid="7279855349299446224">"መረጃ አግኝ"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"የተደበቁ ፋይሎችን አሳይ"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"ከሌላ መተግበሪያ ፋይሎችን መውሰድ አይችሉም።"</string> <string name="grid_mode_showing" msgid="2803166871485028508">"በፍርግርግ ሁነታ ላይ በማሳየት ላይ።"</string> <string name="list_mode_showing" msgid="1225413902295895166">"በዝርዝር ሁነታ ላይ በማሳየት ላይ።"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-ar/strings.xml b/res/values-ar/strings.xml index 84917168f..32bb7f65b 100644 --- a/res/values-ar/strings.xml +++ b/res/values-ar/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"نقل إلى..."</string> <string name="menu_compress" msgid="37539111904724188">"ضغط"</string> <string name="menu_extract" msgid="8171946945982532262">"الاستخراج إلى…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"استخراج الكل…"</string> <string name="menu_rename" msgid="1883113442688817554">"إعادة تسمية"</string> <string name="menu_inspect" msgid="7279855349299446224">"الحصول على المعلومات"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"إظهار الملفات المخفية"</string> @@ -377,4 +378,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"لا يمكنك نقل الملفات من تطبيق آخر."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"يتم العرض في وضع الشبكة."</string> <string name="list_mode_showing" msgid="1225413902295895166">"يتم العرض في وضع القائمة."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-as/strings.xml b/res/values-as/strings.xml index 285cd0bfe..accf7d508 100644 --- a/res/values-as/strings.xml +++ b/res/values-as/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"ইয়ালৈ স্থানান্তৰ কৰক…"</string> <string name="menu_compress" msgid="37539111904724188">"সংকুচিত কৰক"</string> <string name="menu_extract" msgid="8171946945982532262">"ইয়ালৈ আহৰণ কৰক…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"আটাইবোৰ আহৰণ কৰক…"</string> <string name="menu_rename" msgid="1883113442688817554">"নতুন নাম দিয়ক"</string> <string name="menu_inspect" msgid="7279855349299446224">"তথ্য পাওক"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"লুকুৱাই থোৱা ফাইল দেখুৱাওক"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"আপুনি অন্য এটা এপৰ পৰা ফাইল স্থানান্তৰ কৰিব নোৱাৰে।"</string> <string name="grid_mode_showing" msgid="2803166871485028508">"গ্ৰিড ম’ডত দেখুৱাই থকা হৈছে।"</string> <string name="list_mode_showing" msgid="1225413902295895166">"সূচীযুক্ত ম’ডত দেখুৱাই থকা হৈছে।"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-az/strings.xml b/res/values-az/strings.xml index dfba630de..0ccc72860 100644 --- a/res/values-az/strings.xml +++ b/res/values-az/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Daşıyın..."</string> <string name="menu_compress" msgid="37539111904724188">"Sıxışdırın"</string> <string name="menu_extract" msgid="8171946945982532262">"Çıxarın…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Hamısını çıxarın…"</string> <string name="menu_rename" msgid="1883113442688817554">"Adını dəyişdirin"</string> <string name="menu_inspect" msgid="7279855349299446224">"Məlumat əldə edin"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Gizli faylları göstərin"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Başqa tətbiqdən faylları köçürə bilməzsiniz."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Tor rejimində göstərilir."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Siyahı rejimində göstərilir."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-b+sr+Latn/strings.xml b/res/values-b+sr+Latn/strings.xml index fca11a776..1984af705 100644 --- a/res/values-b+sr+Latn/strings.xml +++ b/res/values-b+sr+Latn/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Premesti u…"</string> <string name="menu_compress" msgid="37539111904724188">"Komprimuj"</string> <string name="menu_extract" msgid="8171946945982532262">"Izdvoj u…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Izdvoji sve…"</string> <string name="menu_rename" msgid="1883113442688817554">"Preimenuj"</string> <string name="menu_inspect" msgid="7279855349299446224">"Prikaži informacije"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Prikazuj skrivene datoteke"</string> @@ -311,4 +312,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Ne možete da premeštate datoteke iz druge aplikacije."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Prikazuje se u režimu mreže."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Prikazuje se u režimu liste."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-be/strings.xml b/res/values-be/strings.xml index 4c2a98cec..4f2418509 100644 --- a/res/values-be/strings.xml +++ b/res/values-be/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Перамясціць у…"</string> <string name="menu_compress" msgid="37539111904724188">"Сціснуць"</string> <string name="menu_extract" msgid="8171946945982532262">"Выняць у…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Выняць усе…"</string> <string name="menu_rename" msgid="1883113442688817554">"Перайменаваць"</string> <string name="menu_inspect" msgid="7279855349299446224">"Атрымаць інфармацыю"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Паказваць схаваныя файлы"</string> @@ -333,4 +334,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Вы не можаце перамяшчаць файлы з іншай праграмы."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Паказ у рэжыме табліцы."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Паказ у рэжыме спіса."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-bg/strings.xml b/res/values-bg/strings.xml index e1444b40b..d73d376fa 100644 --- a/res/values-bg/strings.xml +++ b/res/values-bg/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Преместване във…"</string> <string name="menu_compress" msgid="37539111904724188">"Компресиране"</string> <string name="menu_extract" msgid="8171946945982532262">"Извличане в/ъв…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Извличане на всички…"</string> <string name="menu_rename" msgid="1883113442688817554">"Преименуване"</string> <string name="menu_inspect" msgid="7279855349299446224">"Получаване на информация"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Показване на скрити файлове"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Не можете да местите файлове от друго приложение."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Показва се в табличен изглед."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Показва се в списъчен изглед."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-bn/strings.xml b/res/values-bn/strings.xml index f51afc50f..59b3d297d 100644 --- a/res/values-bn/strings.xml +++ b/res/values-bn/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"এতে সরান…"</string> <string name="menu_compress" msgid="37539111904724188">"সঙ্কুচিত করুন"</string> <string name="menu_extract" msgid="8171946945982532262">"এখানে রাখুন…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"সব এক্সট্র্যাক্ট করুন…"</string> <string name="menu_rename" msgid="1883113442688817554">"পুনঃনামকরণ"</string> <string name="menu_inspect" msgid="7279855349299446224">"তথ্য পান"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"লুকানো ফাইল দেখুন"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"অন্য অ্যাপ থেকে ফাইল সরাতে পারবেন না।"</string> <string name="grid_mode_showing" msgid="2803166871485028508">"গ্রিড মোডে দেখানো হচ্ছে।"</string> <string name="list_mode_showing" msgid="1225413902295895166">"তালিকা মোডে দেখানো হচ্ছে।"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-bs/strings.xml b/res/values-bs/strings.xml index bb5627dbe..96328a42c 100644 --- a/res/values-bs/strings.xml +++ b/res/values-bs/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Premjesti u…"</string> <string name="menu_compress" msgid="37539111904724188">"Kompresiraj"</string> <string name="menu_extract" msgid="8171946945982532262">"Izdvoji u…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Izdvajanje svega…"</string> <string name="menu_rename" msgid="1883113442688817554">"Promijeni naziv"</string> <string name="menu_inspect" msgid="7279855349299446224">"Prikaži informacije"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Prikaži skrivene fajlove"</string> @@ -311,4 +312,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Ne možete premjestiti fajlove iz druge aplikacije."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Prikazivanje u vidu mreže."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Prikazivanje u vidu liste."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-ca/strings.xml b/res/values-ca/strings.xml index 76d717243..6b4a256cd 100644 --- a/res/values-ca/strings.xml +++ b/res/values-ca/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Mou a…"</string> <string name="menu_compress" msgid="37539111904724188">"Comprimeix"</string> <string name="menu_extract" msgid="8171946945982532262">"Extreu a…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Extreu-ho tot…"</string> <string name="menu_rename" msgid="1883113442688817554">"Canvia el nom"</string> <string name="menu_inspect" msgid="7279855349299446224">"Obtén informació"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Mostra els fitxers amagats"</string> @@ -311,4 +312,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"No pots moure fitxers des d\'una altra aplicació."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Es mostra en mode de quadrícula."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Es mostra en mode de llista."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-cs/strings.xml b/res/values-cs/strings.xml index 44ee41907..f0a4b6424 100644 --- a/res/values-cs/strings.xml +++ b/res/values-cs/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Přesunout do…"</string> <string name="menu_compress" msgid="37539111904724188">"Zkomprimovat"</string> <string name="menu_extract" msgid="8171946945982532262">"Rozbalit do…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Extrahovat vše…"</string> <string name="menu_rename" msgid="1883113442688817554">"Přejmenovat"</string> <string name="menu_inspect" msgid="7279855349299446224">"Zobrazit informace"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Zobrazit skryté soubory"</string> @@ -333,4 +334,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Není možné přesouvat soubory z jiné aplikace."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Zobrazuje se mřížka s položkami."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Zobrazuje se seznam položek."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-da/strings.xml b/res/values-da/strings.xml index c0d5cf0c8..8248ffaca 100644 --- a/res/values-da/strings.xml +++ b/res/values-da/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Flyt til…"</string> <string name="menu_compress" msgid="37539111904724188">"Komprimer"</string> <string name="menu_extract" msgid="8171946945982532262">"Pak ud i…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Pak alle ud…"</string> <string name="menu_rename" msgid="1883113442688817554">"Omdøb"</string> <string name="menu_inspect" msgid="7279855349299446224">"Få oplysninger"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Vis skjulte filer"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Du kan ikke flytte filer fra en anden app."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Vises i gittervisning."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Vises i listevisning."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-de/strings.xml b/res/values-de/strings.xml index 381f3b696..3c80bf742 100644 --- a/res/values-de/strings.xml +++ b/res/values-de/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Verschieben nach…"</string> <string name="menu_compress" msgid="37539111904724188">"Komprimieren"</string> <string name="menu_extract" msgid="8171946945982532262">"Extrahieren nach…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Alle extrahieren…"</string> <string name="menu_rename" msgid="1883113442688817554">"Umbenennen"</string> <string name="menu_inspect" msgid="7279855349299446224">"Weitere Informationen"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Ausgeblendete Dateien anzeigen"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Du kannst keine Dateien aus einer anderen App verschieben."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Wird im Rastermodus angezeigt."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Wird im Listenmodus angezeigt."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-el/strings.xml b/res/values-el/strings.xml index 9233d5d20..e270d7d83 100644 --- a/res/values-el/strings.xml +++ b/res/values-el/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Μετακίνηση σε…"</string> <string name="menu_compress" msgid="37539111904724188">"Συμπίεση"</string> <string name="menu_extract" msgid="8171946945982532262">"Εξαγωγή σε…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Εξαγωγή όλων…"</string> <string name="menu_rename" msgid="1883113442688817554">"Μετονομασία"</string> <string name="menu_inspect" msgid="7279855349299446224">"Λήψη πληροφοριών"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Εμφάνιση κρυφών αρχείων"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Δεν είναι δυνατή η μεταφορά αρχείων από άλλη εφαρμογή."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Εμφάνιση σε λειτουργία πλέγματος."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Εμφάνιση σε λειτουργία λίστας."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-en-rAU/strings.xml b/res/values-en-rAU/strings.xml index 5523ed3f1..477944a51 100644 --- a/res/values-en-rAU/strings.xml +++ b/res/values-en-rAU/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Move to…"</string> <string name="menu_compress" msgid="37539111904724188">"Compress"</string> <string name="menu_extract" msgid="8171946945982532262">"Extract to…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Extract all…"</string> <string name="menu_rename" msgid="1883113442688817554">"Rename"</string> <string name="menu_inspect" msgid="7279855349299446224">"Get info"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Show hidden files"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"You can’t move files from another app."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Showing in grid mode."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Showing in list mode."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-en-rCA/strings.xml b/res/values-en-rCA/strings.xml index 0dcc1f5c1..6d10f06b8 100644 --- a/res/values-en-rCA/strings.xml +++ b/res/values-en-rCA/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Move to…"</string> <string name="menu_compress" msgid="37539111904724188">"Compress"</string> <string name="menu_extract" msgid="8171946945982532262">"Extract to…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Extract all…"</string> <string name="menu_rename" msgid="1883113442688817554">"Rename"</string> <string name="menu_inspect" msgid="7279855349299446224">"Get info"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Show hidden files"</string> @@ -289,4 +290,5 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"You can’t move files from another app."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Showing in grid mode."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Showing in list mode."</string> + <string name="bullet" msgid="5606740650312122766">"•"</string> </resources> diff --git a/res/values-en-rGB/strings.xml b/res/values-en-rGB/strings.xml index 5523ed3f1..477944a51 100644 --- a/res/values-en-rGB/strings.xml +++ b/res/values-en-rGB/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Move to…"</string> <string name="menu_compress" msgid="37539111904724188">"Compress"</string> <string name="menu_extract" msgid="8171946945982532262">"Extract to…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Extract all…"</string> <string name="menu_rename" msgid="1883113442688817554">"Rename"</string> <string name="menu_inspect" msgid="7279855349299446224">"Get info"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Show hidden files"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"You can’t move files from another app."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Showing in grid mode."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Showing in list mode."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-en-rIN/strings.xml b/res/values-en-rIN/strings.xml index 5523ed3f1..477944a51 100644 --- a/res/values-en-rIN/strings.xml +++ b/res/values-en-rIN/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Move to…"</string> <string name="menu_compress" msgid="37539111904724188">"Compress"</string> <string name="menu_extract" msgid="8171946945982532262">"Extract to…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Extract all…"</string> <string name="menu_rename" msgid="1883113442688817554">"Rename"</string> <string name="menu_inspect" msgid="7279855349299446224">"Get info"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Show hidden files"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"You can’t move files from another app."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Showing in grid mode."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Showing in list mode."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-es-rUS/strings.xml b/res/values-es-rUS/strings.xml index c7f1e717e..09b071f76 100644 --- a/res/values-es-rUS/strings.xml +++ b/res/values-es-rUS/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Mover a…"</string> <string name="menu_compress" msgid="37539111904724188">"Comprimir"</string> <string name="menu_extract" msgid="8171946945982532262">"Extraer en…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Extraer todo…"</string> <string name="menu_rename" msgid="1883113442688817554">"Cambiar nombre"</string> <string name="menu_inspect" msgid="7279855349299446224">"Obtener información"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Mostrar archivos ocultos"</string> @@ -311,4 +312,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"No puedes transferir archivos de otra app."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Se muestra en modo de cuadrícula."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Se muestra en modo de lista."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-es/strings.xml b/res/values-es/strings.xml index 573a6768b..c82e9fa4c 100644 --- a/res/values-es/strings.xml +++ b/res/values-es/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Mover a…"</string> <string name="menu_compress" msgid="37539111904724188">"Comprimir"</string> <string name="menu_extract" msgid="8171946945982532262">"Extraer a…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Extraer todo…"</string> <string name="menu_rename" msgid="1883113442688817554">"Cambiar nombre"</string> <string name="menu_inspect" msgid="7279855349299446224">"Obtener información"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Mostrar archivos ocultos"</string> @@ -311,4 +312,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"No puedes mover archivos de otra aplicación."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Mostrando modo de cuadrícula."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Mostrando modo de lista."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-et/strings.xml b/res/values-et/strings.xml index e7dbe750b..3b6338d57 100644 --- a/res/values-et/strings.xml +++ b/res/values-et/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Teisalda asukohta …"</string> <string name="menu_compress" msgid="37539111904724188">"Tihenda"</string> <string name="menu_extract" msgid="8171946945982532262">"Ekstrakti …"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Ekstrakti kõik …"</string> <string name="menu_rename" msgid="1883113442688817554">"Nimeta ümber"</string> <string name="menu_inspect" msgid="7279855349299446224">"Hangi teavet"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Kuva peidetud failid"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Te ei saa faile teisest rakendusest teisaldada."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Kuvatud ruudustikuvaates."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Kuvatud loendivaates."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-eu/strings.xml b/res/values-eu/strings.xml index c8dbc0577..5b8c88f14 100644 --- a/res/values-eu/strings.xml +++ b/res/values-eu/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Eraman hona…"</string> <string name="menu_compress" msgid="37539111904724188">"Konprimatu"</string> <string name="menu_extract" msgid="8171946945982532262">"Atera hona…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Atera guztia…"</string> <string name="menu_rename" msgid="1883113442688817554">"Aldatu izena"</string> <string name="menu_inspect" msgid="7279855349299446224">"Lortu informazioa"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Erakutsi fitxategi ezkutuak"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Ezin dituzu mugitu beste aplikazio batzuetako fitxategiak."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Sareta moduan ikusgai."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Zerrenda moduan ikusgai."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-fa/strings.xml b/res/values-fa/strings.xml index f5a469861..1f3c0e261 100644 --- a/res/values-fa/strings.xml +++ b/res/values-fa/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"انتقال به…"</string> <string name="menu_compress" msgid="37539111904724188">"فشرده کردن"</string> <string name="menu_extract" msgid="8171946945982532262">"استخراج در…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"استخراج همه…"</string> <string name="menu_rename" msgid="1883113442688817554">"تغییر نام"</string> <string name="menu_inspect" msgid="7279855349299446224">"دریافت اطلاعات"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"فایلهای پنهان نشان داده شود"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"نمیتوانید فایلها را از برنامه دیگری انتقال دهید."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"نمایش در حالت جدولی."</string> <string name="list_mode_showing" msgid="1225413902295895166">"نمایش در حالت فهرست."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-fi/strings.xml b/res/values-fi/strings.xml index 6fa10bf1a..4f7a53706 100644 --- a/res/values-fi/strings.xml +++ b/res/values-fi/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Siirrä kohteeseen…"</string> <string name="menu_compress" msgid="37539111904724188">"Pakkaa"</string> <string name="menu_extract" msgid="8171946945982532262">"Pura kohteeseen…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Pura kaikki…"</string> <string name="menu_rename" msgid="1883113442688817554">"Nimeä uudelleen"</string> <string name="menu_inspect" msgid="7279855349299446224">"Näytä tiedot"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Näytä piilotetut tiedostot"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Et voi siirtää tiedostoja toisesta sovelluksesta."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Näytetään ruudukkotilassa."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Näytetään luettelotilassa."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-fr-rCA/strings.xml b/res/values-fr-rCA/strings.xml index c61478375..22031245e 100644 --- a/res/values-fr-rCA/strings.xml +++ b/res/values-fr-rCA/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Déplacer dans…"</string> <string name="menu_compress" msgid="37539111904724188">"Compresser"</string> <string name="menu_extract" msgid="8171946945982532262">"Extraire vers…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Tout extraire…"</string> <string name="menu_rename" msgid="1883113442688817554">"Renommer"</string> <string name="menu_inspect" msgid="7279855349299446224">"En savoir plus"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Afficher les fichiers masqués"</string> @@ -311,4 +312,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Vous ne pouvez pas déplacer de fichiers d\'une autre appli."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Affichage en mode grille."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Affichage en mode liste."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-fr/strings.xml b/res/values-fr/strings.xml index 85bdeb0fd..b248c4859 100644 --- a/res/values-fr/strings.xml +++ b/res/values-fr/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Déplacer vers…"</string> <string name="menu_compress" msgid="37539111904724188">"Compresser"</string> <string name="menu_extract" msgid="8171946945982532262">"Extraire sur…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Tout extraire…"</string> <string name="menu_rename" msgid="1883113442688817554">"Renommer"</string> <string name="menu_inspect" msgid="7279855349299446224">"Obtenir les informations"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Afficher les fichiers masqués"</string> @@ -303,7 +304,7 @@ <string name="directory_blocked_header_title" msgid="1164584889578740066">"Impossible d\'utiliser ce dossier"</string> <string name="directory_blocked_header_subtitle" msgid="2829150911849033408">"Pour protéger votre vie privée, choisissez un autre dossier"</string> <string name="create_new_folder_button" msgid="8859613309559794890">"Créer un dossier"</string> - <string name="search_bar_hint" msgid="146031513183888721">"Rechercher cet appareil"</string> + <string name="search_bar_hint" msgid="146031513183888721">"Rechercher sur cet appareil"</string> <string name="delete_search_history" msgid="2202015025607694515">"Supprimer l\'historique des recherches <xliff:g id="TEXT">%1$s</xliff:g>"</string> <string name="personal_tab" msgid="3878576287868528503">"Personnel"</string> <string name="work_tab" msgid="7265359366883747413">"Professionnel"</string> @@ -311,4 +312,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Vous ne pouvez pas déplacer de fichiers depuis une autre application."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Affichage en mode grille."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Affichage en mode liste."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-gl/strings.xml b/res/values-gl/strings.xml index 1b9fe1d2f..323ec058d 100644 --- a/res/values-gl/strings.xml +++ b/res/values-gl/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Mover a…"</string> <string name="menu_compress" msgid="37539111904724188">"Comprimir"</string> <string name="menu_extract" msgid="8171946945982532262">"Extraer en..."</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Extraer todo…"</string> <string name="menu_rename" msgid="1883113442688817554">"Cambiar nome"</string> <string name="menu_inspect" msgid="7279855349299446224">"Obter información"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Amosar ficheiros ocultos"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Non se poden mover ficheiros desde outra aplicación."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Mostrando modo de grade."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Mostrando modo de lista."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-gu/strings.xml b/res/values-gu/strings.xml index 766540c51..db1be59cb 100644 --- a/res/values-gu/strings.xml +++ b/res/values-gu/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"આમાં ખસેડો…"</string> <string name="menu_compress" msgid="37539111904724188">"સંકુચિત કરો"</string> <string name="menu_extract" msgid="8171946945982532262">"આમાં એક્સટ્રેક્ટ કરો…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"બધું એક્સટ્રેક્ટ કરો…"</string> <string name="menu_rename" msgid="1883113442688817554">"નામ બદલો"</string> <string name="menu_inspect" msgid="7279855349299446224">"માહિતી મેળવો"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"છુપાવેલી ફાઇલો બતાવો"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"તમે કોઈ અન્ય ઍપમાંથી ફાઇલો ખસેડી શકતાં નથી."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"ગ્રિડ મોડમાં બતાવી રહ્યાં છીએ."</string> <string name="list_mode_showing" msgid="1225413902295895166">"સૂચિ મોડમાં બતાવી રહ્યાં છીએ."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-hi/strings.xml b/res/values-hi/strings.xml index 3183bd297..b6fc7d83d 100644 --- a/res/values-hi/strings.xml +++ b/res/values-hi/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"यहां ले जाएं…"</string> <string name="menu_compress" msgid="37539111904724188">"कंप्रेस करें"</string> <string name="menu_extract" msgid="8171946945982532262">"यहां निकालें…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"सभी को एक्सट्रैक्ट करें…"</string> <string name="menu_rename" msgid="1883113442688817554">"नाम बदलें"</string> <string name="menu_inspect" msgid="7279855349299446224">"जानकारी पाएं"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"छिपी हुई फ़ाइलें दिखाएं"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"आप किसी दूसरे ऐप्लिकेशन से फ़ाइलें नहीं ले जा सकते."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"ग्रिड मोड में दिखाया जा रहा है."</string> <string name="list_mode_showing" msgid="1225413902295895166">"सूची मोड में दिखाया जा रहा है."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-hr/strings.xml b/res/values-hr/strings.xml index e6cfbf934..5998f4a98 100644 --- a/res/values-hr/strings.xml +++ b/res/values-hr/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Premjesti u…"</string> <string name="menu_compress" msgid="37539111904724188">"Sažmi"</string> <string name="menu_extract" msgid="8171946945982532262">"Izdvoji u…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Izdvoji sve…"</string> <string name="menu_rename" msgid="1883113442688817554">"Promijeni naziv"</string> <string name="menu_inspect" msgid="7279855349299446224">"Informacije"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Prikaži skrivene datoteke"</string> @@ -311,4 +312,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Ne možete premjestiti datoteke iz druge aplikacije."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Prikazivanje u načinu rešetke."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Prikazivanje u načinu popisa."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-hu/strings.xml b/res/values-hu/strings.xml index 9d7a2a675..db82943b7 100644 --- a/res/values-hu/strings.xml +++ b/res/values-hu/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Áthelyezés…"</string> <string name="menu_compress" msgid="37539111904724188">"Tömörítés"</string> <string name="menu_extract" msgid="8171946945982532262">"Kicsomagolás ide…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Összes kibontása"</string> <string name="menu_rename" msgid="1883113442688817554">"Átnevezés"</string> <string name="menu_inspect" msgid="7279855349299446224">"Információ megjelenítése"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Rejtett fájlok megjelenítése"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Nem lehet áthelyezni fájlokat más alkalmazásból."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Megjelenítés rácsnézetben."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Megjelenítés listanézetben."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-hy/strings.xml b/res/values-hy/strings.xml index fb55c8bee..9cf0e2a60 100644 --- a/res/values-hy/strings.xml +++ b/res/values-hy/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Տեղափոխել…"</string> <string name="menu_compress" msgid="37539111904724188">"Սեղմել"</string> <string name="menu_extract" msgid="8171946945982532262">"Արտահանել…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Դուրս բերել բոլորը…"</string> <string name="menu_rename" msgid="1883113442688817554">"Վերանվանել"</string> <string name="menu_inspect" msgid="7279855349299446224">"Տեղեկություններ"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Ցուցադրել թաքցված ֆայլերը"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Դուք չեք կարող տեղափոխել ֆայլեր այլ հավելվածից։"</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Ցուցադրվում է ցանցի տեսքով։"</string> <string name="list_mode_showing" msgid="1225413902295895166">"Ցուցադրվում է ցանկի տեսքով։"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-in/strings.xml b/res/values-in/strings.xml index f89f539c3..d5e864c0f 100644 --- a/res/values-in/strings.xml +++ b/res/values-in/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Pindahkan ke..."</string> <string name="menu_compress" msgid="37539111904724188">"Kompresi"</string> <string name="menu_extract" msgid="8171946945982532262">"Ekstrak ke…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Ekstrak semua…"</string> <string name="menu_rename" msgid="1883113442688817554">"Ganti nama"</string> <string name="menu_inspect" msgid="7279855349299446224">"Dapatkan info"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Tampilkan file tersembunyi"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Anda tidak dapat memindahkan file ke aplikasi lain."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Menampilkan dalam mode petak."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Menampilkan dalam mode daftar."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-is/strings.xml b/res/values-is/strings.xml index 676c483d4..4ec26ccb4 100644 --- a/res/values-is/strings.xml +++ b/res/values-is/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Færa í…"</string> <string name="menu_compress" msgid="37539111904724188">"Þjappa"</string> <string name="menu_extract" msgid="8171946945982532262">"Flytja út í…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Draga allt út …"</string> <string name="menu_rename" msgid="1883113442688817554">"Endurnefna"</string> <string name="menu_inspect" msgid="7279855349299446224">"Sækja upplýsingar"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Sýna faldar skrár"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Ekki er hægt að færa skrár úr öðru forriti."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Sýnir töfluyfirlit."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Sýnir listayfirlit."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-it/strings.xml b/res/values-it/strings.xml index 905815c87..4ce7438fc 100644 --- a/res/values-it/strings.xml +++ b/res/values-it/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Sposta in…"</string> <string name="menu_compress" msgid="37539111904724188">"Comprimi"</string> <string name="menu_extract" msgid="8171946945982532262">"Estrai in…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Estrai tutto…"</string> <string name="menu_rename" msgid="1883113442688817554">"Rinomina"</string> <string name="menu_inspect" msgid="7279855349299446224">"Informazioni"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Mostra file nascosti"</string> @@ -311,4 +312,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Impossibile spostare file da un\'altra app."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Visualizzazione in modalità griglia."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Visualizzazione in modalità elenco."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-iw/strings.xml b/res/values-iw/strings.xml index 822177a1e..76860185b 100644 --- a/res/values-iw/strings.xml +++ b/res/values-iw/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"העברה אל…"</string> <string name="menu_compress" msgid="37539111904724188">"דחיסה"</string> <string name="menu_extract" msgid="8171946945982532262">"חילוץ לתיקייה…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"חילוץ הכול…"</string> <string name="menu_rename" msgid="1883113442688817554">"שינוי שם"</string> <string name="menu_inspect" msgid="7279855349299446224">"מידע על המסמך"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"הצגת קבצים מוסתרים"</string> @@ -311,4 +312,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"אי אפשר להעביר קבצים מאפליקציה אחרת."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"הצגה בתצוגת טבלה."</string> <string name="list_mode_showing" msgid="1225413902295895166">"הצגה בתצוגת רשימה."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-ja/strings.xml b/res/values-ja/strings.xml index 35685ca9a..aeb7992f6 100644 --- a/res/values-ja/strings.xml +++ b/res/values-ja/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"移動…"</string> <string name="menu_compress" msgid="37539111904724188">"圧縮"</string> <string name="menu_extract" msgid="8171946945982532262">"次の場所に解凍…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"すべて抽出…"</string> <string name="menu_rename" msgid="1883113442688817554">"名前を変更"</string> <string name="menu_inspect" msgid="7279855349299446224">"詳細情報"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"非表示のファイルを表示"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"別のアプリからファイルを移動することはできません。"</string> <string name="grid_mode_showing" msgid="2803166871485028508">"グリッドモードで表示しています。"</string> <string name="list_mode_showing" msgid="1225413902295895166">"リストモードで表示しています。"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-ka/strings.xml b/res/values-ka/strings.xml index 1a698668f..592103f84 100644 --- a/res/values-ka/strings.xml +++ b/res/values-ka/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"გადაადგილება..."</string> <string name="menu_compress" msgid="37539111904724188">"შეკუმშვა"</string> <string name="menu_extract" msgid="8171946945982532262">"ამოღება…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"ყველას ამოღება…"</string> <string name="menu_rename" msgid="1883113442688817554">"გადარქმევა"</string> <string name="menu_inspect" msgid="7279855349299446224">"ინფორმაციის მიღება"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"გამოჩნდეს დამალული ფაილები"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"ფაილების სხვა აპიდან გადმოტანა შეუძლებელია."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"ნაჩვენებია ბადის რეჟიმში."</string> <string name="list_mode_showing" msgid="1225413902295895166">"ნაჩვენებია სიის რეჟიმში."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-kk/strings.xml b/res/values-kk/strings.xml index 7d1f402e1..a8f6f25e6 100644 --- a/res/values-kk/strings.xml +++ b/res/values-kk/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Тасымалдау…"</string> <string name="menu_compress" msgid="37539111904724188">"Сығу"</string> <string name="menu_extract" msgid="8171946945982532262">"Алынуда…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Барлығын шығарып алу…"</string> <string name="menu_rename" msgid="1883113442688817554">"Атын өзгерту"</string> <string name="menu_inspect" msgid="7279855349299446224">"Ақпарат алу"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Жасырын файлдарды көрсету"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Файлдарды басқа қолданбадан тасымалдай алмайсыз."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Тор режимінде көрсетіледі."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Тізім режимінде көрсетіледі."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-km/strings.xml b/res/values-km/strings.xml index 365efd1fe..ba7900c21 100644 --- a/res/values-km/strings.xml +++ b/res/values-km/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"ផ្លាស់ទីទៅ…"</string> <string name="menu_compress" msgid="37539111904724188">"បង្ហាប់"</string> <string name="menu_extract" msgid="8171946945982532262">"ស្រង់ទៅ…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"ស្រង់ចេញទាំងអស់…"</string> <string name="menu_rename" msgid="1883113442688817554">"ប្ដូរឈ្មោះ"</string> <string name="menu_inspect" msgid="7279855349299446224">"ទទួលព័ត៌មាន"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"បង្ហាញឯកសារដែលបានលាក់"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"អ្នកមិនអាចផ្លាស់ទីឯកសារពីកម្មវិធីផ្សេងទៀតបានទេ។"</string> <string name="grid_mode_showing" msgid="2803166871485028508">"បង្ហាញក្នុងមុខងារក្រឡា។"</string> <string name="list_mode_showing" msgid="1225413902295895166">"បង្ហាញក្នុងមុខងារបញ្ជី។"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-kn/strings.xml b/res/values-kn/strings.xml index 1cf89f41f..47bb3985d 100644 --- a/res/values-kn/strings.xml +++ b/res/values-kn/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"ಇದಕ್ಕೆ ಸರಿಸಿ…"</string> <string name="menu_compress" msgid="37539111904724188">"ಕುಗ್ಗಿಸಿ"</string> <string name="menu_extract" msgid="8171946945982532262">"ಇದಕ್ಕೆ ಬೇರ್ಪಡಿಸಲಾಗಿದೆ…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"ಎಲ್ಲವನ್ನೂ ಎಕ್ಸ್ಟ್ರ್ಯಾಕ್ಟ್ ಮಾಡಿ…"</string> <string name="menu_rename" msgid="1883113442688817554">"ಮರುಹೆಸರಿಸು"</string> <string name="menu_inspect" msgid="7279855349299446224">"ಮಾಹಿತಿಯನ್ನು ಪಡೆಯಿರಿ"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"ಮರೆಮಾಡಿದ ಫೈಲ್ಗಳನ್ನು ತೋರಿಸಿ"</string> @@ -221,19 +222,19 @@ <item quantity="one"><xliff:g id="COUNT_1">%1$d</xliff:g> ಐಟಂಗಳು</item> <item quantity="other"><xliff:g id="COUNT_1">%1$d</xliff:g> ಐಟಂಗಳು</item> </plurals> - <string name="delete_filename_confirmation_message" msgid="8338069763240613258">"\"<xliff:g id="NAME">%1$s</xliff:g>\" ಅಳಿಸುವುದೇ?"</string> - <string name="delete_foldername_confirmation_message" msgid="9084085260877704140">"\"<xliff:g id="NAME">%1$s</xliff:g>\" ಫೋಲ್ಡರ್ ಮತ್ತು ಅದರ ವಿಷಯಗಳನ್ನು ಅಳಿಸುವುದೇ?"</string> + <string name="delete_filename_confirmation_message" msgid="8338069763240613258">"\"<xliff:g id="NAME">%1$s</xliff:g>\" ಅಳಿಸಬೇಕೆ?"</string> + <string name="delete_foldername_confirmation_message" msgid="9084085260877704140">"\"<xliff:g id="NAME">%1$s</xliff:g>\" ಫೋಲ್ಡರ್ ಮತ್ತು ಅದರ ವಿಷಯಗಳನ್ನು ಅಳಿಸಬೇಕೆ?"</string> <plurals name="delete_files_confirmation_message" formatted="false" msgid="4866664063250034142"> - <item quantity="one"> <xliff:g id="COUNT_1">%1$d</xliff:g> ಫೈಲ್ಗಳನ್ನು ಅಳಿಸುವುದೇ?</item> - <item quantity="other"> <xliff:g id="COUNT_1">%1$d</xliff:g> ಫೈಲ್ಗಳನ್ನು ಅಳಿಸುವುದೇ?</item> + <item quantity="one"> <xliff:g id="COUNT_1">%1$d</xliff:g> ಫೈಲ್ಗಳನ್ನು ಅಳಿಸಬೇಕೆ?</item> + <item quantity="other"> <xliff:g id="COUNT_1">%1$d</xliff:g> ಫೈಲ್ಗಳನ್ನು ಅಳಿಸಬೇಕೆ?</item> </plurals> <plurals name="delete_folders_confirmation_message" formatted="false" msgid="1028946402799686388"> - <item quantity="one"> <xliff:g id="COUNT_1">%1$d</xliff:g> ಫೋಲ್ಡರ್ಗಳು ಮತ್ತು ಅವುಗಳ ವಿಷಯಗಳನ್ನು ಅಳಿಸುವುದೇ?</item> - <item quantity="other"> <xliff:g id="COUNT_1">%1$d</xliff:g> ಫೋಲ್ಡರ್ಗಳು ಮತ್ತು ಅವುಗಳ ವಿಷಯಗಳನ್ನು ಅಳಿಸುವುದೇ?</item> + <item quantity="one"> <xliff:g id="COUNT_1">%1$d</xliff:g> ಫೋಲ್ಡರ್ಗಳು ಮತ್ತು ಅವುಗಳ ಕಂಟೆಂಟ್ಗಳನ್ನು ಅಳಿಸಬೇಕೆ?</item> + <item quantity="other"> <xliff:g id="COUNT_1">%1$d</xliff:g> ಫೋಲ್ಡರ್ಗಳು ಮತ್ತು ಅವುಗಳ ಕಂಟೆಂಟ್ಗಳನ್ನು ಅಳಿಸಬೇಕೆ?</item> </plurals> <plurals name="delete_items_confirmation_message" formatted="false" msgid="7285090426511028179"> - <item quantity="one"> <xliff:g id="COUNT_1">%1$d</xliff:g> ಐಟಂಗಳನ್ನು ಅಳಿಸುವುದೇ?</item> - <item quantity="other"> <xliff:g id="COUNT_1">%1$d</xliff:g> ಐಟಂಗಳನ್ನು ಅಳಿಸುವುದೇ?</item> + <item quantity="one"> <xliff:g id="COUNT_1">%1$d</xliff:g> ಐಟಂಗಳನ್ನು ಅಳಿಸಬೇಕೆ?</item> + <item quantity="other"> <xliff:g id="COUNT_1">%1$d</xliff:g> ಐಟಂಗಳನ್ನು ಅಳಿಸಬೇಕೆ?</item> </plurals> <string name="images_shortcut_label" msgid="2545168016070493574">"ಚಿತ್ರಗಳು"</string> <string name="archive_loading_failed" msgid="7243436722828766996">"ಬ್ರೌಸಿಂಗ್ಗಾಗಿ ಆರ್ಕೈವ್ ಅನ್ನು ತೆರೆಯಲು ಸಾಧ್ಯವಿಲ್ಲ. ಫೈಲ್ ದೋಷಪೂರಿತವಾಗಿದೆ ಅಥವಾ ಬೆಂಬಲಿಸದ ಸ್ವರೂಪದಲ್ಲಿರಬಹುದು."</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"ನೀವು ಬೇರೊಂದು ಆ್ಯಪ್ನಿಂದ ಫೈಲ್ಗಳನ್ನು ಸರಿಸಲು ಸಾಧ್ಯವಿಲ್ಲ."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"ಗ್ರಿಡ್ ಮೋಡ್ನಲ್ಲಿ ತೋರಿಸಲಾಗುತ್ತಿದೆ."</string> <string name="list_mode_showing" msgid="1225413902295895166">"ಪಟ್ಟಿ ಮೋಡ್ನಲ್ಲಿ ತೋರಿಸಲಾಗುತ್ತಿದೆ."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-ko/strings.xml b/res/values-ko/strings.xml index bb313e892..6090f1185 100644 --- a/res/values-ko/strings.xml +++ b/res/values-ko/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"다음으로 이동:"</string> <string name="menu_compress" msgid="37539111904724188">"압축"</string> <string name="menu_extract" msgid="8171946945982532262">"다음 위치에 추출..."</string> + <string name="menu_extract_all" msgid="7335680068521252718">"모두 추출…"</string> <string name="menu_rename" msgid="1883113442688817554">"이름 바꾸기"</string> <string name="menu_inspect" msgid="7279855349299446224">"정보 확인"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"숨겨진 파일 표시"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"파일을 다른 앱에서 이동할 수 없습니다."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"그리드 모드로 표시 중입니다."</string> <string name="list_mode_showing" msgid="1225413902295895166">"목록 모드로 표시 중입니다."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-ky/strings.xml b/res/values-ky/strings.xml index 83ac3fead..3853ecbf7 100644 --- a/res/values-ky/strings.xml +++ b/res/values-ky/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Төмөнкүгө жылдыруу..."</string> <string name="menu_compress" msgid="37539111904724188">"Кысуу"</string> <string name="menu_extract" msgid="8171946945982532262">"Төмөнкүгө чыгаруу…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Баарын чыгаруу…"</string> <string name="menu_rename" msgid="1883113442688817554">"Аталышын өзгөртүү"</string> <string name="menu_inspect" msgid="7279855349299446224">"Маалымат алуу"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Жашырылган файлдар көрүнсүн"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Башка колдонмодогу файлдарды жылдырууга болбойт."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Торчо режиминде көрсөтүлүүдө."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Тизме режиминде көрсөтүлүүдө."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-lo/strings.xml b/res/values-lo/strings.xml index 8794da0e8..321c2c528 100644 --- a/res/values-lo/strings.xml +++ b/res/values-lo/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"ຍ້າຍໄປໃສ່..."</string> <string name="menu_compress" msgid="37539111904724188">"ບີບອັດ"</string> <string name="menu_extract" msgid="8171946945982532262">"ແຕກໄຟລ໌ໄປ…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"ດຶງຂໍ້ມູນຈາກເອກະສານທັງໝົດ…"</string> <string name="menu_rename" msgid="1883113442688817554">"ປ່ຽນຊື່"</string> <string name="menu_inspect" msgid="7279855349299446224">"ຂໍຂໍ້ມູນ"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"ສະແດງໄຟລ໌ທີ່ເຊື່ອງໄວ້"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"ບໍ່ສາມາດຍ້າຍໄຟລ໌ຈາກແອັບອື່ນໄດ້."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"ກຳລັງສະແດງໃນໂໝດຕາຕະລາງ."</string> <string name="list_mode_showing" msgid="1225413902295895166">"ກຳລັງສະແດງໃນໂໝດລາຍຊື່."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-lt/strings.xml b/res/values-lt/strings.xml index 88420bf91..415b5469e 100644 --- a/res/values-lt/strings.xml +++ b/res/values-lt/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Perkelti į…"</string> <string name="menu_compress" msgid="37539111904724188">"Suglaudinti"</string> <string name="menu_extract" msgid="8171946945982532262">"Išskleisti į..."</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Išskleisti viską…"</string> <string name="menu_rename" msgid="1883113442688817554">"Pervardyti"</string> <string name="menu_inspect" msgid="7279855349299446224">"Gauti informacijos"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Rodyti paslėptus failus"</string> @@ -333,4 +334,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Negalite perkelti failų iš kitos programos."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Rodoma tinklelio režimu."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Rodoma sąrašo režimu."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-lv/strings.xml b/res/values-lv/strings.xml index 384395b89..444f8e5bd 100644 --- a/res/values-lv/strings.xml +++ b/res/values-lv/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Pārvietot uz…"</string> <string name="menu_compress" msgid="37539111904724188">"Saspiest"</string> <string name="menu_extract" msgid="8171946945982532262">"Izvilkt..."</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Izgūt visu…"</string> <string name="menu_rename" msgid="1883113442688817554">"Pārdēvēt"</string> <string name="menu_inspect" msgid="7279855349299446224">"Iegūt informāciju"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Rādīt paslēptos failus"</string> @@ -311,4 +312,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Jūs nevarat pārvietot failus no citas lietotnes."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Tiek attēlots režģa režīms."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Tiek attēlots saraksta režīms."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-mk/strings.xml b/res/values-mk/strings.xml index 92d58f55a..ef94b2eba 100644 --- a/res/values-mk/strings.xml +++ b/res/values-mk/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Премести во…"</string> <string name="menu_compress" msgid="37539111904724188">"Компримирај"</string> <string name="menu_extract" msgid="8171946945982532262">"Отпакувај во…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Извлечи сѐ…"</string> <string name="menu_rename" msgid="1883113442688817554">"Преименувај"</string> <string name="menu_inspect" msgid="7279855349299446224">"Добијте информации"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Прикажи скриени датотеки"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Не може да преместувате датотеки од друга апликација."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Се прикажува во режим на решетка."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Се прикажува во режим на список."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-ml/strings.xml b/res/values-ml/strings.xml index 5de00993a..462184507 100644 --- a/res/values-ml/strings.xml +++ b/res/values-ml/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"ഇതിലേക്ക് നീക്കുക..."</string> <string name="menu_compress" msgid="37539111904724188">"കംപ്രസ് ചെയ്യുക"</string> <string name="menu_extract" msgid="8171946945982532262">"എക്സ്ട്രാക്റ്റുചെയ്യുക…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"എക്സ്ട്രാക്റ്റ് ചെയ്യൂ…"</string> <string name="menu_rename" msgid="1883113442688817554">"പേര് മാറ്റുക"</string> <string name="menu_inspect" msgid="7279855349299446224">"വിവരങ്ങൾ നേടുക"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"മറച്ചിരിക്കുന്ന ഫയൽ കാണിക്കൂ"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"മറ്റ് ആപ്പിൽ നിന്ന് ഫയലുകൾ നീക്കാൻ നിങ്ങൾക്ക് കഴിയില്ല."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"ഗ്രിഡ് മോഡിൽ കാണിക്കുന്നു."</string> <string name="list_mode_showing" msgid="1225413902295895166">"ലിസ്റ്റ് മോഡിൽ കാണിക്കുന്നു."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-mn/strings.xml b/res/values-mn/strings.xml index cfbd53e1e..e5d0af2a0 100644 --- a/res/values-mn/strings.xml +++ b/res/values-mn/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Дараахад зөөх"</string> <string name="menu_compress" msgid="37539111904724188">"Шахах"</string> <string name="menu_extract" msgid="8171946945982532262">"Дараахад задлах…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Бүгдийг задлах…"</string> <string name="menu_rename" msgid="1883113442688817554">"Нэр өөрчлөх"</string> <string name="menu_inspect" msgid="7279855349299446224">"Мэдээлэл авах"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Нуусан файлудыг харуул"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Та өөр аппаас файлууд зөөх боломжгүй."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Хүснэгтийн горимд харуулж байна."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Жагсаалтын горимд харуулж байна."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-mr/strings.xml b/res/values-mr/strings.xml index a98a5c279..2579e8d79 100644 --- a/res/values-mr/strings.xml +++ b/res/values-mr/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"यामध्ये हलवा…"</string> <string name="menu_compress" msgid="37539111904724188">"कॉंप्रेस करा"</string> <string name="menu_extract" msgid="8171946945982532262">"मध्ये काढा..."</string> + <string name="menu_extract_all" msgid="7335680068521252718">"सर्व काढा…"</string> <string name="menu_rename" msgid="1883113442688817554">"नाव बदला"</string> <string name="menu_inspect" msgid="7279855349299446224">"माहिती मिळवा"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"लपवलेल्या फाइल दाखवा"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"तुम्ही दुसऱ्या ॲपमधील फाइल हलवू शकत नाही."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"ग्रिड मोडमध्ये दाखवत आहे."</string> <string name="list_mode_showing" msgid="1225413902295895166">"सूची मोडमध्ये दाखवत आहे."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-ms/strings.xml b/res/values-ms/strings.xml index f72d99f10..af2c3d4c1 100644 --- a/res/values-ms/strings.xml +++ b/res/values-ms/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Alihkan ke…"</string> <string name="menu_compress" msgid="37539111904724188">"Mampatkan"</string> <string name="menu_extract" msgid="8171946945982532262">"Ekstrak ke…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Ekstrak semua…"</string> <string name="menu_rename" msgid="1883113442688817554">"Namakan semula"</string> <string name="menu_inspect" msgid="7279855349299446224">"Dapatkan maklumat"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Tunjukkan fail tersembunyi"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Anda tidak boleh mengalihkan fail daripada apl lain."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Ditunjukkan dalam mod grid."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Ditunjukkan dalam mod senarai."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-my/strings.xml b/res/values-my/strings.xml index e9c9f40ba..ea68c88ec 100644 --- a/res/values-my/strings.xml +++ b/res/values-my/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"အောက်ပါနေရာသို့ ရွှေ့ပါ…"</string> <string name="menu_compress" msgid="37539111904724188">"ချုံ့ရန်"</string> <string name="menu_extract" msgid="8171946945982532262">"ရွှေးချယ်ထည့်သွင်းရန်…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"အားလုံးထုတ်ယူရန်…"</string> <string name="menu_rename" msgid="1883113442688817554">"အမည်ပြောင်းပါ"</string> <string name="menu_inspect" msgid="7279855349299446224">"အချက်အလက် ရယူရန်"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"ဝှက်ထားသည့်ဖိုင်များ ပြရန်"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"အခြားအက်ပ်မှဖိုင်ကို ရွှေ့၍မရပါ။"</string> <string name="grid_mode_showing" msgid="2803166871485028508">"ဇယားကွက်မုဒ်ဖြင့် ပြသရန်။"</string> <string name="list_mode_showing" msgid="1225413902295895166">"စာရင်းမုဒ်ဖြင့် ပြသရန်။"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-nb/strings.xml b/res/values-nb/strings.xml index 4fb2e0e36..1c1e6cf0c 100644 --- a/res/values-nb/strings.xml +++ b/res/values-nb/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Flytt til"</string> <string name="menu_compress" msgid="37539111904724188">"Komprimer"</string> <string name="menu_extract" msgid="8171946945982532262">"Pakk ut til …"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Pakk ut alt …"</string> <string name="menu_rename" msgid="1883113442688817554">"Gi nytt navn"</string> <string name="menu_inspect" msgid="7279855349299446224">"Hent informasjon"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Vis skjulte filer"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Du kan ikke flytte filer fra en annen app."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Viser i rutenettmodus."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Viser i listemodus."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-ne/strings.xml b/res/values-ne/strings.xml index 550ae21e0..c52382d79 100644 --- a/res/values-ne/strings.xml +++ b/res/values-ne/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"निम्नमा सार्नुहोस्…"</string> <string name="menu_compress" msgid="37539111904724188">"कम्प्रेस गर्नुहोस्"</string> <string name="menu_extract" msgid="8171946945982532262">"यसमा एकस्ट्र्याक्ट गर्नुहोस्…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"सबै एक्स्ट्रयाक्ट गर्नुहोस्…"</string> <string name="menu_rename" msgid="1883113442688817554">"पुनःनामाकरण गर्नुहोस्"</string> <string name="menu_inspect" msgid="7279855349299446224">"जानकारी प्राप्त गर्नुहोस्"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"लुकाइएका फाइलहरू देखाउनुहोस्"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"तपाईं अर्को एपमा रहेका फाइलहरू सार्न सक्नुहुन्न।"</string> <string name="grid_mode_showing" msgid="2803166871485028508">"ग्रिड मोडमा देखाइँदै।"</string> <string name="list_mode_showing" msgid="1225413902295895166">"सूची मोडमा देखाइँदै।"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-nl/strings.xml b/res/values-nl/strings.xml index fc266ef30..0b720010b 100644 --- a/res/values-nl/strings.xml +++ b/res/values-nl/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Verplaatsen naar…"</string> <string name="menu_compress" msgid="37539111904724188">"Comprimeren"</string> <string name="menu_extract" msgid="8171946945982532262">"Uitpakken naar…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Alles uitpakken…"</string> <string name="menu_rename" msgid="1883113442688817554">"Naam wijzigen"</string> <string name="menu_inspect" msgid="7279855349299446224">"Informatie bekijken"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Verborgen bestanden tonen"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Je kunt geen bestanden verplaatsen vanuit een andere app."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Tonen in rastermodus."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Tonen in lijstmodus."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-or/strings.xml b/res/values-or/strings.xml index 4d3d59831..186dd9b78 100644 --- a/res/values-or/strings.xml +++ b/res/values-or/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"ଏଠାକୁ ନିଅନ୍ତୁ…"</string> <string name="menu_compress" msgid="37539111904724188">"କମ୍ପ୍ରେସ୍ କରନ୍ତୁ"</string> <string name="menu_extract" msgid="8171946945982532262">"ଏଠାକୁ ଏକ୍ସଟ୍ରାକ୍ଟ କରନ୍ତୁ…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"ସବୁ ଏକ୍ସଟ୍ରାକ୍ଟ କରନ୍ତୁ…"</string> <string name="menu_rename" msgid="1883113442688817554">"ରିନେମ କରନ୍ତୁ"</string> <string name="menu_inspect" msgid="7279855349299446224">"ସୂଚନା ପାଆନ୍ତୁ"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"ଲୁକ୍କାୟିତ ଫାଇଲ ଦେଖାନ୍ତୁ"</string> @@ -150,7 +151,7 @@ <item quantity="other"><xliff:g id="COUNT_1">%1$d</xliff:g>ଟି ଆଇଟମ୍ ଡିଲିଟ୍ କରାଯାଉଛି।</item> <item quantity="one"><xliff:g id="COUNT_0">%1$d</xliff:g>ଟି ଆଇଟମ୍ ଡିଲିଟ୍ କରାଯାଉଛି।</item> </plurals> - <string name="undo" msgid="2902438994196400565">"ପୂର୍ବପରି କରନ୍ତୁ"</string> + <string name="undo" msgid="2902438994196400565">"ଅନଡୁ କରନ୍ତୁ"</string> <string name="copy_preparing" msgid="4759516490222449324">"ପ୍ରସ୍ତୁତ କରାଯାଉଛି…"</string> <string name="compress_preparing" msgid="7401605598969019696">"ପ୍ରସ୍ତୁତ କରାଯାଉଛି…"</string> <string name="extract_preparing" msgid="4796626960061745796">"ପ୍ରସ୍ତୁତ କରାଯାଉଛି…"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"ଆପଣ ଅନ୍ୟ ଏକ ଆପରୁ ଫାଇଲଗୁଡ଼ିକ ମୁଭ୍ କରିପାରିବେ ନାହିଁ।"</string> <string name="grid_mode_showing" msgid="2803166871485028508">"ଗ୍ରିଡ୍ ମୋଡରେ ଦେଖାଉଛି।"</string> <string name="list_mode_showing" msgid="1225413902295895166">"ତାଲିକା ମୋଡରେ ଦେଖାଉଛି।"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-pa/strings.xml b/res/values-pa/strings.xml index f1390ce6e..1aec03539 100644 --- a/res/values-pa/strings.xml +++ b/res/values-pa/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"ਇਸ ਵਿੱਚ ਲਿਜਾਓ…"</string> <string name="menu_compress" msgid="37539111904724188">"ਨਪੀੜੋ"</string> <string name="menu_extract" msgid="8171946945982532262">"ਇਸ ਵਿੱਚ ਐਕਸਟ੍ਰੈਕਟ ਕਰੋ…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"ਸਭ ਐਕਸਟਰੈਕਟ ਕਰੋ…"</string> <string name="menu_rename" msgid="1883113442688817554">"ਨਾਮ ਬਦਲੋ"</string> <string name="menu_inspect" msgid="7279855349299446224">"ਜਾਣਕਾਰੀ ਪ੍ਰਾਪਤ ਕਰੋ"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"ਲੁਕਾਈਆਂ ਗਈਆਂ ਫ਼ਾਈਲਾਂ ਦਿਖਾਓ"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"ਤੁਸੀਂ ਕਿਸੇ ਹੋਰ ਐਪ ਤੋਂ ਫ਼ਾਈਲਾਂ ਨਹੀਂ ਲਿਜਾ ਸਕਦੇ।"</string> <string name="grid_mode_showing" msgid="2803166871485028508">"ਗ੍ਰਿਡ ਮੋਡ ਵਿੱਚ ਦਿਖਾਈਆਂ ਜਾ ਰਹੀਆਂ ਹਨ"</string> <string name="list_mode_showing" msgid="1225413902295895166">"ਸੂਚੀ ਮੋਡ ਵਿੱਚ ਦਿਖਾਈਆਂ ਜਾ ਰਹੀਆਂ ਹਨ।"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-pl/strings.xml b/res/values-pl/strings.xml index 4a76fafb5..01c9a7339 100644 --- a/res/values-pl/strings.xml +++ b/res/values-pl/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Przenieś do…"</string> <string name="menu_compress" msgid="37539111904724188">"Skompresuj"</string> <string name="menu_extract" msgid="8171946945982532262">"Rozpakuj do…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Wyodrębnij wszystko…"</string> <string name="menu_rename" msgid="1883113442688817554">"Zmień nazwę"</string> <string name="menu_inspect" msgid="7279855349299446224">"Zobacz informacje"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Pokaż ukryte pliki"</string> @@ -333,4 +334,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Nie można przenosić plików z innej aplikacji."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Wyświetlanie w trybie siatki."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Wyświetlanie w trybie listy."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-pt-rBR/strings.xml b/res/values-pt-rBR/strings.xml index 8dd6249e8..a203d5885 100644 --- a/res/values-pt-rBR/strings.xml +++ b/res/values-pt-rBR/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Mover para…"</string> <string name="menu_compress" msgid="37539111904724188">"Compactar"</string> <string name="menu_extract" msgid="8171946945982532262">"Extrair para…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Extrair tudo…"</string> <string name="menu_rename" msgid="1883113442688817554">"Renomear"</string> <string name="menu_inspect" msgid="7279855349299446224">"Ver informações"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Mostrar arquivos ocultos"</string> @@ -311,4 +312,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Não é possível mover arquivos de outro app."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Exibindo modo de grade."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Exibindo modo de lista."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-pt-rPT/strings.xml b/res/values-pt-rPT/strings.xml index 03a36e95c..f9a0238b3 100644 --- a/res/values-pt-rPT/strings.xml +++ b/res/values-pt-rPT/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Mover para..."</string> <string name="menu_compress" msgid="37539111904724188">"Comprimir"</string> <string name="menu_extract" msgid="8171946945982532262">"Extrair para…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Extrair tudo…"</string> <string name="menu_rename" msgid="1883113442688817554">"Mudar o nome"</string> <string name="menu_inspect" msgid="7279855349299446224">"Obter informações"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Mostrar ficheiros ocultos"</string> @@ -311,4 +312,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Não pode mover ficheiros de outra app."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"A mostrar no modo de grelha…"</string> <string name="list_mode_showing" msgid="1225413902295895166">"A mostrar no modo de lista…"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-pt/strings.xml b/res/values-pt/strings.xml index 8dd6249e8..a203d5885 100644 --- a/res/values-pt/strings.xml +++ b/res/values-pt/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Mover para…"</string> <string name="menu_compress" msgid="37539111904724188">"Compactar"</string> <string name="menu_extract" msgid="8171946945982532262">"Extrair para…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Extrair tudo…"</string> <string name="menu_rename" msgid="1883113442688817554">"Renomear"</string> <string name="menu_inspect" msgid="7279855349299446224">"Ver informações"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Mostrar arquivos ocultos"</string> @@ -311,4 +312,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Não é possível mover arquivos de outro app."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Exibindo modo de grade."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Exibindo modo de lista."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-ro/strings.xml b/res/values-ro/strings.xml index b3bb84146..114b7b5fd 100644 --- a/res/values-ro/strings.xml +++ b/res/values-ro/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Mută în…"</string> <string name="menu_compress" msgid="37539111904724188">"Comprimă"</string> <string name="menu_extract" msgid="8171946945982532262">"Extrage în…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Extrage tot…"</string> <string name="menu_rename" msgid="1883113442688817554">"Redenumește"</string> <string name="menu_inspect" msgid="7279855349299446224">"Vezi informațiile"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Afișează fișierele ascunse"</string> @@ -311,4 +312,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Nu poți muta fișiere din altă aplicație."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Se afișează în modul grilă."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Se afișează în modul listă."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-ru/strings.xml b/res/values-ru/strings.xml index cedb653d5..003dcf581 100644 --- a/res/values-ru/strings.xml +++ b/res/values-ru/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Переместить в..."</string> <string name="menu_compress" msgid="37539111904724188">"Сжать"</string> <string name="menu_extract" msgid="8171946945982532262">"Извлечь"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Извлечь все…"</string> <string name="menu_rename" msgid="1883113442688817554">"Переименовать"</string> <string name="menu_inspect" msgid="7279855349299446224">"Сведения о файле"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Показывать скрытые файлы"</string> @@ -328,9 +329,11 @@ <string name="search_bar_hint" msgid="146031513183888721">"Поиск на устройстве"</string> <string name="delete_search_history" msgid="2202015025607694515">"Очистить историю поиска: <xliff:g id="TEXT">%1$s</xliff:g>"</string> <string name="personal_tab" msgid="3878576287868528503">"Личный"</string> - <string name="work_tab" msgid="7265359366883747413">"Рабочее"</string> + <string name="work_tab" msgid="7265359366883747413">"Рабочий"</string> <string name="a11y_work" msgid="7504431382825242153">"Работа"</string> <string name="drag_from_another_app" msgid="8310249276199969905">"Перемещать файлы из другого приложения нельзя."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Показано в виде таблицы."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Показано в виде списка."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-si/strings.xml b/res/values-si/strings.xml index 3c2b2429e..9ae7e52fd 100644 --- a/res/values-si/strings.xml +++ b/res/values-si/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"වෙත ගෙනයන්න..."</string> <string name="menu_compress" msgid="37539111904724188">"සම්පීඩනය කරන්න"</string> <string name="menu_extract" msgid="8171946945982532262">"උපුටා ගන්න…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"සියල්ල උපුටා ගන්න…"</string> <string name="menu_rename" msgid="1883113442688817554">"යළි නම් කරන්න"</string> <string name="menu_inspect" msgid="7279855349299446224">"තොරතුරු ලබා ගන්න"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"සැඟවුණු ගොනු පෙන්වන්න"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"ඔබට වෙනත් යෙදුමකින් ගොනු ගෙන ඒමට නොහැකිය."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"ජාලක ප්රකාරය තුළ පෙන්වමින්."</string> <string name="list_mode_showing" msgid="1225413902295895166">"ලැයිස්තු ප්රකාරය තුළ පෙන්වමින්."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-sk/strings.xml b/res/values-sk/strings.xml index 8b5e87fbb..e73cc5f0f 100644 --- a/res/values-sk/strings.xml +++ b/res/values-sk/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Presunúť do…"</string> <string name="menu_compress" msgid="37539111904724188">"Komprimovať"</string> <string name="menu_extract" msgid="8171946945982532262">"Rozbaliť do…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Extrahovať všetko…"</string> <string name="menu_rename" msgid="1883113442688817554">"Premenovať"</string> <string name="menu_inspect" msgid="7279855349299446224">"Zobraziť informácie"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Zobraziť skryté súbory"</string> @@ -333,4 +334,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Nemôžete presúvať súbory z inej aplikácie."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Zobrazované v režime mriežky."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Zobrazované v režime zoznamu."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-sl/strings.xml b/res/values-sl/strings.xml index e2faad032..40430030b 100644 --- a/res/values-sl/strings.xml +++ b/res/values-sl/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Premakni v ..."</string> <string name="menu_compress" msgid="37539111904724188">"Stisni"</string> <string name="menu_extract" msgid="8171946945982532262">"Razširi v …"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Razširi vse …"</string> <string name="menu_rename" msgid="1883113442688817554">"Preimenuj"</string> <string name="menu_inspect" msgid="7279855349299446224">"Prikaži informacije"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Prikaži skrite datoteke"</string> @@ -333,4 +334,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Datotek iz druge aplikacije ni mogoče premakniti."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Prikazano v načinu mreže"</string> <string name="list_mode_showing" msgid="1225413902295895166">"Prikazano v načinu seznama"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-sq/strings.xml b/res/values-sq/strings.xml index 5de2e9d3b..66acc554c 100644 --- a/res/values-sq/strings.xml +++ b/res/values-sq/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Zhvendos te..."</string> <string name="menu_compress" msgid="37539111904724188">"Ngjish"</string> <string name="menu_extract" msgid="8171946945982532262">"Nxirre te…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Nxirri të gjitha…"</string> <string name="menu_rename" msgid="1883113442688817554">"Riemërto"</string> <string name="menu_inspect" msgid="7279855349299446224">"Merr informacione"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Shfaq skedarët e fshehur"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Nuk mund t\'i zhvendosësh skedarët nga një aplikacion tjetër."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Po shfaq në modalitetin \"rrjetë\"."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Po shfaq në modalitetin \"listë\"."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-sr/strings.xml b/res/values-sr/strings.xml index 2dcc91983..035bdf97d 100644 --- a/res/values-sr/strings.xml +++ b/res/values-sr/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Премести у…"</string> <string name="menu_compress" msgid="37539111904724188">"Компримуј"</string> <string name="menu_extract" msgid="8171946945982532262">"Издвој у…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Издвоји све…"</string> <string name="menu_rename" msgid="1883113442688817554">"Преименуј"</string> <string name="menu_inspect" msgid="7279855349299446224">"Прикажи информације"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Приказуј скривене датотеке"</string> @@ -311,4 +312,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Не можете да премештате датотеке из друге апликације."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Приказује се у режиму мреже."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Приказује се у режиму листе."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-sv/strings.xml b/res/values-sv/strings.xml index 281612761..0040eaa36 100644 --- a/res/values-sv/strings.xml +++ b/res/values-sv/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Flytta till ..."</string> <string name="menu_compress" msgid="37539111904724188">"Komprimera"</string> <string name="menu_extract" msgid="8171946945982532262">"Extrahera till …"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Extrahera alla …"</string> <string name="menu_rename" msgid="1883113442688817554">"Byt namn"</string> <string name="menu_inspect" msgid="7279855349299446224">"Hämta information"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Visa dolda filer"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Du kan inte flytta filer från en annan app."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Visas i rutnätsläge."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Visas i listläge."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-sw/strings.xml b/res/values-sw/strings.xml index c09e09ff5..18a0bce3e 100644 --- a/res/values-sw/strings.xml +++ b/res/values-sw/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Hamishia kwenye..."</string> <string name="menu_compress" msgid="37539111904724188">"Bana"</string> <string name="menu_extract" msgid="8171946945982532262">"Weka kwenye…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Dondoa zote…"</string> <string name="menu_rename" msgid="1883113442688817554">"Badilisha jina"</string> <string name="menu_inspect" msgid="7279855349299446224">"Pata maelezo zaidi"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Onyesha faili zilizofichwa"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Huwezi kuhamisha faili kutoka programu nyingine."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Kuonyesha katika hali ya gridi."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Kuonyesha katika hali ya orodha."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-ta/strings.xml b/res/values-ta/strings.xml index 1e8b754d7..e4b46e2e0 100644 --- a/res/values-ta/strings.xml +++ b/res/values-ta/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"இங்கு நகர்த்து…"</string> <string name="menu_compress" msgid="37539111904724188">"அளவைக் குறை"</string> <string name="menu_extract" msgid="8171946945982532262">"இங்கு பிரி…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"அனைத்தையும் பிரித்தெடு…"</string> <string name="menu_rename" msgid="1883113442688817554">"பெயர் மாற்று"</string> <string name="menu_inspect" msgid="7279855349299446224">"தகவலைப் பெறு"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"மறைக்கப்பட்ட ஃபைல்களைக் காட்டு"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"வேறொரு ஆப்ஸிலிருந்து ஃபைல்களை நகர்த்த முடியாது."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"கட்டக் காட்சியில் காட்டுகிறது."</string> <string name="list_mode_showing" msgid="1225413902295895166">"பட்டியல் காட்சியில் காட்டுகிறது."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-te/strings.xml b/res/values-te/strings.xml index aff5a7337..178a14435 100644 --- a/res/values-te/strings.xml +++ b/res/values-te/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"ఇక్కడికి తరలించు..."</string> <string name="menu_compress" msgid="37539111904724188">"కుదించు"</string> <string name="menu_extract" msgid="8171946945982532262">"దీనిలోకి సంగ్రహించు…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"అన్నీ ఎక్స్ట్రాక్ట్ చేయండి…"</string> <string name="menu_rename" msgid="1883113442688817554">"పేరు మార్చు"</string> <string name="menu_inspect" msgid="7279855349299446224">"సమాచారాన్ని పొందండి"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"దాచబడిన ఫైళ్లను చూపించు"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"మీరు వేరే ఇతర యాప్ నుండి ఫైల్స్ను తరలించలేరు."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"గ్రిడ్ మోడ్లో చూపుతోంది."</string> <string name="list_mode_showing" msgid="1225413902295895166">"లిస్ట్ మోడ్లో చూపుతోంది."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-th/strings.xml b/res/values-th/strings.xml index b33809db7..6fdee1ef1 100644 --- a/res/values-th/strings.xml +++ b/res/values-th/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"ย้ายไปที่…"</string> <string name="menu_compress" msgid="37539111904724188">"บีบอัด"</string> <string name="menu_extract" msgid="8171946945982532262">"แตกข้อมูลไปยัง…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"แตกเอกสารทั้งหมด…"</string> <string name="menu_rename" msgid="1883113442688817554">"เปลี่ยนชื่อ"</string> <string name="menu_inspect" msgid="7279855349299446224">"รับข้อมูล"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"แสดงไฟล์ที่ซ่อนไว้"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"คุณย้ายไฟล์จากแอปอื่นไม่ได้"</string> <string name="grid_mode_showing" msgid="2803166871485028508">"กำลังแสดงในโหมดตารางกริด"</string> <string name="list_mode_showing" msgid="1225413902295895166">"กำลังแสดงในโหมดรายการ"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-tl/strings.xml b/res/values-tl/strings.xml index dc7719470..a834e85d6 100644 --- a/res/values-tl/strings.xml +++ b/res/values-tl/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Ilipat sa…"</string> <string name="menu_compress" msgid="37539111904724188">"I-compress"</string> <string name="menu_extract" msgid="8171946945982532262">"I-extract sa…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"I-extract lahat…"</string> <string name="menu_rename" msgid="1883113442688817554">"Palitan ang pangalan"</string> <string name="menu_inspect" msgid="7279855349299446224">"Kumuha ng impormasyon"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Ipakita ang hidden files"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Hindi ka makakapaglipat ng mga file mula sa ibang app."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Ipinapakita sa grid mode."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Ipinapakita sa list mode."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-tr/strings.xml b/res/values-tr/strings.xml index f8bbc893b..7d63323c7 100644 --- a/res/values-tr/strings.xml +++ b/res/values-tr/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Klasöre taşı..."</string> <string name="menu_compress" msgid="37539111904724188">"Sıkıştır"</string> <string name="menu_extract" msgid="8171946945982532262">"Şuraya çıkar:"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Tümünü çıkar…"</string> <string name="menu_rename" msgid="1883113442688817554">"Yeniden adlandır"</string> <string name="menu_inspect" msgid="7279855349299446224">"Bilgi al"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Gizli dosyaları göster"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Başka bir uygulamadan dosya taşıyamazsınız."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Tablo modunda gösteriliyor."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Liste modunda gösteriliyor."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-uk/strings.xml b/res/values-uk/strings.xml index 170e4fc11..ff0908cd1 100644 --- a/res/values-uk/strings.xml +++ b/res/values-uk/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Перемістити в…"</string> <string name="menu_compress" msgid="37539111904724188">"Стиснути"</string> <string name="menu_extract" msgid="8171946945982532262">"Розпакувати…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Розархівувати все…"</string> <string name="menu_rename" msgid="1883113442688817554">"Перейменувати"</string> <string name="menu_inspect" msgid="7279855349299446224">"Переглянути інформацію"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Показувати приховані файли"</string> @@ -333,4 +334,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Не можна переміщувати файли з іншого додатка."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Перегляд у режимі таблиці."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Перегляд у режимі списку."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-ur/strings.xml b/res/values-ur/strings.xml index b0ee0558b..8e18775b3 100644 --- a/res/values-ur/strings.xml +++ b/res/values-ur/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"اس میں منتقل کریں…"</string> <string name="menu_compress" msgid="37539111904724188">"کمپریس کریں"</string> <string name="menu_extract" msgid="8171946945982532262">"اس میں کھولیں۔۔۔"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"سبھی نکالیں…"</string> <string name="menu_rename" msgid="1883113442688817554">"نام تبدیل کریں"</string> <string name="menu_inspect" msgid="7279855349299446224">"معلومات حاصل کریں"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"پوشیدہ فائلز دکھائیں"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"آپ کسی اور ایپ سے فائلیں منتقل نہیں کر سکتے ہیں۔"</string> <string name="grid_mode_showing" msgid="2803166871485028508">"گرڈ وضع میں دکھائی جا رہی ہیں۔"</string> <string name="list_mode_showing" msgid="1225413902295895166">"فہرست وضع میں دکھائی جا رہی ہیں۔"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-uz/strings.xml b/res/values-uz/strings.xml index 02bdfb3a2..28ef40fd0 100644 --- a/res/values-uz/strings.xml +++ b/res/values-uz/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Boshqa joyga olish…"</string> <string name="menu_compress" msgid="37539111904724188">"Arxivlash"</string> <string name="menu_extract" msgid="8171946945982532262">"Arxivdan chiqarish"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Barchasini ajratish…"</string> <string name="menu_rename" msgid="1883113442688817554">"Qayta nomlash"</string> <string name="menu_inspect" msgid="7279855349299446224">"Axborot olish"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Yashirin fayllarni chiqarish"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Fayllarni boshqa ilovadan koʻchirish imkonsiz."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Jadval shaklida chiqarilmoqda."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Roʻyxat shaklida chiqarilmoqda."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-vi/strings.xml b/res/values-vi/strings.xml index 7f922d0fc..06812bf71 100644 --- a/res/values-vi/strings.xml +++ b/res/values-vi/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Chuyển tới..."</string> <string name="menu_compress" msgid="37539111904724188">"Nén"</string> <string name="menu_extract" msgid="8171946945982532262">"Trích xuất sang…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Trích xuất tất cả…"</string> <string name="menu_rename" msgid="1883113442688817554">"Đổi tên"</string> <string name="menu_inspect" msgid="7279855349299446224">"Xem thông tin"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Hiện các tệp bị ẩn"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Bạn không thể di chuyển các tệp từ một ứng dụng khác."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Đang hiển thị ở chế độ lưới."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Đang hiển thị ở chế độ danh sách."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-zh-rCN/strings.xml b/res/values-zh-rCN/strings.xml index 992c57151..262899eaf 100644 --- a/res/values-zh-rCN/strings.xml +++ b/res/values-zh-rCN/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"移至…"</string> <string name="menu_compress" msgid="37539111904724188">"压缩"</string> <string name="menu_extract" msgid="8171946945982532262">"解压到…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"提取全部…"</string> <string name="menu_rename" msgid="1883113442688817554">"重命名"</string> <string name="menu_inspect" msgid="7279855349299446224">"获取信息"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"显示隐藏的文件"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"您无法移动其他应用中的文件。"</string> <string name="grid_mode_showing" msgid="2803166871485028508">"目前以网格模式显示。"</string> <string name="list_mode_showing" msgid="1225413902295895166">"目前以列表模式显示。"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-zh-rHK/strings.xml b/res/values-zh-rHK/strings.xml index eec2956d0..5412fd3f6 100644 --- a/res/values-zh-rHK/strings.xml +++ b/res/values-zh-rHK/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"移至…"</string> <string name="menu_compress" msgid="37539111904724188">"壓縮"</string> <string name="menu_extract" msgid="8171946945982532262">"壓縮至…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"擷取全部…"</string> <string name="menu_rename" msgid="1883113442688817554">"重新命名"</string> <string name="menu_inspect" msgid="7279855349299446224">"顯示資訊"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"顯示已隱藏的檔案"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"你無法移動其他應用程式的檔案。"</string> <string name="grid_mode_showing" msgid="2803166871485028508">"用格狀模式顯示緊。"</string> <string name="list_mode_showing" msgid="1225413902295895166">"用清單模式顯示緊。"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-zh-rTW/strings.xml b/res/values-zh-rTW/strings.xml index 16d0a7305..4f933af44 100644 --- a/res/values-zh-rTW/strings.xml +++ b/res/values-zh-rTW/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"移至…"</string> <string name="menu_compress" msgid="37539111904724188">"壓縮"</string> <string name="menu_extract" msgid="8171946945982532262">"解壓縮到…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"全部解壓縮…"</string> <string name="menu_rename" msgid="1883113442688817554">"重新命名"</string> <string name="menu_inspect" msgid="7279855349299446224">"取得資訊"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"顯示隱藏的檔案"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"你無法將其他應用程式中的檔案移動至此。"</string> <string name="grid_mode_showing" msgid="2803166871485028508">"目前以格線模式顯示。"</string> <string name="list_mode_showing" msgid="1225413902295895166">"目前以清單模式顯示。"</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values-zu/strings.xml b/res/values-zu/strings.xml index 83a654282..35483b5c1 100644 --- a/res/values-zu/strings.xml +++ b/res/values-zu/strings.xml @@ -45,6 +45,7 @@ <string name="menu_move" msgid="2310760789561129882">"Hambisa ku…"</string> <string name="menu_compress" msgid="37539111904724188">"Cindezela"</string> <string name="menu_extract" msgid="8171946945982532262">"Khiphela ku…"</string> + <string name="menu_extract_all" msgid="7335680068521252718">"Khipha konke…"</string> <string name="menu_rename" msgid="1883113442688817554">"Qamba kabusha"</string> <string name="menu_inspect" msgid="7279855349299446224">"Thola ulwazi"</string> <string name="menu_show_hidden_files" msgid="5140676344684492769">"Bonisa amafayela afihliwe"</string> @@ -289,4 +290,6 @@ <string name="drag_from_another_app" msgid="8310249276199969905">"Awukwazi ukuhambisa amafayela kusuka kolunye uhlelo lokusebenza."</string> <string name="grid_mode_showing" msgid="2803166871485028508">"Ibonisa kumodi yegridi."</string> <string name="list_mode_showing" msgid="1225413902295895166">"Ibonisa kumodi yohlu."</string> + <!-- no translation found for bullet (5606740650312122766) --> + <skip /> </resources> diff --git a/res/values/strings.xml b/res/values/strings.xml index 89e40ac4c..fc97d2c7d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -78,6 +78,12 @@ <string name="menu_compress">Compress</string> <!-- Menu item title that extracts the selected documents [CHAR LIMIT=28] --> <string name="menu_extract">Extract to\u2026</string> + <!-- Menu item title that extracts all the documents in the current directory [CHAR LIMIT=28] --> + <string name="menu_extract_all">Extract all\u2026</string> + <!-- Menu item title that extracts the contents of the selected archive [CHAR LIMIT=28] --> + <string name="menu_extract_here">Extract here</string> + <!-- Menu item title that browses the contents of the selected archive [CHAR LIMIT=28] --> + <string name="menu_browse">Browse</string> <!-- Menu item that renames the selected document [CHAR LIMIT=28] --> <string name="menu_rename">Rename</string> <!-- Menu item that displays properties about the selected document [CHAR LIMIT=28] --> @@ -407,6 +413,23 @@ during a copy. [CHAR LIMIT=48] --> <string name="notification_copy_files_converted_title">Some files were converted</string> + <string name="copy_in_progress" translatable="false">{count, plural, + =1 {Copying <xliff:g id="filename" example="foobar.txt">{filename}</xliff:g> to <xliff:g id="directory" example="example folder">{directory}</xliff:g>} + other {Copying # files to <xliff:g id="directory" example="example folder">{directory}</xliff:g>} + }</string> + <string name="move_in_progress" translatable="false">{count, plural, + =1 {Moving <xliff:g id="filename" example="foobar.txt">{filename}</xliff:g> to <xliff:g id="directory" example="example folder">{directory}</xliff:g>} + other {Moving # files to <xliff:g id="directory" example="example folder">{directory}</xliff:g>} + }</string> + <string name="delete_in_progress" translatable="false">{count, plural, + =1 {Deleting <xliff:g id="filename" example="foobar.txt">{filename}</xliff:g>} + other {Deleting # files} + }</string> + <string name="compress_in_progress" translatable="false">{count, plural, + =1 {Zipping <xliff:g id="filename" example="foobar.txt">{filename}</xliff:g>} + other {Zipping # files} + }</string> + <!-- Text in an alert dialog asking user to grant app access to a given directory in an external storage volume --> <string name="open_external_dialog_request">Grant <xliff:g id="appName" example="System Settings"><b>^1</b></xliff:g> access to <xliff:g id="directory" example="Pictures"><i>^2</i></xliff:g> directory on @@ -585,4 +608,7 @@ <!-- Accessibility announcement when switching to list mode of files and directories shown. [CHAR_LIMIT=100] --> <string name="list_mode_showing">Showing in list mode.</string> + <!-- Unicode Character “•” (U+2022). --> + <string name="bullet">\u2022</string> + </resources> diff --git a/src/com/android/documentsui/AbstractActionHandler.java b/src/com/android/documentsui/AbstractActionHandler.java index 2f64ebf64..de193e235 100644 --- a/src/com/android/documentsui/AbstractActionHandler.java +++ b/src/com/android/documentsui/AbstractActionHandler.java @@ -19,6 +19,8 @@ package com.android.documentsui; import static com.android.documentsui.base.DocumentInfo.getCursorInt; import static com.android.documentsui.base.DocumentInfo.getCursorString; import static com.android.documentsui.base.SharedMinimal.DEBUG; +import static com.android.documentsui.util.FlagUtils.isDesktopFileHandlingFlagEnabled; +import static com.android.documentsui.util.FlagUtils.isUseSearchV2RwFlagEnabled; import android.app.PendingIntent; import android.content.ActivityNotFoundException; @@ -37,6 +39,7 @@ import android.util.Log; import android.util.Pair; import android.view.DragEvent; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import androidx.fragment.app.FragmentActivity; import androidx.loader.app.LoaderManager.LoaderCallbacks; @@ -62,6 +65,9 @@ import com.android.documentsui.dirlist.AnimationView.AnimationType; import com.android.documentsui.dirlist.FocusHandler; import com.android.documentsui.files.LauncherActivity; import com.android.documentsui.files.QuickViewIntentBuilder; +import com.android.documentsui.loaders.FolderLoader; +import com.android.documentsui.loaders.QueryOptions; +import com.android.documentsui.loaders.SearchLoader; import com.android.documentsui.queries.SearchViewManager; import com.android.documentsui.roots.GetRootDocumentTask; import com.android.documentsui.roots.LoadFirstRootTask; @@ -72,10 +78,14 @@ import com.android.documentsui.sorting.SortListFragment; import com.android.documentsui.ui.DialogController; import com.android.documentsui.ui.Snackbars; +import java.time.Duration; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; import java.util.function.Consumer; @@ -251,7 +261,12 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA } @Override - public void showInspector(DocumentInfo doc) { + public void openDocumentViewOnly(DocumentInfo doc) { + throw new UnsupportedOperationException("Open doc not supported!"); + } + + @Override + public void showPreview(DocumentInfo doc) { throw new UnsupportedOperationException("Can't open properties."); } @@ -560,6 +575,15 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA if (doc.isWriteSupported()) { flags |= Intent.FLAG_GRANT_WRITE_URI_PERMISSION; } + // On desktop users expect files to open in a new window. + if (isDesktopFileHandlingFlagEnabled()) { + // The combination of NEW_DOCUMENT and MULTIPLE_TASK allows multiple instances of the + // same activity to open in separate windows. + flags |= Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK; + // If the activity has documentLaunchMode="never", NEW_TASK forces the activity to still + // open in a new window. + flags |= Intent.FLAG_ACTIVITY_NEW_TASK; + } intent.setFlags(flags); return intent; @@ -879,16 +903,28 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA private final class LoaderBindings implements LoaderCallbacks<DirectoryResult> { + private ExecutorService mExecutorService = null; + private static final long MAX_SEARCH_TIME_MS = 3000; + private static final int MAX_RESULTS = 500; + + @NonNull @Override public Loader<DirectoryResult> onCreateLoader(int id, Bundle args) { - Context context = mActivity; - // If document stack is not initialized, i.e. if the root is null, create "Recents" root // with the selected user. if (!mState.stack.isInitialized()) { mState.stack.changeRoot(mActivity.getCurrentRoot()); } + if (isUseSearchV2RwFlagEnabled()) { + return onCreateLoaderV2(id, args); + } + return onCreateLoaderV1(id, args); + } + + private Loader<DirectoryResult> onCreateLoaderV1(int id, Bundle args) { + Context context = mActivity; + if (mState.stack.isRecents()) { final LockingContentObserver observer = new LockingContentObserver( mContentLock, AbstractActionHandler.this::loadDocumentsForCurrentStack); @@ -965,6 +1001,69 @@ public abstract class AbstractActionHandler<T extends FragmentActivity & CommonA } } + private Loader<DirectoryResult> onCreateLoaderV2(int id, Bundle args) { + if (mExecutorService == null) { + // TODO(b:388130971): Fine tune the size of the thread pool. + mExecutorService = Executors.newFixedThreadPool( + GlobalSearchLoader.MAX_OUTSTANDING_TASK); + } + DocumentStack stack = mState.stack; + RootInfo root = stack.getRoot(); + List<UserId> userIdList = DocumentsApplication.getUserIdManager(mActivity).getUserIds(); + + Duration lastModifiedDelta = stack.isRecents() + ? Duration.ofMillis(RecentsLoader.REJECT_OLDER_THAN) + : null; + int maxResults = (root == null || root.isRecents()) + ? RecentsLoader.MAX_DOCS_FROM_ROOT : MAX_RESULTS; + QueryOptions options = new QueryOptions( + maxResults, lastModifiedDelta, Duration.ofMillis(MAX_SEARCH_TIME_MS), + mState.showHiddenFiles, mState.acceptMimes, mSearchMgr.buildQueryArgs()); + + if (stack.isRecents() || mSearchMgr.isSearching()) { + Log.d(TAG, "Creating search loader V2"); + // For search and recent we create an observer that restart the loader every time + // one of the searched content providers reports a change. + final LockingContentObserver observer = new LockingContentObserver( + mContentLock, AbstractActionHandler.this::loadDocumentsForCurrentStack); + Collection<RootInfo> rootList = new ArrayList<>(); + if (stack.isRecents()) { + // TODO(b:381346575): Pass roots based on user selection. + rootList.addAll(mProviders.getMatchingRootsBlocking(mState).stream().filter( + r -> r.supportsSearch() && r.authority != null + && r.rootId != null).toList()); + } else { + rootList.add(root); + } + return new SearchLoader( + mActivity, + userIdList, + mInjector.fileTypeLookup, + observer, + rootList, + mSearchMgr.getCurrentSearch(), + options, + mState.sortModel, + mExecutorService + ); + } + Log.d(TAG, "Creating folder loader V2"); + // For folder scan we pass the content lock to the loader so that it can register + // an a callback to its internal method that forces a reload of the folder, every + // time the content provider reports a change. + return new FolderLoader( + mActivity, + userIdList, + mInjector.fileTypeLookup, + mContentLock, + root, + stack.peek(), + options, + mState.sortModel + ); + + } + @Override public void onLoadFinished(Loader<DirectoryResult> loader, DirectoryResult result) { if (DEBUG) { diff --git a/src/com/android/documentsui/ActionHandler.java b/src/com/android/documentsui/ActionHandler.java index 15124eb33..190f5d960 100644 --- a/src/com/android/documentsui/ActionHandler.java +++ b/src/com/android/documentsui/ActionHandler.java @@ -118,7 +118,7 @@ public interface ActionHandler { void showCreateDirectoryDialog(); - void showInspector(DocumentInfo doc); + void showPreview(DocumentInfo doc); @Nullable DocumentInfo renameDocument(String name, DocumentInfo document); @@ -129,6 +129,12 @@ public interface ActionHandler { boolean openItem(ItemDetails<String> doc, @ViewType int type, @ViewType int fallback); /** + * Similar to openItem but takes DocumentInfo instead of DocumentItemDetails and uses + * VIEW_TYPE_VIEW with no fallback. + */ + void openDocumentViewOnly(DocumentInfo doc); + + /** * This is called when user hovers over a doc for enough time during a drag n' drop, to open a * folder that accepts drop. We should only open a container that's not an archive, since archives * do not accept dropping. diff --git a/src/com/android/documentsui/ActionModeController.java b/src/com/android/documentsui/ActionModeController.java index 1bd4eea3c..931942fca 100644 --- a/src/com/android/documentsui/ActionModeController.java +++ b/src/com/android/documentsui/ActionModeController.java @@ -17,6 +17,7 @@ package com.android.documentsui; import static com.android.documentsui.base.SharedMinimal.DEBUG; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; import android.app.Activity; import android.util.Log; @@ -38,6 +39,8 @@ import com.android.documentsui.ui.MessageBuilder; /** * A controller that listens to selection changes and manages life cycles of action modes. + * TODO(b/379776735): This class (and action mode in general) is no longer in use when the + * use_material3 flag is enabled. Remove the class once the flag is rolled out. */ public class ActionModeController extends SelectionObserver<String> implements ActionMode.Callback, ActionModeAddons { @@ -134,8 +137,12 @@ public class ActionModeController extends SelectionObserver<String> mActivity.getWindow().setTitle(mActivity.getTitle()); // Re-enable TalkBack for the toolbars, as they are no longer covered by action mode. + int[] toolbarIds = + isUseMaterial3FlagEnabled() + ? new int[] {R.id.toolbar} + : new int[] {R.id.toolbar, R.id.roots_toolbar}; mScope.accessibilityImportanceSetter.setAccessibilityImportance( - View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, R.id.toolbar, R.id.roots_toolbar); + View.IMPORTANT_FOR_ACCESSIBILITY_AUTO, toolbarIds); mNavigator.setActionModeActivated(false); } @@ -151,10 +158,13 @@ public class ActionModeController extends SelectionObserver<String> // Hide the toolbars if action mode is enabled, so TalkBack doesn't navigate to // these controls when using linear navigation. + int[] toolbarIds = + isUseMaterial3FlagEnabled() + ? new int[] {R.id.toolbar} + : new int[] {R.id.toolbar, R.id.roots_toolbar}; mScope.accessibilityImportanceSetter.setAccessibilityImportance( View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS, - R.id.toolbar, - R.id.roots_toolbar); + toolbarIds); return true; } diff --git a/src/com/android/documentsui/BaseActivity.java b/src/com/android/documentsui/BaseActivity.java index b3439d585..0b5d96da9 100644 --- a/src/com/android/documentsui/BaseActivity.java +++ b/src/com/android/documentsui/BaseActivity.java @@ -19,7 +19,7 @@ package com.android.documentsui; import static com.android.documentsui.base.Shared.EXTRA_BENCHMARK; import static com.android.documentsui.base.SharedMinimal.DEBUG; import static com.android.documentsui.base.State.MODE_GRID; -import static com.android.documentsui.flags.Flags.useMaterial3; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; import android.content.Context; import android.content.Intent; @@ -79,6 +79,7 @@ import com.android.documentsui.sorting.SortModel; import com.android.modules.utils.build.SdkLevel; import com.google.android.material.appbar.AppBarLayout; +import com.google.android.material.button.MaterialButton; import com.google.android.material.color.DynamicColors; import java.util.ArrayList; @@ -183,7 +184,7 @@ public abstract class BaseActivity // in case Activity continuously encounter resource not found exception. getTheme().applyStyle(R.style.DocumentsDefaultTheme, false); - if (useMaterial3() && SdkLevel.isAtLeastS()) { + if (isUseMaterial3FlagEnabled() && SdkLevel.isAtLeastS()) { DynamicColors.applyToActivityIfAvailable(this); } @@ -204,6 +205,16 @@ public abstract class BaseActivity mDrawer = DrawerController.create(this, mInjector.config); Metrics.logActivityLaunch(mState, intent); + if (isUseMaterial3FlagEnabled()) { + View navRailRoots = findViewById(R.id.nav_rail_container_roots); + if (navRailRoots != null) { + // Bind event listener for the burger menu on nav rail. + MaterialButton burgerMenu = findViewById(R.id.nav_rail_burger_menu); + burgerMenu.setOnClickListener(v -> mDrawer.setOpen(true)); + burgerMenu.setOnFocusChangeListener(this::onBurgerMenuFocusChange); + } + } + mProviders = DocumentsApplication.getProvidersCache(this); mDocs = DocumentsAccess.create(this, mState); @@ -358,6 +369,14 @@ public abstract class BaseActivity if (roots != null) { roots.onSelectedUserChanged(); } + if (isUseMaterial3FlagEnabled()) { + final RootsFragment navRailRoots = + RootsFragment.getNavRail(getSupportFragmentManager()); + if (navRailRoots != null) { + navRailRoots.onSelectedUserChanged(); + } + } + if (mState.stack.size() <= 1) { // We do not load cross-profile root if the stack contains two documents. The @@ -378,6 +397,13 @@ public abstract class BaseActivity }); mSortController = SortController.create(this, mState.derivedMode, mState.sortModel); + if (isUseMaterial3FlagEnabled()) { + View previewIconPlaceholder = findViewById(R.id.preview_icon_placeholder); + if (previewIconPlaceholder != null) { + previewIconPlaceholder.setVisibility( + mState.shouldShowPreview() ? View.VISIBLE : View.GONE); + } + } mPreferencesMonitor = new PreferencesMonitor( getApplicationContext().getPackageName(), @@ -393,13 +419,27 @@ public abstract class BaseActivity private NavigationViewManager getNavigationViewManager(Breadcrumb breadcrumb, View profileTabsContainer) { if (mConfigStore.isPrivateSpaceInDocsUIEnabled()) { - return new NavigationViewManager(this, mDrawer, mState, this, breadcrumb, - profileTabsContainer, DocumentsApplication.getUserManagerState(this), - mConfigStore); + return new NavigationViewManager( + this, + mDrawer, + mState, + this, + breadcrumb, + profileTabsContainer, + DocumentsApplication.getUserManagerState(this), + mConfigStore, + mInjector); } - return new NavigationViewManager(this, mDrawer, mState, this, breadcrumb, - profileTabsContainer, DocumentsApplication.getUserIdManager(this), - mConfigStore); + return new NavigationViewManager( + this, + mDrawer, + mState, + this, + breadcrumb, + profileTabsContainer, + DocumentsApplication.getUserIdManager(this), + mConfigStore, + mInjector); } public void onPreferenceChanged(String pref) { @@ -413,14 +453,21 @@ public abstract class BaseActivity protected void onPostCreate(Bundle savedInstanceState) { super.onPostCreate(savedInstanceState); - mRootsMonitor = new RootsMonitor<>( - this, - mInjector.actions, - mProviders, - mDocs, - mState, - mSearchManager, - mInjector.actionModeController::finishActionMode); + Runnable finishActionMode = + (isUseMaterial3FlagEnabled()) + ? mNavigator::closeSelectionBar + : mInjector.actionModeController::finishActionMode; + + mRootsMonitor = + new RootsMonitor<>( + this, + mInjector.actions, + mProviders, + mDocs, + mState, + mSearchManager, + finishActionMode); + mRootsMonitor.start(); } @@ -432,6 +479,13 @@ public abstract class BaseActivity @Override public boolean onCreateOptionsMenu(Menu menu) { + if (isUseMaterial3FlagEnabled()) { + // In Material3 the menu is now inflated in the `NavigationViewMenu`. This is currently + // to allow for us to inflate between the action_menu and the activity menu. Once the + // Material 3 flag is removed, the menus will be merged and we can rely on this single + // inflation point. + return super.onCreateOptionsMenu(menu); + } boolean showMenu = super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.activity, menu); @@ -440,9 +494,10 @@ public abstract class BaseActivity boolean showSearchBar = getResources().getBoolean(R.bool.show_search_bar); mSearchManager.install(menu, fullBarSearch, showSearchBar); + // Remove the subMenu when material3 is launched b/379776735. final ActionMenuView subMenuView = findViewById(R.id.sub_menu); // If size is 0, it means the menu has not inflated and it should only do once. - if (subMenuView.getMenu().size() == 0) { + if (subMenuView != null && subMenuView.getMenu().size() == 0) { subMenuView.setOnMenuItemClickListener(this::onOptionsItemSelected); getMenuInflater().inflate(R.menu.sub_menu, subMenuView.getMenu()); } @@ -454,9 +509,17 @@ public abstract class BaseActivity @CallSuper public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); - mSearchManager.showMenu(mState.stack); - final ActionMenuView subMenuView = findViewById(R.id.sub_menu); - mInjector.menuManager.updateSubMenu(subMenuView.getMenu()); + // Remove the subMenu when material3 is launched b/379776735. + if (isUseMaterial3FlagEnabled()) { + if (mNavigator != null) { + mNavigator.updateActionMenu(); + } + } else { + mSearchManager.showMenu(mState.stack); + final ActionMenuView subMenuView = findViewById(R.id.sub_menu); + mInjector.menuManager.updateSubMenu(subMenuView.getMenu()); + } + return true; } @@ -510,11 +573,16 @@ public abstract class BaseActivity root.setPadding(insets.getSystemWindowInsetLeft(), insets.getSystemWindowInsetTop(), insets.getSystemWindowInsetRight(), 0); - View saveContainer = findViewById(R.id.container_save); - saveContainer.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom()); + // When use_material3 flag is ON, no additional bottom gap in full screen mode. + if (!isUseMaterial3FlagEnabled()) { + View saveContainer = findViewById(R.id.container_save); + saveContainer.setPadding( + 0, 0, 0, insets.getSystemWindowInsetBottom()); - View rootsContainer = findViewById(R.id.container_roots); - rootsContainer.setPadding(0, 0, 0, insets.getSystemWindowInsetBottom()); + View rootsContainer = findViewById(R.id.container_roots); + rootsContainer.setPadding( + 0, 0, 0, insets.getSystemWindowInsetBottom()); + } return insets.consumeSystemWindowInsets(); }); @@ -549,7 +617,11 @@ public abstract class BaseActivity return; } - mInjector.actionModeController.finishActionMode(); + if (isUseMaterial3FlagEnabled()) { + mNavigator.closeSelectionBar(); + } else { + mInjector.actionModeController.finishActionMode(); + } mSortController.onViewModeChanged(mState.derivedMode); // Set summary header's visibility. Only recents and downloads root may have summary in @@ -648,6 +720,10 @@ public abstract class BaseActivity mNavigator.update(); } + public final NavigationViewManager getNavigator() { + return mNavigator; + } + @Override public void restoreRootAndDirectory() { // We're trying to restore stuff in document stack from saved instance. If we didn't have a @@ -683,6 +759,13 @@ public abstract class BaseActivity if (roots != null) { roots.onCurrentRootChanged(); } + if (isUseMaterial3FlagEnabled()) { + final RootsFragment navRailRoots = + RootsFragment.getNavRail(getSupportFragmentManager()); + if (navRailRoots != null) { + navRailRoots.onCurrentRootChanged(); + } + } String appName = getString(R.string.files_label); String currentTitle = getTitle() != null ? getTitle().toString() : ""; @@ -759,8 +842,13 @@ public abstract class BaseActivity LocalPreferences.setViewMode(this, getCurrentRoot(), mode); mState.derivedMode = mode; - final ActionMenuView subMenuView = findViewById(R.id.sub_menu); - mInjector.menuManager.updateSubMenu(subMenuView.getMenu()); + // Remove the subMenu when material3 is launched b/379776735. + if (isUseMaterial3FlagEnabled()) { + mInjector.menuManager.updateSubMenu(null); + } else { + final ActionMenuView subMenuView = findViewById(R.id.sub_menu); + mInjector.menuManager.updateSubMenu(subMenuView.getMenu()); + } DirectoryFragment dir = getDirectoryFragment(); if (dir != null) { @@ -793,6 +881,7 @@ public abstract class BaseActivity * @param shouldHideHeader whether to hide header container or not */ public void updateHeader(boolean shouldHideHeader) { + // Remove headContainer when material3 is launched. b/379776735. View headerContainer = findViewById(R.id.header_container); if (headerContainer == null) { updateHeaderTitle(); @@ -840,8 +929,11 @@ public abstract class BaseActivity break; } + // Remove the headerTitle when material3 is launched b/379776735. TextView headerTitle = findViewById(R.id.header_title); - headerTitle.setText(result); + if (headerTitle != null) { + headerTitle.setText(result); + } } private String getHeaderRecentTitle() { @@ -1081,4 +1173,19 @@ public abstract class BaseActivity } setRecentsScreenshotEnabled(!mUserManagerState.areHiddenInQuietModeProfilesPresent()); } + + /** + * When the burger menu is focused, adding a focus ring indicator using Stroke. + * TODO(b/381957932): Remove this once Material Button supports focus ring. + */ + private void onBurgerMenuFocusChange(View v, boolean hasFocus) { + MaterialButton burgerMenu = (MaterialButton) v; + if (hasFocus) { + final int focusRingWidth = getResources() + .getDimensionPixelSize(R.dimen.focus_ring_width); + burgerMenu.setStrokeWidth(focusRingWidth); + } else { + burgerMenu.setStrokeWidth(0); + } + } } diff --git a/src/com/android/documentsui/DrawerController.java b/src/com/android/documentsui/DrawerController.java index 56b3a879f..cc90c9869 100644 --- a/src/com/android/documentsui/DrawerController.java +++ b/src/com/android/documentsui/DrawerController.java @@ -17,6 +17,7 @@ package com.android.documentsui; import static com.android.documentsui.base.SharedMinimal.DEBUG; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; import android.app.Activity; import android.util.Log; @@ -59,6 +60,8 @@ public abstract class DrawerController implements DrawerListener { } View drawer = activity.findViewById(R.id.drawer_roots); + // This will be null when use_material3 flag is ON, we will check the flag when it's used in + // RuntimeDrawerController. Toolbar toolbar = (Toolbar) activity.findViewById(R.id.roots_toolbar); drawer.getLayoutParams().width = calculateDrawerWidth(activity); @@ -124,7 +127,10 @@ public abstract class DrawerController implements DrawerListener { if (activityConfig.dragAndDropEnabled()) { View edge = layout.findViewById(R.id.drawer_edge); - edge.setOnDragListener(new ItemDragListener<>(this, SPRING_TIMEOUT)); + // nav_rail_layout also uses DrawerLayout, but it doesn't have drawer edge. + if (edge != null) { + edge.setOnDragListener(new ItemDragListener<>(this, SPRING_TIMEOUT)); + } } } @@ -202,7 +208,9 @@ public abstract class DrawerController implements DrawerListener { @Override void setTitle(String title) { - mToolbar.setTitle(title); + if (!isUseMaterial3FlagEnabled()) { + mToolbar.setTitle(title); + } } @Override diff --git a/src/com/android/documentsui/Injector.java b/src/com/android/documentsui/Injector.java index 82cbfcccb..6b68ba1f1 100644 --- a/src/com/android/documentsui/Injector.java +++ b/src/com/android/documentsui/Injector.java @@ -15,6 +15,8 @@ */ package com.android.documentsui; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; + import static java.lang.annotation.ElementType.FIELD; import static java.lang.annotation.RetentionPolicy.SOURCE; @@ -127,6 +129,9 @@ public class Injector<T extends ActionHandler> { public final ActionModeController getActionModeController( SelectionDetails selectionDetails, EventHandler<MenuItem> menuItemClicker) { + if (isUseMaterial3FlagEnabled()) { + return null; + } return actionModeController.reset(selectionDetails, menuItemClicker); } diff --git a/src/com/android/documentsui/MenuManager.java b/src/com/android/documentsui/MenuManager.java index f46ffe482..5f17d7e02 100644 --- a/src/com/android/documentsui/MenuManager.java +++ b/src/com/android/documentsui/MenuManager.java @@ -16,6 +16,9 @@ package com.android.documentsui; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; +import static com.android.documentsui.util.FlagUtils.isZipNgFlagEnabled; + import android.view.KeyboardShortcutGroup; import android.view.Menu; import android.view.MenuInflater; @@ -25,6 +28,7 @@ import android.view.View; import androidx.annotation.VisibleForTesting; import androidx.fragment.app.Fragment; +import com.android.documentsui.archives.ArchivesProvider; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.Menus; import com.android.documentsui.base.RootInfo; @@ -89,6 +93,9 @@ public abstract class MenuManager { return; } updateCreateDir(mOptionMenu.findItem(R.id.option_menu_create_dir)); + if (isZipNgFlagEnabled()) { + updateExtractAll(mOptionMenu.findItem(R.id.option_menu_extract_all)); + } updateSettings(mOptionMenu.findItem(R.id.option_menu_settings)); updateSelectAll(mOptionMenu.findItem(R.id.option_menu_select_all)); updateNewWindow(mOptionMenu.findItem(R.id.option_menu_new_window)); @@ -98,12 +105,25 @@ public abstract class MenuManager { updateLauncher(mOptionMenu.findItem(R.id.option_menu_launcher)); updateShowHiddenFiles(mOptionMenu.findItem(R.id.option_menu_show_hidden_files)); + if (isUseMaterial3FlagEnabled()) { + updateModePicker(mOptionMenu.findItem(R.id.sub_menu_grid), + mOptionMenu.findItem(R.id.sub_menu_list)); + } + Menus.disableHiddenItems(mOptionMenu); mSearchManager.updateMenu(); } public void updateSubMenu(Menu menu) { + // Remove the subMenu when material3 is launched b/379776735. + if (isUseMaterial3FlagEnabled()) { + menu = mOptionMenu; + if (menu == null) { + return; + } + } updateModePicker(menu.findItem(R.id.sub_menu_grid), menu.findItem(R.id.sub_menu_list)); + } public void updateModel(Model model) {} @@ -214,10 +234,7 @@ public abstract class MenuManager { Menus.setEnabledAndVisible(inspect, selectionDetails.size() == 1); - final MenuItem compress = menu.findItem(R.id.dir_menu_compress); - if (compress != null) { - updateCompress(compress, selectionDetails); - } + updateCompress(menu.findItem(R.id.dir_menu_compress), selectionDetails); } /** @@ -382,6 +399,10 @@ public abstract class MenuManager { Menus.setEnabledAndVisible(launcher, false); } + protected void updateExtractAll(MenuItem it) { + Menus.setEnabledAndVisible(it, false); + } + protected abstract void updateSelectAll(MenuItem selectAll); protected abstract void updateSelectAll(MenuItem selectAll, SelectionDetails selectionDetails); protected abstract void updateDeselectAll( @@ -412,7 +433,7 @@ public abstract class MenuManager { boolean canExtract(); - boolean canOpenWith(); + boolean canOpen(); boolean canViewInOwner(); } @@ -440,6 +461,12 @@ public abstract class MenuManager { return mActivity.isInRecents(); } + /** Is the current directory showing the contents of an archive? */ + public boolean isInArchive() { + final DocumentInfo dir = mActivity.getCurrentDirectory(); + return dir != null && ArchivesProvider.AUTHORITY.equals(dir.authority); + } + public boolean canCreateDirectory() { return mActivity.canCreateDirectory(); } diff --git a/src/com/android/documentsui/MultiRootDocumentsLoader.java b/src/com/android/documentsui/MultiRootDocumentsLoader.java index db78daa48..1213a6711 100644 --- a/src/com/android/documentsui/MultiRootDocumentsLoader.java +++ b/src/com/android/documentsui/MultiRootDocumentsLoader.java @@ -71,7 +71,7 @@ public abstract class MultiRootDocumentsLoader extends AsyncTaskLoader<Directory // previously returned cursors for filtering/sorting; this currently races // with the UI thread. - private static final int MAX_OUTSTANDING_TASK = 4; + public static final int MAX_OUTSTANDING_TASK = 4; private static final int MAX_OUTSTANDING_TASK_SVELTE = 2; /** diff --git a/src/com/android/documentsui/NavigationViewManager.java b/src/com/android/documentsui/NavigationViewManager.java index c376c86db..86b5e517f 100644 --- a/src/com/android/documentsui/NavigationViewManager.java +++ b/src/com/android/documentsui/NavigationViewManager.java @@ -17,6 +17,7 @@ package com.android.documentsui; import static com.android.documentsui.base.SharedMinimal.VERBOSE; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; import android.content.res.Resources; import android.content.res.TypedArray; @@ -24,6 +25,7 @@ import android.graphics.Outline; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; import android.util.Log; +import android.view.MenuItem; import android.view.View; import android.view.ViewOutlineProvider; import android.view.Window; @@ -34,7 +36,10 @@ import androidx.annotation.ColorRes; import androidx.annotation.Nullable; import androidx.appcompat.widget.Toolbar; import androidx.core.content.ContextCompat; +import androidx.recyclerview.selection.SelectionTracker; +import com.android.documentsui.Injector.Injected; +import com.android.documentsui.base.EventHandler; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.State; import com.android.documentsui.base.UserId; @@ -47,10 +52,9 @@ import com.google.android.material.appbar.CollapsingToolbarLayout; import java.util.function.IntConsumer; -/** - * A facade over the portions of the app and drawer toolbars. - */ -public class NavigationViewManager implements AppBarLayout.OnOffsetChangedListener { +/** A facade over the portions of the app and drawer toolbars. */ +public class NavigationViewManager extends SelectionTracker.SelectionObserver<String> + implements AppBarLayout.OnOffsetChangedListener { private static final String TAG = "NavigationViewManager"; @@ -69,10 +73,11 @@ public class NavigationViewManager implements AppBarLayout.OnOffsetChangedListen private final ViewOutlineProvider mSearchBarOutlineProvider; private final boolean mShowSearchBar; private final ConfigStore mConfigStore; - + @Injected private final Injector<?> mInjector; private boolean mIsActionModeActivated = false; - @ColorRes - private int mDefaultStatusBarColorResId; + @ColorRes private int mDefaultStatusBarColorResId; + private MenuManager.SelectionDetails mSelectionDetails; + private EventHandler<MenuItem> mActionMenuItemClicker; public NavigationViewManager( BaseActivity activity, @@ -82,9 +87,19 @@ public class NavigationViewManager implements AppBarLayout.OnOffsetChangedListen Breadcrumb breadcrumb, View tabLayoutContainer, UserIdManager userIdManager, - ConfigStore configStore) { - this(activity, drawer, state, env, breadcrumb, tabLayoutContainer, userIdManager, null, - configStore); + ConfigStore configStore, + Injector injector) { + this( + activity, + drawer, + state, + env, + breadcrumb, + tabLayoutContainer, + userIdManager, + null, + configStore, + injector); } public NavigationViewManager( @@ -95,9 +110,19 @@ public class NavigationViewManager implements AppBarLayout.OnOffsetChangedListen Breadcrumb breadcrumb, View tabLayoutContainer, UserManagerState userManagerState, - ConfigStore configStore) { - this(activity, drawer, state, env, breadcrumb, tabLayoutContainer, null, userManagerState, - configStore); + ConfigStore configStore, + Injector injector) { + this( + activity, + drawer, + state, + env, + breadcrumb, + tabLayoutContainer, + null, + userManagerState, + configStore, + injector); } public NavigationViewManager( @@ -109,7 +134,8 @@ public class NavigationViewManager implements AppBarLayout.OnOffsetChangedListen View tabLayoutContainer, UserIdManager userIdManager, UserManagerState userManagerState, - ConfigStore configStore) { + ConfigStore configStore, + Injector injector) { mActivity = activity; mToolbar = activity.findViewById(R.id.toolbar); @@ -120,6 +146,7 @@ public class NavigationViewManager implements AppBarLayout.OnOffsetChangedListen mBreadcrumb = breadcrumb; mBreadcrumb.setup(env, state, this::onNavigationItemSelected); mConfigStore = configStore; + mInjector = injector; mProfileTabs = getProfileTabs(tabLayoutContainer, userIdManager, userManagerState, activity); @@ -130,6 +157,15 @@ public class NavigationViewManager implements AppBarLayout.OnOffsetChangedListen onNavigationIconClicked(); } }); + if (isUseMaterial3FlagEnabled()) { + mToolbar.setOnMenuItemClickListener( + new Toolbar.OnMenuItemClickListener() { + @Override + public boolean onMenuItemClick(MenuItem menuItem) { + return onToolbarMenuItemClicked(menuItem); + } + }); + } mSearchBarView = activity.findViewById(R.id.searchbar_title); mCollapsingBarLayout = activity.findViewById(R.id.collapsing_toolbar); mDefaultActionBarBackground = mToolbar.getBackground(); @@ -225,11 +261,21 @@ public class NavigationViewManager implements AppBarLayout.OnOffsetChangedListen } private void onNavigationIconClicked() { - if (mDrawer.isPresent()) { + if (isUseMaterial3FlagEnabled() && inSelectionMode()) { + closeSelectionBar(); + } else if (mDrawer.isPresent()) { mDrawer.setOpen(true); } } + private boolean onToolbarMenuItemClicked(MenuItem menuItem) { + if (inSelectionMode()) { + mActionMenuItemClicker.accept(menuItem); + return true; + } + return mActivity.onOptionsItemSelected(menuItem); + } + void onNavigationItemSelected(int position) { boolean changed = false; while (mState.stack.size() > position + 1) { @@ -264,22 +310,107 @@ public class NavigationViewManager implements AppBarLayout.OnOffsetChangedListen mDrawer.setTitle(mEnv.getDrawerTitle()); - mToolbar.setNavigationIcon(getActionBarIcon()); - mToolbar.setNavigationContentDescription(R.string.drawer_open); + boolean showBurgerMenuOnToolbar = true; + if (isUseMaterial3FlagEnabled()) { + View navRailRoots = mActivity.findViewById(R.id.nav_rail_container_roots); + if (navRailRoots != null) { + // If nav rail exists, burger menu will show on the nav rail instead. + showBurgerMenuOnToolbar = false; + } + } + + if (showBurgerMenuOnToolbar) { + mToolbar.setNavigationIcon(getActionBarIcon()); + mToolbar.setNavigationContentDescription(R.string.drawer_open); + } else { + mToolbar.setNavigationIcon(null); + mToolbar.setNavigationContentDescription(null); + } if (shouldShowSearchBar()) { mBreadcrumb.show(false); mToolbar.setTitle(null); mSearchBarView.setVisibility(View.VISIBLE); - } else { - mSearchBarView.setVisibility(View.GONE); - String title = mState.stack.size() <= 1 - ? mEnv.getCurrentRoot().title : mState.stack.getTitle(); - if (VERBOSE) Log.v(TAG, "New toolbar title is: " + title); - mToolbar.setTitle(title); - mBreadcrumb.show(true); - mBreadcrumb.postUpdate(); + return; } + + mSearchBarView.setVisibility(View.GONE); + + if (isUseMaterial3FlagEnabled()) { + updateActionMenu(); + if (inSelectionMode()) { + final int quantity = mInjector.selectionMgr.getSelection().size(); + final String title = + mToolbar.getContext() + .getResources() + .getQuantityString(R.plurals.elements_selected, quantity, quantity); + mToolbar.setTitle(title); + mActivity.getWindow().setTitle(title); + mToolbar.setNavigationIcon(R.drawable.ic_cancel); + mToolbar.setNavigationContentDescription(android.R.string.cancel); + return; + } + } + + String title = + mState.stack.size() <= 1 ? mEnv.getCurrentRoot().title : mState.stack.getTitle(); + if (VERBOSE) Log.v(TAG, "New toolbar title is: " + title); + mToolbar.setTitle(title); + mBreadcrumb.show(true); + mBreadcrumb.postUpdate(); + } + + @Override + public void onSelectionChanged() { + update(); + } + + /** Identifies if the `NavigationViewManager` is in selection mode or not. */ + public boolean inSelectionMode() { + return mInjector != null + && mInjector.selectionMgr != null + && mInjector.selectionMgr.hasSelection(); + } + + private boolean hasActionMenu() { + return mToolbar.getMenu().findItem(R.id.action_menu_open_with) != null; + } + + /** Updates the action menu based on whether a selection is currently being made or not. */ + public void updateActionMenu() { + // For the first start up of the application, the menu might not exist at all but we also + // don't want to inflate the menu multiple times. So along with checking if the expected + // menu is already inflated, validate that a menu exists at all as well. + boolean isMenuInflated = mToolbar.getMenu() != null && mToolbar.getMenu().size() > 0; + if (inSelectionMode()) { + if (!isMenuInflated || !hasActionMenu()) { + mToolbar.getMenu().clear(); + mToolbar.inflateMenu(R.menu.action_mode_menu); + mToolbar.invalidateMenu(); + } + mInjector.menuManager.updateActionMenu(mToolbar.getMenu(), mSelectionDetails); + return; + } + + if (!isMenuInflated || hasActionMenu()) { + mToolbar.getMenu().clear(); + mToolbar.inflateMenu(R.menu.activity); + mToolbar.invalidateMenu(); + boolean fullBarSearch = + mActivity.getResources().getBoolean(R.bool.full_bar_search_view); + boolean showSearchBar = mActivity.getResources().getBoolean(R.bool.show_search_bar); + mInjector.searchManager.install(mToolbar.getMenu(), fullBarSearch, showSearchBar); + } + mInjector.menuManager.updateOptionMenu(mToolbar.getMenu()); + mInjector.searchManager.showMenu(mState.stack); + } + + /** Everytime a selection is made, update the selection. */ + public void updateSelection( + MenuManager.SelectionDetails selectionDetails, + EventHandler<MenuItem> actionMenuItemClicker) { + mSelectionDetails = selectionDetails; + mActionMenuItemClicker = actionMenuItemClicker; } private void updateScrollFlag() { @@ -361,6 +492,11 @@ public class NavigationViewManager implements AppBarLayout.OnOffsetChangedListen mDrawer.setOpen(open); } + /** Helper method to close the selection bar. */ + public void closeSelectionBar() { + mInjector.selectionMgr.clearSelection(); + } + interface Breadcrumb { void setup(Environment env, State state, IntConsumer listener); diff --git a/src/com/android/documentsui/ProfileTabs.java b/src/com/android/documentsui/ProfileTabs.java index df525d527..5aacc22b0 100644 --- a/src/com/android/documentsui/ProfileTabs.java +++ b/src/com/android/documentsui/ProfileTabs.java @@ -20,6 +20,7 @@ import static androidx.core.util.Preconditions.checkNotNull; import static com.android.documentsui.DevicePolicyResources.Strings.PERSONAL_TAB; import static com.android.documentsui.DevicePolicyResources.Strings.WORK_TAB; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; import android.app.admin.DevicePolicyManager; import android.os.Build; @@ -155,7 +156,14 @@ public class ProfileTabs implements ProfileTabsAddons { (ViewGroup.MarginLayoutParams) tab.getLayoutParams(); int tabMarginSide = (int) mTabsContainer.getContext().getResources() .getDimension(R.dimen.profile_tab_margin_side); - marginLayoutParams.setMargins(tabMarginSide, 0, tabMarginSide, 0); + if (isUseMaterial3FlagEnabled()) { + // M3 uses the margin value as the right margin, except for the last child. + if (i != mTabs.getTabCount() - 1) { + marginLayoutParams.setMargins(0, 0, tabMarginSide, 0); + } + } else { + marginLayoutParams.setMargins(tabMarginSide, 0, tabMarginSide, 0); + } int tabHeightInDp = (int) mTabsContainer.getContext().getResources() .getDimension(R.dimen.tab_height); tab.getLayoutParams().height = tabHeightInDp; diff --git a/src/com/android/documentsui/RecentsLoader.java b/src/com/android/documentsui/RecentsLoader.java index b3cfa0180..9a3e06fba 100644 --- a/src/com/android/documentsui/RecentsLoader.java +++ b/src/com/android/documentsui/RecentsLoader.java @@ -37,13 +37,13 @@ public class RecentsLoader extends MultiRootDocumentsLoader { private static final String TAG = "RecentsLoader"; /** Ignore documents older than this age. */ - private static final long REJECT_OLDER_THAN = 45 * DateUtils.DAY_IN_MILLIS; + public static final long REJECT_OLDER_THAN = 45 * DateUtils.DAY_IN_MILLIS; - /** MIME types that should always be excluded from recents. */ + /** MIME types that should always be excluded from the Recents view. */ private static final String[] REJECT_MIMES = new String[]{Document.MIME_TYPE_DIR}; /** Maximum documents from a single root. */ - private static final int MAX_DOCS_FROM_ROOT = 64; + public static final int MAX_DOCS_FROM_ROOT = 64; private final UserId mUserId; diff --git a/src/com/android/documentsui/UserManagerState.java b/src/com/android/documentsui/UserManagerState.java index b1c1a0d59..d2ddae615 100644 --- a/src/com/android/documentsui/UserManagerState.java +++ b/src/com/android/documentsui/UserManagerState.java @@ -33,6 +33,7 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; import android.content.pm.UserProperties; import android.graphics.drawable.Drawable; import android.os.Build; @@ -42,16 +43,17 @@ import android.util.Log; import androidx.annotation.GuardedBy; import androidx.annotation.RequiresApi; +import androidx.annotation.RequiresPermission; import androidx.annotation.VisibleForTesting; import com.android.documentsui.base.Features; import com.android.documentsui.base.UserId; -import com.android.documentsui.util.CrossProfileUtils; import com.android.documentsui.util.VersionUtils; import com.android.modules.utils.build.SdkLevel; import com.google.common.base.Objects; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -62,26 +64,23 @@ public interface UserManagerState { /** * Returns the {@link UserId} of each profile which should be queried for documents. This will - * always - * include {@link UserId#CURRENT_USER}. + * always include {@link UserId#CURRENT_USER}. */ List<UserId> getUserIds(); - /** - * Returns mapping between the {@link UserId} and the label for the profile - */ + /** Returns mapping between the {@link UserId} and the label for the profile */ Map<UserId, String> getUserIdToLabelMap(); /** * Returns mapping between the {@link UserId} and the drawable badge for the profile * - * returns {@code null} for non-profile userId + * <p>returns {@code null} for non-profile userId */ Map<UserId, Drawable> getUserIdToBadgeMap(); /** - * Returns a map of {@link UserId} to boolean value indicating whether - * the {@link UserId}.CURRENT_USER can forward {@link Intent} to that {@link UserId} + * Returns a map of {@link UserId} to boolean value indicating whether the {@link + * UserId}.CURRENT_USER can forward {@link Intent} to that {@link UserId} */ Map<UserId, Boolean> getCanForwardToProfileIdMap(Intent intent); @@ -96,25 +95,19 @@ public interface UserManagerState { */ void onProfileActionStatusChange(String action, UserId userId); - /** - * Sets the intent that triggered the launch of the DocsUI - */ + /** Sets the intent that triggered the launch of the DocsUI */ void setCurrentStateIntent(Intent intent); /** Returns true if there are hidden profiles */ boolean areHiddenInQuietModeProfilesPresent(); - /** - * Creates an implementation of {@link UserManagerState}. - */ + /** Creates an implementation of {@link UserManagerState}. */ // TODO: b/314746383 Make this class a singleton static UserManagerState create(Context context) { return new RuntimeUserManagerState(context); } - /** - * Implementation of {@link UserManagerState} - */ + /** Implementation of {@link UserManagerState} */ final class RuntimeUserManagerState implements UserManagerState { private static final String TAG = "UserManagerState"; @@ -123,58 +116,63 @@ public interface UserManagerState { private final boolean mIsDeviceSupported; private final UserManager mUserManager; private final ConfigStore mConfigStore; + /** * List of all the {@link UserId} that have the {@link UserProperties.ShowInSharingSurfaces} * set as `SHOW_IN_SHARING_SURFACES_SEPARATE` OR it is a system/personal user */ @GuardedBy("mUserIds") private final List<UserId> mUserIds = new ArrayList<>(); - /** - * Mapping between the {@link UserId} to the corresponding profile label - */ + + /** Mapping between the {@link UserId} to the corresponding profile label */ @GuardedBy("mUserIdToLabelMap") private final Map<UserId, String> mUserIdToLabelMap = new HashMap<>(); - /** - * Mapping between the {@link UserId} to the corresponding profile badge - */ + + /** Mapping between the {@link UserId} to the corresponding profile badge */ @GuardedBy("mUserIdToBadgeMap") private final Map<UserId, Drawable> mUserIdToBadgeMap = new HashMap<>(); + /** * Map containing {@link UserId}, other than that of the current user, as key and boolean * denoting whether it is accessible by the current user or not as value */ - @GuardedBy("mCanFrowardToProfileIdMap") - private final Map<UserId, Boolean> mCanFrowardToProfileIdMap = new HashMap<>(); + @GuardedBy("mCanForwardToProfileIdMap") + private final Map<UserId, Boolean> mCanForwardToProfileIdMap = new HashMap<>(); private Intent mCurrentStateIntent; - private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - synchronized (mUserIds) { - mUserIds.clear(); - } - synchronized (mUserIdToLabelMap) { - mUserIdToLabelMap.clear(); - } - synchronized (mUserIdToBadgeMap) { - mUserIdToBadgeMap.clear(); - } - synchronized (mCanFrowardToProfileIdMap) { - mCanFrowardToProfileIdMap.clear(); - } - } - }; - + private final BroadcastReceiver mIntentReceiver = + new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + synchronized (mUserIds) { + mUserIds.clear(); + } + synchronized (mUserIdToLabelMap) { + mUserIdToLabelMap.clear(); + } + synchronized (mUserIdToBadgeMap) { + mUserIdToBadgeMap.clear(); + } + synchronized (mCanForwardToProfileIdMap) { + mCanForwardToProfileIdMap.clear(); + } + } + }; private RuntimeUserManagerState(Context context) { - this(context, UserId.CURRENT_USER, + this( + context, + UserId.CURRENT_USER, Features.CROSS_PROFILE_TABS && isDeviceSupported(context), DocumentsApplication.getConfigStore()); } @VisibleForTesting - RuntimeUserManagerState(Context context, UserId currentUser, boolean isDeviceSupported, + RuntimeUserManagerState( + Context context, + UserId currentUser, + boolean isDeviceSupported, ConfigStore configStore) { mContext = context.getApplicationContext(); mCurrentUser = checkNotNull(currentUser); @@ -224,11 +222,11 @@ public interface UserManagerState { @Override public Map<UserId, Boolean> getCanForwardToProfileIdMap(Intent intent) { - synchronized (mCanFrowardToProfileIdMap) { - if (mCanFrowardToProfileIdMap.isEmpty()) { + synchronized (mCanForwardToProfileIdMap) { + if (mCanForwardToProfileIdMap.isEmpty()) { getCanForwardToProfileIdMapInternal(intent); } - return mCanFrowardToProfileIdMap; + return mCanForwardToProfileIdMap; } } @@ -236,8 +234,8 @@ public interface UserManagerState { @SuppressLint("NewApi") public void onProfileActionStatusChange(String action, UserId userId) { if (!SdkLevel.isAtLeastV()) return; - UserProperties userProperties = mUserManager.getUserProperties( - UserHandle.of(userId.getIdentifier())); + UserProperties userProperties = + mUserManager.getUserProperties(UserHandle.of(userId.getIdentifier())); if (userProperties.getShowInQuietMode() != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) { return; } @@ -263,19 +261,37 @@ public interface UserManagerState { mUserIdToBadgeMap.put(userId, getProfileBadge(userId)); } } - synchronized (mCanFrowardToProfileIdMap) { - if (!mCanFrowardToProfileIdMap.containsKey(userId)) { - if (userId.getIdentifier() == ActivityManager.getCurrentUser() - || isCrossProfileContentSharingStrategyDelegatedFromParent( - UserHandle.of(userId.getIdentifier())) - || CrossProfileUtils.getCrossProfileResolveInfo(mCurrentUser, - mContext.getPackageManager(), mCurrentStateIntent, mContext, - mConfigStore.isPrivateSpaceInDocsUIEnabled()) != null) { - mCanFrowardToProfileIdMap.put(userId, true); + synchronized (mCanForwardToProfileIdMap) { + if (!mCanForwardToProfileIdMap.containsKey(userId)) { + + UserHandle handle = UserHandle.of(userId.getIdentifier()); + + // Decide if to use the parent's access, or this handle's access. + if (isCrossProfileContentSharingStrategyDelegatedFromParent(handle)) { + UserHandle parentHandle = mUserManager.getProfileParent(handle); + // Couldn't resolve parent to check access, so fail closed. + if (parentHandle == null) { + mCanForwardToProfileIdMap.put(userId, false); + } else if (mCurrentUser.getIdentifier() + == parentHandle.getIdentifier()) { + // Check if the parent is the current user, if so this profile + // is also accessible. + mCanForwardToProfileIdMap.put(userId, true); + + } else { + UserId parent = UserId.of(parentHandle); + mCanForwardToProfileIdMap.put( + userId, + doesCrossProfileForwardingActivityExistForUser( + mCurrentStateIntent, parent)); + } } else { - mCanFrowardToProfileIdMap.put(userId, false); + // Update the profile map for this profile. + mCanForwardToProfileIdMap.put( + userId, + doesCrossProfileForwardingActivityExistForUser( + mCurrentStateIntent, userId)); } - } } } else { @@ -343,7 +359,7 @@ public interface UserManagerState { // returned should satisfy both the following conditions: // 1. It has user property SHOW_IN_SHARING_SURFACES_SEPARATE // 2. Quite mode is not enabled, if it is enabled then the profile's user - // property is not SHOW_IN_QUIET_MODE_HIDDEN + // property is not SHOW_IN_QUIET_MODE_HIDDEN if (isProfileAllowed(userHandle)) { result.add(UserId.of(userHandle)); } @@ -354,16 +370,77 @@ public interface UserManagerState { } } + /** + * Checks if a package is installed for a given user. + * + * @param userHandle The ID of the user. + * @return {@code true} if the package is installed for the user, {@code false} otherwise. + */ + @RequiresPermission( + anyOf = { + "android.permission.MANAGE_USERS", + "android.permission.INTERACT_ACROSS_USERS" + }) + private boolean isPackageInstalledForUser(UserHandle userHandle) { + String packageName = mContext.getPackageName(); + try { + Context userPackageContext = + mContext.createPackageContextAsUser( + mContext.getPackageName(), 0 /* flags */, userHandle); + return userPackageContext != null; + } catch (PackageManager.NameNotFoundException e) { + Log.w(TAG, "Package " + packageName + " not found for user " + userHandle); + return false; + } + } + + /** + * Checks if quiet mode is enabled for a given user. + * + * @param userHandle The UserHandle of the profile to check. + * @return {@code true} if quiet mode is enabled, {@code false} otherwise. + */ + private boolean isQuietModeEnabledForUser(UserHandle userHandle) { + return UserId.of(userHandle.getIdentifier()).isQuietModeEnabled(mContext); + } + + /** + * Checks if a profile should be allowed, taking into account quiet mode and package + * installation. + * + * @param userHandle The UserHandle of the profile to check. + * @return {@code true} if the profile should be allowed, {@code false} otherwise. + */ @SuppressLint("NewApi") + @RequiresPermission( + anyOf = { + "android.permission.MANAGE_USERS", + "android.permission.INTERACT_ACROSS_USERS" + }) private boolean isProfileAllowed(UserHandle userHandle) { - final UserProperties userProperties = - mUserManager.getUserProperties(userHandle); + final UserProperties userProperties = mUserManager.getUserProperties(userHandle); + + // 1. Check if the package is installed for the user + if (!isPackageInstalledForUser(userHandle)) { + Log.w( + TAG, + "Package " + + mContext.getPackageName() + + " is not installed for user " + + userHandle); + return false; + } + + // 2. Check user properties and quiet mode if (userProperties.getShowInSharingSurfaces() == UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) { - return !UserId.of(userHandle).isQuietModeEnabled(mContext) + // Return true if profile is not in quiet mode or if it is in quiet mode + // then its user properties do not require it to be hidden + return !isQuietModeEnabledForUser(userHandle) || userProperties.getShowInQuietMode() - != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN; + != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN; } + return false; } @@ -384,9 +461,12 @@ public interface UserManagerState { result.add(0, systemUser); } else { if (DEBUG) { - Log.w(TAG, "The current user " + UserId.CURRENT_USER - + " is neither system nor managed user. has system user: " - + (systemUser != null)); + Log.w( + TAG, + "The current user " + + UserId.CURRENT_USER + + " is neither system nor managed user. has system user: " + + (systemUser != null)); } } } @@ -422,13 +502,13 @@ public interface UserManagerState { for (UserId userId : userIds) { if (mUserManager.isManagedProfile(userId.getIdentifier())) { synchronized (mUserIdToLabelMap) { - mUserIdToLabelMap.put(userId, - getEnterpriseString(WORK_TAB, R.string.work_tab)); + mUserIdToLabelMap.put( + userId, getEnterpriseString(WORK_TAB, R.string.work_tab)); } } else { synchronized (mUserIdToLabelMap) { - mUserIdToLabelMap.put(userId, - getEnterpriseString(PERSONAL_TAB, R.string.personal_tab)); + mUserIdToLabelMap.put( + userId, getEnterpriseString(PERSONAL_TAB, R.string.personal_tab)); } } } @@ -440,8 +520,9 @@ public interface UserManagerState { return getEnterpriseString(PERSONAL_TAB, R.string.personal_tab); } try { - Context userContext = mContext.createContextAsUser( - UserHandle.of(userId.getIdentifier()), 0 /* flags */); + Context userContext = + mContext.createContextAsUser( + UserHandle.of(userId.getIdentifier()), 0 /* flags */); UserManager userManagerAsUser = userContext.getSystemService(UserManager.class); if (userManagerAsUser == null) { Log.e(TAG, "cannot obtain user manager"); @@ -469,9 +550,8 @@ public interface UserManagerState { Log.e(TAG, "can not get device policy manager"); return mContext.getString(defaultStringId); } - return dpm.getResources().getString( - updatableStringId, - () -> mContext.getString(defaultStringId)); + return dpm.getResources() + .getString(updatableStringId, () -> mContext.getString(defaultStringId)); } private void getUserIdToBadgeMapInternal() { @@ -506,8 +586,10 @@ public interface UserManagerState { for (UserId userId : userIds) { if (mUserManager.isManagedProfile(userId.getIdentifier())) { synchronized (mUserIdToBadgeMap) { - mUserIdToBadgeMap.put(userId, - SdkLevel.isAtLeastT() ? getWorkProfileBadge() + mUserIdToBadgeMap.put( + userId, + SdkLevel.isAtLeastT() + ? getWorkProfileBadge() : mContext.getDrawable(R.drawable.ic_briefcase)); } } @@ -520,8 +602,9 @@ public interface UserManagerState { return null; } try { - Context userContext = mContext.createContextAsUser( - UserHandle.of(userId.getIdentifier()), 0 /* flags */); + Context userContext = + mContext.createContextAsUser( + UserHandle.of(userId.getIdentifier()), 0 /* flags */); UserManager userManagerAsUser = userContext.getSystemService(UserManager.class); if (userManagerAsUser == null) { Log.e(TAG, "cannot obtain user manager"); @@ -537,86 +620,142 @@ public interface UserManagerState { @RequiresApi(Build.VERSION_CODES.TIRAMISU) private Drawable getWorkProfileBadge() { DevicePolicyManager dpm = mContext.getSystemService(DevicePolicyManager.class); - Drawable drawable = dpm.getResources().getDrawable(WORK_PROFILE_ICON, SOLID_COLORED, - () -> - mContext.getDrawable(R.drawable.ic_briefcase)); + Drawable drawable = + dpm.getResources() + .getDrawable( + WORK_PROFILE_ICON, + SOLID_COLORED, + () -> mContext.getDrawable(R.drawable.ic_briefcase)); return drawable; } + /** + * Updates Cross Profile access for all UserProfiles in {@code getUserIds()} + * + * <p>This method looks at a variety of situations for each Profile and decides if the + * profile's content is accessible by the current process owner user id. + * + * <ol> + * <li>UserProperties attributes for CrossProfileDelegation are checked first. When the + * profile delegates to the parent profile, the parent's access is used. + * <li>{@link CrossProfileIntentForwardingActivity}s are resolved via the process owner's + * PackageManager, and are considered when evaluating cross profile to the target + * profile. + * </ol> + * + * <p>In the event none of the above checks succeeds, the profile is considered to be + * inaccessible to the current process user. + * + * @param intent The intent Photopicker is currently running under, for + * CrossProfileForwardActivity checking. + */ private void getCanForwardToProfileIdMapInternal(Intent intent) { - // Versions less than V will not have the user properties required to determine whether - // cross profile check is delegated from parent or not - if (!SdkLevel.isAtLeastV()) { - getCanForwardToProfileIdMapPreV(intent); - return; - } - if (mUserManager == null) { - Log.e(TAG, "can not get user manager"); - return; - } - List<UserId> parentOrDelegatedFromParent = new ArrayList<>(); - List<UserId> canForwardToProfileIds = new ArrayList<>(); - List<UserId> noDelegation = new ArrayList<>(); + Map<UserId, Boolean> profileIsAccessibleToProcessOwner = new HashMap<>(); - List<UserId> userIds = getUserIds(); - for (UserId userId : userIds) { - final UserHandle userHandle = UserHandle.of(userId.getIdentifier()); - // Parent (personal) profile and all the child profiles that delegate cross profile - // content sharing check to parent can share among each other - if (userId.getIdentifier() == ActivityManager.getCurrentUser() - || isCrossProfileContentSharingStrategyDelegatedFromParent(userHandle)) { - parentOrDelegatedFromParent.add(userId); - } else { - noDelegation.add(userId); + List<UserId> delegatedFromParent = new ArrayList<>(); + + for (UserId userId : getUserIds()) { + + // Early exit, self is always accessible. + if (userId.getIdentifier() == mCurrentUser.getIdentifier()) { + profileIsAccessibleToProcessOwner.put(userId, true); + continue; } - } - if (noDelegation.size() > 1) { - Log.e(TAG, "There cannot be more than one profile delegating cross profile " - + "content sharing check from self."); - } - - /* - * Cross profile resolve info need to be checked in the following 2 cases: - * 1. current user is either parent or delegates check to parent and the target user - * does not delegate to parent - * 2. current user does not delegate check to the parent and the target user is the - * parent profile - */ - UserId needToCheck = null; - if (parentOrDelegatedFromParent.contains(mCurrentUser) - && !noDelegation.isEmpty()) { - needToCheck = noDelegation.get(0); - } else if (mCurrentUser.getIdentifier() != ActivityManager.getCurrentUser()) { - final UserHandle parentProfile = mUserManager.getProfileParent( - UserHandle.of(mCurrentUser.getIdentifier())); - needToCheck = UserId.of(parentProfile); - } - - if (needToCheck != null && CrossProfileUtils.getCrossProfileResolveInfo(mCurrentUser, - mContext.getPackageManager(), intent, mContext, - mConfigStore.isPrivateSpaceInDocsUIEnabled()) != null) { - if (parentOrDelegatedFromParent.contains(needToCheck)) { - canForwardToProfileIds.addAll(parentOrDelegatedFromParent); - } else { - canForwardToProfileIds.add(needToCheck); + // CrossProfileContentSharingStrategyDelegatedFromParent is only V+ sdks. + if (SdkLevel.isAtLeastV() + && isCrossProfileContentSharingStrategyDelegatedFromParent( + UserHandle.of(userId.getIdentifier()))) { + delegatedFromParent.add(userId); + continue; } + + // Check for cross profile & add to the map. + profileIsAccessibleToProcessOwner.put( + userId, doesCrossProfileForwardingActivityExistForUser(intent, userId)); } - if (parentOrDelegatedFromParent.contains(mCurrentUser)) { - canForwardToProfileIds.addAll(parentOrDelegatedFromParent); + // For profiles that delegate their access to the parent, set the access for + // those profiles + // equal to the same as their parent. + for (UserId userId : delegatedFromParent) { + UserHandle parent = + mUserManager.getProfileParent(UserHandle.of(userId.getIdentifier())); + profileIsAccessibleToProcessOwner.put( + userId, + profileIsAccessibleToProcessOwner.getOrDefault( + UserId.of(parent), /* default= */ false)); } - for (UserId userId : userIds) { - synchronized (mCanFrowardToProfileIdMap) { - if (userId.equals(mCurrentUser)) { - mCanFrowardToProfileIdMap.put(userId, true); - continue; + synchronized (mCanForwardToProfileIdMap) { + mCanForwardToProfileIdMap.clear(); + for (Map.Entry<UserId, Boolean> entry : + profileIsAccessibleToProcessOwner.entrySet()) { + mCanForwardToProfileIdMap.put(entry.getKey(), entry.getValue()); + } + } + } + + /** + * Looks for a matching CrossProfileIntentForwardingActivity in the targetUserId for the + * given intent. + * + * @param intent The intent the forwarding activity needs to match. + * @param targetUserId The target user to check for. + * @return whether a CrossProfileIntentForwardingActivity could be found for the given + * intent, and user. + */ + private boolean doesCrossProfileForwardingActivityExistForUser( + Intent intent, UserId targetUserId) { + + final PackageManager pm = mContext.getPackageManager(); + final Intent intentToCheck = (Intent) intent.clone(); + intentToCheck.setComponent(null); + intentToCheck.setPackage(null); + + for (ResolveInfo resolveInfo : + pm.queryIntentActivities(intentToCheck, PackageManager.MATCH_DEFAULT_ONLY)) { + + if (resolveInfo.isCrossProfileIntentForwarderActivity()) { + /* + * IMPORTANT: This is a reflection based hack to ensure the profile is + * actually the installer of the CrossProfileIntentForwardingActivity. + * + * ResolveInfo.targetUserId exists, but is a hidden API not available to + * mainline modules, and no such API exists, so it is accessed via + * reflection below. All exceptions are caught to protect against + * reflection related issues such as: + * NoSuchFieldException / IllegalAccessException / SecurityException. + * + * In the event of an exception, the code fails "closed" for the current + * profile to avoid showing content that should not be visible. + */ + try { + Field targetUserIdField = + resolveInfo.getClass().getDeclaredField("targetUserId"); + targetUserIdField.setAccessible(true); + int activityTargetUserId = (int) targetUserIdField.get(resolveInfo); + + if (activityTargetUserId == targetUserId.getIdentifier()) { + + // Found a match for this profile + return true; + } + + } catch (NoSuchFieldException | IllegalAccessException | SecurityException ex) { + // Couldn't check the targetUserId via reflection, so fail without + // further iterations. + Log.e(TAG, "Could not access targetUserId via reflection.", ex); + return false; + } catch (Exception ex) { + Log.e(TAG, "Exception occurred during cross profile checks", ex); } - mCanFrowardToProfileIdMap.put(userId, canForwardToProfileIds.contains(userId)); } } + + // No match found, so return false. + return false; } @SuppressLint("NewApi") @@ -636,30 +775,12 @@ public interface UserManagerState { == UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT; } - private void getCanForwardToProfileIdMapPreV(Intent intent) { - // There only two profiles pre V - List<UserId> userIds = getUserIds(); - for (UserId userId : userIds) { - synchronized (mCanFrowardToProfileIdMap) { - if (mCurrentUser.equals(userId)) { - mCanFrowardToProfileIdMap.put(userId, true); - } else { - mCanFrowardToProfileIdMap.put(userId, - CrossProfileUtils.getCrossProfileResolveInfo( - mCurrentUser, mContext.getPackageManager(), intent, - mContext, mConfigStore.isPrivateSpaceInDocsUIEnabled()) - != null); - } - } - } - } - private static boolean isDeviceSupported(Context context) { - // The feature requires Android R DocumentsContract APIs and INTERACT_ACROSS_USERS_FULL - // permission. + // The feature requires Android R DocumentsContract APIs and + // INTERACT_ACROSS_USERS_FULL permission. return VersionUtils.isAtLeastR() && context.checkSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS) - == PackageManager.PERMISSION_GRANTED; + == PackageManager.PERMISSION_GRANTED; } } } diff --git a/src/com/android/documentsui/archives/Archive.java b/src/com/android/documentsui/archives/Archive.java index 7c0f47147..9889631ea 100644 --- a/src/com/android/documentsui/archives/Archive.java +++ b/src/com/android/documentsui/archives/Archive.java @@ -16,6 +16,8 @@ package com.android.documentsui.archives; +import static com.android.documentsui.base.SharedMinimal.DEBUG; + import android.content.Context; import android.content.res.AssetFileDescriptor; import android.database.Cursor; @@ -29,23 +31,23 @@ import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; import android.text.TextUtils; +import android.util.Log; import android.webkit.MimeTypeMap; import androidx.annotation.GuardedBy; import androidx.annotation.Nullable; -import androidx.core.util.Preconditions; + +import org.apache.commons.compress.archivers.ArchiveEntry; import java.io.Closeable; import java.io.File; import java.io.FileNotFoundException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; -import org.apache.commons.compress.archivers.ArchiveEntry; -import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; - /** * Provides basic implementation for creating, extracting and accessing * files within archives exposed by a document provider. @@ -55,7 +57,7 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry; public abstract class Archive implements Closeable { private static final String TAG = "Archive"; - public static final String[] DEFAULT_PROJECTION = new String[] { + public static final String[] DEFAULT_PROJECTION = new String[]{ Document.COLUMN_DOCUMENT_ID, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_MIME_TYPE, @@ -90,28 +92,85 @@ public abstract class Archive implements Closeable { mEntries = new HashMap<>(); } - /** - * Returns a valid, normalized path for an entry. - */ + /** Returns a valid, normalized path for an entry. */ public static String getEntryPath(ArchiveEntry entry) { - if (entry instanceof ZipArchiveEntry) { - /** - * Some of archive entry doesn't have the same naming rule. - * For example: The name of 7 zip directory entry doesn't end with '/'. - * Only check for Zip archive. - */ - Preconditions.checkArgument(entry.isDirectory() == entry.getName().endsWith("/"), - "Ill-formated ZIP-file."); + final List<String> parts = new ArrayList<String>(); + boolean isDir = true; + + // Get the path that will be decomposed and normalized + final String in = entry.getName(); + + decompose: + for (int i = 0; i < in.length(); ) { + // Skip separators + if (in.charAt(i) == '/') { + isDir = true; + do { + if (++i == in.length()) break decompose; + } while (in.charAt(i) == '/'); + } + + // Found the beginning of a part + final int b = i; + assert (b < in.length()); + assert (in.charAt(b) != '/'); + + // Find the end of the part + do { + ++i; + } while (i < in.length() && in.charAt(i) != '/'); + + // Extract part + final String part = in.substring(b, i); + assert (!part.isEmpty()); + + // Special case if part is "." + if (part.equals(".")) { + isDir = true; + continue; + } + + // Special case if part is ".." + if (part.equals("..")) { + isDir = true; + if (!parts.isEmpty()) parts.remove(parts.size() - 1); + continue; + } + + // The part is either a directory or a file name + isDir = false; + parts.add(part); } - if (entry.getName().startsWith("/")) { - return entry.getName(); - } else { - return "/" + entry.getName(); + + // If the decomposed path looks like a directory but the archive entry says that it is not + // a directory entry, append "?" for the file name + if (isDir && !entry.isDirectory()) { + isDir = false; + parts.add("?"); } + + if (parts.isEmpty()) return "/"; + + // Construct the normalized path + final StringBuilder sb = new StringBuilder(in.length() + 3); + + for (final String part : parts) { + sb.append('/'); + sb.append(part); + } + + if (entry.isDirectory()) { + sb.append('/'); + } + + final String out = sb.toString(); + if (DEBUG) Log.d(TAG, "getEntryPath(" + in + ") -> " + out); + return out; } /** * Returns true if the file descriptor is seekable. + * * @param descriptor File descriptor to check. */ public static boolean canSeek(ParcelFileDescriptor descriptor) { diff --git a/src/com/android/documentsui/archives/ReadableArchive.java b/src/com/android/documentsui/archives/ReadableArchive.java index 302f582f5..ad9c8242e 100644 --- a/src/com/android/documentsui/archives/ReadableArchive.java +++ b/src/com/android/documentsui/archives/ReadableArchive.java @@ -105,10 +105,10 @@ public class ReadableArchive extends Archive { continue; } entryPath = getEntryPath(entry); - if (mEntries.containsKey(entryPath)) { - throw new IOException("Multiple entries with the same name are not supported."); + if (mEntries.putIfAbsent(entryPath, entry) != null) { + if (DEBUG) Log.d(TAG, "Ignored conflicting entry for '" + entryPath + "'"); + continue; } - mEntries.put(entryPath, entry); if (entry.isDirectory()) { mTree.put(entryPath, new ArrayList<ArchiveEntry>()); } diff --git a/src/com/android/documentsui/base/Menus.java b/src/com/android/documentsui/base/Menus.java index eba240c83..6ceea3269 100644 --- a/src/com/android/documentsui/base/Menus.java +++ b/src/com/android/documentsui/base/Menus.java @@ -19,6 +19,8 @@ package com.android.documentsui.base; import android.view.Menu; import android.view.MenuItem; +import androidx.annotation.NonNull; + public final class Menus { private Menus() {} @@ -41,7 +43,7 @@ public final class Menus { } /** Set enabled/disabled state of a menuItem, and updates its visibility. */ - public static void setEnabledAndVisible(MenuItem item, boolean enabled) { + public static void setEnabledAndVisible(@NonNull MenuItem item, boolean enabled) { item.setEnabled(enabled); item.setVisible(enabled); } diff --git a/src/com/android/documentsui/dirlist/AppsRowManager.java b/src/com/android/documentsui/dirlist/AppsRowManager.java index 297d1b2e3..eb0d8bf2e 100644 --- a/src/com/android/documentsui/dirlist/AppsRowManager.java +++ b/src/com/android/documentsui/dirlist/AppsRowManager.java @@ -16,6 +16,8 @@ package com.android.documentsui.dirlist; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; + import android.text.TextUtils; import android.view.LayoutInflater; import android.view.View; @@ -45,6 +47,7 @@ import java.util.Map; /** * A manager class stored apps row chip data list. Data will be synced by RootsFragment. + * TODO(b/379776735): Remove this after use_material3 flag is launched. */ public class AppsRowManager { @@ -102,6 +105,10 @@ public class AppsRowManager { } private boolean shouldShow(State state, boolean isSearchExpanded) { + if (isUseMaterial3FlagEnabled()) { + return false; + } + boolean isHiddenAction = state.action == State.ACTION_CREATE || state.action == State.ACTION_OPEN_TREE || state.action == State.ACTION_PICK_COPY_DESTINATION; diff --git a/src/com/android/documentsui/dirlist/DirectoryFragment.java b/src/com/android/documentsui/dirlist/DirectoryFragment.java index e099ca734..855a8273d 100644 --- a/src/com/android/documentsui/dirlist/DirectoryFragment.java +++ b/src/com/android/documentsui/dirlist/DirectoryFragment.java @@ -21,6 +21,8 @@ import static com.android.documentsui.base.SharedMinimal.DEBUG; import static com.android.documentsui.base.SharedMinimal.VERBOSE; import static com.android.documentsui.base.State.MODE_GRID; import static com.android.documentsui.base.State.MODE_LIST; +import static com.android.documentsui.util.FlagUtils.isDesktopFileHandlingFlagEnabled; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; import android.app.ActivityManager; import android.content.BroadcastReceiver; @@ -609,11 +611,16 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On new RefreshHelper(mRefreshLayout::setEnabled) .attach(mRecView); - mActionModeController = mInjector.getActionModeController( - mSelectionMetadata, - this::handleMenuItemClick); - - mSelectionMgr.addObserver(mActionModeController); + if (isUseMaterial3FlagEnabled()) { + mSelectionMgr.addObserver(mActivity.getNavigator()); + mActivity.getNavigator().updateSelection(mSelectionMetadata, this::handleMenuItemClick); + } else { + mActionModeController = + mInjector.getActionModeController( + mSelectionMetadata, this::handleMenuItemClick); + assert (mActionModeController != null); + mSelectionMgr.addObserver(mActionModeController); + } mProfileTabsController = mInjector.profileTabsController; mSelectionMgr.addObserver(mProfileTabsController); @@ -916,6 +923,14 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On } } + private void closeSelectionBar() { + if (isUseMaterial3FlagEnabled()) { + mActivity.getNavigator().closeSelectionBar(); + } else { + mActionModeController.finishActionMode(); + } + } + private boolean handleMenuItemClick(MenuItem item) { if (mInjector.pickResult != null) { mInjector.pickResult.increaseActionCount(); @@ -924,9 +939,19 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On mSelectionMgr.copySelection(selection); final int id = item.getItemId(); - if (id == R.id.action_menu_select || id == R.id.dir_menu_open) { + if (isDesktopFileHandlingFlagEnabled() && id == R.id.dir_menu_open) { + // On desktop, "open" is displayed in file management mode (i.e. `files.MenuManager`). + // This menu item behaves the same as double click on the menu item which is handled by + // onItemActivated but since onItemActivated requires a RecylcerView ItemDetails, we're + // using viewDocument that takes a Selection. + viewDocument(selection); + return true; + } else if (id == R.id.action_menu_select || id == R.id.dir_menu_open) { + // Note: this code path is never executed for `dir_menu_open`. The menu item is always + // hidden unless the desktopFileHandling flag is enabled, in which case the menu item + // will be handled by the condition above. openDocuments(selection); - mActionModeController.finishActionMode(); + closeSelectionBar(); return true; } else if (id == R.id.action_menu_open_with || id == R.id.dir_menu_open_with) { showChooserForDoc(selection); @@ -946,22 +971,22 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On transferDocuments(selection, null, FileOperationService.OPERATION_COPY); // TODO: Only finish selection mode if copy-to is not canceled. // Need to plum down into handling the way we do with deleteDocuments. - mActionModeController.finishActionMode(); + closeSelectionBar(); return true; } else if (id == R.id.action_menu_compress || id == R.id.dir_menu_compress) { transferDocuments(selection, mState.stack, FileOperationService.OPERATION_COMPRESS); // TODO: Only finish selection mode if compress is not canceled. // Need to plum down into handling the way we do with deleteDocuments. - mActionModeController.finishActionMode(); + closeSelectionBar(); return true; // TODO: Implement extract (to the current directory). - } else if (id == R.id.action_menu_extract_to) { + } else if (id == R.id.action_menu_extract_to || id == R.id.option_menu_extract_all) { transferDocuments(selection, null, FileOperationService.OPERATION_EXTRACT); // TODO: Only finish selection mode if compress-to is not canceled. // Need to plum down into handling the way we do with deleteDocuments. - mActionModeController.finishActionMode(); + closeSelectionBar(); return true; } else if (id == R.id.action_menu_move_to) { if (mModel.hasDocuments(selection, DocumentFilters.NOT_MOVABLE)) { @@ -969,17 +994,17 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On return true; } // Exit selection mode first, so we avoid deselecting deleted documents. - mActionModeController.finishActionMode(); + closeSelectionBar(); transferDocuments(selection, null, FileOperationService.OPERATION_MOVE); return true; } else if (id == R.id.action_menu_inspect || id == R.id.dir_menu_inspect) { - mActionModeController.finishActionMode(); + closeSelectionBar(); assert selection.size() <= 1; DocumentInfo doc = selection.isEmpty() ? mActivity.getCurrentDirectory() : mModel.getDocuments(selection).get(0); - mActions.showInspector(doc); + mActions.showPreview(doc); return true; } else if (id == R.id.dir_menu_cut_to_clipboard) { mActions.cutToClipboard(); @@ -1012,9 +1037,11 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On mActions.showSortDialog(); return true; } + if (DEBUG) { - Log.d(TAG, "Unhandled menu item selected: " + item); + Log.d(TAG, "Cannot handle unexpected menu item " + id); } + return false; } @@ -1080,6 +1107,20 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On mActions.showChooserForDoc(doc); } + private void viewDocument(final Selection<String> selected) { + Metrics.logUserAction(MetricConsts.USER_ACTION_OPEN); + + if (selected.isEmpty()) { + return; + } + + assert selected.size() == 1; + DocumentInfo doc = + DocumentInfo.fromDirectoryCursor(mModel.getItem(selected.iterator().next())); + + mActions.openDocumentViewOnly(doc); + } + private void transferDocuments( final Selection<String> selected, @Nullable DocumentStack destination, final @OpType int mode) { @@ -1171,7 +1212,7 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On intent.putExtra(FileOperationService.EXTRA_OPERATION_TYPE, mode); // This just identifies the type of request...we'll check it - // when we reveive a response. + // when we receive a response. startActivityForResult(intent, REQUEST_COPY_DESTINATION); } @@ -1494,7 +1535,11 @@ public class DirectoryFragment extends Fragment implements SwipeRefreshLayout.On // For orientation changed case, sometimes the docs loading comes after the menu // update. We need to update the menu here to ensure the status is correct. mInjector.menuManager.updateModel(mModel); - mInjector.menuManager.updateOptionMenu(); + if (isUseMaterial3FlagEnabled()) { + mActivity.getNavigator().updateActionMenu(); + } else { + mInjector.menuManager.updateOptionMenu(); + } if (VersionUtils.isAtLeastS()) { mActivity.updateHeader(update.hasCrossProfileException()); } else { diff --git a/src/com/android/documentsui/dirlist/DocumentHolder.java b/src/com/android/documentsui/dirlist/DocumentHolder.java index 26f527896..8e5f50636 100644 --- a/src/com/android/documentsui/dirlist/DocumentHolder.java +++ b/src/com/android/documentsui/dirlist/DocumentHolder.java @@ -18,6 +18,7 @@ package com.android.documentsui.dirlist; import static com.android.documentsui.DevicePolicyResources.Strings.PREVIEW_WORK_FILE_ACCESSIBILITY; import static com.android.documentsui.DevicePolicyResources.Strings.UNDEFINED; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; import android.app.admin.DevicePolicyManager; import android.content.Context; @@ -55,7 +56,9 @@ import javax.annotation.Nullable; public abstract class DocumentHolder extends RecyclerView.ViewHolder implements View.OnKeyListener { - static final float DISABLED_ALPHA = 0.3f; + static final float DISABLED_ALPHA = isUseMaterial3FlagEnabled() ? 0.6f : 0.3f; + + static final int THUMBNAIL_STROKE_WIDTH = isUseMaterial3FlagEnabled() ? 2 : 0; protected final Context mContext; diff --git a/src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java b/src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java index 4b66b857f..838b1fa72 100644 --- a/src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java +++ b/src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java @@ -16,10 +16,13 @@ package com.android.documentsui.dirlist; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; + import android.content.Context; import android.content.res.TypedArray; import android.util.AttributeSet; import android.util.Log; +import android.util.TypedValue; import android.view.MotionEvent; import androidx.annotation.ColorRes; @@ -42,20 +45,37 @@ public class DocumentsSwipeRefreshLayout extends SwipeRefreshLayout { public DocumentsSwipeRefreshLayout(Context context, AttributeSet attrs) { super(context, attrs); - final int[] styledAttrs = {android.R.attr.colorAccent}; + if (isUseMaterial3FlagEnabled()) { + TypedValue spinnerColor = new TypedValue(); + context.getTheme() + .resolveAttribute( + com.google.android.material.R.attr.colorOnPrimaryContainer, + spinnerColor, + true); + setColorSchemeResources(spinnerColor.resourceId); + TypedValue spinnerBackgroundColor = new TypedValue(); + context.getTheme() + .resolveAttribute( + com.google.android.material.R.attr.colorPrimaryContainer, + spinnerBackgroundColor, + true); + setProgressBackgroundColorSchemeResource(spinnerBackgroundColor.resourceId); + } else { + final int[] styledAttrs = {android.R.attr.colorAccent}; - TypedArray a = context.obtainStyledAttributes(styledAttrs); - @ColorRes int colorId = a.getResourceId(0, -1); - if (colorId == -1) { - Log.w(TAG, "Retrieve colorAccent colorId from theme fail, assign R.color.primary"); - colorId = R.color.primary; + TypedArray a = context.obtainStyledAttributes(styledAttrs); + @ColorRes int colorId = a.getResourceId(0, -1); + if (colorId == -1) { + Log.w(TAG, "Retrieve colorAccent colorId from theme fail, assign R.color.primary"); + colorId = R.color.primary; + } + a.recycle(); + setColorSchemeResources(colorId); } - a.recycle(); - setColorSchemeResources(colorId); } @Override public boolean onInterceptTouchEvent(MotionEvent e) { return false; } -}
\ No newline at end of file +} diff --git a/src/com/android/documentsui/dirlist/GridDirectoryHolder.java b/src/com/android/documentsui/dirlist/GridDirectoryHolder.java index 7e9a32df2..b7a8b6d05 100644 --- a/src/com/android/documentsui/dirlist/GridDirectoryHolder.java +++ b/src/com/android/documentsui/dirlist/GridDirectoryHolder.java @@ -46,6 +46,7 @@ import com.android.modules.utils.build.SdkLevel; import java.util.Map; +// TODO(b/379776735): remove this file after use_material3 flag is launched. final class GridDirectoryHolder extends DocumentHolder { final TextView mTitle; diff --git a/src/com/android/documentsui/dirlist/GridDocumentHolder.java b/src/com/android/documentsui/dirlist/GridDocumentHolder.java index eb35b1a5f..f2802ff66 100644 --- a/src/com/android/documentsui/dirlist/GridDocumentHolder.java +++ b/src/com/android/documentsui/dirlist/GridDocumentHolder.java @@ -21,6 +21,7 @@ import static com.android.documentsui.DevicePolicyResources.Drawables.WORK_PROFI import static com.android.documentsui.base.DocumentInfo.getCursorInt; import static com.android.documentsui.base.DocumentInfo.getCursorLong; import static com.android.documentsui.base.DocumentInfo.getCursorString; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; import android.app.admin.DevicePolicyManager; import android.content.Context; @@ -35,6 +36,7 @@ import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import com.android.documentsui.ConfigStore; @@ -42,11 +44,14 @@ import com.android.documentsui.DocumentsApplication; import com.android.documentsui.R; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.Shared; +import com.android.documentsui.base.State; import com.android.documentsui.base.UserId; import com.android.documentsui.roots.RootCursorWrapper; import com.android.documentsui.ui.Views; import com.android.modules.utils.build.SdkLevel; +import com.google.android.material.card.MaterialCardView; + import java.util.Map; import java.util.function.Function; @@ -55,30 +60,49 @@ final class GridDocumentHolder extends DocumentHolder { final TextView mTitle; final TextView mDate; final TextView mDetails; + // Non-null only when useMaterial3 flag is ON. + final @Nullable TextView mBullet; final ImageView mIconMimeLg; - final ImageView mIconMimeSm; + // Null when useMaterial3 flag is ON. + final @Nullable ImageView mIconMimeSm; final ImageView mIconThumb; - final ImageView mIconCheck; + // Null when useMaterial3 flag is ON. + final @Nullable ImageView mIconCheck; final ImageView mIconBadge; final IconHelper mIconHelper; - final View mIconLayout; + // Null when useMaterial3 flag is ON. + final @Nullable View mIconLayout; final View mPreviewIcon; // This is used in as a convenience in our bind method. private final DocumentInfo mDoc = new DocumentInfo(); + // Non-null only when useMaterial3 flag is ON. + private final @Nullable MaterialCardView mIconWrapper; + GridDocumentHolder(Context context, ViewGroup parent, IconHelper iconHelper, ConfigStore configStore) { super(context, parent, R.layout.item_doc_grid, configStore); - mIconLayout = itemView.findViewById(R.id.icon); + if (isUseMaterial3FlagEnabled()) { + mBullet = itemView.findViewById(R.id.bullet); + mIconWrapper = itemView.findViewById(R.id.icon_wrapper); + mIconLayout = null; + mIconMimeSm = null; + mIconCheck = null; + } else { + mBullet = null; + mIconWrapper = null; + mIconLayout = itemView.findViewById(R.id.icon); + mIconMimeSm = (ImageView) itemView.findViewById(R.id.icon_mime_sm); + mIconCheck = (ImageView) itemView.findViewById(R.id.icon_check); + } + mTitle = (TextView) itemView.findViewById(android.R.id.title); mDate = (TextView) itemView.findViewById(R.id.date); mDetails = (TextView) itemView.findViewById(R.id.details); mIconMimeLg = (ImageView) itemView.findViewById(R.id.icon_mime_lg); - mIconMimeSm = (ImageView) itemView.findViewById(R.id.icon_mime_sm); mIconThumb = (ImageView) itemView.findViewById(R.id.icon_thumb); - mIconCheck = (ImageView) itemView.findViewById(R.id.icon_check); mIconBadge = (ImageView) itemView.findViewById(R.id.icon_profile_badge); mPreviewIcon = itemView.findViewById(R.id.preview_icon); @@ -99,17 +123,20 @@ final class GridDocumentHolder extends DocumentHolder { @Override public void setSelected(boolean selected, boolean animate) { - // We always want to make sure our check box disappears if we're not selected, - // even if the item is disabled. This is because this object can be reused - // and this method will be called to setup initial state. float checkAlpha = selected ? 1f : 0f; - if (animate) { - fade(mIconMimeSm, checkAlpha).start(); - fade(mIconCheck, checkAlpha).start(); - } else { - mIconCheck.setAlpha(checkAlpha); + if (!isUseMaterial3FlagEnabled()) { + // We always want to make sure our check box disappears if we're not selected, + // even if the item is disabled. This is because this object can be reused + // and this method will be called to setup initial state. + if (animate) { + fade(mIconMimeSm, checkAlpha).start(); + fade(mIconCheck, checkAlpha).start(); + } else { + mIconCheck.setAlpha(checkAlpha); + } } + // But it should be an error to be set to selected && be disabled. if (!itemView.isEnabled()) { assert (!selected); @@ -117,10 +144,21 @@ final class GridDocumentHolder extends DocumentHolder { super.setSelected(selected, animate); - if (animate) { - fade(mIconMimeSm, 1f - checkAlpha).start(); - } else { - mIconMimeSm.setAlpha(1f - checkAlpha); + if (!isUseMaterial3FlagEnabled()) { + if (animate) { + fade(mIconMimeSm, 1f - checkAlpha).start(); + } else { + mIconMimeSm.setAlpha(1f - checkAlpha); + } + } + + // Do not show stroke when selected, only show stroke when not selected if it has thumbnail. + if (mIconWrapper != null) { + if (selected) { + mIconWrapper.setStrokeWidth(0); + } else if (mIconThumb.getDrawable() != null) { + mIconWrapper.setStrokeWidth(THUMBNAIL_STROKE_WIDTH); + } } } @@ -131,19 +169,26 @@ final class GridDocumentHolder extends DocumentHolder { float imgAlpha = enabled ? 1f : DISABLED_ALPHA; mIconMimeLg.setAlpha(imgAlpha); - mIconMimeSm.setAlpha(imgAlpha); + if (!isUseMaterial3FlagEnabled()) { + mIconMimeSm.setAlpha(imgAlpha); + } mIconThumb.setAlpha(imgAlpha); } @Override public void bindPreviewIcon(boolean show, Function<View, Boolean> clickCallback) { + if (isUseMaterial3FlagEnabled() && mDoc.isDirectory()) { + mPreviewIcon.setVisibility(View.GONE); + return; + } mPreviewIcon.setVisibility(show ? View.VISIBLE : View.GONE); if (show) { mPreviewIcon.setContentDescription( getPreviewIconContentDescription( mIconHelper.shouldShowBadge(mDoc.userId.getIdentifier()), mDoc.displayName, mDoc.userId)); - mPreviewIcon.setAccessibilityDelegate(new PreviewAccessibilityDelegate(clickCallback)); + mPreviewIcon.setAccessibilityDelegate( + new PreviewAccessibilityDelegate(clickCallback)); } } @@ -171,6 +216,10 @@ final class GridDocumentHolder extends DocumentHolder { @Override public boolean inSelectRegion(MotionEvent event) { + if (isUseMaterial3FlagEnabled()) { + return (mDoc.isDirectory() && !(mAction == State.ACTION_BROWSE)) ? false + : Views.isEventOver(event, itemView.getParent(), mIconWrapper); + } return Views.isEventOver(event, itemView.getParent(), mIconLayout); } @@ -202,7 +251,21 @@ final class GridDocumentHolder extends DocumentHolder { mIconThumb.animate().cancel(); mIconThumb.setAlpha(0f); - mIconHelper.load(mDoc, mIconThumb, mIconMimeLg, mIconMimeSm); + if (isUseMaterial3FlagEnabled()) { + mIconHelper.load( + mDoc, mIconThumb, mIconMimeLg, /* subIconMime= */ null, + thumbnailLoaded -> { + // Show stroke when thumbnail is loaded. + if (mIconWrapper != null) { + mIconWrapper.setStrokeWidth( + thumbnailLoaded ? THUMBNAIL_STROKE_WIDTH : 0); + } + }); + } else { + mIconHelper.load( + mDoc, mIconThumb, mIconMimeLg, mIconMimeSm, /* thumbnailLoadedCallback= */ + null); + } mTitle.setText(mDoc.displayName, TextView.BufferType.SPANNABLE); mTitle.setVisibility(View.VISIBLE); @@ -229,5 +292,11 @@ final class GridDocumentHolder extends DocumentHolder { mDetails.setText(Formatter.formatFileSize(mContext, docSize)); } } + + if (mBullet != null && (mDetails.getVisibility() == View.GONE + || mDate.getText().isEmpty())) { + // There is no need for the bullet separating the details and date. + mBullet.setVisibility(View.GONE); + } } } diff --git a/src/com/android/documentsui/dirlist/GridPhotoHolder.java b/src/com/android/documentsui/dirlist/GridPhotoHolder.java index e86d3131f..70c8a6ffc 100644 --- a/src/com/android/documentsui/dirlist/GridPhotoHolder.java +++ b/src/com/android/documentsui/dirlist/GridPhotoHolder.java @@ -189,7 +189,12 @@ final class GridPhotoHolder extends DocumentHolder { mIconThumb.animate().cancel(); mIconThumb.setAlpha(0f); - mIconHelper.load(mDoc, mIconThumb, mIconMimeLg, null); + mIconHelper.load( + mDoc, + mIconThumb, + mIconMimeLg, + /* subIconMime= */ null, + /* thumbnailLoadedCallback= */ null); final String docSize = Formatter.formatFileSize(mContext, getCursorLong(cursor, Document.COLUMN_SIZE)); diff --git a/src/com/android/documentsui/dirlist/IconHelper.java b/src/com/android/documentsui/dirlist/IconHelper.java index 44d1d95d3..6d53bbee1 100644 --- a/src/com/android/documentsui/dirlist/IconHelper.java +++ b/src/com/android/documentsui/dirlist/IconHelper.java @@ -52,6 +52,7 @@ import com.android.documentsui.base.UserId; import com.android.modules.utils.build.SdkLevel; import java.util.function.BiConsumer; +import java.util.function.Consumer; /** * A class to assist with loading and managing the Images (i.e. thumbnails and icons) associated @@ -151,14 +152,18 @@ public class IconHelper { * @param iconThumb The itemview's thumbnail icon. * @param iconMime The itemview's mime icon. Hidden when iconThumb is shown. * @param subIconMime The second itemview's mime icon. Always visible. + * @param thumbnailLoadedCallback The callback function which will be invoked after the + * thumbnail is loaded, with a boolean parameter to indicate + * if it's loaded or not. */ public void load( DocumentInfo doc, ImageView iconThumb, ImageView iconMime, - @Nullable ImageView subIconMime) { + @Nullable ImageView subIconMime, + @Nullable Consumer<Boolean> thumbnailLoadedCallback) { load(doc.derivedUri, doc.userId, doc.mimeType, doc.flags, doc.icon, doc.lastModified, - iconThumb, iconMime, subIconMime); + iconThumb, iconMime, subIconMime, thumbnailLoadedCallback); } /** @@ -172,10 +177,13 @@ public class IconHelper { * @param iconThumb The itemview's thumbnail icon. * @param iconMime The itemview's mime icon. Hidden when iconThumb is shown. * @param subIconMime The second itemview's mime icon. Always visible. + * @param thumbnailLoadedCallback The callback function which will be invoked after the + * thumbnail is loaded, with a boolean parameter to indicate + * if it's loaded or not. */ public void load(Uri uri, UserId userId, String mimeType, int docFlags, int docIcon, long docLastModified, ImageView iconThumb, ImageView iconMime, - @Nullable ImageView subIconMime) { + @Nullable ImageView subIconMime, @Nullable Consumer<Boolean> thumbnailLoadedCallback) { boolean loadedThumbnail = false; final String docAuthority = uri.getAuthority(); @@ -186,7 +194,14 @@ public class IconHelper { final boolean showThumbnail = supportsThumbnail && allowThumbnail && mThumbnailsEnabled; if (showThumbnail) { loadedThumbnail = - loadThumbnail(uri, userId, docAuthority, docLastModified, iconThumb, iconMime); + loadThumbnail( + uri, + userId, + docAuthority, + docLastModified, + iconThumb, + iconMime, + thumbnailLoadedCallback); } final Drawable mimeIcon = getDocumentIcon(mContext, userId, docAuthority, @@ -202,15 +217,22 @@ public class IconHelper { setMimeIcon(iconMime, mimeIcon); hideImageView(iconThumb); } + if (thumbnailLoadedCallback != null) { + thumbnailLoadedCallback.accept(loadedThumbnail); + } } private boolean loadThumbnail(Uri uri, UserId userId, String docAuthority, long docLastModified, - ImageView iconThumb, ImageView iconMime) { + ImageView iconThumb, ImageView iconMime, + @Nullable Consumer<Boolean> thumbnailLoadedCallback) { final Result result = mThumbnailCache.getThumbnail(uri, userId, mCurrentSize); try { final Bitmap cachedThumbnail = result.getThumbnail(); iconThumb.setImageBitmap(cachedThumbnail); + if (thumbnailLoadedCallback != null) { + thumbnailLoadedCallback.accept(cachedThumbnail != null); + } boolean stale = (docLastModified > result.getLastModified()); if (VERBOSE) { @@ -230,6 +252,9 @@ public class IconHelper { iconThumb.setImageBitmap(bitmap); animator.accept(iconMime, iconThumb); } + if (thumbnailLoadedCallback != null) { + thumbnailLoadedCallback.accept(bitmap != null); + } }, true /* addToCache */); ProviderExecutor.forAuthority(docAuthority).execute(task); diff --git a/src/com/android/documentsui/dirlist/ListDocumentHolder.java b/src/com/android/documentsui/dirlist/ListDocumentHolder.java index 2c09d3aec..0d0f79919 100644 --- a/src/com/android/documentsui/dirlist/ListDocumentHolder.java +++ b/src/com/android/documentsui/dirlist/ListDocumentHolder.java @@ -20,6 +20,7 @@ import static com.android.documentsui.DevicePolicyResources.Drawables.Style.SOLI import static com.android.documentsui.DevicePolicyResources.Drawables.WORK_PROFILE_ICON; import static com.android.documentsui.base.DocumentInfo.getCursorInt; import static com.android.documentsui.base.DocumentInfo.getCursorString; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; import android.app.admin.DevicePolicyManager; import android.content.Context; @@ -52,6 +53,8 @@ import com.android.documentsui.roots.RootCursorWrapper; import com.android.documentsui.ui.Views; import com.android.modules.utils.build.SdkLevel; +import com.google.android.material.card.MaterialCardView; + import java.util.ArrayList; import java.util.Map; import java.util.function.Function; @@ -67,6 +70,8 @@ final class ListDocumentHolder extends DocumentHolder { private final @Nullable LinearLayout mDetails; // TextView for date + size + summary, null only for tablets/sw720dp private final @Nullable TextView mMetadataView; + // Non-null only when use_material3 flag is ON. + private final @Nullable MaterialCardView mIconWrapper; private final ImageView mIconMime; private final ImageView mIconThumb; private final ImageView mIconCheck; @@ -84,6 +89,8 @@ final class ListDocumentHolder extends DocumentHolder { super(context, parent, R.layout.item_doc_list, configStore); mIconLayout = itemView.findViewById(R.id.icon); + mIconWrapper = + isUseMaterial3FlagEnabled() ? itemView.findViewById(R.id.icon_wrapper) : null; mIconMime = (ImageView) itemView.findViewById(R.id.icon_mime); mIconThumb = (ImageView) itemView.findViewById(R.id.icon_thumb); mIconCheck = (ImageView) itemView.findViewById(R.id.icon_check); @@ -139,16 +146,29 @@ final class ListDocumentHolder extends DocumentHolder { mIconMime.setAlpha(1f - checkAlpha); mIconThumb.setAlpha(1f - checkAlpha); } + + // Do not show stroke when selected, only show stroke when not selected if it has thumbnail. + if (isUseMaterial3FlagEnabled() && mIconWrapper != null) { + if (selected) { + mIconWrapper.setStrokeWidth(0); + } else if (mIconThumb.getDrawable() != null) { + mIconWrapper.setStrokeWidth(2); + } + } } @Override public void setEnabled(boolean enabled) { super.setEnabled(enabled); - // Text colors enabled/disabled is handle via a color set. - final float imgAlpha = enabled ? 1f : DISABLED_ALPHA; - mIconMime.setAlpha(imgAlpha); - mIconThumb.setAlpha(imgAlpha); + if (isUseMaterial3FlagEnabled()) { + itemView.setAlpha(enabled ? 1f : DISABLED_ALPHA); + } else { + // Text colors enabled/disabled is handle via a color set. + final float imgAlpha = enabled ? 1f : DISABLED_ALPHA; + mIconMime.setAlpha(imgAlpha); + mIconThumb.setAlpha(imgAlpha); + } } @Override @@ -243,7 +263,17 @@ final class ListDocumentHolder extends DocumentHolder { mIconThumb.animate().cancel(); mIconThumb.setAlpha(0f); - mIconHelper.load(mDoc, mIconThumb, mIconMime, null); + mIconHelper.load( + mDoc, + mIconThumb, + mIconMime, + /* subIconMime= */ null, + thumbnailLoaded -> { + // Show stroke when thumbnail is loaded. + if (isUseMaterial3FlagEnabled() && mIconWrapper != null) { + mIconWrapper.setStrokeWidth(thumbnailLoaded ? THUMBNAIL_STROKE_WIDTH : 0); + } + }); mTitle.setText(mDoc.displayName, TextView.BufferType.SPANNABLE); mTitle.setVisibility(View.VISIBLE); diff --git a/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java b/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java index fc60d07a3..112b70da8 100644 --- a/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java +++ b/src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java @@ -20,6 +20,7 @@ import static com.android.documentsui.base.DocumentInfo.getCursorInt; import static com.android.documentsui.base.DocumentInfo.getCursorString; import static com.android.documentsui.base.State.MODE_GRID; import static com.android.documentsui.base.State.MODE_LIST; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; import android.database.Cursor; import android.provider.DocumentsContract.Document; @@ -95,8 +96,12 @@ final class ModelBackedDocumentsAdapter extends DocumentsAdapter { case MODE_GRID: switch (viewType) { case ITEM_TYPE_DIRECTORY: - holder = - new GridDirectoryHolder( + // Under the Material3 flag, the GridDocumentHolder is the holder for all + // grid items. + holder = isUseMaterial3FlagEnabled() + ? new GridDocumentHolder( + mEnv.getContext(), parent, mIconHelper, mConfigStore) + : new GridDirectoryHolder( mEnv.getContext(), parent, mIconHelper, mConfigStore); break; case ITEM_TYPE_DOCUMENT: diff --git a/src/com/android/documentsui/dirlist/SelectionMetadata.java b/src/com/android/documentsui/dirlist/SelectionMetadata.java index 3abc3e190..74b6061b3 100644 --- a/src/com/android/documentsui/dirlist/SelectionMetadata.java +++ b/src/com/android/documentsui/dirlist/SelectionMetadata.java @@ -168,7 +168,7 @@ public class SelectionMetadata extends SelectionObserver<String> } @Override - public boolean canOpenWith() { + public boolean canOpen() { return size() == 1 && mDirectoryCount == 0 && mInArchiveCount == 0 && mPartialCount == 0; } } diff --git a/src/com/android/documentsui/files/ActionHandler.java b/src/com/android/documentsui/files/ActionHandler.java index 20b831856..86f7a1a14 100644 --- a/src/com/android/documentsui/files/ActionHandler.java +++ b/src/com/android/documentsui/files/ActionHandler.java @@ -19,10 +19,14 @@ package com.android.documentsui.files; import static android.content.ContentResolver.wrap; import static com.android.documentsui.base.SharedMinimal.DEBUG; +import static com.android.documentsui.util.FlagUtils.isDesktopFileHandlingFlagEnabled; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; +import static com.android.documentsui.util.FlagUtils.isUsePeekPreviewFlagEnabled; import android.app.DownloadManager; import android.content.ActivityNotFoundException; import android.content.ClipData; +import android.content.ComponentName; import android.content.ContentProviderClient; import android.content.ContentResolver; import android.content.Intent; @@ -96,6 +100,7 @@ public class ActionHandler<T extends FragmentActivity & AbstractActionHandler.Co private final DocumentClipper mClipper; private final ClipStore mClipStore; private final DragAndDropManager mDragAndDropManager; + private final Runnable mCloseSelectionBar; ActionHandler( T activity, @@ -104,7 +109,8 @@ public class ActionHandler<T extends FragmentActivity & AbstractActionHandler.Co DocumentsAccess docs, SearchViewManager searchMgr, Lookup<String, Executor> executors, - ActionModeAddons actionModeAddons, + @Nullable ActionModeAddons actionModeAddons, + Runnable closeSelectionBar, DocumentClipper clipper, ClipStore clipStore, DragAndDropManager dragAndDropManager, @@ -113,6 +119,7 @@ public class ActionHandler<T extends FragmentActivity & AbstractActionHandler.Co super(activity, state, providers, docs, searchMgr, executors, injector); mActionModeAddons = actionModeAddons; + mCloseSelectionBar = closeSelectionBar; mFeatures = injector.features; mConfig = injector.config; mClipper = clipper; @@ -221,9 +228,19 @@ public class ActionHandler<T extends FragmentActivity & AbstractActionHandler.Co } @Override + public void openDocumentViewOnly(DocumentInfo doc) { + mInjector.searchManager.recordHistory(); + openDocument(doc, VIEW_TYPE_REGULAR, VIEW_TYPE_NONE); + } + + @Override public void springOpenDirectory(DocumentInfo doc) { - assert(doc.isDirectory()); - mActionModeAddons.finishActionMode(); + assert (doc.isDirectory()); + if (isUseMaterial3FlagEnabled()) { + mCloseSelectionBar.run(); + } else { + mActionModeAddons.finishActionMode(); + } openContainerDocument(doc); } @@ -315,7 +332,11 @@ public class ActionHandler<T extends FragmentActivity & AbstractActionHandler.Co return; } - mActionModeAddons.finishActionMode(); + if (isUseMaterial3FlagEnabled()) { + mCloseSelectionBar.run(); + } else { + mActionModeAddons.finishActionMode(); + } List<Uri> uris = new ArrayList<>(docs.size()); for (DocumentInfo doc : docs) { @@ -543,17 +564,27 @@ public class ActionHandler<T extends FragmentActivity & AbstractActionHandler.Co return; } - Intent intent = Intent.createChooser(buildViewIntent(doc), null); - intent.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false); - try { - doc.userId.startActivityAsUser(mActivity, intent); - } catch (ActivityNotFoundException e) { - mDialogs.showNoApplicationFound(); + if (isDesktopFileHandlingFlagEnabled()) { + Intent intent = buildViewIntent(doc); + intent.setComponent( + new ComponentName("android", "com.android.internal.app.ResolverActivity")); + try { + doc.userId.startActivityAsUser(mActivity, intent); + } catch (ActivityNotFoundException e) { + mDialogs.showNoApplicationFound(); + } + } else { + Intent intent = Intent.createChooser(buildViewIntent(doc), null); + intent.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false); + try { + doc.userId.startActivityAsUser(mActivity, intent); + } catch (ActivityNotFoundException e) { + mDialogs.showNoApplicationFound(); + } } } - @Override - public void showInspector(DocumentInfo doc) { + private void showInspector(DocumentInfo doc) { Metrics.logUserAction(MetricConsts.USER_ACTION_INSPECTOR); Intent intent = InspectorActivity.createIntent(mActivity, doc.derivedUri, doc.userId); @@ -577,4 +608,17 @@ public class ActionHandler<T extends FragmentActivity & AbstractActionHandler.Co } mActivity.startActivity(intent); } + + private void showPeek() { + Log.d(TAG, "Peek not implemented"); + } + + @Override + public void showPreview(DocumentInfo doc) { + if (isUseMaterial3FlagEnabled() && isUsePeekPreviewFlagEnabled()) { + showPeek(); + } else { + showInspector(doc); + } + } } diff --git a/src/com/android/documentsui/files/FilesActivity.java b/src/com/android/documentsui/files/FilesActivity.java index 1ebe2374f..50e266d38 100644 --- a/src/com/android/documentsui/files/FilesActivity.java +++ b/src/com/android/documentsui/files/FilesActivity.java @@ -17,12 +17,16 @@ package com.android.documentsui.files; import static com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_UNKNOWN; +import static com.android.documentsui.base.SharedMinimal.DEBUG; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; +import static com.android.documentsui.util.FlagUtils.isZipNgFlagEnabled; import android.app.ActivityManager.TaskDescription; import android.content.Intent; import android.graphics.Color; import android.net.Uri; import android.os.Bundle; +import android.util.Log; import android.view.KeyEvent; import android.view.KeyboardShortcutGroup; import android.view.Menu; @@ -136,25 +140,30 @@ public class FilesActivity extends BaseActivity implements AbstractActionHandler mInjector.getModel()::getItemUri, mInjector.getModel()::getItemCount); - mInjector.actionModeController = new ActionModeController( - this, - mInjector.selectionMgr, - mNavigator, - mInjector.menuManager, - mInjector.messages); + if (!isUseMaterial3FlagEnabled()) { + mInjector.actionModeController = + new ActionModeController( + this, + mInjector.selectionMgr, + mNavigator, + mInjector.menuManager, + mInjector.messages); + } - mInjector.actions = new ActionHandler<>( - this, - mState, - mProviders, - mDocs, - mSearchManager, - ProviderExecutor::forAuthority, - mInjector.actionModeController, - clipper, - DocumentsApplication.getClipStore(this), - DocumentsApplication.getDragAndDropManager(this), - mInjector); + mInjector.actions = + new ActionHandler<>( + this, + mState, + mProviders, + mDocs, + mSearchManager, + ProviderExecutor::forAuthority, + mInjector.actionModeController, + getNavigator()::closeSelectionBar, + clipper, + DocumentsApplication.getClipStore(this), + DocumentsApplication.getDragAndDropManager(this), + mInjector); mInjector.searchManager = mSearchManager; @@ -181,6 +190,14 @@ public class FilesActivity extends BaseActivity implements AbstractActionHandler RootsFragment.show(getSupportFragmentManager(), /* includeApps= */ false, /* intent= */ null); + if (isUseMaterial3FlagEnabled()) { + View navRailRoots = findViewById(R.id.nav_rail_container_roots); + if (navRailRoots != null) { + // Medium layout, populate navigation rail layout. + RootsFragment.showNavRail(getSupportFragmentManager(), /* includeApps= */ false, + /* intent= */ null); + } + } final Intent intent = getIntent(); @@ -315,7 +332,9 @@ public class FilesActivity extends BaseActivity implements AbstractActionHandler @Override public boolean onPrepareOptionsMenu(Menu menu) { super.onPrepareOptionsMenu(menu); - mInjector.menuManager.updateOptionMenu(menu); + if (!isUseMaterial3FlagEnabled()) { + mInjector.menuManager.updateOptionMenu(menu); + } return true; } @@ -329,12 +348,22 @@ public class FilesActivity extends BaseActivity implements AbstractActionHandler mInjector.actions.openInNewWindow(mState.stack); } else if (id == R.id.option_menu_settings) { mInjector.actions.openSettings(getCurrentRoot()); + } else if (id == R.id.option_menu_extract_all) { + if (!isZipNgFlagEnabled()) return false; + final DirectoryFragment dir = getDirectoryFragment(); + if (dir == null) return false; + mInjector.actions.selectAllFiles(); + return dir.onContextItemSelected(item); } else if (id == R.id.option_menu_select_all) { mInjector.actions.selectAllFiles(); } else if (id == R.id.option_menu_inspect) { - mInjector.actions.showInspector(getCurrentDirectory()); + mInjector.actions.showPreview(getCurrentDirectory()); } else { - return super.onOptionsItemSelected(item); + final boolean ok = super.onOptionsItemSelected(item); + if (DEBUG && !ok) { + Log.d(TAG, "Unhandled option item " + id); + } + return ok; } return true; } diff --git a/src/com/android/documentsui/files/MenuManager.java b/src/com/android/documentsui/files/MenuManager.java index 742bc9739..9b3564eeb 100644 --- a/src/com/android/documentsui/files/MenuManager.java +++ b/src/com/android/documentsui/files/MenuManager.java @@ -16,6 +16,8 @@ package com.android.documentsui.files; +import static com.android.documentsui.util.FlagUtils.isDesktopFileHandlingFlagEnabled; + import android.content.Context; import android.content.res.Resources; import android.net.Uri; @@ -161,7 +163,13 @@ public final class MenuManager extends com.android.documentsui.MenuManager { @Override protected void updateOpenWith(MenuItem openWith, SelectionDetails selectionDetails) { - Menus.setEnabledAndVisible(openWith, selectionDetails.canOpenWith()); + Menus.setEnabledAndVisible(openWith, selectionDetails.canOpen()); + } + + @Override + protected void updateOpenInContextMenu(MenuItem open, SelectionDetails selectionDetails) { + Menus.setEnabledAndVisible( + open, isDesktopFileHandlingFlagEnabled() && selectionDetails.canOpen()); } @Override @@ -218,6 +226,11 @@ public final class MenuManager extends com.android.documentsui.MenuManager { } @Override + protected void updateExtractAll(MenuItem it) { + Menus.setEnabledAndVisible(it, mDirDetails.isInArchive()); + } + + @Override protected void updateSelectAll(MenuItem selectAll) { Menus.setEnabledAndVisible(selectAll, true); } diff --git a/src/com/android/documentsui/loaders/BaseFileLoader.kt b/src/com/android/documentsui/loaders/BaseFileLoader.kt new file mode 100644 index 000000000..dd76217ac --- /dev/null +++ b/src/com/android/documentsui/loaders/BaseFileLoader.kt @@ -0,0 +1,208 @@ +/* + * Copyright (C) 2024 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.documentsui.loaders + +import android.content.Context +import android.database.Cursor +import android.database.MatrixCursor +import android.database.MergeCursor +import android.net.Uri +import android.os.Bundle +import android.os.CancellationSignal +import android.os.RemoteException +import android.provider.DocumentsContract.Document +import android.util.Log +import androidx.loader.content.AsyncTaskLoader +import com.android.documentsui.DirectoryResult +import com.android.documentsui.base.Lookup +import com.android.documentsui.base.UserId +import com.android.documentsui.roots.RootCursorWrapper + +const val TAG = "SearchV2" + +val FILE_ENTRY_COLUMNS = arrayOf( + Document.COLUMN_DOCUMENT_ID, + Document.COLUMN_MIME_TYPE, + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_LAST_MODIFIED, + Document.COLUMN_FLAGS, + Document.COLUMN_SUMMARY, + Document.COLUMN_SIZE, + Document.COLUMN_ICON, +) + +fun emptyCursor(): Cursor { + return MatrixCursor(FILE_ENTRY_COLUMNS) +} + +/** + * Helper function that returns a single, non-null cursor constructed from the given list of + * cursors. + */ +fun toSingleCursor(cursorList: List<Cursor>): Cursor { + if (cursorList.isEmpty()) { + return emptyCursor() + } + if (cursorList.size == 1) { + return cursorList[0] + } + return MergeCursor(cursorList.toTypedArray()) +} + +/** + * The base class for search and directory loaders. This class implements common functionality + * shared by these loaders. The extending classes should implement loadInBackground, which + * should call the queryLocation method. + */ +abstract class BaseFileLoader( + context: Context, + private val mUserIdList: List<UserId>, + protected val mMimeTypeLookup: Lookup<String, String>, +) : AsyncTaskLoader<DirectoryResult>(context) { + + private var mSignal: CancellationSignal? = null + private var mResult: DirectoryResult? = null + + override fun cancelLoadInBackground() { + Log.d(TAG, "BasedFileLoader.cancelLoadInBackground") + super.cancelLoadInBackground() + + synchronized(this) { + mSignal?.cancel() + } + } + + override fun deliverResult(result: DirectoryResult?) { + Log.d(TAG, "BasedFileLoader.deliverResult") + if (isReset) { + closeResult(result) + return + } + val oldResult: DirectoryResult? = mResult + mResult = result + + if (isStarted) { + super.deliverResult(result) + } + + if (oldResult != null && oldResult !== result) { + closeResult(oldResult) + } + } + + override fun onStartLoading() { + Log.d(TAG, "BasedFileLoader.onStartLoading") + val isCursorStale: Boolean = checkIfCursorStale(mResult) + if (mResult != null && !isCursorStale) { + deliverResult(mResult) + } + if (takeContentChanged() || mResult == null || isCursorStale) { + forceLoad() + } + } + + override fun onStopLoading() { + Log.d(TAG, "BasedFileLoader.onStopLoading") + cancelLoad() + } + + override fun onCanceled(result: DirectoryResult?) { + Log.d(TAG, "BasedFileLoader.onCanceled") + closeResult(result) + } + + override fun onReset() { + Log.d(TAG, "BasedFileLoader.onReset") + super.onReset() + + // Ensure the loader is stopped + onStopLoading() + + closeResult(mResult) + mResult = null + } + + /** + * Quietly closes the result cursor, if results are still available. + */ + fun closeResult(result: DirectoryResult?) { + try { + result?.close() + } catch (e: Exception) { + Log.d(TAG, "Failed to close result", e) + } + } + + private fun checkIfCursorStale(result: DirectoryResult?): Boolean { + if (result == null) { + return true + } + val cursor = result.cursor ?: return true + if (cursor.isClosed) { + return true + } + Log.d(TAG, "Long check of cursor staleness") + val count = cursor.count + if (!cursor.moveToPosition(-1)) { + return true + } + for (i in 1..count) { + if (!cursor.moveToNext()) { + return true + } + } + return false + } + + /** + * A function that, for the specified location rooted in the root with the given rootId + * attempts to obtain a non-null cursor from the content provider client obtained for the + * given locationUri. It returns the first non-null cursor, if one can be found, or null, + * if it fails to query the given location for all known users. + */ + fun queryLocation( + rootId: String, + locationUri: Uri, + queryArgs: Bundle?, + maxResults: Int, + ): Cursor? { + val authority = locationUri.authority ?: return null + for (userId in mUserIdList) { + Log.d(TAG, "BaseFileLoader.queryLocation for $userId at $locationUri") + val resolver = userId.getContentResolver(context) + try { + resolver.acquireUnstableContentProviderClient( + authority + ).use { client -> + if (client == null) { + return null + } + try { + val cursor = + client.query(locationUri, null, queryArgs, mSignal) ?: return null + return RootCursorWrapper(userId, authority, rootId, cursor, maxResults) + } catch (e: RemoteException) { + Log.d(TAG, "Failed to get cursor for $locationUri", e) + } + } + } catch (e: Exception) { + Log.d(TAG, "Failed to get a content provider client for $locationUri", e) + } + } + + return null + } +} diff --git a/src/com/android/documentsui/loaders/FolderLoader.kt b/src/com/android/documentsui/loaders/FolderLoader.kt new file mode 100644 index 000000000..a166ca752 --- /dev/null +++ b/src/com/android/documentsui/loaders/FolderLoader.kt @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2024 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.documentsui.loaders + +import android.content.Context +import android.provider.DocumentsContract +import com.android.documentsui.ContentLock +import com.android.documentsui.DirectoryResult +import com.android.documentsui.LockingContentObserver +import com.android.documentsui.base.DocumentInfo +import com.android.documentsui.base.FilteringCursorWrapper +import com.android.documentsui.base.Lookup +import com.android.documentsui.base.RootInfo +import com.android.documentsui.base.UserId +import com.android.documentsui.sorting.SortModel + +/** + * A specialization of the BaseFileLoader that loads the children of a single folder. To list + * a directory you need to provide: + * + * - The current application context + * - A content lock for which a locking content observer is built + * - A list of user IDs on behalf of which the search is conducted + * - The root info of the listed directory + * - The document info of the listed directory + * - a lookup from file extension to file type + * - The model capable of sorting results + */ +class FolderLoader( + context: Context, + userIdList: List<UserId>, + mimeTypeLookup: Lookup<String, String>, + contentLock: ContentLock, + private val mRoot: RootInfo, + private val mListedDir: DocumentInfo, + private val mOptions: QueryOptions, + private val mSortModel: SortModel, +) : BaseFileLoader(context, userIdList, mimeTypeLookup) { + + // An observer registered on the cursor to force a reload if the cursor reports a change. + private val mObserver = LockingContentObserver(contentLock, this::onContentChanged) + + // Creates a directory result object corresponding to the current parameters of the loader. + override fun loadInBackground(): DirectoryResult? { + val rejectBeforeTimestamp = mOptions.getRejectBeforeTimestamp() + val folderChildrenUri = DocumentsContract.buildChildDocumentsUri( + mListedDir.authority, + mListedDir.documentId + ) + var cursor = + queryLocation(mRoot.rootId, folderChildrenUri, mOptions.otherQueryArgs, ALL_RESULTS) + ?: emptyCursor() + cursor.registerContentObserver(mObserver) + + val filteredCursor = FilteringCursorWrapper(cursor) + filteredCursor.filterHiddenFiles(mOptions.showHidden) + filteredCursor.filterMimes(mOptions.acceptableMimeTypes, null) + if (rejectBeforeTimestamp > 0L) { + filteredCursor.filterLastModified(rejectBeforeTimestamp) + } + // TODO(b:380945065): Add filtering by category, such as images, audio, video. + val sortedCursor = mSortModel.sortCursor(filteredCursor, mMimeTypeLookup) + + val result = DirectoryResult() + result.doc = mListedDir + result.cursor = sortedCursor + return result + } +} diff --git a/src/com/android/documentsui/loaders/QueryOptions.kt b/src/com/android/documentsui/loaders/QueryOptions.kt new file mode 100644 index 000000000..385815e99 --- /dev/null +++ b/src/com/android/documentsui/loaders/QueryOptions.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2024 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.documentsui.loaders + +import android.os.Bundle +import java.time.Duration + +/** + * The constant to be used for the maxResults parameter, if we wish to get all (unlimited) results. + */ +const val ALL_RESULTS: Int = -1 + +/** + * Common query options. These are: + * - maximum number to return; pass ALL_RESULTS to impose no limits. + * - maximum lastModified delta in milliseconds: the delta from now used to reject files that were + * not modified in the specified milliseconds; pass null for no limits. + * - maximum time the query should return, including empty, results; pass null for no limits. + * - whether or not to show hidden files. + * - A list of MIME types used to filter returned files. + * - "Other" query arguments not covered by the above. + * + * The "other" query arguments are added as due to existing code communicating information such + * as acceptable file kind (images, videos, etc.) is done via Bundle arguments. This could be + * and should be changed if this code ever is rewritten. + * TODO(b:397095797): Merge otherQueryArgs with acceptableMimeTypes and maxLastModifiedDelta. + */ +data class QueryOptions( + val maxResults: Int, + val maxLastModifiedDelta: Duration?, + val maxQueryTime: Duration?, + val showHidden: Boolean, + val acceptableMimeTypes: Array<String>, + val otherQueryArgs: Bundle, +) { + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as QueryOptions + + return maxResults == other.maxResults && + maxLastModifiedDelta == other.maxLastModifiedDelta && + maxQueryTime == other.maxQueryTime && + showHidden == other.showHidden && + acceptableMimeTypes.contentEquals(other.acceptableMimeTypes) + } + + /** + * Helper method that computes the earliest valid last modified timestamp. Converts last + * modified duration to milliseconds past now. If the maxLastModifiedDelta is negative + * this method returns 0L. + */ + fun getRejectBeforeTimestamp() = + if (maxLastModifiedDelta == null) { + 0L + } else { + System.currentTimeMillis() - maxLastModifiedDelta.toMillis() + } + + /** + * Helper function that indicates if query time is unlimited. Due to internal reliance on + * Java's Duration class it assumes anything larger than 60 seconds has unlimited waiting + * time. + */ + fun isQueryTimeUnlimited() = maxQueryTime == null + + override fun hashCode(): Int { + var result = maxResults + result = 31 * result + maxLastModifiedDelta.hashCode() + result = 31 * result + maxQueryTime.hashCode() + result = 31 * result + showHidden.hashCode() + result = 31 * result + acceptableMimeTypes.contentHashCode() + return result + } +} diff --git a/src/com/android/documentsui/loaders/SearchLoader.kt b/src/com/android/documentsui/loaders/SearchLoader.kt new file mode 100644 index 000000000..f0da924e2 --- /dev/null +++ b/src/com/android/documentsui/loaders/SearchLoader.kt @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2024 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.documentsui.loaders + +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Bundle +import android.provider.DocumentsContract +import android.provider.DocumentsContract.Document +import android.text.TextUtils +import android.util.Log +import com.android.documentsui.DirectoryResult +import com.android.documentsui.LockingContentObserver +import com.android.documentsui.base.DocumentInfo +import com.android.documentsui.base.FilteringCursorWrapper +import com.android.documentsui.base.Lookup +import com.android.documentsui.base.RootInfo +import com.android.documentsui.base.UserId +import com.android.documentsui.sorting.SortModel +import com.google.common.util.concurrent.AbstractFuture +import java.io.Closeable +import java.util.concurrent.CountDownLatch +import java.util.concurrent.ExecutorService +import java.util.concurrent.TimeUnit +import kotlin.time.measureTime + +/** + * A specialization of the BaseFileLoader that searches the set of specified roots. To search + * the roots you must provider: + * + * - The current application context + * - A content lock for which a locking content observer is built + * - A list of user IDs, on whose behalf we query content provider clients. + * - A list of RootInfo objects representing searched roots + * - A query used to search for matching files. + * - Query options such as maximum number of results, last modified time delta, etc. + * - a lookup from file extension to file type + * - The model capable of sorting results + * - An executor for running searches across multiple roots in parallel + * + * SearchLoader requires that either a query is not null and not empty or that QueryOptions + * specify a last modified time restriction. This is to prevent searching for every file + * across every specified root. + */ +class SearchLoader( + context: Context, + userIdList: List<UserId>, + mimeTypeLookup: Lookup<String, String>, + private val mObserver: LockingContentObserver, + private val mRootList: Collection<RootInfo>, + private val mQuery: String?, + private val mOptions: QueryOptions, + private val mSortModel: SortModel, + private val mExecutorService: ExecutorService, +) : BaseFileLoader(context, userIdList, mimeTypeLookup) { + + /** + * Helper class that runs query on a single user for the given parameter. This class implements + * an abstract future so that if the task is completed, we can retrieve the cursor via the get + * method. + */ + inner class SearchTask( + private val mRootId: String, + private val mSearchUri: Uri, + private val mQueryArgs: Bundle, + private val mLatch: CountDownLatch, + ) : Closeable, Runnable, AbstractFuture<Cursor>() { + private var mCursor: Cursor? = null + val cursor: Cursor? get() = mCursor + val taskId: String get() = mSearchUri.toString() + + override fun close() { + mCursor = null + } + + override fun run() { + val queryDuration = measureTime { + try { + mCursor = queryLocation(mRootId, mSearchUri, mQueryArgs, mOptions.maxResults) + set(mCursor) + } finally { + mLatch.countDown() + } + } + Log.d(TAG, "Query on $mSearchUri took $queryDuration") + } + } + + @Volatile + private var mSearchTaskList: List<SearchTask> = listOf() + + // Creates a directory result object corresponding to the current parameters of the loader. + override fun loadInBackground(): DirectoryResult? { + val result = DirectoryResult() + // TODO(b:378590632): If root list has one root use it to construct result.doc + result.doc = DocumentInfo() + result.cursor = emptyCursor() + + val searchedRoots = mRootList + val countDownLatch = CountDownLatch(searchedRoots.size) + val rejectBeforeTimestamp = mOptions.getRejectBeforeTimestamp() + + // Step 1: Build a list of search tasks. + val searchTaskList = + createSearchTaskList(rejectBeforeTimestamp, countDownLatch, mRootList) + Log.d(TAG, "${searchTaskList.size} tasks have been created") + + // Check if we are cancelled; if not copy the task list. + if (isLoadInBackgroundCanceled) { + return result + } + mSearchTaskList = searchTaskList + + // Step 2: Enqueue tasks and wait for them to complete or time out. + for (task in mSearchTaskList) { + mExecutorService.execute(task) + } + Log.d(TAG, "${mSearchTaskList.size} tasks have been enqueued") + + // Step 3: Wait for the results. + try { + if (mOptions.isQueryTimeUnlimited()) { + Log.d(TAG, "Waiting for results with no time limit") + countDownLatch.await() + } else { + Log.d(TAG, "Waiting ${mOptions.maxQueryTime!!.toMillis()}ms for results") + countDownLatch.await( + mOptions.maxQueryTime.toMillis(), + TimeUnit.MILLISECONDS + ) + } + Log.d(TAG, "Waiting for results is done") + } catch (e: InterruptedException) { + Log.d(TAG, "Failed to complete all searches within ${mOptions.maxQueryTime}") + // TODO(b:388336095): Record a metrics indicating incomplete search. + throw RuntimeException(e) + } + + // Step 4: Collect cursors from done tasks. + val cursorList = mutableListOf<Cursor>() + for (task in mSearchTaskList) { + Log.d(TAG, "Processing task ${task.taskId}") + if (isLoadInBackgroundCanceled) { + break + } + // TODO(b:388336095): Record a metric for each done and not done task. + val cursor = task.cursor + if (task.isDone && cursor != null) { + // TODO(b:388336095): Record a metric for null and not null cursor. + Log.d(TAG, "Task ${task.taskId} has ${cursor.count} results") + cursorList.add(cursor) + } + } + Log.d(TAG, "Search complete with ${cursorList.size} cursors collected") + + // Step 5: Assign the cursor, after adding filtering and sorting, to the results. + val mergedCursor = toSingleCursor(cursorList) + mergedCursor.registerContentObserver(mObserver) + val filteringCursor = FilteringCursorWrapper(mergedCursor) + filteringCursor.filterHiddenFiles(mOptions.showHidden) + filteringCursor.filterMimes( + mOptions.acceptableMimeTypes, + if (TextUtils.isEmpty(mQuery)) arrayOf<String>(Document.MIME_TYPE_DIR) else null + ) + if (rejectBeforeTimestamp > 0L) { + filteringCursor.filterLastModified(rejectBeforeTimestamp) + } + result.cursor = mSortModel.sortCursor(filteringCursor, mMimeTypeLookup) + + // TODO(b:388336095): Record the total time it took to complete search. + return result + } + + private fun createContentProviderQuery(root: RootInfo) = + if (TextUtils.isEmpty(mQuery) && mOptions.otherQueryArgs.isEmpty) { + // NOTE: recent document URI does not respect query-arg-mime-types restrictions. Thus + // we only create the recents URI if both the query and other args are empty. + DocumentsContract.buildRecentDocumentsUri( + root.authority, + root.rootId + ) + } else { + // NOTE: We pass empty query, as the name matching query is placed in queryArgs. + DocumentsContract.buildSearchDocumentsUri( + root.authority, + root.rootId, + "" + ) + } + + private fun createQueryArgs(rejectBeforeTimestamp: Long): Bundle { + val queryArgs = Bundle() + mSortModel.addQuerySortArgs(queryArgs) + if (rejectBeforeTimestamp > 0L) { + queryArgs.putLong( + DocumentsContract.QUERY_ARG_LAST_MODIFIED_AFTER, + rejectBeforeTimestamp + ) + } + if (!TextUtils.isEmpty(mQuery)) { + queryArgs.putString(DocumentsContract.QUERY_ARG_DISPLAY_NAME, mQuery) + } + queryArgs.putAll(mOptions.otherQueryArgs) + return queryArgs + } + + /** + * Helper function that creates a list of search tasks for the given countdown latch. + */ + private fun createSearchTaskList( + rejectBeforeTimestamp: Long, + countDownLatch: CountDownLatch, + rootList: Collection<RootInfo> + ): List<SearchTask> { + val searchTaskList = mutableListOf<SearchTask>() + for (root in rootList) { + if (isLoadInBackgroundCanceled) { + break + } + val rootSearchUri = createContentProviderQuery(root) + // TODO(b:385789236): Correctly pass sort order information. + val queryArgs = createQueryArgs(rejectBeforeTimestamp) + mSortModel.addQuerySortArgs(queryArgs) + Log.d(TAG, "Query $rootSearchUri and queryArgs $queryArgs") + val task = SearchTask( + root.rootId, + rootSearchUri, + queryArgs, + countDownLatch + ) + searchTaskList.add(task) + } + return searchTaskList + } + + override fun onReset() { + for (task in mSearchTaskList) { + task.close() + } + Log.d(TAG, "Resetting search loader; search task list emptied.") + super.onReset() + } +} diff --git a/src/com/android/documentsui/picker/ActionHandler.java b/src/com/android/documentsui/picker/ActionHandler.java index 4ea7bbc2d..553fa6986 100644 --- a/src/com/android/documentsui/picker/ActionHandler.java +++ b/src/com/android/documentsui/picker/ActionHandler.java @@ -272,6 +272,9 @@ class ActionHandler<T extends FragmentActivity & Addons> extends AbstractActionH private void onLastAccessedStackLoaded(@Nullable DocumentStack stack) { if (stack == null) { loadDefaultLocation(); + } else if (shouldPreemptivelyRestrictRequestedInitialUri(stack.peek().getDocumentUri())) { + // If the last accessed stack has restricted uri, load default location + loadDefaultLocation(); } else { mState.stack.reset(stack); mActivity.refreshCurrentRootAndDirectory(AnimationView.ANIM_NONE); diff --git a/src/com/android/documentsui/picker/PickActivity.java b/src/com/android/documentsui/picker/PickActivity.java index e9b91b1a0..68a797397 100644 --- a/src/com/android/documentsui/picker/PickActivity.java +++ b/src/com/android/documentsui/picker/PickActivity.java @@ -21,6 +21,7 @@ import static com.android.documentsui.base.State.ACTION_GET_CONTENT; import static com.android.documentsui.base.State.ACTION_OPEN; import static com.android.documentsui.base.State.ACTION_OPEN_TREE; import static com.android.documentsui.base.State.ACTION_PICK_COPY_DESTINATION; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; import android.content.Intent; import android.content.res.Resources; @@ -127,12 +128,15 @@ public class PickActivity extends BaseActivity implements ActionHandler.Addons { new DirectoryDetails(this), mInjector.getModel()::getItemCount); - mInjector.actionModeController = new ActionModeController( - this, - mInjector.selectionMgr, - mNavigator, - mInjector.menuManager, - mInjector.messages); + if (!isUseMaterial3FlagEnabled()) { + mInjector.actionModeController = + new ActionModeController( + this, + mInjector.selectionMgr, + mNavigator, + mInjector.menuManager, + mInjector.messages); + } mInjector.profileTabsController = new ProfileTabsController( mInjector.selectionMgr, @@ -249,6 +253,15 @@ public class PickActivity extends BaseActivity implements ActionHandler.Addons { RootsFragment.show(getSupportFragmentManager(), /* includeApps= */ mState.action == ACTION_GET_CONTENT, /* intent= */ moreApps); + if (isUseMaterial3FlagEnabled()) { + View navRailRoots = findViewById(R.id.nav_rail_container_roots); + if (navRailRoots != null) { + // Medium layout, populate navigation rail layout. + RootsFragment.showNavRail(getSupportFragmentManager(), + /* includeApps= */ mState.action == ACTION_GET_CONTENT, + /* intent= */ moreApps); + } + } } } diff --git a/src/com/android/documentsui/picker/TrampolineActivity.kt b/src/com/android/documentsui/picker/TrampolineActivity.kt new file mode 100644 index 000000000..dba09f2f3 --- /dev/null +++ b/src/com/android/documentsui/picker/TrampolineActivity.kt @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2024 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.documentsui.picker + +import android.content.ComponentName +import android.content.Intent +import android.content.Intent.ACTION_GET_CONTENT +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.os.ext.SdkExtensions +import android.provider.MediaStore.ACTION_PICK_IMAGES +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import com.android.documentsui.base.SharedMinimal.DEBUG + +/** + * DocumentsUI PickActivity currently defers picking of media mime types to the Photopicker. This + * activity trampolines the intent to either Photopicker or to the PickActivity depending on whether + * there are non-media mime types to handle. + */ +class TrampolineActivity : AppCompatActivity() { + companion object { + const val TAG = "TrampolineActivity" + } + + override fun onCreate(savedInstanceBundle: Bundle?) { + super.onCreate(savedInstanceBundle) + + // This activity should not be present in the back stack nor should handle any of the + // corresponding results when picking items. + intent?.apply { + addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT) + addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP) + } + + // In the event there is no photopicker returned, just refer to DocumentsUI. + val photopickerComponentName = getPhotopickerComponentName(intent.type) + if (photopickerComponentName == null) { + forwardIntentToDocumentsUI() + return + } + + // The Photopicker has an entry point to take them back to DocumentsUI. In the event the + // user originated from Photopicker, we don't want to send them back. + val referredFromPhotopicker = referrer?.host == photopickerComponentName.packageName + if (referredFromPhotopicker || !shouldForwardIntentToPhotopicker(intent)) { + forwardIntentToDocumentsUI() + return + } + + // Forward intent to Photopicker. + intent.setComponent(photopickerComponentName) + startActivity(intent) + finish() + } + + private fun forwardIntentToDocumentsUI() { + intent.setClass(applicationContext, PickActivity::class.java) + startActivity(intent) + finish() + } + + private fun getPhotopickerComponentName(type: String?): ComponentName? { + // Intent.ACTION_PICK_IMAGES is only available from SdkExtensions v2 onwards. Prior to that + // the Photopicker was not available, so in those cases should always send to DocumentsUI. + if (SdkExtensions.getExtensionVersion(Build.VERSION_CODES.R) < 2) { + return null + } + + // Attempt to resolve the `ACTION_PICK_IMAGES` intent to get the Photopicker package. + // On T+ devices this is is a standalone package, whilst prior to T it is part of the + // MediaProvider module. + val pickImagesIntent = Intent( + ACTION_PICK_IMAGES + ).apply { addCategory(Intent.CATEGORY_DEFAULT) } + val photopickerComponentName: ComponentName? = pickImagesIntent.resolveActivity( + packageManager + ) + + // For certain devices the activity that handles ACTION_GET_CONTENT can be disabled (when + // the ACTION_PICK_IMAGES is enabled) so double check by explicitly checking the + // ACTION_GET_CONTENT activity on the same activity that handles ACTION_PICK_IMAGES. + val photopickerGetContentIntent = Intent(ACTION_GET_CONTENT).apply { + setType(type) + setPackage(photopickerComponentName?.packageName) + } + val photopickerGetContentComponent: ComponentName? = + photopickerGetContentIntent.resolveActivity(packageManager) + + // Ensure the `ACTION_GET_CONTENT` activity is enabled. + if (!isComponentEnabled(photopickerGetContentComponent)) { + if (DEBUG) { + Log.d(TAG, "Photopicker PICK_IMAGES component has no enabled GET_CONTENT handler") + } + return null + } + + return photopickerGetContentComponent + } + + private fun isComponentEnabled(componentName: ComponentName?): Boolean { + if (componentName == null) { + return false + } + + return when (packageManager.getComponentEnabledSetting(componentName)) { + PackageManager.COMPONENT_ENABLED_STATE_ENABLED -> true + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT -> { + // DEFAULT is a state that essentially defers to the state defined in the + // AndroidManifest which can be either enabled or disabled. + packageManager.getPackageInfo( + componentName.packageName, + PackageManager.GET_ACTIVITIES + )?.let { packageInfo: PackageInfo -> + if (packageInfo.activities == null) { + return false + } + for (val info in packageInfo.activities) { + if (info.name == componentName.className) { + return info.enabled + } + } + } + return false + } + + // Everything else is considered disabled. + else -> false + } + } +} + +fun shouldForwardIntentToPhotopicker(intent: Intent): Boolean { + // Photopicker can only handle `ACTION_GET_CONTENT` intents. + if (intent.action != ACTION_GET_CONTENT) { + return false + } + + // Photopicker only handles media mime types (i.e. image/* or video/*), however, it also handles + // requests that have type */* with EXTRA_MIME_TYPES that are media mime types. In that scenario + // it provides an escape hatch to the user to go back to DocumentsUI. + val intentTypeIsMedia = isMediaMimeType(intent.type) + if (!intentTypeIsMedia && intent.type != "*/*") { + return false + } + + val extraMimeTypes = intent.getStringArrayExtra(Intent.EXTRA_MIME_TYPES) + + // In the event there were no `EXTRA_MIME_TYPES` this should exclusively be handled by + // DocumentsUI and not Photopicker. + if (intent.type == "*/*" && extraMimeTypes == null) { + return false + } + + if (extraMimeTypes == null) { + return intentTypeIsMedia + } + + return extraMimeTypes.isNotEmpty() && extraMimeTypes.none { !isMediaMimeType(it) } +} + +fun isMediaMimeType(mimeType: String?): Boolean { + return mimeType?.let { mimeType -> + mimeType.startsWith("image/") || mimeType.startsWith("video/") + } == true +} diff --git a/src/com/android/documentsui/queries/SearchChipViewManager.java b/src/com/android/documentsui/queries/SearchChipViewManager.java index 3dbc6ff74..f673b7408 100644 --- a/src/com/android/documentsui/queries/SearchChipViewManager.java +++ b/src/com/android/documentsui/queries/SearchChipViewManager.java @@ -16,7 +16,7 @@ package com.android.documentsui.queries; -import static com.android.documentsui.flags.Flags.useMaterial3; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; import android.animation.ObjectAnimator; import android.content.Context; @@ -377,6 +377,7 @@ public class SearchChipViewManager { /** * When the chip is focused, adding a focus ring indicator using Stroke. + * TODO(b/381957932): Remove this once Material Chip supports focus ring. */ private void onChipFocusChange(View v, boolean hasFocus) { Chip chip = (Chip) v; @@ -394,21 +395,11 @@ public class SearchChipViewManager { final Context context = mChipGroup.getContext(); chip.setTag(chipData); chip.setText(context.getString(chipData.getTitleRes())); - Drawable chipIcon; - if (chipData.getChipType() == TYPE_LARGE_FILES) { - chipIcon = context.getDrawable(R.drawable.ic_chip_large_files); - } else if (chipData.getChipType() == TYPE_FROM_THIS_WEEK) { - chipIcon = context.getDrawable(R.drawable.ic_chip_from_this_week); - } else if (chipData.getChipType() == TYPE_DOCUMENTS) { - chipIcon = IconUtils.loadMimeIcon(context, MimeTypes.GENERIC_TYPE); - } else { - // get the icon drawable with the first mimeType in chipData - chipIcon = IconUtils.loadMimeIcon(context, chipData.getMimeTypes()[0]); - } + Drawable chipIcon = getChipIcon(chipData); chip.setChipIcon(chipIcon); chip.setOnClickListener(this::onChipClick); - if (useMaterial3()) { + if (isUseMaterial3FlagEnabled()) { chip.setOnFocusChangeListener(this::onChipFocusChange); } @@ -417,6 +408,35 @@ public class SearchChipViewManager { } } + private Drawable getChipIcon(SearchChipData chipData) { + final Context context = mChipGroup.getContext(); + int chipType = chipData.getChipType(); + if (chipType == TYPE_LARGE_FILES) { + return context.getDrawable(R.drawable.ic_chip_large_files); + } + if (chipType == TYPE_FROM_THIS_WEEK) { + return context.getDrawable(R.drawable.ic_chip_from_this_week); + } + + // When use_material3 flag is ON, we don't want to use MIME type icons for + // image/audio/video/document from the system. + if (isUseMaterial3FlagEnabled()) { + return switch (chipType) { + case TYPE_IMAGES -> context.getDrawable(R.drawable.ic_chip_image); + case TYPE_AUDIO -> context.getDrawable(R.drawable.ic_chip_audio); + case TYPE_VIDEOS -> context.getDrawable(R.drawable.ic_chip_video); + case TYPE_DOCUMENTS -> context.getDrawable(R.drawable.ic_chip_document); + default -> null; + }; + } + + if (chipType == TYPE_DOCUMENTS) { + return IconUtils.loadMimeIcon(context, MimeTypes.GENERIC_TYPE); + } + // get the icon drawable with the first mimeType in chipData + return IconUtils.loadMimeIcon(context, chipData.getMimeTypes()[0]); + } + /** * Reorder the chips in chip group. The checked chip has higher order. * @@ -448,19 +468,19 @@ public class SearchChipViewManager { } final int chipSpacing = - useMaterial3() + isUseMaterial3FlagEnabled() ? ((ChipGroup) mChipGroup).getChipSpacingHorizontal() : mChipGroup .getResources() .getDimensionPixelSize(R.dimen.search_chip_spacing); final boolean isRtl = mChipGroup.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; - final float chipMarginStartEnd = - useMaterial3() - ? 0 + final float chipGroupPaddingStart = + isUseMaterial3FlagEnabled() + ? mChipGroup.getPaddingStart() : mChipGroup .getResources() .getDimensionPixelSize(R.dimen.search_chip_half_spacing); - float lastX = isRtl ? mChipGroup.getWidth() - chipMarginStartEnd : chipMarginStartEnd; + float lastX = isRtl ? mChipGroup.getWidth() - chipGroupPaddingStart : chipGroupPaddingStart; // remove all chips except current clicked chip to avoid losing // accessibility focus. diff --git a/src/com/android/documentsui/queries/SearchViewManager.java b/src/com/android/documentsui/queries/SearchViewManager.java index 053dc93c8..ca132a187 100644 --- a/src/com/android/documentsui/queries/SearchViewManager.java +++ b/src/com/android/documentsui/queries/SearchViewManager.java @@ -20,6 +20,7 @@ import static com.android.documentsui.base.SharedMinimal.DEBUG; import static com.android.documentsui.base.State.ACTION_GET_CONTENT; import static com.android.documentsui.base.State.ACTION_OPEN; import static com.android.documentsui.base.State.ActionType; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; import android.content.Intent; import android.os.Bundle; @@ -332,7 +333,14 @@ public class SearchViewManager implements } // Recent root show open search bar, do not show duplicate search icon. - mMenuItem.setVisible(supportsSearch && (!stack.isRecents() || !mShowSearchBar)); + boolean enabled = supportsSearch && (!stack.isRecents() || !mShowSearchBar); + mMenuItem.setVisible(enabled); + if (isUseMaterial3FlagEnabled()) { + // When the use_material3 flag is enabled, we inflate and deflate the menu. + // This causes the search button to be disabled on inflation, toggle it in + // this scenario. + mMenuItem.setEnabled(enabled); + } mChipViewManager.setChipsRowVisible(supportsSearch && root.supportsMimeTypesSearch()); } diff --git a/src/com/android/documentsui/services/CompressJob.java b/src/com/android/documentsui/services/CompressJob.java index e9ba6e4c8..ccb3ee835 100644 --- a/src/com/android/documentsui/services/CompressJob.java +++ b/src/com/android/documentsui/services/CompressJob.java @@ -24,11 +24,13 @@ import android.app.Notification; import android.app.Notification.Builder; import android.content.ContentResolver; import android.content.Context; +import android.icu.text.MessageFormat; import android.net.Uri; import android.os.Messenger; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.provider.DocumentsContract; +import android.text.BidiFormatter; import android.util.Log; import com.android.documentsui.R; @@ -40,6 +42,9 @@ import com.android.documentsui.base.UserId; import com.android.documentsui.clipping.UrisSupplier; import java.io.FileNotFoundException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; // TODO: Stop extending CopyJob. final class CompressJob extends CopyJob { @@ -87,6 +92,26 @@ final class CompressJob extends CopyJob { } @Override + protected String getProgressMessage() { + switch (getState()) { + case Job.STATE_SET_UP: + case Job.STATE_COMPLETED: + case Job.STATE_CANCELED: + Map<String, Object> formatArgs = new HashMap<>(); + formatArgs.put("count", mResolvedDocs.size()); + if (mResolvedDocs.size() == 1) { + formatArgs.put("filename", BidiFormatter.getInstance().unicodeWrap( + mResolvedDocs.get(0).displayName)); + } + return (new MessageFormat( + service.getString(R.string.compress_in_progress), Locale.getDefault())) + .format(formatArgs); + default: + return ""; + } + } + + @Override public boolean setUp() { if (!super.setUp()) { return false; @@ -115,11 +140,11 @@ final class CompressJob extends CopyJob { mArchiveUri, ParcelFileDescriptor.MODE_WRITE_ONLY), UserId.DEFAULT_USER); ArchivesProvider.acquireArchive(getClient(mDstInfo), mDstInfo.derivedUri); } catch (FileNotFoundException e) { - Log.e(TAG, "Failed to create dstInfo.", e); + Log.e(TAG, "Cannot create document info", e); failureCount = mResourceUris.getItemCount(); return false; } catch (RemoteException e) { - Log.e(TAG, "Failed to acquire the archive.", e); + Log.e(TAG, "Cannot acquire archive", e); failureCount = mResourceUris.getItemCount(); return false; } @@ -132,7 +157,7 @@ final class CompressJob extends CopyJob { try { ArchivesProvider.releaseArchive(getClient(mDstInfo), mDstInfo.derivedUri); } catch (RemoteException e) { - Log.e(TAG, "Failed to release the archive."); + Log.e(TAG, "Cannot release archive", e); } // Remove the archive file in case of an error. @@ -141,7 +166,7 @@ final class CompressJob extends CopyJob { DocumentsContract.deleteDocument(wrap(getClient(mArchiveUri)), mArchiveUri); } } catch (RemoteException | FileNotFoundException e) { - Log.w(TAG, "Failed to cleanup after compress error: " + mDstInfo.toString(), e); + Log.w(TAG, "Cannot clean up after compress error: " + mDstInfo.toString(), e); } super.finish(); diff --git a/src/com/android/documentsui/services/CopyJob.java b/src/com/android/documentsui/services/CopyJob.java index c972c33ef..9fb3f5d09 100644 --- a/src/com/android/documentsui/services/CopyJob.java +++ b/src/com/android/documentsui/services/CopyJob.java @@ -46,6 +46,7 @@ import android.content.Intent; import android.content.res.AssetFileDescriptor; import android.database.ContentObserver; import android.database.Cursor; +import android.icu.text.MessageFormat; import android.net.Uri; import android.os.DeadObjectException; import android.os.FileUtils; @@ -66,6 +67,7 @@ import android.system.Int64Ref; import android.system.Os; import android.system.OsConstants; import android.system.StructStat; +import android.text.BidiFormatter; import android.util.ArrayMap; import android.util.Log; import android.webkit.MimeTypeMap; @@ -93,6 +95,8 @@ import java.io.InputStream; import java.io.SyncFailedException; import java.text.NumberFormat; import java.util.ArrayList; +import java.util.HashMap; +import java.util.Locale; import java.util.Map; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; @@ -195,6 +199,49 @@ class CopyJob extends ResolvedResourcesJob { return warningBuilder.build(); } + protected String getProgressMessage() { + switch (getState()) { + case Job.STATE_SET_UP: + case Job.STATE_COMPLETED: + case Job.STATE_CANCELED: + Map<String, Object> formatArgs = new HashMap<>(); + formatArgs.put("count", mResolvedDocs.size()); + formatArgs.put("directory", + BidiFormatter.getInstance().unicodeWrap(mDstInfo.displayName)); + if (mResolvedDocs.size() == 1) { + formatArgs.put("filename", + BidiFormatter.getInstance().unicodeWrap( + mResolvedDocs.get(0).displayName)); + } + return (new MessageFormat( + service.getString(R.string.copy_in_progress), Locale.getDefault())) + .format(formatArgs); + + default: + return ""; + } + } + + @Override + JobProgress getJobProgress() { + if (mProgressTracker == null) { + return new JobProgress( + id, + getState(), + getProgressMessage(), + hasFailures()); + } + mProgressTracker.updateEstimateRemainingTime(); + return new JobProgress( + id, + getState(), + getProgressMessage(), + hasFailures(), + mProgressTracker.getCurrentBytes(), + mProgressTracker.getRequiredBytes(), + mProgressTracker.getRemainingTimeEstimate()); + } + @Override boolean setUp() { if (!super.setUp()) { @@ -986,6 +1033,10 @@ class CopyJob extends ResolvedResourcesJob { return -1; } + protected long getCurrentBytes() { + return -1; + } + protected void start() { mStartTime = mElapsedRealTimeSupplier.getAsLong(); } @@ -1058,6 +1109,16 @@ class CopyJob extends ResolvedResourcesJob { } @Override + protected long getRequiredBytes() { + return mBytesRequired; + } + + @Override + protected long getCurrentBytes() { + return mBytesCopied.get(); + } + + @Override public void onBytesCopied(long numBytes) { mBytesCopied.getAndAdd(numBytes); } diff --git a/src/com/android/documentsui/services/DeleteJob.java b/src/com/android/documentsui/services/DeleteJob.java index ede46a937..801cc6dd3 100644 --- a/src/com/android/documentsui/services/DeleteJob.java +++ b/src/com/android/documentsui/services/DeleteJob.java @@ -23,7 +23,9 @@ import android.app.Notification; import android.app.Notification.Builder; import android.content.ContentResolver; import android.content.Context; +import android.icu.text.MessageFormat; import android.net.Uri; +import android.text.BidiFormatter; import android.util.Log; import com.android.documentsui.MetricConsts; @@ -36,6 +38,9 @@ import com.android.documentsui.base.UserId; import com.android.documentsui.clipping.UrisSupplier; import java.io.FileNotFoundException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; import javax.annotation.Nullable; @@ -97,6 +102,34 @@ final class DeleteJob extends ResolvedResourcesJob { throw new UnsupportedOperationException(); } + private String getProgressMessage() { + switch (getState()) { + case Job.STATE_SET_UP: + case Job.STATE_COMPLETED: + case Job.STATE_CANCELED: + Map<String, Object> formatArgs = new HashMap<>(); + formatArgs.put("count", mResolvedDocs.size()); + if (mResolvedDocs.size() == 1) { + formatArgs.put("filename", BidiFormatter.getInstance().unicodeWrap( + mResolvedDocs.get(0).displayName)); + } + return (new MessageFormat( + service.getString(R.string.delete_in_progress), Locale.getDefault())) + .format(formatArgs); + default: + return ""; + } + } + + @Override + JobProgress getJobProgress() { + return new JobProgress( + id, + getState(), + getProgressMessage(), + hasFailures()); + } + @Override void start() { ContentResolver resolver = appContext.getContentResolver(); diff --git a/src/com/android/documentsui/services/FileOperationService.java b/src/com/android/documentsui/services/FileOperationService.java index c7be5f4fc..dcb2c1db4 100644 --- a/src/com/android/documentsui/services/FileOperationService.java +++ b/src/com/android/documentsui/services/FileOperationService.java @@ -17,6 +17,7 @@ package com.android.documentsui.services; import static com.android.documentsui.base.SharedMinimal.DEBUG; +import static com.android.documentsui.util.FlagUtils.isVisualSignalsFlagEnabled; import android.app.Notification; import android.app.NotificationChannel; @@ -126,9 +127,15 @@ public class FileOperationService extends Service implements Job.Listener { // Use a features to determine if notification channel is enabled. @VisibleForTesting Features features; + // Used so tests can force the state of visual signals. + @VisibleForTesting Boolean mVisualSignalsEnabled = isVisualSignalsFlagEnabled(); + @GuardedBy("mJobs") private final Map<String, JobRecord> mJobs = new LinkedHashMap<>(); + // Used to send periodic broadcasts for job progress. + private GlobalJobMonitor mJobMonitor; + // The job whose notification is used to keep the service in foreground mode. @GuardedBy("mJobs") private Job mForegroundJob; @@ -162,6 +169,10 @@ public class FileOperationService extends Service implements Job.Listener { notificationManager = getSystemService(NotificationManager.class); } + if (mVisualSignalsEnabled && mJobMonitor == null) { + mJobMonitor = new GlobalJobMonitor(); + } + UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); features = new Features.RuntimeFeatures(getResources(), userManager); setUpNotificationChannel(); @@ -188,6 +199,10 @@ public class FileOperationService extends Service implements Job.Listener { Log.d(TAG, "Shutting down executor."); } + if (mJobMonitor != null) { + mJobMonitor.stop(); + } + List<Runnable> unfinishedCopies = executor.shutdownNow(); List<Runnable> unfinishedDeletions = deletionExecutor.shutdownNow(); List<Runnable> unfinished = @@ -330,6 +345,10 @@ public class FileOperationService extends Service implements Job.Listener { assert(record != null); record.job.cleanup(); + if (mVisualSignalsEnabled && mJobs.isEmpty()) { + mJobMonitor.stop(); + } + // Delay the shutdown until we've cleaned up all notifications. shutdown() is now posted in // onFinished(Job job) to main thread. } @@ -389,8 +408,12 @@ public class FileOperationService extends Service implements Job.Listener { } // Set up related monitor - JobMonitor monitor = new JobMonitor(job); - monitor.start(); + if (mVisualSignalsEnabled) { + mJobMonitor.start(); + } else { + JobMonitor monitor = new JobMonitor(job); + monitor.start(); + } } @Override @@ -399,6 +422,9 @@ public class FileOperationService extends Service implements Job.Listener { if (DEBUG) { Log.d(TAG, "onFinished: " + job.id); } + if (mVisualSignalsEnabled) { + mJobMonitor.sendProgress(); + } synchronized (mJobs) { // Delete the job from mJobs first to avoid this job being selected as the foreground @@ -545,6 +571,52 @@ public class FileOperationService extends Service implements Job.Listener { } } + /** + * A class used to periodically poll the state of every running job. + * + * We need to be sending the progress of every job, so rather than having a single monitor per + * job, have one for the whole service. + */ + private final class GlobalJobMonitor implements Runnable { + private static final long PROGRESS_INTERVAL_MILLIS = 500L; + private boolean mRunning = false; + + private void start() { + if (!mRunning) { + handler.post(this); + } + mRunning = true; + } + + private void stop() { + mRunning = false; + handler.removeCallbacks(this); + } + + private void sendProgress() { + var progress = new ArrayList<JobProgress>(); + synchronized (mJobs) { + for (JobRecord rec : mJobs.values()) { + progress.add(rec.job.getJobProgress()); + } + } + Intent intent = new Intent(); + intent.setPackage(getPackageName()); + intent.setAction("com.android.documentsui.PROGRESS"); + intent.putExtra("id", 0); + intent.putParcelableArrayListExtra("progress", progress); + sendBroadcast(intent); + } + + @Override + public void run() { + sendProgress(); + if (mRunning) { + handler.postDelayed(this, PROGRESS_INTERVAL_MILLIS); + } + } + } + @Override public IBinder onBind(Intent intent) { return null; // Boilerplate. See super#onBind diff --git a/src/com/android/documentsui/services/Job.java b/src/com/android/documentsui/services/Job.java index 71f0ae861..0f432cc19 100644 --- a/src/com/android/documentsui/services/Job.java +++ b/src/com/android/documentsui/services/Job.java @@ -190,6 +190,8 @@ abstract public class Job implements Runnable { abstract Notification getWarningNotification(); + abstract JobProgress getJobProgress(); + Uri getDataUriForIntent(String tag) { return Uri.parse(String.format("data,%s-%s", tag, id)); } diff --git a/src/com/android/documentsui/services/JobProgress.kt b/src/com/android/documentsui/services/JobProgress.kt new file mode 100644 index 000000000..98be92f6a --- /dev/null +++ b/src/com/android/documentsui/services/JobProgress.kt @@ -0,0 +1,69 @@ +/* + * Copyright 2025 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.documentsui.services + +import android.os.Parcel +import android.os.Parcelable + +/** + * Represents the current progress on an individual job owned by the FileOperationService. + * JobProgress objects are broadcast from the service to activities in order to update the UI. + */ +data class JobProgress @JvmOverloads constructor( + @JvmField val id: String, + @JvmField @Job.State val state: Int, + @JvmField val msg: String?, + @JvmField val hasFailures: Boolean, + @JvmField val currentBytes: Long = -1, + @JvmField val requiredBytes: Long = -1, + @JvmField val msRemaining: Long = -1, +) : Parcelable { + + override fun describeContents(): Int { + return 0 + } + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.apply { + writeString(id) + writeInt(state) + writeString(msg) + writeBoolean(hasFailures) + writeLong(currentBytes) + writeLong(requiredBytes) + writeLong(msRemaining) + } + } + + companion object CREATOR : Parcelable.Creator<JobProgress?> { + override fun createFromParcel(parcel: Parcel): JobProgress? { + return JobProgress( + parcel.readString()!!, + parcel.readInt(), + parcel.readString(), + parcel.readBoolean(), + parcel.readLong(), + parcel.readLong(), + parcel.readLong(), + ) + } + + override fun newArray(size: Int): Array<JobProgress?> { + return arrayOfNulls(size) + } + } +} diff --git a/src/com/android/documentsui/services/MoveJob.java b/src/com/android/documentsui/services/MoveJob.java index ddbe727ac..b2974c5e7 100644 --- a/src/com/android/documentsui/services/MoveJob.java +++ b/src/com/android/documentsui/services/MoveJob.java @@ -24,12 +24,14 @@ import static com.android.documentsui.services.FileOperationService.OPERATION_MO import android.app.Notification; import android.app.Notification.Builder; import android.content.Context; +import android.icu.text.MessageFormat; import android.net.Uri; import android.os.DeadObjectException; import android.os.Messenger; import android.os.RemoteException; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; +import android.text.BidiFormatter; import android.util.Log; import com.android.documentsui.MetricConsts; @@ -42,6 +44,9 @@ import com.android.documentsui.base.UserId; import com.android.documentsui.clipping.UrisSupplier; import java.io.FileNotFoundException; +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; import javax.annotation.Nullable; @@ -94,6 +99,28 @@ final class MoveJob extends CopyJob { } @Override + protected String getProgressMessage() { + switch (getState()) { + case Job.STATE_SET_UP: + case Job.STATE_COMPLETED: + case Job.STATE_CANCELED: + Map<String, Object> formatArgs = new HashMap<>(); + formatArgs.put("count", mResolvedDocs.size()); + formatArgs.put("directory", + BidiFormatter.getInstance().unicodeWrap(mDstInfo.displayName)); + if (mResolvedDocs.size() == 1) { + formatArgs.put("filename", BidiFormatter.getInstance().unicodeWrap( + mResolvedDocs.get(0).displayName)); + } + return (new MessageFormat( + service.getString(R.string.move_in_progress), Locale.getDefault())) + .format(formatArgs); + default: + return ""; + } + } + + @Override public boolean setUp() { if (mSrcParentUri != null) { try { diff --git a/src/com/android/documentsui/services/ResolvedResourcesJob.java b/src/com/android/documentsui/services/ResolvedResourcesJob.java index 500958978..a6001d5f8 100644 --- a/src/com/android/documentsui/services/ResolvedResourcesJob.java +++ b/src/com/android/documentsui/services/ResolvedResourcesJob.java @@ -16,6 +16,10 @@ package com.android.documentsui.services; +import static android.os.SystemClock.uptimeMillis; + +import static com.android.documentsui.base.SharedMinimal.DEBUG; + import android.content.ContentResolver; import android.content.Context; import android.net.Uri; @@ -44,6 +48,9 @@ import java.util.List; public abstract class ResolvedResourcesJob extends Job { private static final String TAG = "ResolvedResourcesJob"; + // Used in logs. + protected final long mStartTime = uptimeMillis(); + final List<DocumentInfo> mResolvedDocs; final List<Uri> mAcquiredArchivedUris = new ArrayList<>(); @@ -72,22 +79,22 @@ public abstract class ResolvedResourcesJob extends Job { mAcquiredArchivedUris.add(uri); } } catch (RemoteException e) { - Log.e(TAG, "Failed to acquire an archive."); + Log.e(TAG, "Cannot acquire an archive", e); return false; } } } catch (IOException e) { - Log.e(TAG, "Failed to read list of target resource Uris. Cannot continue.", e); + Log.e(TAG, "Cannot read list of target resource URIs", e); return false; } int docsResolved = buildDocumentList(); if (!isCanceled() && docsResolved < mResourceUris.getItemCount()) { if (docsResolved == 0) { - Log.e(TAG, "Failed to load any documents. Aborting."); + Log.e(TAG, "Cannot load any documents. Aborting."); return false; } else { - Log.e(TAG, "Failed to load some documents. Processing loaded documents only."); + Log.e(TAG, "Cannot load some documents"); } } @@ -101,9 +108,14 @@ public abstract class ResolvedResourcesJob extends Job { try { ArchivesProvider.releaseArchive(getClient(uri), uri); } catch (RemoteException e) { - Log.e(TAG, "Failed to release an archived document."); + Log.e(TAG, "Cannot release an archived document", e); } } + + if (DEBUG) { + Log.d(TAG, String.format("%s %s finished after %d ms", getClass().getSimpleName(), id, + uptimeMillis() - mStartTime)); + } } /** @@ -123,7 +135,7 @@ public abstract class ResolvedResourcesJob extends Job { try { uris = mResourceUris.getUris(appContext); } catch (IOException e) { - Log.e(TAG, "Failed to read list of target resource Uris. Cannot continue.", e); + Log.e(TAG, "Cannot read list of target resource URIs", e); failureCount = this.mResourceUris.getItemCount(); return 0; } @@ -135,8 +147,7 @@ public abstract class ResolvedResourcesJob extends Job { try { doc = DocumentInfo.fromUri(resolver, uri, UserId.DEFAULT_USER); } catch (FileNotFoundException e) { - Log.e(TAG, "Failed to resolve content from Uri: " + uri - + ". Skipping to next resource.", e); + Log.e(TAG, "Cannot resolve content from URI " + uri, e); onResolveFailed(uri); continue; } diff --git a/src/com/android/documentsui/sidebar/AppItem.java b/src/com/android/documentsui/sidebar/AppItem.java index 4bb7257b6..b72e4041c 100644 --- a/src/com/android/documentsui/sidebar/AppItem.java +++ b/src/com/android/documentsui/sidebar/AppItem.java @@ -16,21 +16,22 @@ package com.android.documentsui.sidebar; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; + import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; -import android.os.UserManager; -import android.text.TextUtils; import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.LayoutRes; + import com.android.documentsui.ActionHandler; import com.android.documentsui.IconUtils; import com.android.documentsui.R; import com.android.documentsui.base.UserId; -import com.android.documentsui.dirlist.AppsRowItemData; /** * An {@link Item} for apps that supports some picking actions like @@ -44,7 +45,16 @@ public class AppItem extends Item { private final ActionHandler mActionHandler; public AppItem(ResolveInfo info, String title, UserId userId, ActionHandler actionHandler) { - super(R.layout.item_root, title, getStringId(info), userId); + this(R.layout.item_root, info, title, userId, actionHandler); + } + + public AppItem( + @LayoutRes int layoutId, + ResolveInfo info, + String title, + UserId userId, + ActionHandler actionHandler) { + super(layoutId, title, getStringId(info), userId); this.info = info; mActionHandler = actionHandler; } @@ -84,14 +94,19 @@ public class AppItem extends Item { final ImageView icon = (ImageView) convertView.findViewById(android.R.id.icon); final TextView titleView = (TextView) convertView.findViewById(android.R.id.title); final TextView summary = (TextView) convertView.findViewById(android.R.id.summary); - final View actionIconArea = convertView.findViewById(R.id.action_icon_area); - final ImageView actionIcon = (ImageView) convertView.findViewById(R.id.action_icon); titleView.setText(title); titleView.setContentDescription(userId.getUserBadgedLabel(convertView.getContext(), title)); bindIcon(icon); - bindActionIcon(actionIconArea, actionIcon); + + // When use_material3 flag is ON, we don't show action icon for the app items, do nothing + // here because the icons are hidden by default. + if (!isUseMaterial3FlagEnabled()) { + final View actionIconArea = convertView.findViewById(R.id.action_icon_area); + final ImageView actionIcon = (ImageView) convertView.findViewById(R.id.action_icon); + bindActionIcon(actionIconArea, actionIcon); + } // TODO: match existing summary behavior from disambig dialog summary.setVisibility(View.GONE); diff --git a/src/com/android/documentsui/sidebar/NavRailAppItem.java b/src/com/android/documentsui/sidebar/NavRailAppItem.java new file mode 100644 index 000000000..befddf0aa --- /dev/null +++ b/src/com/android/documentsui/sidebar/NavRailAppItem.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2024 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.documentsui.sidebar; + +import android.content.pm.ResolveInfo; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.documentsui.ActionHandler; +import com.android.documentsui.R; +import com.android.documentsui.base.UserId; + +/** + * Similar to {@link AppItem} but only used in the navigation rail. + */ +public class NavRailAppItem extends AppItem { + + public NavRailAppItem( + ResolveInfo info, String title, UserId userId, ActionHandler actionHandler) { + super(R.layout.nav_rail_item_root, info, title, userId, actionHandler); + } + + @Override + public void bindView(View convertView) { + final ImageView icon = convertView.findViewById(android.R.id.icon); + final TextView titleView = convertView.findViewById(android.R.id.title); + + titleView.setText(title); + titleView.setContentDescription(userId.getUserBadgedLabel(convertView.getContext(), title)); + + bindIcon(icon); + } +} diff --git a/src/com/android/documentsui/sidebar/NavRailProfileItem.java b/src/com/android/documentsui/sidebar/NavRailProfileItem.java new file mode 100644 index 000000000..fe69c286f --- /dev/null +++ b/src/com/android/documentsui/sidebar/NavRailProfileItem.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2024 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.documentsui.sidebar; + +import android.content.pm.ResolveInfo; +import android.view.View; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.documentsui.ActionHandler; +import com.android.documentsui.R; + + +/** + * Similar to {@link ProfileItem} but only used in the navigation rail. + */ +public class NavRailProfileItem extends ProfileItem { + + public NavRailProfileItem(ResolveInfo info, String title, ActionHandler actionHandler) { + super(R.layout.nav_rail_item_root, info, title, actionHandler); + } + + @Override + public void bindView(View convertView) { + final ImageView icon = convertView.findViewById(android.R.id.icon); + final TextView titleView = convertView.findViewById(android.R.id.title); + + titleView.setText(title); + titleView.setContentDescription(userId.getUserBadgedLabel(convertView.getContext(), title)); + + bindIcon(icon); + } +} diff --git a/src/com/android/documentsui/sidebar/NavRailRootAndAppItem.java b/src/com/android/documentsui/sidebar/NavRailRootAndAppItem.java new file mode 100644 index 000000000..1bcc42f5c --- /dev/null +++ b/src/com/android/documentsui/sidebar/NavRailRootAndAppItem.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2024 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.documentsui.sidebar; + +import android.content.pm.ResolveInfo; +import android.view.View; + +import com.android.documentsui.ActionHandler; +import com.android.documentsui.R; +import com.android.documentsui.base.RootInfo; + +/** + * Similar to {@link RootAndAppItem} but only used in the navigation rail. + */ +public class NavRailRootAndAppItem extends RootAndAppItem { + + public NavRailRootAndAppItem( + RootInfo root, ResolveInfo info, ActionHandler actionHandler, boolean maybeShowBadge) { + super(R.layout.nav_rail_item_root, root, info, actionHandler, maybeShowBadge); + } + + @Override + public void bindView(View convertView) { + bindIconAndTitle(convertView); + } +} diff --git a/src/com/android/documentsui/sidebar/NavRailRootItem.java b/src/com/android/documentsui/sidebar/NavRailRootItem.java new file mode 100644 index 000000000..3d4042f22 --- /dev/null +++ b/src/com/android/documentsui/sidebar/NavRailRootItem.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2024 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.documentsui.sidebar; + + +import android.view.View; + +import com.android.documentsui.ActionHandler; +import com.android.documentsui.R; +import com.android.documentsui.base.RootInfo; + +/** + * Similar to {@link RootItem} but only used in the navigation rail. + */ +public class NavRailRootItem extends RootItem { + + public NavRailRootItem(RootInfo root, ActionHandler actionHandler, boolean maybeShowBadge) { + super( + R.layout.nav_rail_item_root, + root, + actionHandler, + "" /* packageName */, + maybeShowBadge); + } + + public NavRailRootItem( + RootInfo root, + ActionHandler actionHandler, + String packageName, + boolean maybeShowBadge) { + super(R.layout.nav_rail_item_root, root, actionHandler, packageName, maybeShowBadge); + } + + @Override + public void bindView(View convertView) { + bindIconAndTitle(convertView); + } +} diff --git a/src/com/android/documentsui/sidebar/ProfileItem.java b/src/com/android/documentsui/sidebar/ProfileItem.java index 15068ad4b..779f54445 100644 --- a/src/com/android/documentsui/sidebar/ProfileItem.java +++ b/src/com/android/documentsui/sidebar/ProfileItem.java @@ -20,6 +20,8 @@ import android.content.pm.ResolveInfo; import android.view.View; import android.widget.ImageView; +import androidx.annotation.LayoutRes; + import com.android.documentsui.ActionHandler; import com.android.documentsui.base.UserId; @@ -32,6 +34,11 @@ class ProfileItem extends AppItem { super(info, title, UserId.CURRENT_USER, actionHandler); } + ProfileItem( + @LayoutRes int layoutId, ResolveInfo info, String title, ActionHandler actionHandler) { + super(layoutId, info, title, UserId.CURRENT_USER, actionHandler); + } + @Override protected void bindIcon(ImageView icon) { icon.setImageResource(com.android.documentsui.R.drawable.ic_user_profile); diff --git a/src/com/android/documentsui/sidebar/RootAndAppItem.java b/src/com/android/documentsui/sidebar/RootAndAppItem.java index b893878f3..8861f6058 100644 --- a/src/com/android/documentsui/sidebar/RootAndAppItem.java +++ b/src/com/android/documentsui/sidebar/RootAndAppItem.java @@ -18,11 +18,11 @@ package com.android.documentsui.sidebar; import android.content.Context; import android.content.pm.ResolveInfo; -import android.os.UserManager; import android.provider.DocumentsProvider; -import android.text.TextUtils; import android.view.View; +import androidx.annotation.LayoutRes; + import com.android.documentsui.ActionHandler; import com.android.documentsui.R; import com.android.documentsui.base.RootInfo; @@ -36,9 +36,18 @@ class RootAndAppItem extends RootItem { public final ResolveInfo resolveInfo; - public RootAndAppItem(RootInfo root, ResolveInfo info, ActionHandler actionHandler, + RootAndAppItem( + RootInfo root, ResolveInfo info, ActionHandler actionHandler, boolean maybeShowBadge) { + this(R.layout.item_root, root, info, actionHandler, maybeShowBadge); + } + + RootAndAppItem( + @LayoutRes int layoutId, + RootInfo root, + ResolveInfo info, + ActionHandler actionHandler, boolean maybeShowBadge) { - super(root, actionHandler, info.activityInfo.packageName, maybeShowBadge); + super(layoutId, root, actionHandler, info.activityInfo.packageName, maybeShowBadge); this.resolveInfo = info; } diff --git a/src/com/android/documentsui/sidebar/RootItem.java b/src/com/android/documentsui/sidebar/RootItem.java index a0a3210f8..af72a5239 100644 --- a/src/com/android/documentsui/sidebar/RootItem.java +++ b/src/com/android/documentsui/sidebar/RootItem.java @@ -16,6 +16,8 @@ package com.android.documentsui.sidebar; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; + import android.content.Context; import android.graphics.drawable.Drawable; import android.provider.DocumentsProvider; @@ -28,6 +30,7 @@ import android.view.View; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.LayoutRes; import androidx.annotation.Nullable; import com.android.documentsui.ActionHandler; @@ -38,6 +41,8 @@ import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.UserId; +import com.google.android.material.button.MaterialButton; + import java.util.Objects; /** @@ -60,7 +65,16 @@ public class RootItem extends Item { public RootItem(RootInfo root, ActionHandler actionHandler, String packageName, boolean maybeShowBadge) { - super(R.layout.item_root, root.title, getStringId(root), root.userId); + this(R.layout.item_root, root, actionHandler, packageName, maybeShowBadge); + } + + public RootItem( + @LayoutRes int layoutId, + RootInfo root, + ActionHandler actionHandler, + String packageName, + boolean maybeShowBadge) { + super(layoutId, root.title, getStringId(root), root.userId); this.root = root; mActionHandler = actionHandler; mPackageName = packageName; @@ -96,19 +110,36 @@ public class RootItem extends Item { } protected final void bindAction(View view, int visibility, int iconId, String description) { - final ImageView actionIcon = (ImageView) view.findViewById(R.id.action_icon); - final View verticalDivider = view.findViewById(R.id.vertical_divider); - final View actionIconArea = view.findViewById(R.id.action_icon_area); - - verticalDivider.setVisibility(visibility); - actionIconArea.setVisibility(visibility); - actionIconArea.setOnClickListener(visibility == View.VISIBLE ? this::onActionClick : null); - if (description != null) { - actionIconArea.setContentDescription(description); - } - if (iconId > 0) { - actionIcon.setImageDrawable(IconUtils.applyTintColor(view.getContext(), iconId, - R.color.item_action_icon)); + if (isUseMaterial3FlagEnabled()) { + final MaterialButton actionIcon = view.findViewById(R.id.action_icon); + + actionIcon.setVisibility(visibility); + actionIcon.setOnClickListener(visibility == View.VISIBLE ? this::onActionClick : null); + actionIcon.setOnFocusChangeListener( + visibility == View.VISIBLE ? this::onActionIconFocusChange : null); + if (description != null) { + actionIcon.setContentDescription(description); + } + if (iconId > 0) { + actionIcon.setIconResource(iconId); + } + } else { + final ImageView actionIcon = (ImageView) view.findViewById(R.id.action_icon); + final View verticalDivider = view.findViewById(R.id.vertical_divider); + final View actionIconArea = view.findViewById(R.id.action_icon_area); + + verticalDivider.setVisibility(visibility); + actionIconArea.setVisibility(visibility); + actionIconArea.setOnClickListener( + visibility == View.VISIBLE ? this::onActionClick : null); + if (description != null) { + actionIconArea.setContentDescription(description); + } + if (iconId > 0) { + actionIcon.setImageDrawable( + IconUtils.applyTintColor( + view.getContext(), iconId, R.color.item_action_icon)); + } } } @@ -116,6 +147,21 @@ public class RootItem extends Item { RootsFragment.ejectClicked(view, root, mActionHandler); } + /** + * When the action icon is focused, adding a focus ring indicator using Stroke. + * TODO(b/381957932): Remove this once Material Button supports focus ring. + */ + protected void onActionIconFocusChange(View view, boolean hasFocus) { + MaterialButton actionIcon = (MaterialButton) view; + if (hasFocus) { + final int focusRingWidth = + actionIcon.getResources().getDimensionPixelSize(R.dimen.focus_ring_width); + actionIcon.setStrokeWidth(focusRingWidth); + } else { + actionIcon.setStrokeWidth(0); + } + } + protected final void bindIconAndTitle(View view) { bindIcon(view, root.loadDrawerIcon(view.getContext(), mMaybeShowBadge)); bindTitle(view); diff --git a/src/com/android/documentsui/sidebar/RootsAdapter.java b/src/com/android/documentsui/sidebar/RootsAdapter.java index a08637c9d..d689705be 100644 --- a/src/com/android/documentsui/sidebar/RootsAdapter.java +++ b/src/com/android/documentsui/sidebar/RootsAdapter.java @@ -16,12 +16,15 @@ package com.android.documentsui.sidebar; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; + import android.app.Activity; import android.os.Looper; import android.view.View; import android.view.View.OnDragListener; import android.view.ViewGroup; import android.widget.ArrayAdapter; +import android.widget.ListView; import com.android.documentsui.R; @@ -83,6 +86,16 @@ class RootsAdapter extends ArrayAdapter<Item> { final Item item = getItem(position); final View view = item.getView(convertView, parent); + if (isUseMaterial3FlagEnabled()) { + // In order to have hover showing on the list item, we need to have + // "android:clickable=true" on the list item level, which will break the click handler + // because it's set at the list level, so here we "bubble up" the item level click + // event to the list level by explicitly calling the "performItemClick" on the list + // level. + view.setOnClickListener( + v -> ((ListView) parent).performItemClick(v, position, getItemId(position))); + } + if (item.isRoot()) { view.setTag(R.id.item_position_tag, position); view.setOnDragListener(mDragListener); diff --git a/src/com/android/documentsui/sidebar/RootsFragment.java b/src/com/android/documentsui/sidebar/RootsFragment.java index 76df696ab..1735f9a29 100644 --- a/src/com/android/documentsui/sidebar/RootsFragment.java +++ b/src/com/android/documentsui/sidebar/RootsFragment.java @@ -19,6 +19,8 @@ package com.android.documentsui.sidebar; import static com.android.documentsui.base.Shared.compareToIgnoreCaseNullable; import static com.android.documentsui.base.SharedMinimal.DEBUG; import static com.android.documentsui.base.SharedMinimal.VERBOSE; +import static com.android.documentsui.util.FlagUtils.isHideRootsOnDesktopFlagEnabled; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; import android.app.admin.DevicePolicyManager; import android.content.Context; @@ -49,6 +51,7 @@ import android.widget.AdapterView.OnItemClickListener; import android.widget.AdapterView.OnItemLongClickListener; import android.widget.ListView; +import androidx.annotation.IdRes; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.VisibleForTesting; @@ -96,12 +99,23 @@ import java.util.stream.Collectors; /** * Display list of known storage backend roots. + * This fragment will be used in: + * * fixed_layout: as navigation tree (sidebar) + * * drawer_layout: as navigation drawer + * * nav_rail_layout: as navigation drawer and navigation rail. */ public class RootsFragment extends Fragment { private static final String TAG = "RootsFragment"; private static final String EXTRA_INCLUDE_APPS = "includeApps"; private static final String EXTRA_INCLUDE_APPS_INTENT = "includeAppsIntent"; + /** + * A key used to store the container id in the RootFragment. + * RootFragment is used in both navigation drawer and navigation rail, there are 2 instances + * of the fragment rendered on the page, we need to know which one is which to render different + * nav items inside. + */ + private static final String EXTRA_CONTAINER_ID = "containerId"; private static final int CONTEXT_MENU_ITEM_TIMEOUT = 500; private final OnItemClickListener mItemListener = new OnItemClickListener() { @@ -135,41 +149,88 @@ public class RootsFragment extends Fragment { private List<Item> mApplicationItemList; + // Weather the fragment is using nav_rail_container_roots as its container (in nav_rail_layout). + // This will always be false if isUseMaterial3FlagEnabled() flag is off. + private boolean mUseRailAsContainer = false; + + /** + * Show the RootsFragment inside the navigation drawer container. + */ + public static RootsFragment show(FragmentManager fm, boolean includeApps, Intent intent) { + return showWithLayout(R.id.container_roots, fm, includeApps, intent); + } + + /** + * Show the RootsFragment inside the navigation rail container. + */ + public static RootsFragment showNavRail(FragmentManager fm, boolean includeApps, + Intent intent) { + return showWithLayout(R.id.nav_rail_container_roots, fm, includeApps, intent); + } + /** * Shows the {@link RootsFragment}. * + * @param containerId the container id where the {@link RootsFragment} will be rendered into * @param fm the FragmentManager for interacting with fragments associated with this * fragment's activity * @param includeApps if {@code true}, query the intent from the system and include apps in * the {@RootsFragment}. * @param intent the intent to query for package manager */ - public static RootsFragment show(FragmentManager fm, boolean includeApps, Intent intent) { + private static RootsFragment showWithLayout( + @IdRes int containerId, FragmentManager fm, boolean includeApps, Intent intent) { final Bundle args = new Bundle(); args.putBoolean(EXTRA_INCLUDE_APPS, includeApps); args.putParcelable(EXTRA_INCLUDE_APPS_INTENT, intent); + if (isUseMaterial3FlagEnabled()) { + args.putInt(EXTRA_CONTAINER_ID, containerId); + } final RootsFragment fragment = new RootsFragment(); fragment.setArguments(args); final FragmentTransaction ft = fm.beginTransaction(); - ft.replace(R.id.container_roots, fragment); + ft.replace(containerId, fragment); ft.commitAllowingStateLoss(); return fragment; } + /** + * Get the RootsFragment instance for the navigation drawer. + */ public static RootsFragment get(FragmentManager fm) { return (RootsFragment) fm.findFragmentById(R.id.container_roots); } + /** + * Get the RootsFragment instance for the navigation drawer. + */ + public static RootsFragment getNavRail(FragmentManager fm) { + return (RootsFragment) fm.findFragmentById(R.id.nav_rail_container_roots); + } + @Override public View onCreateView( LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + if (isUseMaterial3FlagEnabled()) { + mUseRailAsContainer = + getArguments() != null + && getArguments().getInt(EXTRA_CONTAINER_ID) + == R.id.nav_rail_container_roots; + } + mInjector = getBaseActivity().getInjector(); - final View view = inflater.inflate(R.layout.fragment_roots, container, false); + final View view = + inflater.inflate( + mUseRailAsContainer + ? R.layout.fragment_nav_rail_roots + : R.layout.fragment_roots, + container, + false); mList = (ListView) view.findViewById(R.id.roots_list); mList.setOnItemClickListener(mItemListener); // ListView does not have right-click specific listeners, so we will have a @@ -312,10 +373,17 @@ public class RootsFragment extends Fragment { if (crossProfileResolveInfo != null && !Features.CROSS_PROFILE_TABS) { // Add profile item if we don't support cross-profile tab. sortedItems.add(new SpacerItem()); - sortedItems.add(new ProfileItem(crossProfileResolveInfo, - crossProfileResolveInfo.loadLabel( - getContext().getPackageManager()).toString(), - mActionHandler)); + if (mUseRailAsContainer) { + sortedItems.add(new NavRailProfileItem(crossProfileResolveInfo, + crossProfileResolveInfo.loadLabel( + getContext().getPackageManager()).toString(), + mActionHandler)); + } else { + sortedItems.add(new ProfileItem(crossProfileResolveInfo, + crossProfileResolveInfo.loadLabel( + getContext().getPackageManager()).toString(), + mActionHandler)); + } } // Disable drawer if only one root @@ -413,16 +481,39 @@ public class RootsFragment extends Fragment { if (root.isExternalStorageHome()) { continue; + } else if (isHideRootsOnDesktopFlagEnabled() + && context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_PC) + && (root.isImages() || root.isVideos() + || root.isDocuments() + || root.isAudio())) { + // Hide Images/Videos/Documents/Audio roots on desktop. + Log.d(TAG, "Hiding " + root); + continue; } else if (root.isLibrary() || root.isDownloads()) { - item = new RootItem(root, mActionHandler, maybeShowBadge); + item = + mUseRailAsContainer + ? new NavRailRootItem(root, mActionHandler, maybeShowBadge) + : new RootItem(root, mActionHandler, maybeShowBadge); librariesBuilder.add(item); } else if (root.isStorage()) { - item = new RootItem(root, mActionHandler, maybeShowBadge); + item = + mUseRailAsContainer + ? new NavRailRootItem(root, mActionHandler, maybeShowBadge) + : new RootItem(root, mActionHandler, maybeShowBadge); storageProvidersBuilder.add(item); } else { - item = new RootItem(root, mActionHandler, - providersAccess.getPackageName(root.userId, root.authority), - maybeShowBadge); + item = + mUseRailAsContainer + ? new NavRailRootItem( + root, + mActionHandler, + providersAccess.getPackageName(root.userId, root.authority), + maybeShowBadge) + : new RootItem( + root, + mActionHandler, + providersAccess.getPackageName(root.userId, root.authority), + maybeShowBadge); otherProviders.add(item); } } @@ -566,8 +657,18 @@ public class RootsFragment extends Fragment { appsMapping.put(userPackage, info); if (!CrossProfileUtils.isCrossProfileIntentForwarderActivity(info)) { - final Item item = new AppItem(info, info.loadLabel(pm).toString(), userId, - mActionHandler); + final Item item = + mUseRailAsContainer + ? new NavRailAppItem( + info, + info.loadLabel(pm).toString(), + userId, + mActionHandler) + : new AppItem( + info, + info.loadLabel(pm).toString(), + userId, + mActionHandler); appItems.put(userPackage, item); if (VERBOSE) Log.v(TAG, "Adding handler app: " + item); } @@ -583,8 +684,12 @@ public class RootsFragment extends Fragment { final Item item; if (resolveInfo != null) { - item = new RootAndAppItem(rootItem.root, resolveInfo, mActionHandler, - maybeShowBadge); + item = + mUseRailAsContainer + ? new NavRailRootAndAppItem( + rootItem.root, resolveInfo, mActionHandler, maybeShowBadge) + : new RootAndAppItem( + rootItem.root, resolveInfo, mActionHandler, maybeShowBadge); appItems.remove(userPackage); } else { item = rootItem; diff --git a/src/com/android/documentsui/sorting/HeaderCell.java b/src/com/android/documentsui/sorting/HeaderCell.java index 43e254e39..7f72f13da 100644 --- a/src/com/android/documentsui/sorting/HeaderCell.java +++ b/src/com/android/documentsui/sorting/HeaderCell.java @@ -16,11 +16,11 @@ package com.android.documentsui.sorting; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; + import android.animation.AnimatorInflater; import android.animation.LayoutTransition; import android.animation.ObjectAnimator; -import androidx.annotation.AnimatorRes; -import androidx.annotation.StringRes; import android.content.Context; import android.util.AttributeSet; import android.view.Gravity; @@ -29,14 +29,14 @@ import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; +import androidx.annotation.AnimatorRes; +import androidx.annotation.StringRes; + import com.android.documentsui.R; -import com.android.documentsui.sorting.SortDimension; /** - * A clickable, sortable table header cell layout. - * - * It updates its display when it binds to {@link SortDimension} and changes the status of sorting - * when it's clicked. + * A clickable, sortable table header cell layout. It updates its display when it binds to {@link + * SortDimension} and changes the status of sorting when it's clicked. */ public class HeaderCell extends LinearLayout { @@ -62,7 +62,7 @@ public class HeaderCell extends LinearLayout { setVisibility(dimension.getVisibility()); if (dimension.getVisibility() == View.VISIBLE) { - TextView label = (TextView) findViewById(R.id.label); + TextView label = findViewById(R.id.label); label.setText(dimension.getLabelId()); switch (dimension.getDataType()) { case SortDimension.DATA_TYPE_NUMBER: @@ -77,17 +77,21 @@ public class HeaderCell extends LinearLayout { } if (mCurDirection != dimension.getSortDirection()) { - ImageView arrow = (ImageView) findViewById(R.id.sort_arrow); + ImageView arrow = findViewById(R.id.sort_arrow); switch (dimension.getSortDirection()) { case SortDimension.SORT_DIRECTION_NONE: arrow.setVisibility(View.GONE); break; case SortDimension.SORT_DIRECTION_ASCENDING: - showArrow(arrow, R.animator.arrow_rotate_up, + showArrow( + arrow, + R.animator.arrow_rotate_up, R.string.sort_direction_ascending); break; case SortDimension.SORT_DIRECTION_DESCENDING: - showArrow(arrow, R.animator.arrow_rotate_down, + showArrow( + arrow, + R.animator.arrow_rotate_down, R.string.sort_direction_descending); break; default: @@ -100,6 +104,22 @@ public class HeaderCell extends LinearLayout { } } + /** + * Sets a listener on the sort arrow image. When Material 3 is enabled, the Sort Arrow has + * "android:clickable" set to true (to enable a hover state). This stops the click listener from + * falling through to the cell click listener and thus the sort arrow will need to handle clicks + * itself. + */ + public void setSortArrowListeners( + View.OnClickListener clickListener, + View.OnKeyListener keyListener, + SortDimension dimension) { + ImageView arrow = findViewById(R.id.sort_arrow); + arrow.setTag(dimension); + arrow.setOnKeyListener(keyListener); + arrow.setOnClickListener(clickListener); + } + private void showArrow( ImageView arrow, @AnimatorRes int anim, @StringRes int contentDescriptionId) { arrow.setVisibility(View.VISIBLE); @@ -114,8 +134,13 @@ public class HeaderCell extends LinearLayout { } private void setDataTypeNumber(View label) { - label.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END); - setGravity(Gravity.CENTER_VERTICAL | Gravity.END); + if (isUseMaterial3FlagEnabled()) { + label.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_START); + setGravity(Gravity.CENTER_VERTICAL | Gravity.START); + } else { + label.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END); + setGravity(Gravity.CENTER_VERTICAL | Gravity.END); + } } private void setDataTypeString(View label) { diff --git a/src/com/android/documentsui/sorting/TableHeaderController.java b/src/com/android/documentsui/sorting/TableHeaderController.java index 549478cfc..cb72ac916 100644 --- a/src/com/android/documentsui/sorting/TableHeaderController.java +++ b/src/com/android/documentsui/sorting/TableHeaderController.java @@ -16,49 +16,54 @@ package com.android.documentsui.sorting; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; + +import android.view.KeyEvent; import android.view.View; import com.android.documentsui.R; import javax.annotation.Nullable; -/** - * View controller for table header that associates header cells in table header and columns. - */ +/** View controller for table header that associates header cells in table header and columns. */ public final class TableHeaderController implements SortController.WidgetController { - private View mTableHeader; - private final HeaderCell mTitleCell; private final HeaderCell mSummaryCell; private final HeaderCell mSizeCell; private final HeaderCell mFileTypeCell; private final HeaderCell mDateCell; - + private final SortModel mModel; // We assign this here porque each method reference creates a new object // instance (which is wasteful). private final View.OnClickListener mOnCellClickListener = this::onCellClicked; + private final View.OnKeyListener mOnCellKeyListener = this::onCellKeyEvent; private final SortModel.UpdateListener mModelListener = this::onModelUpdate; - - private final SortModel mModel; + private final View mTableHeader; private TableHeaderController(SortModel sortModel, View tableHeader) { - assert(sortModel != null); - assert(tableHeader != null); + assert (sortModel != null); + assert (tableHeader != null); mModel = sortModel; mTableHeader = tableHeader; - mTitleCell = (HeaderCell) tableHeader.findViewById(android.R.id.title); - mSummaryCell = (HeaderCell) tableHeader.findViewById(android.R.id.summary); - mSizeCell = (HeaderCell) tableHeader.findViewById(R.id.size); - mFileTypeCell = (HeaderCell) tableHeader.findViewById(R.id.file_type); - mDateCell = (HeaderCell) tableHeader.findViewById(R.id.date); + mTitleCell = tableHeader.findViewById(android.R.id.title); + mSummaryCell = tableHeader.findViewById(android.R.id.summary); + mSizeCell = tableHeader.findViewById(R.id.size); + mFileTypeCell = tableHeader.findViewById(R.id.file_type); + mDateCell = tableHeader.findViewById(R.id.date); onModelUpdate(mModel, SortModel.UPDATE_TYPE_UNSPECIFIED); mModel.addListener(mModelListener); } + /** Creates a TableHeaderController. */ + public static @Nullable TableHeaderController create( + SortModel sortModel, @Nullable View tableHeader) { + return (tableHeader == null) ? null : new TableHeaderController(sortModel, tableHeader); + } + private void onModelUpdate(SortModel model, int updateTypeUnspecified) { bindCell(mTitleCell, SortModel.SORT_DIMENSION_ID_TITLE); bindCell(mSummaryCell, SortModel.SORT_DIMENSION_ID_SUMMARY); @@ -78,7 +83,7 @@ public final class TableHeaderController implements SortController.WidgetControl } private void bindCell(HeaderCell cell, int id) { - assert(cell != null); + assert (cell != null); SortDimension dimension = mModel.getDimensionById(id); cell.setTag(dimension); @@ -87,8 +92,12 @@ public final class TableHeaderController implements SortController.WidgetControl if (dimension.getVisibility() == View.VISIBLE && dimension.getSortCapability() != SortDimension.SORT_CAPABILITY_NONE) { cell.setOnClickListener(mOnCellClickListener); + if (isUseMaterial3FlagEnabled()) { + cell.setSortArrowListeners(mOnCellClickListener, mOnCellKeyListener, dimension); + } } else { cell.setOnClickListener(null); + if (isUseMaterial3FlagEnabled()) cell.setSortArrowListeners(null, null, null); } } @@ -98,8 +107,17 @@ public final class TableHeaderController implements SortController.WidgetControl mModel.sortByUser(dimension.getId(), dimension.getNextDirection()); } - public static @Nullable TableHeaderController create( - SortModel sortModel, @Nullable View tableHeader) { - return (tableHeader == null) ? null : new TableHeaderController(sortModel, tableHeader); + /** Sorts the column if the key pressed was Enter or Space. */ + private boolean onCellKeyEvent(View v, int keyCode, KeyEvent event) { + if (!isUseMaterial3FlagEnabled()) { + return false; + } + // Only the enter and space bar should trigger the sort header to engage. + if (event.getAction() == KeyEvent.ACTION_UP + && (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SPACE)) { + onCellClicked(v); + return true; + } + return false; } } diff --git a/src/com/android/documentsui/ui/DocumentDebugInfo.java b/src/com/android/documentsui/ui/DocumentDebugInfo.java deleted file mode 100644 index b7712c60a..000000000 --- a/src/com/android/documentsui/ui/DocumentDebugInfo.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright (C) 2015 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.documentsui.ui; - -import androidx.annotation.Nullable; -import android.content.Context; -import android.util.AttributeSet; -import android.widget.TextView; - -import com.android.documentsui.base.DocumentInfo; - -/** - * Document debug info view. - */ -public class DocumentDebugInfo extends TextView { - public DocumentDebugInfo(Context context) { - super(context); - - } - - public DocumentDebugInfo(Context context, @Nullable AttributeSet attrs) { - super(context, attrs); - } - - public void update(DocumentInfo doc) { - - String dbgInfo = new StringBuilder() - .append("** PROPERTIES **\n\n") - .append("docid: " + doc.documentId).append("\n") - .append("name: " + doc.displayName).append("\n") - .append("mimetype: " + doc.mimeType).append("\n") - .append("container: " + doc.isContainer()).append("\n") - .append("virtual: " + doc.isVirtual()).append("\n") - .append("\n") - .append("** OPERATIONS **\n\n") - .append("create: " + doc.isCreateSupported()).append("\n") - .append("delete: " + doc.isDeleteSupported()).append("\n") - .append("rename: " + doc.isRenameSupported()).append("\n\n") - .append("** URI **\n\n") - .append(doc.derivedUri).append("\n") - .toString(); - - setText(dbgInfo); - } -} diff --git a/src/com/android/documentsui/ui/Snackbars.java b/src/com/android/documentsui/ui/Snackbars.java index b45c247b5..795b6248b 100644 --- a/src/com/android/documentsui/ui/Snackbars.java +++ b/src/com/android/documentsui/ui/Snackbars.java @@ -16,6 +16,8 @@ package com.android.documentsui.ui; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; + import android.app.Activity; import android.view.Gravity; import android.view.View; @@ -110,7 +112,10 @@ public final class Snackbars { public static final Snackbar makeSnackbar( Activity activity, CharSequence message, int duration) { - final View view = activity.findViewById(R.id.container_save); + final View view = activity.findViewById(isUseMaterial3FlagEnabled() + ? R.id.coordinator_layout + : R.id.container_save + ); return Snackbar.make(view, message, duration); } } diff --git a/src/com/android/documentsui/util/FlagUtils.kt b/src/com/android/documentsui/util/FlagUtils.kt new file mode 100644 index 000000000..eee51be89 --- /dev/null +++ b/src/com/android/documentsui/util/FlagUtils.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2025 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.documentsui.util + +import com.android.documentsui.flags.Flags + +/** + * Wraps the static flags classes to enable a single place to refactor flag usage + * (or combine usage when required). + */ +class FlagUtils { + companion object { + @JvmStatic + fun isUseMaterial3FlagEnabled(): Boolean { + return Flags.useMaterial3() + } + + @JvmStatic + fun isZipNgFlagEnabled(): Boolean { + return Flags.zipNgRo() + } + + @JvmStatic + fun isUseSearchV2RwFlagEnabled(): Boolean { + return Flags.useSearchV2Rw() + } + + @JvmStatic + fun isDesktopFileHandlingFlagEnabled(): Boolean { + return Flags.desktopFileHandlingRo() + } + + @JvmStatic + fun isVisualSignalsFlagEnabled(): Boolean { + return Flags.visualSignalsRo() && isUseMaterial3FlagEnabled() + } + + @JvmStatic + fun isHideRootsOnDesktopFlagEnabled(): Boolean { + return Flags.hideRootsOnDesktopRo() + } + + @JvmStatic + fun isUsePeekPreviewFlagEnabled(): Boolean { + return Flags.usePeekPreviewRo() + } + } +} diff --git a/tests/Android.bp b/tests/Android.bp index e412919bd..41ccc1ab1 100644 --- a/tests/Android.bp +++ b/tests/Android.bp @@ -25,12 +25,16 @@ java_defaults { ], static_libs: [ - "androidx.test.rules", + "androidx.test.core", "androidx.test.espresso.core", "androidx.test.ext.truth", + "androidx.test.rules", + "androidx.test.ext.junit", + "androidx.test.uiautomator_uiautomator", + "docsui-flags-aconfig-java-lib", + "flag-junit", "guava", "mockito-target", - "androidx.test.uiautomator_uiautomator", ], } @@ -50,11 +54,11 @@ android_library { static_libs: [ "androidx.legacy_legacy-support-v4", - "androidx.test.rules", "androidx.test.espresso.core", + "androidx.test.rules", + "androidx.test.uiautomator_uiautomator", "mockito-target", "ub-janktesthelper", - "androidx.test.uiautomator_uiautomator", ], } @@ -67,6 +71,7 @@ android_library { srcs: [ "common/**/*.java", "unit/**/*.java", + "unit/**/*.kt", ], libs: [ @@ -77,8 +82,8 @@ android_library { "res", ], - min_sdk_version: "29", - target_sdk_version: "29", + min_sdk_version: "30", + target_sdk_version: "30", } android_library { @@ -90,7 +95,9 @@ android_library { srcs: [ "common/**/*.java", "functional/**/*.java", + "functional/**/*.kt", "unit/**/*.java", + "unit/**/*.kt", ], libs: [ @@ -110,8 +117,8 @@ android_library { "-0 .zip", ], - min_sdk_version: "29", - target_sdk_version: "29", + min_sdk_version: "30", + target_sdk_version: "30", lint: { baseline_filename: "lint-baseline.xml", }, @@ -141,6 +148,6 @@ android_test { certificate: "platform", instrumentation_for: "DocumentsUI", - min_sdk_version: "29", - target_sdk_version: "29", + min_sdk_version: "30", + target_sdk_version: "30", } diff --git a/tests/AndroidManifest.xml b/tests/AndroidManifest.xml index d5e9a6045..c64b7aedd 100644 --- a/tests/AndroidManifest.xml +++ b/tests/AndroidManifest.xml @@ -2,7 +2,7 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.documentsui.tests"> - <uses-sdk android:minSdkVersion="29" android:targetSdkVersion="29"/> + <uses-sdk android:minSdkVersion="30" android:targetSdkVersion="30"/> <uses-permission android:name="android.permission.INTERNET" /> diff --git a/tests/common/com/android/documentsui/bots/DirectoryListBot.java b/tests/common/com/android/documentsui/bots/DirectoryListBot.java index 3e9fa30b8..314013e53 100644 --- a/tests/common/com/android/documentsui/bots/DirectoryListBot.java +++ b/tests/common/com/android/documentsui/bots/DirectoryListBot.java @@ -46,7 +46,6 @@ import androidx.test.uiautomator.Until; import java.util.ArrayList; import java.util.Arrays; import java.util.List; -import java.util.regex.Pattern; /** * A test helper class that provides support for controlling directory list @@ -56,16 +55,13 @@ public class DirectoryListBot extends Bots.BaseBot { private static final int MAX_LAYOUT_LEVEL = 10; - private static final BySelector SNACK_DELETE = - By.text(Pattern.compile("^Deleting [0-9]+ item.+")); - private final String mDirContainerId; private final String mDirListId; private final String mItemRootId; private final String mPreviewId; private final String mIconId; - private UiAutomation mAutomation; + private final UiAutomation mAutomation; public DirectoryListBot( UiDevice device, UiAutomation automation, Context context, int timeout) { @@ -178,10 +174,6 @@ public class DirectoryListBot extends Bots.BaseBot { findPlaceholderMessageTextView().waitForExists(mTimeout); } - public void assertSnackbar(int id) { - assertNotNull(getSnackbar(mContext.getString(id))); - } - public void openDocument(String label) throws UiObjectNotFoundException { int toolType = Configurator.getInstance().getToolType(); Configurator.getInstance().setToolType(MotionEvent.TOOL_TYPE_FINGER); @@ -208,13 +200,6 @@ public class DirectoryListBot extends Bots.BaseBot { assertSelection(number); } - public boolean isDocumentSelected(String label) throws UiObjectNotFoundException { - waitForDocument(label); - UiObject2 selectionHotspot = findSelectionHotspot(label); - return selectionHotspot.getResourceName() - .equals(mTargetPackage + ":id/icon_check"); - } - public UiObject2 findSelectionHotspot(String label) throws UiObjectNotFoundException { final BySelector list = By.res(mDirListId); @@ -224,20 +209,15 @@ public class DirectoryListBot extends Bots.BaseBot { new UiScrollable(docList).scrollIntoView(new UiSelector().text(label)); UiObject2 parent = mDevice.findObject(list).findObject(selector); + UiObject2 selectionHotspot = null; for (int i = 1; i <= MAX_LAYOUT_LEVEL; i++) { parent = parent.getParent(); - if (mItemRootId.equals(parent.getResourceName())) { + selectionHotspot = parent.findObject(By.res(mIconId)); + if (selectionHotspot != null) { break; } } - return parent.findObject(By.res(mIconId)); - } - - public void copyFilesToClipboard(String...labels) throws UiObjectNotFoundException { - for (String label: labels) { - selectDocument(label); - } - mDevice.pressKeyCode(KeyEvent.KEYCODE_C, KeyEvent.META_CTRL_ON); + return selectionHotspot; } public void pasteFilesFromClipboard() { @@ -248,21 +228,6 @@ public class DirectoryListBot extends Bots.BaseBot { return mDevice.wait(Until.findObject(By.text(message)), mTimeout); } - public void clickSnackbarAction() throws UiObjectNotFoundException { - UiObject snackbarAction = - findObject(mTargetPackage + ":id/snackbar_action"); - snackbarAction.click(); - } - - public void waitForDeleteSnackbar() { - mDevice.wait(Until.findObject(SNACK_DELETE), mTimeout); - } - - public void waitForDeleteSnackbarGone() { - // wait a little longer for snackbar to go away, as it disappears after a timeout. - mDevice.wait(Until.gone(SNACK_DELETE), mTimeout * 2); - } - public void waitForDocument(String label) throws UiObjectNotFoundException { findDocument(label).waitForExists(mTimeout); } @@ -295,9 +260,8 @@ public class DirectoryListBot extends Bots.BaseBot { public boolean hasDocumentPreview(String label) { final BySelector list = By.res(mDirListId); - final UiObject2 text = mDevice.findObject(list).findObject(By.text(label)); - UiObject2 parent = text; + UiObject2 parent = mDevice.findObject(list).findObject(By.text(label)); for (int i = 1; i <= MAX_LAYOUT_LEVEL; i++) { parent = parent.getParent(); if (mItemRootId.equals(parent.getResourceName())) { @@ -338,7 +302,7 @@ public class DirectoryListBot extends Bots.BaseBot { String assertSelectionText = numSelected + " selected"; UiObject2 selectionText = mDevice.wait( Until.findObject(By.text(assertSelectionText)), mTimeout); - assertTrue(selectionText != null); + assertNotNull(selectionText); } public void assertOrder(String[] dirs, String[] files) throws UiObjectNotFoundException { diff --git a/tests/common/com/android/documentsui/bots/SortBot.java b/tests/common/com/android/documentsui/bots/SortBot.java index 0a77de4ee..f72f16f86 100644 --- a/tests/common/com/android/documentsui/bots/SortBot.java +++ b/tests/common/com/android/documentsui/bots/SortBot.java @@ -49,10 +49,11 @@ import com.android.documentsui.sorting.SortModel; import org.hamcrest.Matcher; /** - * A test helper class that provides support for controlling the UI Breadcrumb - * programmatically, and making assertions against the state of the UI. - * <p> - * Support for working directly with Roots and Directory view can be found in the respective bots. + * A test helper class that provides support for controlling the UI Breadcrumb programmatically, and + * making assertions against the state of the UI. + * + * <p>Support for working directly with Roots and Directory view can be found in the respective + * bots. */ public class SortBot extends Bots.BaseBot { @@ -67,7 +68,7 @@ public class SortBot extends Bots.BaseBot { } public void sortBy(int id, @SortDirection int direction) { - assert(direction != SortDimension.SORT_DIRECTION_NONE); + assert (direction != SortDimension.SORT_DIRECTION_NONE); final @StringRes int labelId = mSortModel.getDimensionById(id).getLabelId(); final String label = mContext.getString(labelId); @@ -78,16 +79,15 @@ public class SortBot extends Bots.BaseBot { result = sortByMenu(id, direction); } - assertTrue("Sorting by id: " + id + " in direction: " + direction + " failed.", - result); + assertTrue("Sorting by id: " + id + " in direction: " + direction + " failed.", result); } public boolean isHeaderShow() { - return Matchers.present(mColumnBot.MATCHER); + return Matchers.present(ColumnSortBot.MATCHER); } public void assertHeaderHide() { - assertFalse(Matchers.present(mColumnBot.MATCHER)); + assertFalse(Matchers.present(ColumnSortBot.MATCHER)); } public void assertHeaderShow() { @@ -98,11 +98,21 @@ public class SortBot extends Bots.BaseBot { // or with espresso. It's sad that I'm leaving you // with this little gremlin, but we all have to // move on and get stuff done :) - assertTrue(Matchers.present(mColumnBot.MATCHER)); + assertTrue(Matchers.present(ColumnSortBot.MATCHER)); + } + + /** + * Identify if the sort arrow in list mode is focused. + * + * @return True if the sort arrow in the file list is found. + */ + public boolean isSortIconFocused() { + UiObject2 sortArrow = mDevice.findObject(By.res(mTargetPackage + ":id/sort_arrow")); + return sortArrow.isFocused(); } private boolean sortByMenu(int id, @SortDirection int direction) { - assert(direction != SortDimension.SORT_DIRECTION_NONE); + assert (direction != SortDimension.SORT_DIRECTION_NONE); clickMenuSort(); mDevice.waitForIdle(); @@ -131,9 +141,8 @@ public class SortBot extends Bots.BaseBot { private static final Matcher<View> MATCHER = withId(R.id.table_header); private boolean sortBy(String label, @SortDirection int direction) { - final Matcher<View> cellMatcher = allOf( - withChild(withText(label)), - isDescendantOfA(MATCHER)); + final Matcher<View> cellMatcher = + allOf(withChild(withText(label)), isDescendantOfA(MATCHER)); onView(cellMatcher).perform(click()); final @SortDirection int viewDirection = getDirection(cellMatcher); diff --git a/tests/common/com/android/documentsui/bots/UiBot.java b/tests/common/com/android/documentsui/bots/UiBot.java index f30cb93b8..47e0acd5a 100644 --- a/tests/common/com/android/documentsui/bots/UiBot.java +++ b/tests/common/com/android/documentsui/bots/UiBot.java @@ -25,6 +25,8 @@ import static androidx.test.espresso.matcher.ViewMatchers.withClassName; import static androidx.test.espresso.matcher.ViewMatchers.withId; import static androidx.test.espresso.matcher.ViewMatchers.withText; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNotNull; import static junit.framework.Assert.assertNull; @@ -68,37 +70,48 @@ import java.util.List; */ public class UiBot extends Bots.BaseBot { - public static String targetPackageName; - @SuppressWarnings("unchecked") private static final Matcher<View> TOOLBAR = allOf( isAssignableFrom(Toolbar.class), withId(R.id.toolbar)); - @SuppressWarnings("unchecked") private static final Matcher<View> ACTIONBAR = allOf( withClassName(endsWith("ActionBarContextView"))); - @SuppressWarnings("unchecked") private static final Matcher<View> TEXT_ENTRY = allOf( withClassName(endsWith("EditText"))); - @SuppressWarnings("unchecked") private static final Matcher<View> TOOLBAR_OVERFLOW = allOf( withClassName(endsWith("OverflowMenuButton")), ViewMatchers.isDescendantOfA(TOOLBAR)); - @SuppressWarnings("unchecked") private static final Matcher<View> ACTIONBAR_OVERFLOW = allOf( withClassName(endsWith("OverflowMenuButton")), ViewMatchers.isDescendantOfA(ACTIONBAR)); + public static String targetPackageName; + public UiBot(UiDevice device, Context context, int timeout) { super(device, context, timeout); targetPackageName = InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName(); } + private static Matcher<Object> withToolbarTitle(final Matcher<CharSequence> textMatcher) { + return new BoundedMatcher<Object, Toolbar>(Toolbar.class) { + @Override + public boolean matchesSafely(Toolbar toolbar) { + return textMatcher.matches(toolbar.getTitle()); + } + + @Override + public void describeTo(Description description) { + description.appendText("with toolbar title: "); + textMatcher.describeTo(description); + } + }; + } + public void assertWindowTitle(String expected) { onView(TOOLBAR) .check(matches(withToolbarTitle(is(expected)))); @@ -198,7 +211,11 @@ public class UiBot extends Bots.BaseBot { } public void clickActionbarOverflowItem(String label) { - onView(ACTIONBAR_OVERFLOW).perform(click()); + if (isUseMaterial3FlagEnabled()) { + onView(TOOLBAR_OVERFLOW).perform(click()); + } else { + onView(ACTIONBAR_OVERFLOW).perform(click()); + } // Click the item by label, since Espresso doesn't support lookup by id on overflow. onView(withText(label)).perform(click()); } @@ -214,9 +231,10 @@ public class UiBot extends Bots.BaseBot { } public boolean waitForActionModeBarToAppear() { + String actionModeId = isUseMaterial3FlagEnabled() ? "toolbar" : "action_mode_bar"; UiObject2 bar = - mDevice.wait(Until.findObject( - By.res(mTargetPackage + ":id/action_mode_bar")), mTimeout); + mDevice.wait( + Until.findObject(By.res(mTargetPackage + ":id/" + actionModeId)), mTimeout); return (bar != null); } @@ -266,14 +284,16 @@ public class UiBot extends Bots.BaseBot { // Espresso has flaky results when keyboard shows up, so hiding it for now // before trying to click on any dialog button Espresso.closeSoftKeyboard(); - onView(withId(android.R.id.button1)).perform(click()); + UiObject2 okButton = mDevice.findObject(By.res("android:id/button1")); + okButton.click(); } public void clickDialogCancelButton() throws UiObjectNotFoundException { // Espresso has flaky results when keyboard shows up, so hiding it for now // before trying to click on any dialog button Espresso.closeSoftKeyboard(); - onView(withId(android.R.id.button2)).perform(click()); + UiObject2 okButton = mDevice.findObject(By.res("android:id/button2")); + okButton.click(); } public UiObject findMenuLabelWithName(String label) { @@ -307,20 +327,4 @@ public class UiBot extends Bots.BaseBot { // TODO: use the system string ? android.R.string.action_menu_overflow_description return mDevice.findObject(selector); } - - private static Matcher<Object> withToolbarTitle( - final Matcher<CharSequence> textMatcher) { - return new BoundedMatcher<Object, Toolbar>(Toolbar.class) { - @Override - public boolean matchesSafely(Toolbar toolbar) { - return textMatcher.matches(toolbar.getTitle()); - } - - @Override - public void describeTo(Description description) { - description.appendText("with toolbar title: "); - textMatcher.describeTo(description); - } - }; - } } diff --git a/tests/common/com/android/documentsui/services/TestJob.java b/tests/common/com/android/documentsui/services/TestJob.java index 10addebd9..426cb9575 100644 --- a/tests/common/com/android/documentsui/services/TestJob.java +++ b/tests/common/com/android/documentsui/services/TestJob.java @@ -23,11 +23,11 @@ import android.app.Notification; import android.app.Notification.Builder; import android.content.Context; -import com.android.documentsui.base.Features; -import com.android.documentsui.clipping.UrisSupplier; import com.android.documentsui.R; import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentStack; +import com.android.documentsui.base.Features; +import com.android.documentsui.clipping.UrisSupplier; import com.android.documentsui.services.FileOperationService.OpType; import java.text.NumberFormat; @@ -97,6 +97,10 @@ public class TestJob extends Job { throw new UnsupportedOperationException(); } + JobProgress getJobProgress() { + return new JobProgress(id, getState(), "test job", false); + } + @Override Builder createProgressBuilder() { ++mNumOfNotifications; diff --git a/tests/common/com/android/documentsui/testing/TestDirectoryDetails.java b/tests/common/com/android/documentsui/testing/TestDirectoryDetails.java index 28416775a..fb1a7b6db 100644 --- a/tests/common/com/android/documentsui/testing/TestDirectoryDetails.java +++ b/tests/common/com/android/documentsui/testing/TestDirectoryDetails.java @@ -24,6 +24,7 @@ import com.android.documentsui.MenuManager.DirectoryDetails; public class TestDirectoryDetails extends DirectoryDetails { public boolean isInRecents; + public boolean isInArchive; public boolean hasRootSettings; public boolean hasItemsToPaste; public boolean canCreateDoc; @@ -50,6 +51,11 @@ public class TestDirectoryDetails extends DirectoryDetails { } @Override + public boolean isInArchive() { + return isInArchive; + } + + @Override public boolean canCreateDoc() { return canCreateDoc; } diff --git a/tests/common/com/android/documentsui/testing/TestDocumentsProvider.java b/tests/common/com/android/documentsui/testing/TestDocumentsProvider.java index 5c5ed856f..c7e884fde 100644 --- a/tests/common/com/android/documentsui/testing/TestDocumentsProvider.java +++ b/tests/common/com/android/documentsui/testing/TestDocumentsProvider.java @@ -17,6 +17,7 @@ package com.android.documentsui.testing; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.Context; import android.content.pm.ProviderInfo; import android.database.Cursor; @@ -25,6 +26,7 @@ import android.net.Uri; import android.os.Bundle; import android.os.CancellationSignal; import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; import android.provider.DocumentsContract.Document; import android.provider.DocumentsProvider; @@ -91,13 +93,59 @@ public class TestDocumentsProvider extends DocumentsProvider { return mNextRecentDocuments; } + private String getStringColumn(Cursor cursor, String name) { + return cursor.getString(cursor.getColumnIndexOrThrow(name)); + } + + private long getLongColumn(Cursor cursor, String name) { + return cursor.getLong(cursor.getColumnIndexOrThrow(name)); + } + + @Override + public Cursor querySearchDocuments(@NonNull String rootId, @Nullable String[] projection, + @NonNull Bundle queryArgs) { + TestCursor cursor = new TestCursor(DOCUMENTS_PROJECTION); + if (mNextChildDocuments == null) { + return cursor; + } + for (boolean hasNext = mNextChildDocuments.moveToFirst(); hasNext; + hasNext = mNextChildDocuments.moveToNext()) { + String displayName = getStringColumn(mNextChildDocuments, Document.COLUMN_DISPLAY_NAME); + String mimeType = getStringColumn(mNextChildDocuments, Document.COLUMN_MIME_TYPE); + long lastModified = getLongColumn(mNextChildDocuments, Document.COLUMN_LAST_MODIFIED); + long size = getLongColumn(mNextChildDocuments, Document.COLUMN_SIZE); + + if (DocumentsContract.matchSearchQueryArguments(queryArgs, displayName, mimeType, + lastModified, size)) { + cursor.newRow() + .add(Document.COLUMN_DOCUMENT_ID, + getStringColumn(mNextChildDocuments, Document.COLUMN_DOCUMENT_ID)) + .add(Document.COLUMN_MIME_TYPE, + getStringColumn(mNextChildDocuments, Document.COLUMN_MIME_TYPE)) + .add(Document.COLUMN_DISPLAY_NAME, + getStringColumn(mNextChildDocuments, Document.COLUMN_DISPLAY_NAME)) + .add(Document.COLUMN_LAST_MODIFIED, + getLongColumn(mNextChildDocuments, Document.COLUMN_LAST_MODIFIED)) + .add(Document.COLUMN_FLAGS, + getLongColumn(mNextChildDocuments, Document.COLUMN_FLAGS)) + .add(Document.COLUMN_SUMMARY, + getStringColumn(mNextChildDocuments, Document.COLUMN_SUMMARY)) + .add(Document.COLUMN_SIZE, + getLongColumn(mNextChildDocuments, Document.COLUMN_SIZE)) + .add(Document.COLUMN_ICON, + getLongColumn(mNextChildDocuments, Document.COLUMN_ICON)); + } + } + return cursor; + } + @Override public Cursor querySearchDocuments(String rootId, String query, String[] projection) { - if (mNextChildDocuments != null) { - return filterCursorByString(mNextChildDocuments, query); + if (mNextChildDocuments == null) { + return null; } - return mNextChildDocuments; + return filterCursorByString(mNextChildDocuments, query); } @Override diff --git a/tests/common/com/android/documentsui/testing/TestMenu.java b/tests/common/com/android/documentsui/testing/TestMenu.java index 10e0ea493..9795fd373 100644 --- a/tests/common/com/android/documentsui/testing/TestMenu.java +++ b/tests/common/com/android/documentsui/testing/TestMenu.java @@ -77,6 +77,7 @@ public abstract class TestMenu implements Menu { R.id.option_menu_debug, R.id.option_menu_new_window, R.id.option_menu_create_dir, + R.id.option_menu_extract_all, R.id.option_menu_select_all, R.id.option_menu_settings, R.id.option_menu_inspect, @@ -101,6 +102,11 @@ public abstract class TestMenu implements Menu { if (id == R.id.option_menu_search) { item.setActionView(Mockito.mock(SearchView.class)); } + + if (id == R.id.option_menu_extract_all) { + item.setEnabled(false); + item.setVisible(false); + } } return menu; } diff --git a/tests/common/com/android/documentsui/testing/TestModel.java b/tests/common/com/android/documentsui/testing/TestModel.java index 452238189..83ece2ebb 100644 --- a/tests/common/com/android/documentsui/testing/TestModel.java +++ b/tests/common/com/android/documentsui/testing/TestModel.java @@ -40,7 +40,8 @@ public class TestModel extends Model { Document.COLUMN_FLAGS, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_SIZE, - Document.COLUMN_MIME_TYPE + Document.COLUMN_MIME_TYPE, + Document.COLUMN_LAST_MODIFIED }; public final UserId mUserId; @@ -104,7 +105,7 @@ public class TestModel extends Model { } public DocumentInfo createDocumentForUser(String name, String mimeType, int flags, - UserId userId) { + long lastModified, UserId userId) { DocumentInfo doc = new DocumentInfo(); doc.userId = userId; doc.authority = mAuthority; @@ -114,6 +115,7 @@ public class TestModel extends Model { doc.mimeType = mimeType; doc.flags = flags; doc.size = mRand.nextInt(); + doc.lastModified = lastModified; addToCursor(doc); @@ -121,7 +123,7 @@ public class TestModel extends Model { } public DocumentInfo createDocument(String name, String mimeType, int flags) { - return createDocumentForUser(name, mimeType, flags, mUserId); + return createDocumentForUser(name, mimeType, flags, System.currentTimeMillis(), mUserId); } private void addToCursor(DocumentInfo doc) { @@ -133,9 +135,17 @@ public class TestModel extends Model { row.add(Document.COLUMN_MIME_TYPE, doc.mimeType); row.add(Document.COLUMN_FLAGS, doc.flags); row.add(Document.COLUMN_SIZE, doc.size); + row.add(Document.COLUMN_LAST_MODIFIED, doc.lastModified); } - private static String guessMimeType(String name) { + /** + * Attempts to guess the MIME type of the file based on its name. If unable to guess, returns + * "text/plain". + * + * @param name The name of the file whose MIME type is guessed. + * @return A guessed MIME type of "text/plain". + */ + public static String guessMimeType(String name) { int i = name.indexOf('.'); while(i != -1) { diff --git a/tests/common/com/android/documentsui/testing/TestSelectionDetails.java b/tests/common/com/android/documentsui/testing/TestSelectionDetails.java index c63fdee59..e798174f8 100644 --- a/tests/common/com/android/documentsui/testing/TestSelectionDetails.java +++ b/tests/common/com/android/documentsui/testing/TestSelectionDetails.java @@ -32,7 +32,7 @@ public class TestSelectionDetails implements SelectionDetails { public boolean containFiles; public boolean canPasteInto; public boolean canExtract; - public boolean canOpenWith; + public boolean canOpen; public boolean canViewInOwner; @Override @@ -76,8 +76,8 @@ public class TestSelectionDetails implements SelectionDetails { } @Override - public boolean canOpenWith() { - return canOpenWith; + public boolean canOpen() { + return canOpen; } @Override diff --git a/tests/functional/com/android/documentsui/ActivityTestJunit4.kt b/tests/functional/com/android/documentsui/ActivityTestJunit4.kt new file mode 100644 index 000000000..8f1d4f860 --- /dev/null +++ b/tests/functional/com/android/documentsui/ActivityTestJunit4.kt @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2024 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.documentsui + +import android.app.Activity +import android.app.UiAutomation +import android.content.ContentResolver +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.os.RemoteException +import android.provider.DocumentsContract +import android.view.KeyEvent +import android.view.MotionEvent +import androidx.test.core.app.ActivityScenario +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.Configurator +import androidx.test.uiautomator.UiDevice +import com.android.documentsui.base.Features +import com.android.documentsui.base.Features.RuntimeFeatures +import com.android.documentsui.base.RootInfo +import com.android.documentsui.base.UserId +import com.android.documentsui.bots.Bots +import com.android.documentsui.files.FilesActivity +import java.io.IOException +import java.util.Objects + +/** + * Provides basic test environment for UI tests: + * - Launches activity + * - Creates and gives access to test root directories and test files + * - Cleans up the test environment + */ +abstract class ActivityTestJunit4<T : Activity?> { + @JvmField + var bots: Bots? = null + + @JvmField + var device: UiDevice? = null + + @JvmField + var context: Context? = null + var userId: UserId? = null + var automation: UiAutomation? = null + + @JvmField + var features: Features? = null + + /** + * Returns the root that will be opened within the activity. + * By default tests are started with one of the test roots. + * Override the method if you want to open different root on start. + * @return Root that will be opened. Return null if you want to open activity's default root. + */ + protected open var initialRoot: RootInfo? = null + + @JvmField + var rootDir0: RootInfo? = null + + @JvmField + var rootDir1: RootInfo? = null + protected var mResolver: ContentResolver? = null + + @JvmField + protected var mDocsHelper: DocumentsProviderHelper? = null + protected var mActivityScenario: ActivityScenario<T?>? = null + private var initialScreenOffTimeoutValue: String? = null + private var initialSleepTimeoutValue: String? = null + + protected val testingProviderAuthority: String + /** + * Returns the authority of the testing provider begin used. + * By default it's StubProvider's authority. + * @return Authority of the provider. + */ + get() = StubProvider.DEFAULT_AUTHORITY + + /** + * Resolves testing roots. + */ + @Throws(RemoteException::class) + protected fun setupTestingRoots() { + rootDir0 = mDocsHelper!!.getRoot(StubProvider.ROOT_0_ID) + rootDir1 = mDocsHelper!!.getRoot(StubProvider.ROOT_1_ID) + this.initialRoot = rootDir0 + } + + @Throws(Exception::class) + open fun setUp() { + device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + // NOTE: Must be the "target" context, else security checks in content provider will fail. + context = InstrumentationRegistry.getInstrumentation().getTargetContext() + userId = UserId.DEFAULT_USER + automation = InstrumentationRegistry.getInstrumentation().getUiAutomation() + features = RuntimeFeatures(context!!.getResources(), null) + + bots = Bots(device, automation, context, TIMEOUT) + + Configurator.getInstance().setToolType(MotionEvent.TOOL_TYPE_MOUSE) + + mResolver = context!!.getContentResolver() + mDocsHelper = DocumentsProviderHelper( + userId, this.testingProviderAuthority, context, + this.testingProviderAuthority + ) + + device!!.setOrientationNatural() + device!!.pressKeyCode(KeyEvent.KEYCODE_WAKEUP) + + disableScreenOffAndSleepTimeouts() + + setupTestingRoots() + + launchActivity() + resetStorage() + + // Since at the launch of activity, ROOT_0 and ROOT_1 have no files, drawer will + // automatically open for phone devices. Espresso register click() as (x, y) MotionEvents, + // so if a drawer is on top of a file we want to select, it will actually click the drawer. + // Thus to start a clean state, we always try to close first. + bots!!.roots!!.closeDrawer() + + // Configure the provider back to default. + mDocsHelper!!.configure(null, Bundle.EMPTY) + } + + @Throws(Exception::class) + open fun tearDown() { + device!!.unfreezeRotation() + mDocsHelper!!.cleanUp() + restoreScreenOffAndSleepTimeouts() + mActivityScenario!!.close() + } + + protected fun launchActivity() { + val intent = Intent(context, FilesActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + if (this.initialRoot != null) { + intent.setAction(Intent.ACTION_VIEW) + intent.setDataAndType( + this.initialRoot!!.uri, + DocumentsContract.Root.MIME_TYPE_ITEM + ) + } + mActivityScenario = ActivityScenario.launch(intent) + } + + @Throws(RemoteException::class) + protected fun resetStorage() { + mDocsHelper!!.clear(null, null) + device!!.waitForIdle() + } + + @Throws(RemoteException::class) + protected open fun initTestFiles() { + mDocsHelper!!.createFolder(this.initialRoot, dirName1) + mDocsHelper!!.createDocument(this.initialRoot, "text/plain", fileName1) + mDocsHelper!!.createDocument(this.initialRoot, "image/png", fileName2) + mDocsHelper!!.createDocumentWithFlags( + initialRoot!!.documentId, + "text/plain", + fileNameNoRename, + DocumentsContract.Document.FLAG_SUPPORTS_WRITE + ) + + mDocsHelper!!.createDocument(rootDir1, "text/plain", fileName3) + mDocsHelper!!.createDocument(rootDir1, "text/plain", fileName4) + } + + @Throws(IOException::class) + private fun disableScreenOffAndSleepTimeouts() { + initialScreenOffTimeoutValue = device!!.executeShellCommand( + "settings get system screen_off_timeout" + ) + initialSleepTimeoutValue = device!!.executeShellCommand( + "settings get secure sleep_timeout" + ) + device!!.executeShellCommand("settings put system screen_off_timeout -1") + device!!.executeShellCommand("settings put secure sleep_timeout -1") + } + + @Throws(IOException::class) + private fun restoreScreenOffAndSleepTimeouts() { + Objects.requireNonNull<String?>(initialScreenOffTimeoutValue) + Objects.requireNonNull<String?>(initialSleepTimeoutValue) + try { + device!!.executeShellCommand( + "settings put system screen_off_timeout $initialScreenOffTimeoutValue" + ) + device!!.executeShellCommand( + "settings put secure sleep_timeout $initialSleepTimeoutValue" + ) + } finally { + initialScreenOffTimeoutValue = null + initialSleepTimeoutValue = null + } + } + + companion object { + // Testing files. For custom ones, override initTestFiles(). + const val dirName1 = "Dir1" + const val childDir1 = "ChildDir1" + const val fileName1 = "file1.log" + const val fileName2 = "file12.png" + const val fileName3 = "anotherFile0.log" + const val fileName4 = "poodles.text" + const val fileNameNoRename = "NO_RENAMEfile.txt" + const val TIMEOUT = 5000 + } +} diff --git a/tests/functional/com/android/documentsui/FileCopyUiTest.java b/tests/functional/com/android/documentsui/FileCopyUiTest.java index 65a5d257f..42681ee82 100644 --- a/tests/functional/com/android/documentsui/FileCopyUiTest.java +++ b/tests/functional/com/android/documentsui/FileCopyUiTest.java @@ -489,7 +489,7 @@ public class FileCopyUiTest extends ActivityTest<FilesActivity> { } @HugeLongTest - public void ignored_testRecursiveCopyDocuments_InternalStorageToDownloadsProvider() + public void testRecursiveCopyDocuments_InternalStorageToDownloadsProvider() throws Exception { // Create Download folder if it doesn't exist. DocumentInfo info = mStorageDocsHelper.findFile(mPrimaryRoot.documentId, "Download"); diff --git a/tests/functional/com/android/documentsui/FileManagementUiTest.java b/tests/functional/com/android/documentsui/FileManagementUiTest.java index 4bbd8c6eb..43b74bd33 100644 --- a/tests/functional/com/android/documentsui/FileManagementUiTest.java +++ b/tests/functional/com/android/documentsui/FileManagementUiTest.java @@ -124,6 +124,21 @@ public class FileManagementUiTest extends ActivityTest<FilesActivity> { bots.directory.waitForDocument("file1.png"); } + @HugeLongTest + public void testKeyboard_PasteDocumentWhileSelectionActive() throws Exception { + bots.directory.selectDocument("file1.png", 1); + bots.keyboard.pressKey(KeyEvent.KEYCODE_C, KeyEvent.META_CTRL_ON); + + device.waitForIdle(); + bots.directory.openDocument("Dir1"); + bots.directory.selectDocument("ChildDir1", 1); + + bots.keyboard.pressKey(KeyEvent.KEYCODE_V, KeyEvent.META_CTRL_ON); + device.waitForIdle(); + + bots.directory.assertDocumentsPresent("file1.png"); + } + public void testDeleteDocument_Cancel() throws Exception { bots.directory.selectDocument("file1.png", 1); device.waitForIdle(); diff --git a/tests/functional/com/android/documentsui/FilesActivityDefaultsUiTest.java b/tests/functional/com/android/documentsui/FilesActivityDefaultsUiTest.java index a33cca37a..e6538966f 100644 --- a/tests/functional/com/android/documentsui/FilesActivityDefaultsUiTest.java +++ b/tests/functional/com/android/documentsui/FilesActivityDefaultsUiTest.java @@ -18,24 +18,46 @@ package com.android.documentsui; import static com.android.documentsui.StubProvider.ROOT_0_ID; import static com.android.documentsui.StubProvider.ROOT_1_ID; +import static com.android.documentsui.flags.Flags.FLAG_HIDE_ROOTS_ON_DESKTOP_RO; -import android.os.RemoteException; +import android.content.pm.PackageManager; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import com.android.documentsui.base.RootInfo; import com.android.documentsui.files.FilesActivity; import com.android.documentsui.filters.HugeLongTest; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + @LargeTest -public class FilesActivityDefaultsUiTest extends ActivityTest<FilesActivity> { +@RunWith(AndroidJUnit4.class) +public class FilesActivityDefaultsUiTest extends ActivityTestJunit4<FilesActivity> { + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Before + public void setUp() throws Exception { + super.setUp(); + } - public FilesActivityDefaultsUiTest() { - super(FilesActivity.class); + @After + public void tearDown() throws Exception { + super.tearDown(); } @Override - protected void initTestFiles() throws RemoteException { + protected void initTestFiles() { // Overriding to init with no items in test roots } @@ -44,6 +66,7 @@ public class FilesActivityDefaultsUiTest extends ActivityTest<FilesActivity> { return null; // test the default, unaffected state of the app. } + @Test @HugeLongTest public void testNavigate_FromEmptyDirectory() throws Exception { device.waitForIdle(); @@ -57,8 +80,10 @@ public class FilesActivityDefaultsUiTest extends ActivityTest<FilesActivity> { device.pressBack(); } + @Test @HugeLongTest - public void testDefaultRoots() throws Exception { + @RequiresFlagsDisabled(FLAG_HIDE_ROOTS_ON_DESKTOP_RO) + public void testDefaultRoots_hideRootsOnDesktopFlagDisabled() throws Exception { device.waitForIdle(); // Should also have Drive, but that requires pre-configuration of devices @@ -71,4 +96,29 @@ public class FilesActivityDefaultsUiTest extends ActivityTest<FilesActivity> { ROOT_0_ID, ROOT_1_ID); } + + @Test + @HugeLongTest + @RequiresFlagsEnabled(FLAG_HIDE_ROOTS_ON_DESKTOP_RO) + public void testDefaultRoots_hideRootsOnDesktopFlagEnabled() throws Exception { + device.waitForIdle(); + + String[] expectedRoots; + if (context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_PC)) { + expectedRoots = new String[]{"Downloads", + ROOT_0_ID, + ROOT_1_ID}; + } else { + expectedRoots = new String[]{ + "Images", + "Videos", + "Audio", + "Downloads", + ROOT_0_ID, + ROOT_1_ID}; + } + // Should also have Drive, but that requires pre-configuration of devices + // We omit for now. + bots.roots.assertRootsPresent(expectedRoots); + } } diff --git a/tests/functional/com/android/documentsui/FilesActivityUiTest.java b/tests/functional/com/android/documentsui/FilesActivityUiTest.java index 697dee6df..f1f505235 100644 --- a/tests/functional/com/android/documentsui/FilesActivityUiTest.java +++ b/tests/functional/com/android/documentsui/FilesActivityUiTest.java @@ -16,29 +16,46 @@ package com.android.documentsui; +import static com.android.documentsui.flags.Flags.FLAG_HIDE_ROOTS_ON_DESKTOP_RO; + import android.app.Instrumentation; import android.net.Uri; import android.os.RemoteException; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import com.android.documentsui.files.FilesActivity; import com.android.documentsui.filters.HugeLongTest; import com.android.documentsui.inspector.InspectorActivity; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + @LargeTest -public class FilesActivityUiTest extends ActivityTest<FilesActivity> { +@RunWith(AndroidJUnit4.class) +public class FilesActivityUiTest extends ActivityTestJunit4<FilesActivity> { - public FilesActivityUiTest() { - super(FilesActivity.class); - } + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); - @Override + @Before public void setUp() throws Exception { super.setUp(); initTestFiles(); } + @After + public void tearDown() throws Exception { + super.tearDown(); + } + @Override public void initTestFiles() throws RemoteException { Uri uri = mDocsHelper.createFolder(rootDir0, dirName1); @@ -55,6 +72,7 @@ public class FilesActivityUiTest extends ActivityTest<FilesActivity> { // Recents is a strange meta root that gathers entries from other providers. // It is special cased in a variety of ways, which is why we just want // to be able to click on it. + @Test public void testClickRecent() throws Exception { bots.roots.openRoot("Recent"); @@ -67,15 +85,19 @@ public class FilesActivityUiTest extends ActivityTest<FilesActivity> { } } + @Test + @RequiresFlagsDisabled(FLAG_HIDE_ROOTS_ON_DESKTOP_RO) public void testRootClick_SetsWindowTitle() throws Exception { bots.roots.openRoot("Images"); bots.main.assertWindowTitle("Images"); } + @Test public void testFilesListed() throws Exception { bots.directory.assertDocumentsPresent("file0.log", "file1.png", "file2.csv"); } + @Test public void testFilesList_LiveUpdate() throws Exception { mDocsHelper.createDocument(rootDir0, "yummers/sandwich", "Ham & Cheese.sandwich"); @@ -84,6 +106,7 @@ public class FilesActivityUiTest extends ActivityTest<FilesActivity> { "file0.log", "file1.png", "file2.csv", "Ham & Cheese.sandwich"); } + @Test public void testNavigate_byBreadcrumb() throws Exception { bots.directory.openDocument(dirName1); bots.directory.waitForDocument(childDir1); // wait for known content @@ -96,6 +119,7 @@ public class FilesActivityUiTest extends ActivityTest<FilesActivity> { bots.directory.waitForDocument(dirName1); } + @Test public void testNavigate_inFixedLayout_whileHasSelection() throws Exception { if (bots.main.inFixedLayout()) { bots.roots.openRoot(rootDir0.title); @@ -107,6 +131,7 @@ public class FilesActivityUiTest extends ActivityTest<FilesActivity> { } } + @Test public void testNavigationToInspector() throws Exception { if(!features.isInspectorEnabled()) { return; @@ -118,7 +143,9 @@ public class FilesActivityUiTest extends ActivityTest<FilesActivity> { monitor.waitForActivityWithTimeout(TIMEOUT); } + @Test @HugeLongTest + @RequiresFlagsDisabled(FLAG_HIDE_ROOTS_ON_DESKTOP_RO) public void testRootChange_UpdatesSortHeader() throws Exception { // switch to separate display modes for two separate roots. Each diff --git a/tests/functional/com/android/documentsui/SortDocumentUiTest.java b/tests/functional/com/android/documentsui/SortDocumentUiTest.java index 05878bbe7..b277884e3 100644 --- a/tests/functional/com/android/documentsui/SortDocumentUiTest.java +++ b/tests/functional/com/android/documentsui/SortDocumentUiTest.java @@ -16,16 +16,30 @@ package com.android.documentsui; +import static com.android.documentsui.flags.Flags.FLAG_USE_MATERIAL3; + import android.net.Uri; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.view.KeyEvent; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.LargeTest; import com.android.documentsui.files.FilesActivity; import com.android.documentsui.sorting.SortDimension; import com.android.documentsui.sorting.SortModel; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + @LargeTest -public class SortDocumentUiTest extends ActivityTest<FilesActivity> { +@RunWith(AndroidJUnit4.class) +public class SortDocumentUiTest extends ActivityTestJunit4<FilesActivity> { private static final String DIR_1 = "folder_1"; private static final String DIR_2 = "dir_2"; @@ -38,63 +52,76 @@ public class SortDocumentUiTest extends ActivityTest<FilesActivity> { private static final String MIME_2 = "text/html"; // HTML document private static final String MIME_3 = "image/jpeg"; // JPG image - private static final String[] FILES = { FILE_1, FILE_3, FILE_2 }; - private static final String[] MIMES = { MIME_1, MIME_3, MIME_2 }; - private static final String[] DIRS = { DIR_1, DIR_2 }; - - private static final String[] DIRS_IN_NAME_ASC = { DIR_2, DIR_1 }; + private static final String[] FILES = {FILE_1, FILE_3, FILE_2}; + private static final String[] FILES_IN_MODIFIED_DESC = reverse(FILES); + private static final String[] MIMES = {MIME_1, MIME_3, MIME_2}; + private static final String[] DIRS = {DIR_1, DIR_2}; + private static final String[] DIRS_IN_MODIFIED_DESC = reverse(DIRS); + private static final String[] DIRS_IN_NAME_ASC = {DIR_2, DIR_1}; private static final String[] DIRS_IN_NAME_DESC = reverse(DIRS_IN_NAME_ASC); - private static final String[] FILES_IN_NAME_ASC = { FILE_2, FILE_1, FILE_3 }; + private static final String[] FILES_IN_NAME_ASC = {FILE_2, FILE_1, FILE_3}; private static final String[] FILES_IN_NAME_DESC = reverse(FILES_IN_NAME_ASC); - - private static final String[] FILES_IN_SIZE_ASC = { FILE_2, FILE_1, FILE_3 }; + private static final String[] FILES_IN_SIZE_ASC = {FILE_2, FILE_1, FILE_3}; private static final String[] FILES_IN_SIZE_DESC = reverse(FILES_IN_SIZE_ASC); + private static final String[] FILES_IN_TYPE_ASC = {FILE_2, FILE_3, FILE_1}; + private static final String[] FILES_IN_TYPE_DESC = reverse(FILES_IN_TYPE_ASC); - private static final String[] DIRS_IN_MODIFIED_DESC = reverse(DIRS); - private static final String[] FILES_IN_MODIFIED_DESC = reverse(FILES); + private static String[] reverse(String[] array) { + String[] ret = new String[array.length]; - private static final String[] FILES_IN_TYPE_ASC = { FILE_2, FILE_3, FILE_1 }; - private static final String[] FILES_IN_TYPE_DESC = reverse(FILES_IN_TYPE_ASC); + for (int i = 0; i < array.length; ++i) { + ret[ret.length - i - 1] = array[i]; + } - public SortDocumentUiTest() { - super(FilesActivity.class); + return ret; } - @Override + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Before public void setUp() throws Exception { super.setUp(); bots.roots.closeDrawer(); } + @After + public void tearDown() throws Exception { + super.tearDown(); + } + private void initFiles() throws Exception { initFiles(0); } /** - * Initiate test files. It allows waiting between creations of files, so that we can assure - * the modified date of each document is different. + * Initiate test files. It allows waiting between creations of files, so that we can assure the + * modified date of each document is different. + * * @param sleep time to sleep in ms */ private void initFiles(long sleep) throws Exception { for (int i = 0; i < FILES.length; ++i) { - Uri uri = mDocsHelper.createDocument(rootDir0, MIMES[i], FILES[i]); + Uri uri = mDocsHelper.createDocument(getInitialRoot(), MIMES[i], FILES[i]); mDocsHelper.writeDocument(uri, FILES[i].getBytes()); Thread.sleep(sleep); } for (String dir : DIRS) { - mDocsHelper.createFolder(rootDir0, dir); + mDocsHelper.createFolder(getInitialRoot(), dir); Thread.sleep(sleep); } } + @Test public void testDefaultSortByNameAscending() throws Exception { initFiles(); bots.directory.assertOrder(DIRS_IN_NAME_ASC, FILES_IN_NAME_ASC); } + @Test public void testSortByName_Descending_listMode() throws Exception { initFiles(); @@ -105,46 +132,47 @@ public class SortDocumentUiTest extends ActivityTest<FilesActivity> { bots.directory.assertOrder(DIRS_IN_NAME_DESC, FILES_IN_NAME_DESC); } + @Test public void testSortBySize_Ascending_listMode() throws Exception { initFiles(); bots.main.switchToListMode(); - bots.sort.sortBy( - SortModel.SORT_DIMENSION_ID_SIZE, SortDimension.SORT_DIRECTION_ASCENDING); + bots.sort.sortBy(SortModel.SORT_DIMENSION_ID_SIZE, SortDimension.SORT_DIRECTION_ASCENDING); bots.directory.assertOrder(DIRS_IN_NAME_ASC, FILES_IN_SIZE_ASC); } + @Test public void testSortBySize_Descending_listMode() throws Exception { initFiles(); bots.main.switchToListMode(); - bots.sort.sortBy( - SortModel.SORT_DIMENSION_ID_SIZE, SortDimension.SORT_DIRECTION_DESCENDING); + bots.sort.sortBy(SortModel.SORT_DIMENSION_ID_SIZE, SortDimension.SORT_DIRECTION_DESCENDING); bots.directory.assertOrder(DIRS_IN_NAME_ASC, FILES_IN_SIZE_DESC); } + @Test public void testSortByModified_Ascending_listMode() throws Exception { initFiles(1000); bots.main.switchToListMode(); - bots.sort.sortBy( - SortModel.SORT_DIMENSION_ID_DATE, SortDimension.SORT_DIRECTION_ASCENDING); + bots.sort.sortBy(SortModel.SORT_DIMENSION_ID_DATE, SortDimension.SORT_DIRECTION_ASCENDING); bots.directory.assertOrder(DIRS, FILES); } + @Test public void testSortByModified_Descending_listMode() throws Exception { initFiles(1000); bots.main.switchToListMode(); - bots.sort.sortBy( - SortModel.SORT_DIMENSION_ID_DATE, SortDimension.SORT_DIRECTION_DESCENDING); + bots.sort.sortBy(SortModel.SORT_DIMENSION_ID_DATE, SortDimension.SORT_DIRECTION_DESCENDING); bots.directory.assertOrder(DIRS_IN_MODIFIED_DESC, FILES_IN_MODIFIED_DESC); } + @Test public void testSortByType_Ascending_listMode() throws Exception { initFiles(); @@ -155,6 +183,7 @@ public class SortDocumentUiTest extends ActivityTest<FilesActivity> { bots.directory.assertOrder(DIRS_IN_NAME_ASC, FILES_IN_TYPE_ASC); } + @Test public void testSortByType_Descending_listMode() throws Exception { initFiles(); @@ -165,6 +194,7 @@ public class SortDocumentUiTest extends ActivityTest<FilesActivity> { bots.directory.assertOrder(DIRS_IN_NAME_ASC, FILES_IN_TYPE_DESC); } + @Test public void testSortByName_Descending_gridMode() throws Exception { initFiles(); @@ -175,46 +205,47 @@ public class SortDocumentUiTest extends ActivityTest<FilesActivity> { bots.directory.assertOrder(DIRS_IN_NAME_DESC, FILES_IN_NAME_DESC); } + @Test public void testSortBySize_Ascending_gridMode() throws Exception { initFiles(); bots.main.switchToGridMode(); - bots.sort.sortBy( - SortModel.SORT_DIMENSION_ID_SIZE, SortDimension.SORT_DIRECTION_ASCENDING); + bots.sort.sortBy(SortModel.SORT_DIMENSION_ID_SIZE, SortDimension.SORT_DIRECTION_ASCENDING); bots.directory.assertOrder(DIRS_IN_NAME_ASC, FILES_IN_SIZE_ASC); } + @Test public void testSortBySize_Descending_gridMode() throws Exception { initFiles(); bots.main.switchToGridMode(); - bots.sort.sortBy( - SortModel.SORT_DIMENSION_ID_SIZE, SortDimension.SORT_DIRECTION_DESCENDING); + bots.sort.sortBy(SortModel.SORT_DIMENSION_ID_SIZE, SortDimension.SORT_DIRECTION_DESCENDING); bots.directory.assertOrder(DIRS_IN_NAME_ASC, FILES_IN_SIZE_DESC); } + @Test public void testSortByModified_Ascending_gridMode() throws Exception { initFiles(1000); bots.main.switchToGridMode(); - bots.sort.sortBy( - SortModel.SORT_DIMENSION_ID_DATE, SortDimension.SORT_DIRECTION_ASCENDING); + bots.sort.sortBy(SortModel.SORT_DIMENSION_ID_DATE, SortDimension.SORT_DIRECTION_ASCENDING); bots.directory.assertOrder(DIRS, FILES); } + @Test public void testSortByModified_Descending_gridMode() throws Exception { initFiles(1000); bots.main.switchToGridMode(); - bots.sort.sortBy( - SortModel.SORT_DIMENSION_ID_DATE, SortDimension.SORT_DIRECTION_DESCENDING); + bots.sort.sortBy(SortModel.SORT_DIMENSION_ID_DATE, SortDimension.SORT_DIRECTION_DESCENDING); bots.directory.assertOrder(DIRS_IN_MODIFIED_DESC, FILES_IN_MODIFIED_DESC); } + @Test public void testSortByType_Ascending_gridMode() throws Exception { initFiles(); @@ -225,6 +256,7 @@ public class SortDocumentUiTest extends ActivityTest<FilesActivity> { bots.directory.assertOrder(DIRS_IN_NAME_ASC, FILES_IN_TYPE_ASC); } + @Test public void testSortByType_Descending_gridMode() throws Exception { initFiles(); @@ -235,13 +267,31 @@ public class SortDocumentUiTest extends ActivityTest<FilesActivity> { bots.directory.assertOrder(DIRS_IN_NAME_ASC, FILES_IN_TYPE_DESC); } - private static String[] reverse(String[] array) { - String[] ret = new String[array.length]; + @Test + @RequiresFlagsEnabled(FLAG_USE_MATERIAL3) + public void testSortByArrowIcon() throws Exception { + initFiles(); - for (int i = 0; i < array.length; ++i) { - ret[ret.length - i - 1] = array[i]; + bots.main.switchToListMode(); + + // Set up the sort in descending direction to allow deterministic behaviour of the sort + // icon. + bots.sort.sortBy( + SortModel.SORT_DIMENSION_ID_TITLE, SortDimension.SORT_DIRECTION_DESCENDING); + bots.directory.assertOrder(DIRS_IN_NAME_DESC, FILES_IN_NAME_DESC); + + // Tab in reverse order until the sort icon is the focus (this avoids tabbing through the + // roots list which is quite long in tests). + while (!bots.sort.isSortIconFocused()) { + bots.keyboard.pressKey(KeyEvent.KEYCODE_TAB, KeyEvent.META_SHIFT_LEFT_ON); } - return ret; + // Press enter on the sort icon and ensure the sort direction is changed. + bots.keyboard.pressKey(KeyEvent.KEYCODE_ENTER); + bots.directory.assertOrder(DIRS_IN_NAME_ASC, FILES_IN_NAME_ASC); + + // Space should also work in the same way as the ENTER key. + bots.keyboard.pressKey(KeyEvent.KEYCODE_SPACE); + bots.directory.assertOrder(DIRS_IN_NAME_DESC, FILES_IN_NAME_DESC); } } diff --git a/tests/functional/com/android/documentsui/TrampolineActivityTest.kt b/tests/functional/com/android/documentsui/TrampolineActivityTest.kt new file mode 100644 index 000000000..6bf0975ad --- /dev/null +++ b/tests/functional/com/android/documentsui/TrampolineActivityTest.kt @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2024 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.documentsui + +import android.app.Instrumentation +import android.content.Intent +import android.content.Intent.ACTION_GET_CONTENT +import android.content.IntentFilter +import android.os.Build.VERSION_CODES +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SdkSuppress +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.By +import androidx.test.uiautomator.UiDevice +import androidx.test.uiautomator.Until +import com.android.documentsui.flags.Flags.FLAG_REDIRECT_GET_CONTENT_RO +import com.android.documentsui.picker.TrampolineActivity +import java.util.Optional +import java.util.regex.Pattern +import org.junit.After +import org.junit.Assert.assertNotNull +import org.junit.Before +import org.junit.BeforeClass +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Suite +import org.junit.runners.Suite.SuiteClasses + +@SmallTest +@RunWith(Suite::class) +@SuiteClasses( + TrampolineActivityTest.ShouldLaunchCorrectPackageTest::class, + TrampolineActivityTest.RedirectTest::class +) +class TrampolineActivityTest() { + companion object { + const val UI_TIMEOUT = 5000L + val PHOTOPICKER_PACKAGE_REGEX: Pattern = Pattern.compile(".*(photopicker|media\\.module).*") + val DOCUMENTSUI_PACKAGE_REGEX: Pattern = Pattern.compile(".*documentsui.*") + + private lateinit var device: UiDevice + + private lateinit var monitor: Instrumentation.ActivityMonitor + + @BeforeClass + @JvmStatic + fun setUp() { + device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation()) + + // Monitor to wait for the activity that starts with the `ACTION_GET_CONTENT` intent. + val intentFilter = IntentFilter().apply { addAction(ACTION_GET_CONTENT) } + monitor = + Instrumentation.ActivityMonitor( + intentFilter, + null, // Expected result from startActivityForResult. + true, // Whether to block until activity started or not. + ) + InstrumentationRegistry.getInstrumentation().addMonitor(monitor) + } + } + + @RunWith(Parameterized::class) + @RequiresFlagsEnabled(FLAG_REDIRECT_GET_CONTENT_RO) + class ShouldLaunchCorrectPackageTest { + enum class AppType { + PHOTOPICKER, + DOCUMENTSUI, + } + + data class GetContentIntentData( + val mimeType: String, + val expectedApp: AppType, + val extraMimeTypes: Optional<Array<String>> = Optional.empty(), + ) { + override fun toString(): String { + if (extraMimeTypes.isPresent) { + return "${mimeType}_${extraMimeTypes.get().joinToString("_")}" + } + return mimeType + } + } + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun parameters() = + listOf( + GetContentIntentData( + mimeType = "*/*", + expectedApp = AppType.DOCUMENTSUI, + ), + GetContentIntentData( + mimeType = "image/*", + expectedApp = AppType.PHOTOPICKER, + ), + GetContentIntentData( + mimeType = "video/*", + expectedApp = AppType.PHOTOPICKER, + ), + GetContentIntentData( + mimeType = "image/*", + extraMimeTypes = Optional.of(arrayOf("video/*")), + expectedApp = AppType.PHOTOPICKER, + ), + GetContentIntentData( + mimeType = "video/*", + extraMimeTypes = Optional.of(arrayOf("image/*")), + expectedApp = AppType.PHOTOPICKER, + ), + GetContentIntentData( + mimeType = "video/*", + extraMimeTypes = Optional.of(arrayOf("text/*")), + expectedApp = AppType.DOCUMENTSUI, + ), + GetContentIntentData( + mimeType = "video/*", + extraMimeTypes = Optional.of(arrayOf("image/*", "text/*")), + expectedApp = AppType.DOCUMENTSUI, + ), + GetContentIntentData( + mimeType = "*/*", + extraMimeTypes = Optional.of(arrayOf("image/*", "video/*")), + expectedApp = AppType.PHOTOPICKER, + ), + GetContentIntentData( + mimeType = "image/*", + extraMimeTypes = Optional.of(arrayOf()), + expectedApp = AppType.DOCUMENTSUI, + ) + ) + } + + @Parameterized.Parameter(0) + lateinit var testData: GetContentIntentData + + @get:Rule + val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + @Before + fun setUp() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val intent = Intent(ACTION_GET_CONTENT) + intent.setClass(context, TrampolineActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.setType(testData.mimeType) + if (testData.extraMimeTypes.isPresent) { + testData.extraMimeTypes.get() + .forEach { intent.putExtra(Intent.EXTRA_MIME_TYPES, it) } + } + + context.startActivity(intent) + } + + @After + fun tearDown() { + monitor.waitForActivityWithTimeout(UI_TIMEOUT)?.finish() + } + + @Test + fun testCorrectAppIsLaunched() { + val bySelector = when (testData.expectedApp) { + AppType.PHOTOPICKER -> By.pkg(PHOTOPICKER_PACKAGE_REGEX) + else -> By.pkg(DOCUMENTSUI_PACKAGE_REGEX) + } + + val builder = StringBuilder() + builder.append("Intent with mimetype ${testData.mimeType}") + if (testData.extraMimeTypes.isPresent) { + builder.append( + " and EXTRA_MIME_TYPES of ${ + testData.extraMimeTypes.get().joinToString(", ") + }" + ) + } + builder.append( + " didn't cause ${testData.expectedApp.name} to appear after ${UI_TIMEOUT}ms" + ) + + assertNotNull( + builder.toString(), + device.wait(Until.findObject(bySelector), UI_TIMEOUT) + ) + } + } + + @RunWith(AndroidJUnit4::class) + @RequiresFlagsEnabled(FLAG_REDIRECT_GET_CONTENT_RO) + class RedirectTest { + @get:Rule + val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + @Test + fun testReferredGetContentFromPhotopickerShouldNotRedirectBack() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val intent = Intent(ACTION_GET_CONTENT) + intent.setClass(context, TrampolineActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.setType("*/*") + intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("image/*")) + + context.startActivity(intent) + val moreButton = device.wait(Until.findObject(By.descContains("More")), UI_TIMEOUT) + moreButton?.click() + + val browseButton = device.wait(Until.findObject(By.textContains("Browse")), UI_TIMEOUT) + browseButton?.click() + + assertNotNull( + "DocumentsUI has not launched", + device.wait(Until.findObject(By.pkg(DOCUMENTSUI_PACKAGE_REGEX)), UI_TIMEOUT) + ) + } + + @Test + @SdkSuppress(minSdkVersion = VERSION_CODES.S, maxSdkVersion = VERSION_CODES.S_V2) + fun testAndroidSWithTakeoverGetContentDisabledShouldNotReferToDocumentsUI() { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val intent = Intent(ACTION_GET_CONTENT) + intent.setClass(context, TrampolineActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + intent.setType("image/*") + + try { + // Disable Photopicker from taking over `ACTION_GET_CONTENT`. In this situation, it + // should ALWAYS defer to DocumentsUI regardless if the mimetype satisfies the + // conditions. + device.executeShellCommand( + "device_config put mediaprovider take_over_get_content false" + ) + context.startActivity(intent) + assertNotNull( + device.wait(Until.findObject(By.pkg(DOCUMENTSUI_PACKAGE_REGEX)), UI_TIMEOUT) + ) + } finally { + device.executeShellCommand( + "device_config delete mediaprovider take_over_get_content" + ) + } + } + } +} diff --git a/tests/functional/com/android/documentsui/archives/ArchiveHandleTest.java b/tests/functional/com/android/documentsui/archives/ArchiveHandleTest.java index 29ed8e431..d8a1f4225 100644 --- a/tests/functional/com/android/documentsui/archives/ArchiveHandleTest.java +++ b/tests/functional/com/android/documentsui/archives/ArchiveHandleTest.java @@ -142,6 +142,45 @@ public class ArchiveHandleTest { new ArchiveEntryRecord("hello/inside_folder/hello_insside.txt", 14, false), new ArchiveEntryRecord("hello/hello2.txt", 48, false)); + private static String getNormalizedPath(String in, boolean isDir) { + return Archive.getEntryPath(new ArchiveEntryRecord(in, -1, isDir)); + } + + @Test + public void normalizePath() { + assertThat(getNormalizedPath("", true)).isEqualTo("/"); + assertThat(getNormalizedPath("", false)).isEqualTo("/?"); + assertThat(getNormalizedPath("/", true)).isEqualTo("/"); + assertThat(getNormalizedPath("/", false)).isEqualTo("/?"); + assertThat(getNormalizedPath("///", true)).isEqualTo("/"); + assertThat(getNormalizedPath("///", false)).isEqualTo("/?"); + assertThat(getNormalizedPath(".", true)).isEqualTo("/"); + assertThat(getNormalizedPath(".", false)).isEqualTo("/?"); + assertThat(getNormalizedPath("./", true)).isEqualTo("/"); + assertThat(getNormalizedPath("./", false)).isEqualTo("/?"); + assertThat(getNormalizedPath("./foo", true)).isEqualTo("/foo/"); + assertThat(getNormalizedPath("./foo", false)).isEqualTo("/foo"); + assertThat(getNormalizedPath("./foo/", true)).isEqualTo("/foo/"); + assertThat(getNormalizedPath("./foo/", false)).isEqualTo("/foo/?"); + assertThat(getNormalizedPath("..", true)).isEqualTo("/"); + assertThat(getNormalizedPath("..", false)).isEqualTo("/?"); + assertThat(getNormalizedPath("../", true)).isEqualTo("/"); + assertThat(getNormalizedPath("../", false)).isEqualTo("/?"); + assertThat(getNormalizedPath("foo", true)).isEqualTo("/foo/"); + assertThat(getNormalizedPath("foo", false)).isEqualTo("/foo"); + assertThat(getNormalizedPath("foo/", true)).isEqualTo("/foo/"); + assertThat(getNormalizedPath("foo/", false)).isEqualTo("/foo/?"); + assertThat(getNormalizedPath("foo/.", true)).isEqualTo("/foo/"); + assertThat(getNormalizedPath("foo/.", false)).isEqualTo("/foo/?"); + assertThat(getNormalizedPath("foo/..", true)).isEqualTo("/"); + assertThat(getNormalizedPath("foo/..", false)).isEqualTo("/?"); + assertThat(getNormalizedPath("/foo", true)).isEqualTo("/foo/"); + assertThat(getNormalizedPath("/foo", false)).isEqualTo("/foo"); + assertThat(getNormalizedPath("//./../a//b///../c.ext", true)).isEqualTo("/a/c.ext/"); + assertThat(getNormalizedPath("//./../a//b///../c.ext", false)).isEqualTo("/a/c.ext"); + assertThat(getNormalizedPath("//./../a//b///../c.ext/", true)).isEqualTo("/a/c.ext/"); + assertThat(getNormalizedPath("//./../a//b///../c.ext/", false)).isEqualTo("/a/c.ext/?"); + } @Test public void buildArchiveHandle_withoutFileDescriptor_shouldBeIllegal() throws Exception { @@ -241,7 +280,7 @@ public class ArchiveHandleTest { public void buildArchiveHandle_tarBrFile_shouldNotNull() throws Exception { ArchiveHandle archiveHandle = prepareArchiveHandle("archives/brotli/hello.tar.br", ".tar.br", - "application/x-brotli-compressed-tar"); + "application/x-brotli-compressed-tar"); assertThat(archiveHandle).isNotNull(); } @@ -346,7 +385,7 @@ public class ArchiveHandleTest { @Test public void close_zipFile_shouldNotOpen() throws Exception { - ParcelFileDescriptor parcelFileDescriptor = mArchiveFileTestRule + ParcelFileDescriptor parcelFileDescriptor = mArchiveFileTestRule .openAssetFile("archives/zip/hello.zip", ".zip"); ArchiveHandle archiveHandle = ArchiveHandle.create(parcelFileDescriptor, @@ -557,7 +596,7 @@ public class ArchiveHandleTest { public void getEntries_tarFile_shouldTheSameWithList() throws Exception { ArchiveHandle archiveHandle = prepareArchiveHandle("archives/tar/hello.tar", ".tar", - "application/x-gtar"); + "application/x-gtar"); assertThat(transformToIterable(archiveHandle.getEntries())) .containsAtLeastElementsIn(sExpectEntries); diff --git a/tests/functional/com/android/documentsui/services/AbstractCopyJobTest.java b/tests/functional/com/android/documentsui/services/AbstractCopyJobTest.java index 525397bc0..b20942473 100644 --- a/tests/functional/com/android/documentsui/services/AbstractCopyJobTest.java +++ b/tests/functional/com/android/documentsui/services/AbstractCopyJobTest.java @@ -44,6 +44,23 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob mOpType = opType; } + private String getVerb() { + switch(mOpType) { + case FileOperationService.OPERATION_COPY: + case FileOperationService.OPERATION_EXTRACT: + return "Copying"; + case FileOperationService.OPERATION_COMPRESS: + return "Zipping"; + case FileOperationService.OPERATION_MOVE: + return "Moving"; + case FileOperationService.OPERATION_DELETE: + // DeleteJob does not inherit from CopyJob + case FileOperationService.OPERATION_UNKNOWN: + default: + return ""; + } + } + public void runCopyFilesTest() throws Exception { Uri testFile1 = mDocs.createDocument(mSrcRoot, "text/plain", "test1.txt"); mDocs.writeDocument(testFile1, HAM_BYTES); @@ -51,7 +68,11 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob Uri testFile2 = mDocs.createDocument(mSrcRoot, "text/plain", "test2.txt"); mDocs.writeDocument(testFile2, FRUITY_BYTES); - createJob(newArrayList(testFile1, testFile2)).run(); + CopyJob job = createJob(newArrayList(testFile1, testFile2)); + JobProgress progress = job.getJobProgress(); + assertEquals(Job.STATE_CREATED, progress.state); + + job.run(); mJobListener.waitForFinished(); mDocs.assertChildCount(mDestRoot, 2); @@ -59,6 +80,13 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob mDocs.assertHasFile(mDestRoot, "test2.txt"); mDocs.assertFileContents(mDestRoot.documentId, "test1.txt", HAM_BYTES); mDocs.assertFileContents(mDestRoot.documentId, "test2.txt", FRUITY_BYTES); + + progress = job.getJobProgress(); + assertEquals(Job.STATE_COMPLETED, progress.state); + assertFalse(progress.hasFailures); + assertEquals(getVerb() + " 2 files to " + mDestRoot.title, progress.msg); + assertEquals(HAM_BYTES.length + FRUITY_BYTES.length, progress.currentBytes); + assertEquals(HAM_BYTES.length + FRUITY_BYTES.length, progress.requiredBytes); } public void runCopyVirtualTypedFileTest() throws Exception { @@ -66,13 +94,20 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob mSrcRoot, "/virtual.sth", "virtual/mime-type", FRUITY_BYTES, "application/pdf", "text/html"); - createJob(newArrayList(testFile)).run(); - + CopyJob job = createJob(newArrayList(testFile)); + job.run(); waitForJobFinished(); mDocs.assertChildCount(mDestRoot, 1); mDocs.assertHasFile(mDestRoot, "virtual.sth.pdf"); // copy should convert file to PDF. mDocs.assertFileContents(mDestRoot.documentId, "virtual.sth.pdf", FRUITY_BYTES); + + JobProgress progress = job.getJobProgress(); + assertEquals(Job.STATE_COMPLETED, progress.state); + assertFalse(progress.hasFailures); + assertEquals("Copying virtual.sth to " + mDestRoot.title, progress.msg); + assertEquals(FRUITY_BYTES.length, progress.currentBytes); + assertEquals(FRUITY_BYTES.length, progress.requiredBytes); } public void runCopyVirtualNonTypedFileTest() throws Exception { @@ -80,13 +115,21 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob mSrcRoot, "/virtual.sth", "virtual/mime-type", FRUITY_BYTES); - createJob(newArrayList(testFile)).run(); - + CopyJob job = createJob(newArrayList(testFile)); + job.run(); waitForJobFinished(); + mJobListener.assertFailed(); mJobListener.assertFilesFailed(newArrayList("virtual.sth")); mDocs.assertChildCount(mDestRoot, 0); + + JobProgress progress = job.getJobProgress(); + assertEquals(Job.STATE_COMPLETED, progress.state); + assertTrue(progress.hasFailures); + assertEquals(getVerb() + " virtual.sth to " + mDestRoot.title, progress.msg); + assertEquals(0, progress.currentBytes); + assertEquals(FRUITY_BYTES.length, progress.requiredBytes); } public void runCopyEmptyDirTest() throws Exception { @@ -105,6 +148,13 @@ public abstract class AbstractCopyJobTest<T extends CopyJob> extends AbstractJob mDocs.assertChildCount(mDestRoot, 1); mDocs.assertHasDirectory(mDestRoot, "emptyDir"); + + JobProgress progress = job.getJobProgress(); + assertEquals(Job.STATE_COMPLETED, progress.state); + assertFalse(progress.hasFailures); + assertEquals(getVerb() + " emptyDir to " + mDestRoot.title, progress.msg); + assertEquals(-1, progress.currentBytes); + assertEquals(-1, progress.requiredBytes); } public void runCopyDirRecursivelyTest() throws Exception { diff --git a/tests/functional/com/android/documentsui/services/CopyJobTest.java b/tests/functional/com/android/documentsui/services/CopyJobTest.java index fd552d1c0..9074ef4c3 100644 --- a/tests/functional/com/android/documentsui/services/CopyJobTest.java +++ b/tests/functional/com/android/documentsui/services/CopyJobTest.java @@ -52,11 +52,19 @@ public class CopyJobTest extends AbstractCopyJobTest<CopyJob> { Document.FLAG_VIRTUAL_DOCUMENT | Document.FLAG_SUPPORTS_COPY | Document.FLAG_SUPPORTS_MOVE, "application/pdf"); - createJob(newArrayList(testFile)).run(); + CopyJob job = createJob(newArrayList(testFile)); + job.run(); waitForJobFinished(); mDocs.assertChildCount(mDestRoot, 1); mDocs.assertHasFile(mDestRoot, "tokyo.sth.pdf"); // Copy should convert file to PDF. + + JobProgress progress = job.getJobProgress(); + assertEquals(Job.STATE_COMPLETED, progress.state); + assertFalse(progress.hasFailures); + assertEquals("Copying tokyo.sth to " + mDestRoot.title, progress.msg); + assertEquals(-1, progress.currentBytes); + assertEquals(-1, progress.requiredBytes); } public void testCopyEmptyDir() throws Exception { diff --git a/tests/functional/com/android/documentsui/services/DeleteJobTest.java b/tests/functional/com/android/documentsui/services/DeleteJobTest.java index 55e804484..86d9930a2 100644 --- a/tests/functional/com/android/documentsui/services/DeleteJobTest.java +++ b/tests/functional/com/android/documentsui/services/DeleteJobTest.java @@ -37,11 +37,17 @@ public class DeleteJobTest extends AbstractJobTest<DeleteJob> { Uri testFile2 = mDocs.createDocument(mSrcRoot, "text/plain", "test2.txt"); mDocs.writeDocument(testFile2, FRUITY_BYTES); - createJob(newArrayList(testFile1, testFile2), - DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId)).run(); + DeleteJob job = createJob(newArrayList(testFile1, testFile2), + DocumentsContract.buildDocumentUri(AUTHORITY, mSrcRoot.documentId)); + job.run(); mJobListener.waitForFinished(); mDocs.assertChildCount(mSrcRoot, 0); + + var progress = job.getJobProgress(); + assertEquals(Job.STATE_COMPLETED, progress.state); + assertFalse(progress.hasFailures); + assertEquals("Deleting 2 files", progress.msg); } public void testDeleteFiles_NoSrcParent() throws Exception { @@ -51,10 +57,15 @@ public class DeleteJobTest extends AbstractJobTest<DeleteJob> { Uri testFile2 = mDocs.createDocument(mSrcRoot, "text/plain", "test2.txt"); mDocs.writeDocument(testFile2, FRUITY_BYTES); - createJob(newArrayList(testFile1, testFile2), null).run(); + DeleteJob job = createJob(newArrayList(testFile1, testFile2), null); + job.run(); mJobListener.waitForFinished(); mDocs.assertChildCount(mSrcRoot, 0); + var progress = job.getJobProgress(); + assertEquals(Job.STATE_COMPLETED, progress.state); + assertFalse(progress.hasFailures); + assertEquals("Deleting 2 files", progress.msg); } /** diff --git a/tests/functional/com/android/documentsui/services/FileOperationServiceTest.java b/tests/functional/com/android/documentsui/services/FileOperationServiceTest.java index 1816ed540..5820f460f 100644 --- a/tests/functional/com/android/documentsui/services/FileOperationServiceTest.java +++ b/tests/functional/com/android/documentsui/services/FileOperationServiceTest.java @@ -109,6 +109,8 @@ public class FileOperationServiceTest extends ServiceTestCase<FileOperationServi assertNull(mService.features); mService.features = features; + + mService.mVisualSignalsEnabled = false; } @Override diff --git a/tests/unit/com/android/documentsui/ProfileTabsTest.java b/tests/unit/com/android/documentsui/ProfileTabsTest.java index 054cdd006..ad674656a 100644 --- a/tests/unit/com/android/documentsui/ProfileTabsTest.java +++ b/tests/unit/com/android/documentsui/ProfileTabsTest.java @@ -50,7 +50,7 @@ import java.util.Map; @RunWith(Parameterized.class) public class ProfileTabsTest { - private final UserId mSystemUser = UserId.of(UserHandle.SYSTEM); + private final UserId mPrimaryUser = UserId.of(UserHandle.myUserId()); private final UserId mManagedUser = UserId.of(100); private final UserId mPrivateUser = UserId.of(101); @@ -111,7 +111,7 @@ public class ProfileTabsTest { @Test public void testUpdateView_singleUser_shouldHide() { - initializeWithUsers(isPrivateSpaceEnabled, mSystemUser); + initializeWithUsers(isPrivateSpaceEnabled, mPrimaryUser); assertThat(mTabLayoutContainer.getVisibility()).isEqualTo(View.GONE); assertThat(mTabLayout.getTabCount()).isEqualTo(0); @@ -119,13 +119,13 @@ public class ProfileTabsTest { @Test public void testUpdateView_twoUsers_shouldShow() { - initializeWithUsers(isPrivateSpaceEnabled, mSystemUser, mManagedUser); + initializeWithUsers(isPrivateSpaceEnabled, mPrimaryUser, mManagedUser); assertThat(mTabLayoutContainer.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mTabLayout.getTabCount()).isEqualTo(2); TabLayout.Tab tab1 = mTabLayout.getTabAt(0); - assertThat(tab1.getTag()).isEqualTo(mSystemUser); + assertThat(tab1.getTag()).isEqualTo(mPrimaryUser); assertThat(tab1.getText()).isEqualTo(mContext.getString(R.string.personal_tab)); TabLayout.Tab tab2 = mTabLayout.getTabAt(1); @@ -136,7 +136,7 @@ public class ProfileTabsTest { @Test public void testUpdateView_multiUsers_shouldShow() { if (!SdkLevel.isAtLeastV() || !isPrivateSpaceEnabled) return; - initializeWithUsers(true, mSystemUser, mManagedUser, mPrivateUser); + initializeWithUsers(true, mPrimaryUser, mManagedUser, mPrivateUser); assertThat(mTabLayoutContainer.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mTabLayout.getTabCount()).isEqualTo(3); @@ -152,7 +152,7 @@ public class ProfileTabsTest { @Test public void testUpdateView_twoUsers_doesNotSupportCrossProfile_shouldHide() { - initializeWithUsers(isPrivateSpaceEnabled, mSystemUser, mManagedUser); + initializeWithUsers(isPrivateSpaceEnabled, mPrimaryUser, mManagedUser); mState.supportsCrossProfile = false; mProfileTabs.updateView(); @@ -162,7 +162,7 @@ public class ProfileTabsTest { @Test public void testUpdateView_twoUsers_subFolder_shouldHide() { - initializeWithUsers(isPrivateSpaceEnabled, mSystemUser, mManagedUser); + initializeWithUsers(isPrivateSpaceEnabled, mPrimaryUser, mManagedUser); // Push 1 more folder. Now the stack has size of 2. mState.stack.push(TestEnv.FOLDER_1); @@ -174,7 +174,7 @@ public class ProfileTabsTest { @Test public void testUpdateView_twoUsers_recents_subFolder_shouldHide() { - initializeWithUsers(isPrivateSpaceEnabled, mSystemUser, mManagedUser); + initializeWithUsers(isPrivateSpaceEnabled, mPrimaryUser, mManagedUser); mState.stack.changeRoot(TestProvidersAccess.RECENTS); // This(stack of size 2 in Recents) may not happen in real world. @@ -187,7 +187,7 @@ public class ProfileTabsTest { @Test public void testUpdateView_twoUsers_thirdParty_shouldHide() { - initializeWithUsers(isPrivateSpaceEnabled, mSystemUser, mManagedUser); + initializeWithUsers(isPrivateSpaceEnabled, mPrimaryUser, mManagedUser); mState.stack.changeRoot(TestProvidersAccess.PICKLES); mState.stack.push((TestEnv.FOLDER_0)); @@ -200,7 +200,7 @@ public class ProfileTabsTest { @Test public void testUpdateView_twoUsers_isSearching_shouldHide() { mTestEnv.isSearchExpanded = true; - initializeWithUsers(isPrivateSpaceEnabled, mSystemUser, mManagedUser); + initializeWithUsers(isPrivateSpaceEnabled, mPrimaryUser, mManagedUser); assertThat(mTabLayoutContainer.getVisibility()).isEqualTo(View.GONE); assertThat(mTabLayout.getTabCount()).isEqualTo(2); @@ -208,28 +208,28 @@ public class ProfileTabsTest { @Test public void testUpdateView_getSelectedUser_afterUsersChanged() { - initializeWithUsers(isPrivateSpaceEnabled, mSystemUser, mManagedUser); + initializeWithUsers(isPrivateSpaceEnabled, mPrimaryUser, mManagedUser); mProfileTabs.updateView(); mTabLayout.selectTab(mTabLayout.getTabAt(1)); assertThat(mTabLayoutContainer.getVisibility()).isEqualTo(View.VISIBLE); assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mManagedUser); if (SdkLevel.isAtLeastS() && isPrivateSpaceEnabled) { - mTestUserManagerState.userIds = Lists.newArrayList(mSystemUser); + mTestUserManagerState.userIds = Lists.newArrayList(mPrimaryUser); } else { - mTestUserIdManager.userIds = Lists.newArrayList(mSystemUser); + mTestUserIdManager.userIds = Lists.newArrayList(mPrimaryUser); } mProfileTabs.updateView(); assertThat(mTabLayoutContainer.getVisibility()).isEqualTo(View.GONE); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mSystemUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mPrimaryUser); } @Test public void testUpdateView_afterCurrentRootChanged_shouldChangeSelectedUser() { - initializeWithUsers(isPrivateSpaceEnabled, mSystemUser, mManagedUser); + initializeWithUsers(isPrivateSpaceEnabled, mPrimaryUser, mManagedUser); mProfileTabs.updateView(); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mSystemUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mPrimaryUser); RootInfo newRoot = RootInfo.copyRootInfo(mTestCommonAddons.mCurrentRoot); newRoot.userId = mManagedUser; @@ -244,10 +244,10 @@ public class ProfileTabsTest { @Test public void testUpdateView_afterCurrentRootChangedMultiUser_shouldChangeSelectedUser() { if (!SdkLevel.isAtLeastV() || !isPrivateSpaceEnabled) return; - initializeWithUsers(true, mSystemUser, mManagedUser, mPrivateUser); + initializeWithUsers(true, mPrimaryUser, mManagedUser, mPrivateUser); mProfileTabs.updateView(); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mSystemUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mPrimaryUser); for (UserId userId : Lists.newArrayList(mManagedUser, mPrivateUser)) { RootInfo newRoot = RootInfo.copyRootInfo(mTestCommonAddons.mCurrentRoot); @@ -263,9 +263,9 @@ public class ProfileTabsTest { @Test public void testUpdateView_afterSelectedUserBecomesUnavailable_shouldSwitchToCurrentUser() { - // here current user refers to UserId.CURRENT_USER, which in this case will be mSystemUser + // here current user refers to UserId.CURRENT_USER, which in this case will be mPrimaryUser if (!SdkLevel.isAtLeastS() || !isPrivateSpaceEnabled) return; - initializeWithUsers(true, mSystemUser, mManagedUser, mPrivateUser); + initializeWithUsers(true, mPrimaryUser, mManagedUser, mPrivateUser); mTabLayout.selectTab(mTabLayout.getTabAt(2)); assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mPrivateUser); @@ -274,15 +274,15 @@ public class ProfileTabsTest { mTestUserManagerState.userIdToLabelMap.remove(mPrivateUser); mProfileTabs.updateView(); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mSystemUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mPrimaryUser); } @Test public void testGetSelectedUser_twoUsers() { - initializeWithUsers(isPrivateSpaceEnabled, mSystemUser, mManagedUser); + initializeWithUsers(isPrivateSpaceEnabled, mPrimaryUser, mManagedUser); mTabLayout.selectTab(mTabLayout.getTabAt(0)); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mSystemUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mPrimaryUser); mTabLayout.selectTab(mTabLayout.getTabAt(1)); assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mManagedUser); @@ -292,9 +292,10 @@ public class ProfileTabsTest { @Test public void testGetSelectedUser_multiUsers() { if (!SdkLevel.isAtLeastV() || !isPrivateSpaceEnabled) return; - initializeWithUsers(true, mSystemUser, mManagedUser, mPrivateUser); + initializeWithUsers(true, mPrimaryUser, mManagedUser, mPrivateUser); - List<UserId> expectedProfiles = Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser); + List<UserId> expectedProfiles = Lists.newArrayList(mPrimaryUser, mManagedUser, + mPrivateUser); for (int i = 0; i < 3; ++i) { mTabLayout.selectTab(mTabLayout.getTabAt(i)); @@ -306,21 +307,21 @@ public class ProfileTabsTest { @Test public void testReselectedUser_doesNotInvokeListener() { - initializeWithUsers(isPrivateSpaceEnabled, mSystemUser, mManagedUser); + initializeWithUsers(isPrivateSpaceEnabled, mPrimaryUser, mManagedUser); assertThat(mTabLayout.getSelectedTabPosition()).isAtLeast(0); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mSystemUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mPrimaryUser); mTabLayout.selectTab(mTabLayout.getTabAt(0)); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mSystemUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mPrimaryUser); assertThat(mIsListenerInvoked).isFalse(); } @Test public void testGetSelectedUser_singleUsers() { - initializeWithUsers(isPrivateSpaceEnabled, mSystemUser); + initializeWithUsers(isPrivateSpaceEnabled, mPrimaryUser); - assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mSystemUser); + assertThat(mProfileTabs.getSelectedUser()).isEqualTo(mPrimaryUser); } private void initializeWithUsers(boolean isPrivateSpaceEnabled, UserId... userIds) { @@ -328,7 +329,7 @@ public class ProfileTabsTest { mTestConfigStore.enablePrivateSpaceInPhotoPicker(); mTestUserManagerState.userIds = Lists.newArrayList(userIds); for (UserId userId : userIds) { - if (userId.isSystem()) { + if (userId.equals(UserId.of(UserHandle.myUserId()))) { mTestUserManagerState.userIdToLabelMap.put(userId, "Personal"); } else if (userId.getIdentifier() == 100) { mTestUserManagerState.userIdToLabelMap.put(userId, "Work"); @@ -342,7 +343,7 @@ public class ProfileTabsTest { mTestConfigStore.disablePrivateSpaceInPhotoPicker(); mTestUserIdManager.userIds = Lists.newArrayList(userIds); for (UserId userId : userIds) { - if (userId.isSystem()) { + if (userId.equals(UserId.of(UserHandle.myUserId()))) { mTestUserIdManager.systemUser = userId; } else { mTestUserIdManager.managedUser = userId; diff --git a/tests/unit/com/android/documentsui/UserIdManagerTest.java b/tests/unit/com/android/documentsui/UserIdManagerTest.java index 31fe7d166..36ff8ac68 100644 --- a/tests/unit/com/android/documentsui/UserIdManagerTest.java +++ b/tests/unit/com/android/documentsui/UserIdManagerTest.java @@ -131,10 +131,12 @@ public class UserIdManagerTest { @Test public void testGetUserIds_deviceNotSupported() { // we should return the current user when device is not supported. - UserId currentUser = UserId.of(systemUser); - when(mockUserManager.getUserProfiles()).thenReturn(Arrays.asList(systemUser, managedUser1)); - mUserIdManager = new UserIdManager.RuntimeUserIdManager(mockContext, currentUser, false); - assertThat(mUserIdManager.getUserIds()).containsExactly(UserId.of(systemUser)); + UserHandle currentUser = UserHandle.of(UserHandle.myUserId()); + when(mockUserManager.getUserProfiles()).thenReturn( + Arrays.asList(currentUser, managedUser1)); + mUserIdManager = new UserIdManager.RuntimeUserIdManager(mockContext, UserId.of(currentUser), + false); + assertThat(mUserIdManager.getUserIds()).containsExactly(UserId.of(currentUser)); } @Test @@ -142,13 +144,14 @@ public class UserIdManagerTest { // This test only tests for Android R or later. This test case always passes before R. if (VersionUtils.isAtLeastR()) { // When permission is denied, only returns the current user. - when(mockContext.checkSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS)) - .thenReturn(PackageManager.PERMISSION_DENIED); - UserId currentUser = UserId.of(systemUser); + when(mockContext.checkSelfPermission( + Manifest.permission.INTERACT_ACROSS_USERS)).thenReturn( + PackageManager.PERMISSION_DENIED); + UserHandle currentUser = UserHandle.of(UserHandle.myUserId()); when(mockUserManager.getUserProfiles()).thenReturn( - Arrays.asList(systemUser, managedUser1)); + Arrays.asList(currentUser, managedUser1)); mUserIdManager = UserIdManager.create(mockContext); - assertThat(mUserIdManager.getUserIds()).containsExactly(UserId.of(systemUser)); + assertThat(mUserIdManager.getUserIds()).containsExactly(UserId.of(currentUser)); } } diff --git a/tests/unit/com/android/documentsui/UserManagerStateTest.java b/tests/unit/com/android/documentsui/UserManagerStateTest.java index 42822d8e2..9d629c574 100644 --- a/tests/unit/com/android/documentsui/UserManagerStateTest.java +++ b/tests/unit/com/android/documentsui/UserManagerStateTest.java @@ -23,8 +23,11 @@ import static com.android.documentsui.DevicePolicyResources.Strings.WORK_TAB; import static com.google.common.truth.Truth.assertWithMessage; +import static org.junit.Assume.assumeTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -64,22 +67,53 @@ import java.util.Map; @SdkSuppress(minSdkVersion = 31, codeName = "S") public class UserManagerStateTest { + /** + * Class that exposes the @hide api [targetUserId] in order to supply proper values for + * reflection based code that is inspecting this field. + * + * @property targetUserId + */ + private static class ReflectedResolveInfo extends ResolveInfo { + + public int targetUserId; + + ReflectedResolveInfo(int targetUserId) { + this.targetUserId = targetUserId; + } + + @Override + public boolean isCrossProfileIntentForwarderActivity() { + return true; + } + } + private static final String PERSONAL = "Personal"; private static final String WORK = "Work"; private static final String PRIVATE = "Private"; - - private final UserHandle mSystemUser = UserHandle.SYSTEM; - private final UserHandle mManagedUser = UserHandle.of(100); - private final UserHandle mPrivateUser = UserHandle.of(101); - private final UserHandle mOtherUser = UserHandle.of(102); - private final UserHandle mNormalUser = UserHandle.of(103); - - private final ResolveInfo mMockInfo1 = mock(ResolveInfo.class); - private final ResolveInfo mMockInfo2 = mock(ResolveInfo.class); - private final ResolveInfo mMockInfo3 = mock(ResolveInfo.class); + private static final String PACKAGE_NAME = "com.android.documentsui"; + + /** + * Assume that the current user is SYSTEM_USER. For HSUM targets, the primary user is set as the + * system user. + */ + private final int mCurrentUserId = UserHandle.myUserId(); + + private final UserHandle mPrimaryUser = UserHandle.of(mCurrentUserId); + private final UserHandle mSystemUser = mPrimaryUser == null ? UserHandle.SYSTEM : mPrimaryUser; + private final UserHandle mManagedUser = UserHandle.of(mCurrentUserId + 10); + private final UserHandle mPrivateUser = UserHandle.of(mCurrentUserId + 20); + private final UserHandle mOtherUser = UserHandle.of(mCurrentUserId + 30); + private final UserHandle mNormalUser = UserHandle.of(mCurrentUserId + 40); + + private final ResolveInfo mMockInfoPrimaryUser = + new ReflectedResolveInfo(mPrimaryUser.getIdentifier()); + private final ResolveInfo mMockInfoManagedUser = + new ReflectedResolveInfo(mManagedUser.getIdentifier()); + private final ResolveInfo mMockInfoPrivateUser = + new ReflectedResolveInfo(mPrivateUser.getIdentifier()); private final Context mMockContext = mock(Context.class); - private final Intent mMockIntent = mock(Intent.class); + private final Intent mMockIntent = new Intent(); private final UserManager mMockUserManager = UserManagers.create(); private final PackageManager mMockPackageManager = mock(PackageManager.class); private final DevicePolicyManager mDevicePolicyManager = mock(DevicePolicyManager.class); @@ -88,6 +122,8 @@ public class UserManagerStateTest { @Before public void setup() throws Exception { when(mMockContext.getApplicationContext()).thenReturn(mMockContext); + when(mMockContext.createContextAsUser(any(UserHandle.class), anyInt())) + .thenReturn(mMockContext); when(mMockUserManager.isManagedProfile(mManagedUser.getIdentifier())).thenReturn(true); when(mMockUserManager.isManagedProfile(mSystemUser.getIdentifier())).thenReturn(false); @@ -95,54 +131,60 @@ public class UserManagerStateTest { when(mMockUserManager.isManagedProfile(mOtherUser.getIdentifier())).thenReturn(false); if (SdkLevel.isAtLeastV()) { - UserProperties systemUserProperties = new UserProperties.Builder() - .setShowInSharingSurfaces(UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) - .setCrossProfileContentSharingStrategy( - UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION) - .build(); - UserProperties managedUserProperties = new UserProperties.Builder() - .setShowInSharingSurfaces(UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) - .setCrossProfileContentSharingStrategy( - UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION) - .setShowInQuietMode(UserProperties.SHOW_IN_QUIET_MODE_PAUSED) - .build(); - UserProperties privateUserProperties = new UserProperties.Builder() - .setShowInSharingSurfaces(UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) - .setCrossProfileContentSharingStrategy( - UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT) - .setShowInQuietMode(UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) - .build(); - UserProperties otherUserProperties = new UserProperties.Builder() - .setShowInSharingSurfaces(UserProperties.SHOW_IN_SHARING_SURFACES_WITH_PARENT) - .setCrossProfileContentSharingStrategy( - UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT) - .build(); - UserProperties normalUserProperties = new UserProperties.Builder() - .setShowInSharingSurfaces(UserProperties.SHOW_IN_SHARING_SURFACES_NO) - .setCrossProfileContentSharingStrategy( - UserProperties.CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT) - .build(); + UserProperties systemUserProperties = + new UserProperties.Builder() + .setShowInSharingSurfaces( + UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) + .setCrossProfileContentSharingStrategy( + UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION) + .build(); + UserProperties managedUserProperties = + new UserProperties.Builder() + .setShowInSharingSurfaces( + UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) + .setCrossProfileContentSharingStrategy( + UserProperties.CROSS_PROFILE_CONTENT_SHARING_NO_DELEGATION) + .setShowInQuietMode(UserProperties.SHOW_IN_QUIET_MODE_PAUSED) + .build(); + UserProperties privateUserProperties = + new UserProperties.Builder() + .setShowInSharingSurfaces( + UserProperties.SHOW_IN_SHARING_SURFACES_SEPARATE) + .setCrossProfileContentSharingStrategy( + UserProperties + .CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT) + .setShowInQuietMode(UserProperties.SHOW_IN_QUIET_MODE_HIDDEN) + .build(); + UserProperties otherUserProperties = + new UserProperties.Builder() + .setShowInSharingSurfaces( + UserProperties.SHOW_IN_SHARING_SURFACES_WITH_PARENT) + .setCrossProfileContentSharingStrategy( + UserProperties + .CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT) + .build(); + UserProperties normalUserProperties = + new UserProperties.Builder() + .setShowInSharingSurfaces(UserProperties.SHOW_IN_SHARING_SURFACES_NO) + .setCrossProfileContentSharingStrategy( + UserProperties + .CROSS_PROFILE_CONTENT_SHARING_DELEGATE_FROM_PARENT) + .build(); when(mMockUserManager.getUserProperties(mSystemUser)).thenReturn(systemUserProperties); - when(mMockUserManager.getUserProperties(mManagedUser)).thenReturn( - managedUserProperties); - when(mMockUserManager.getUserProperties(mPrivateUser)).thenReturn( - privateUserProperties); + when(mMockUserManager.getUserProperties(mManagedUser)) + .thenReturn(managedUserProperties); + when(mMockUserManager.getUserProperties(mPrivateUser)) + .thenReturn(privateUserProperties); when(mMockUserManager.getUserProperties(mOtherUser)).thenReturn(otherUserProperties); when(mMockUserManager.getUserProperties(mNormalUser)).thenReturn(normalUserProperties); } when(mMockUserManager.getProfileParent(mSystemUser)).thenReturn(null); - when(mMockUserManager.getProfileParent(mManagedUser)).thenReturn(mSystemUser); - when(mMockUserManager.getProfileParent(mPrivateUser)).thenReturn(mSystemUser); - when(mMockUserManager.getProfileParent(mOtherUser)).thenReturn(mSystemUser); + when(mMockUserManager.getProfileParent(mManagedUser)).thenReturn(mPrimaryUser); + when(mMockUserManager.getProfileParent(mPrivateUser)).thenReturn(mPrimaryUser); + when(mMockUserManager.getProfileParent(mOtherUser)).thenReturn(mPrimaryUser); when(mMockUserManager.getProfileParent(mNormalUser)).thenReturn(null); - if (SdkLevel.isAtLeastR()) { - when(mMockInfo1.isCrossProfileIntentForwarderActivity()).thenReturn(true); - when(mMockInfo2.isCrossProfileIntentForwarderActivity()).thenReturn(false); - when(mMockInfo3.isCrossProfileIntentForwarderActivity()).thenReturn(false); - } - when(mMockContext.getPackageManager()).thenReturn(mMockPackageManager); when(mMockContext.getSystemServiceName(UserManager.class)).thenReturn("mMockUserManager"); when(mMockContext.getSystemService(UserManager.class)).thenReturn(mMockUserManager); @@ -150,8 +192,25 @@ public class UserManagerStateTest { .thenReturn(Context.DEVICE_POLICY_SERVICE); when(mMockContext.getSystemService(Context.DEVICE_POLICY_SERVICE)) .thenReturn(mDevicePolicyManager); - when(mMockContext.getResources()).thenReturn( - InstrumentationRegistry.getInstrumentation().getTargetContext().getResources()); + when(mMockContext.getResources()) + .thenReturn( + InstrumentationRegistry.getInstrumentation() + .getTargetContext() + .getResources()); + + when(mMockContext.getPackageName()).thenReturn(PACKAGE_NAME); + when(mMockContext.createPackageContextAsUser(PACKAGE_NAME, 0, mSystemUser)) + .thenReturn(mMockContext); + when(mMockContext.createPackageContextAsUser(PACKAGE_NAME, 0, mManagedUser)) + .thenReturn(mMockContext); + when(mMockContext.createPackageContextAsUser(PACKAGE_NAME, 0, mPrivateUser)) + .thenReturn(mMockContext); + when(mMockContext.createPackageContextAsUser(PACKAGE_NAME, 0, mOtherUser)) + .thenReturn(mMockContext); + when(mMockContext.createPackageContextAsUser(PACKAGE_NAME, 0, mNormalUser)) + .thenReturn(mMockContext); + when(mMockContext.createPackageContextAsUser(PACKAGE_NAME, 0, mPrimaryUser)) + .thenReturn(mMockContext); } @Test @@ -160,48 +219,52 @@ public class UserManagerStateTest { initializeUserManagerState(currentUser, Lists.newArrayList(mSystemUser)); assertWithMessage("getUserIds returns unexpected list of user ids") - .that(mUserManagerState.getUserIds()).containsExactly(UserId.of(mSystemUser)); + .that(mUserManagerState.getUserIds()) + .containsExactly(UserId.of(mSystemUser)); } @Test public void testGetUserIds_allProfilesCurrentUserSystem_allShowInSharingSurfacesSeparate() { if (!SdkLevel.isAtLeastV()) return; UserId currentUser = UserId.of(mSystemUser); - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser, mOtherUser, - mNormalUser)); + initializeUserManagerState( + currentUser, + Lists.newArrayList( + mSystemUser, mManagedUser, mPrivateUser, mOtherUser, mNormalUser)); assertWithMessage("getUserIds returns unexpected list of user ids") .that(mUserManagerState.getUserIds()) - .containsExactly(UserId.of(mSystemUser), UserId.of(mManagedUser), - UserId.of(mPrivateUser)); + .containsExactly( + UserId.of(mSystemUser), UserId.of(mManagedUser), UserId.of(mPrivateUser)); } @Test public void testGetUserIds_allProfilesCurrentUserManaged_allShowInSharingSurfacesSeparate() { if (!SdkLevel.isAtLeastV()) return; UserId currentUser = UserId.of(mManagedUser); - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser, mOtherUser, - mNormalUser)); + initializeUserManagerState( + currentUser, + Lists.newArrayList( + mSystemUser, mManagedUser, mPrivateUser, mOtherUser, mNormalUser)); assertWithMessage("getUserIds returns unexpected list of user ids") .that(mUserManagerState.getUserIds()) - .containsExactly(UserId.of(mSystemUser), UserId.of(mManagedUser), - UserId.of(mPrivateUser)); + .containsExactly( + UserId.of(mSystemUser), UserId.of(mManagedUser), UserId.of(mPrivateUser)); } @Test public void testGetUserIds_allProfilesCurrentUserPrivate_allShowInSharingSurfacesSeparate() { if (!SdkLevel.isAtLeastV()) return; UserId currentUser = UserId.of(mPrivateUser); - initializeUserManagerState(currentUser, + initializeUserManagerState( + currentUser, Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser, mOtherUser)); assertWithMessage("getUserIds returns unexpected list of user ids") .that(mUserManagerState.getUserIds()) - .containsExactly(UserId.of(mSystemUser), UserId.of(mManagedUser), - UserId.of(mPrivateUser)); + .containsExactly( + UserId.of(mSystemUser), UserId.of(mManagedUser), UserId.of(mPrivateUser)); } @Test @@ -270,7 +333,8 @@ public class UserManagerStateTest { @Test public void testGetUserIds_normalAndOtherUserCurrentUserNormal_returnsCurrentUser() { - // since both users do not have show in sharing surfaces separate, returns current user + // since both users do not have show in sharing surfaces separate, returns + // current user UserId currentUser = UserId.of(mNormalUser); initializeUserManagerState(currentUser, Lists.newArrayList(mOtherUser, mNormalUser)); @@ -287,7 +351,8 @@ public class UserManagerStateTest { initializeUserManagerState(currentUser, Lists.newArrayList(mSystemUser, mManagedUser)); assertWithMessage("getUserIds returns unexpected list of user ids") .that(mUserManagerState.getUserIds()) - .containsExactly(UserId.of(mSystemUser), UserId.of(mManagedUser)).inOrder(); + .containsExactly(UserId.of(mSystemUser), UserId.of(mManagedUser)) + .inOrder(); } @Test @@ -298,66 +363,79 @@ public class UserManagerStateTest { initializeUserManagerState(currentUser, Lists.newArrayList(mSystemUser, mManagedUser)); assertWithMessage("getUserIds returns unexpected list of user ids") .that(mUserManagerState.getUserIds()) - .containsExactly(UserId.of(mSystemUser), UserId.of(mManagedUser)).inOrder(); + .containsExactly(UserId.of(mSystemUser), UserId.of(mManagedUser)) + .inOrder(); } @Test public void testGetUserIds_managedAndSystemUserCurrentUserSystem_returnsBothInOrder() { - // Returns the both if there are system and managed users, regardless of input list order. + // Returns the both if there are system and managed users, regardless of input + // list order. if (SdkLevel.isAtLeastV()) return; UserId currentUser = UserId.of(mSystemUser); initializeUserManagerState(currentUser, Lists.newArrayList(mManagedUser, mSystemUser)); assertWithMessage("getUserIds returns unexpected list of user ids") .that(mUserManagerState.getUserIds()) - .containsExactly(UserId.of(mSystemUser), UserId.of(mManagedUser)).inOrder(); + .containsExactly(UserId.of(mSystemUser), UserId.of(mManagedUser)) + .inOrder(); } @Test public void testGetUserIds_otherAndManagedUserCurrentUserOtherPreV_returnsCurrentUser() { // When there is no system user, returns the current user. - // This is a case theoretically can happen but we don't expect. So we return the current + // This is a case theoretically can happen but we don't expect. So we return the + // current // user only. if (SdkLevel.isAtLeastV()) return; UserId currentUser = UserId.of(mOtherUser); initializeUserManagerState(currentUser, Lists.newArrayList(mOtherUser, mManagedUser)); assertWithMessage("getUserIds returns unexpected list of user ids") - .that(mUserManagerState.getUserIds()).containsExactly(currentUser); + .that(mUserManagerState.getUserIds()) + .containsExactly(currentUser); } @Test public void testGetUserIds_otherAndManagedUserCurrentUserOtherPostV_returnsManagedUser() { - // Only the users with show in sharing surfaces separate are eligible to be returned + // Only the users with show in sharing surfaces separate are eligible to be + // returned if (!SdkLevel.isAtLeastV()) return; UserId currentUser = UserId.of(mOtherUser); initializeUserManagerState(currentUser, Lists.newArrayList(mOtherUser, mManagedUser)); assertWithMessage("getUserIds returns unexpected list of user ids") - .that(mUserManagerState.getUserIds()).containsExactly(UserId.of(mManagedUser)); + .that(mUserManagerState.getUserIds()) + .containsExactly(UserId.of(mManagedUser)); } @Test public void testGetUserIds_otherAndManagedUserCurrentUserManaged_returnsCurrentUser() { // When there is no system user, returns the current user. - // This is a case theoretically can happen, but we don't expect. So we return the current + // This is a case theoretically can happen, but we don't expect. So we return + // the current // user only. UserId currentUser = UserId.of(mManagedUser); initializeUserManagerState(currentUser, Lists.newArrayList(mOtherUser, mManagedUser)); assertWithMessage("getUserIds returns unexpected list of user ids") - .that(mUserManagerState.getUserIds()).containsExactly(currentUser); + .that(mUserManagerState.getUserIds()) + .containsExactly(currentUser); } @Test public void testGetUserIds_unsupportedDeviceCurrent_returnsCurrentUser() { - // This test only tests for Android R or later. This test case always passes before R. + // This test only tests for Android R or later. This test case always passes + // before R. if (VersionUtils.isAtLeastR()) { // When permission is denied, only returns the current user. when(mMockContext.checkSelfPermission(Manifest.permission.INTERACT_ACROSS_USERS)) .thenReturn(PackageManager.PERMISSION_DENIED); UserId currentUser = UserId.of(mSystemUser); - when(mMockUserManager.getUserProfiles()).thenReturn( - Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser, mOtherUser)); + when(mMockUserManager.getUserProfiles()) + .thenReturn( + Lists.newArrayList( + mSystemUser, mManagedUser, mPrivateUser, mOtherUser)); mUserManagerState = UserManagerState.create(mMockContext); assertWithMessage("Unsupported device should have returned only the current user") - .that(mUserManagerState.getUserIds()).containsExactly(currentUser); + .that(mUserManagerState.getUserIds()) + .containsExactly(currentUser); } } @@ -365,7 +443,8 @@ public class UserManagerStateTest { public void testGetUserIds_returnCachedList() { // Returns all three if there are system, managed and private users. UserId currentUser = UserId.of(mSystemUser); - initializeUserManagerState(currentUser, + initializeUserManagerState( + currentUser, Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser, mOtherUser)); assertWithMessage("getUserIds does not return cached instance") .that(mUserManagerState.getUserIds()) @@ -373,47 +452,13 @@ public class UserManagerStateTest { } @Test - public void testGetCanForwardToProfileIdMap_systemUserCanForwardToAll() { - UserId currentUser = UserId.of(mSystemUser); - final List<ResolveInfo> mMockResolveInfoList = Lists.newArrayList(mMockInfo1, mMockInfo2); - if (SdkLevel.isAtLeastV()) { - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); - when(mMockPackageManager.queryIntentActivitiesAsUser(mMockIntent, - PackageManager.MATCH_DEFAULT_ONLY, mSystemUser)).thenReturn( - mMockResolveInfoList); - } else { - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser)); - when(mMockPackageManager.queryIntentActivities(mMockIntent, - PackageManager.MATCH_DEFAULT_ONLY)).thenReturn(mMockResolveInfoList); - } - - Map<UserId, Boolean> expectedCanForwardToProfileIdMap = new HashMap<>(); - expectedCanForwardToProfileIdMap.put(UserId.of(mSystemUser), true); - expectedCanForwardToProfileIdMap.put(UserId.of(mManagedUser), true); - if (SdkLevel.isAtLeastV()) { - expectedCanForwardToProfileIdMap.put(UserId.of(mPrivateUser), true); - } - - assertWithMessage("getCanForwardToProfileIdMap returns incorrect mappings") - .that(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)) - .isEqualTo(expectedCanForwardToProfileIdMap); - } - - @Test public void testGetCanForwardToProfileIdMap_systemUserCanForwardToManaged() { UserId currentUser = UserId.of(mSystemUser); initializeUserManagerState(currentUser, Lists.newArrayList(mSystemUser, mManagedUser)); - final List<ResolveInfo> mMockResolveInfoList = Lists.newArrayList(mMockInfo1, mMockInfo2); - if (SdkLevel.isAtLeastV()) { - when(mMockPackageManager.queryIntentActivitiesAsUser(mMockIntent, - PackageManager.MATCH_DEFAULT_ONLY, mSystemUser)).thenReturn( - mMockResolveInfoList); - } else { - when(mMockPackageManager.queryIntentActivities(mMockIntent, - PackageManager.MATCH_DEFAULT_ONLY)).thenReturn(mMockResolveInfoList); - } + final List<ResolveInfo> mMockResolveInfoList = Lists.newArrayList(mMockInfoManagedUser); + + when(mMockPackageManager.queryIntentActivities(any(Intent.class), anyInt())) + .thenReturn(mMockResolveInfoList); Map<UserId, Boolean> expectedCanForwardToProfileIdMap = new HashMap<>(); expectedCanForwardToProfileIdMap.put(UserId.of(mSystemUser), true); @@ -442,18 +487,19 @@ public class UserManagerStateTest { @Test public void testGetCanForwardToProfileIdMap_systemUserCanNotForwardToManagedUser() { UserId currentUser = UserId.of(mSystemUser); - final List<ResolveInfo> mMockResolveInfoList = Lists.newArrayList(mMockInfo2, mMockInfo3); + final List<ResolveInfo> mMockResolveInfoList = + Lists.newArrayList(mMockInfoPrivateUser, mMockInfoPrimaryUser); if (SdkLevel.isAtLeastV()) { - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); - when(mMockPackageManager.queryIntentActivitiesAsUser(mMockIntent, - PackageManager.MATCH_DEFAULT_ONLY, mSystemUser)).thenReturn( - mMockResolveInfoList); + initializeUserManagerState( + currentUser, Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); + when(mMockPackageManager.queryIntentActivitiesAsUser( + mMockIntent, PackageManager.MATCH_DEFAULT_ONLY, mSystemUser)) + .thenReturn(mMockResolveInfoList); } else { - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser)); - when(mMockPackageManager.queryIntentActivities(mMockIntent, - PackageManager.MATCH_DEFAULT_ONLY)).thenReturn(mMockResolveInfoList); + initializeUserManagerState(currentUser, Lists.newArrayList(mSystemUser, mManagedUser)); + when(mMockPackageManager.queryIntentActivities( + mMockIntent, PackageManager.MATCH_DEFAULT_ONLY)) + .thenReturn(mMockResolveInfoList); } Map<UserId, Boolean> expectedCanForwardToProfileIdMap = new HashMap<>(); @@ -471,26 +517,17 @@ public class UserManagerStateTest { @Test public void testGetCanForwardToProfileIdMap_managedCanForwardToAll() { UserId currentUser = UserId.of(mManagedUser); - final List<ResolveInfo> mMockResolveInfoList = Lists.newArrayList(mMockInfo1, mMockInfo2); - if (SdkLevel.isAtLeastV()) { - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); - when(mMockPackageManager.queryIntentActivitiesAsUser(mMockIntent, - PackageManager.MATCH_DEFAULT_ONLY, mManagedUser)).thenReturn( - mMockResolveInfoList); - } else { - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser)); - when(mMockPackageManager.queryIntentActivities(mMockIntent, - PackageManager.MATCH_DEFAULT_ONLY)).thenReturn(mMockResolveInfoList); - } + final List<ResolveInfo> mMockResolveInfoList = Lists.newArrayList(mMockInfoPrimaryUser); + when(mMockPackageManager.queryIntentActivities(any(Intent.class), anyInt())) + .thenReturn(mMockResolveInfoList); + + initializeUserManagerState( + currentUser, Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); Map<UserId, Boolean> expectedCanForwardToProfileIdMap = new HashMap<>(); expectedCanForwardToProfileIdMap.put(UserId.of(mSystemUser), true); expectedCanForwardToProfileIdMap.put(UserId.of(mManagedUser), true); - if (SdkLevel.isAtLeastV()) { - expectedCanForwardToProfileIdMap.put(UserId.of(mPrivateUser), true); - } + expectedCanForwardToProfileIdMap.put(UserId.of(mPrivateUser), true); assertWithMessage("getCanForwardToProfileIdMap returns incorrect mappings") .that(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)) @@ -500,19 +537,20 @@ public class UserManagerStateTest { @Test public void testGetCanForwardToProfileIdMap_managedCanNotForwardToAll() { UserId currentUser = UserId.of(mManagedUser); - final List<ResolveInfo> mMockResolveInfoList = Lists.newArrayList(mMockInfo2, mMockInfo3); + final List<ResolveInfo> mMockResolveInfoList = + Lists.newArrayList(mMockInfoPrivateUser, mMockInfoPrimaryUser); if (SdkLevel.isAtLeastV()) { - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); - when(mMockPackageManager.queryIntentActivitiesAsUser(mMockIntent, - PackageManager.MATCH_DEFAULT_ONLY, mSystemUser)).thenReturn( - mMockResolveInfoList); + initializeUserManagerState( + currentUser, Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); + when(mMockPackageManager.queryIntentActivitiesAsUser( + mMockIntent, PackageManager.MATCH_DEFAULT_ONLY, mSystemUser)) + .thenReturn(mMockResolveInfoList); } else { - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser)); - when(mMockPackageManager.queryIntentActivities(mMockIntent, - PackageManager.MATCH_DEFAULT_ONLY)).thenReturn(mMockResolveInfoList); + initializeUserManagerState(currentUser, Lists.newArrayList(mSystemUser, mManagedUser)); + when(mMockPackageManager.queryIntentActivities( + mMockIntent, PackageManager.MATCH_DEFAULT_ONLY)) + .thenReturn(mMockResolveInfoList); } Map<UserId, Boolean> expectedCanForwardToProfileIdMap = new HashMap<>(); @@ -529,13 +567,13 @@ public class UserManagerStateTest { @Test public void testGetCanForwardToProfileIdMap_privateCanForwardToAll() { - if (!SdkLevel.isAtLeastV()) return; UserId currentUser = UserId.of(mPrivateUser); - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); - final List<ResolveInfo> mMockResolveInfoList = Lists.newArrayList(mMockInfo1, mMockInfo2); - when(mMockPackageManager.queryIntentActivitiesAsUser(mMockIntent, - PackageManager.MATCH_DEFAULT_ONLY, mSystemUser)).thenReturn(mMockResolveInfoList); + initializeUserManagerState( + currentUser, Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); + final List<ResolveInfo> mMockResolveInfoList = + Lists.newArrayList(mMockInfoPrimaryUser, mMockInfoManagedUser); + when(mMockPackageManager.queryIntentActivities(any(Intent.class), anyInt())) + .thenReturn(mMockResolveInfoList); Map<UserId, Boolean> expectedCanForwardToProfileIdMap = new HashMap<>(); expectedCanForwardToProfileIdMap.put(UserId.of(mSystemUser), true); @@ -549,13 +587,13 @@ public class UserManagerStateTest { @Test public void testGetCanForwardToProfileIdMap_privateCanNotForwardToManagedUser() { - if (!SdkLevel.isAtLeastV()) return; UserId currentUser = UserId.of(mPrivateUser); - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); - final List<ResolveInfo> mMockResolveInfoList = Lists.newArrayList(mMockInfo2, mMockInfo3); - when(mMockPackageManager.queryIntentActivitiesAsUser(mMockIntent, - PackageManager.MATCH_DEFAULT_ONLY, mSystemUser)).thenReturn(mMockResolveInfoList); + initializeUserManagerState( + currentUser, Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); + final List<ResolveInfo> mMockResolveInfoList = + Lists.newArrayList(mMockInfoPrivateUser, mMockInfoPrimaryUser); + when(mMockPackageManager.queryIntentActivities(any(Intent.class), anyInt())) + .thenReturn(mMockResolveInfoList); Map<UserId, Boolean> expectedCanForwardToProfileIdMap = new HashMap<>(); expectedCanForwardToProfileIdMap.put(UserId.of(mSystemUser), true); @@ -573,6 +611,10 @@ public class UserManagerStateTest { UserId currentUser = UserId.of(mPrivateUser); initializeUserManagerState(currentUser, Lists.newArrayList(mSystemUser, mPrivateUser)); + final List<ResolveInfo> mMockResolveInfoList = Lists.newArrayList(mMockInfoPrimaryUser); + when(mMockPackageManager.queryIntentActivities(any(Intent.class), anyInt())) + .thenReturn(mMockResolveInfoList); + Map<UserId, Boolean> expectedCanForwardToProfileIdMap = new HashMap<>(); expectedCanForwardToProfileIdMap.put(UserId.of(mSystemUser), true); expectedCanForwardToProfileIdMap.put(UserId.of(mPrivateUser), true); @@ -586,24 +628,27 @@ public class UserManagerStateTest { public void testOnProfileStatusChange_anyIntentActionForManagedProfile() { if (!SdkLevel.isAtLeastV()) return; UserId currentUser = UserId.of(mSystemUser); - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); + initializeUserManagerState( + currentUser, Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); - // UserManagerState#mUserId and UserManagerState#mCanForwardToProfileIdMap will empty + // UserManagerState#mUserId and UserManagerState#mCanForwardToProfileIdMap will + // empty // by default if the getters of these member variables have not been called List<UserId> userIdsBeforeIntent = new ArrayList<>(mUserManagerState.getUserIds()); - Map<UserId, Boolean> canForwardToProfileIdMapBeforeIntent = new HashMap<>( - mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)); + Map<UserId, Boolean> canForwardToProfileIdMapBeforeIntent = + new HashMap<>(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)); String action = "any_intent"; mUserManagerState.onProfileActionStatusChange(action, UserId.of(mManagedUser)); assertWithMessage("Unexpected changes to user id list on receiving intent: " + action) - .that(mUserManagerState.getUserIds()).isEqualTo(userIdsBeforeIntent); + .that(mUserManagerState.getUserIds()) + .isEqualTo(userIdsBeforeIntent); assertWithMessage( - "Unexpected changes to canForwardToProfileIdMap on receiving intent: " + action) - .that(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)).isEqualTo( - canForwardToProfileIdMapBeforeIntent); + "Unexpected changes to canForwardToProfileIdMap on receiving intent: " + + action) + .that(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)) + .isEqualTo(canForwardToProfileIdMapBeforeIntent); } @Test @@ -612,18 +657,20 @@ public class UserManagerStateTest { UserId currentUser = UserId.of(mSystemUser); UserId managedUser = UserId.of(mManagedUser); UserId privateUser = UserId.of(mPrivateUser); - final List<ResolveInfo> mMockResolveInfoList = Lists.newArrayList(mMockInfo1, mMockInfo2); - when(mMockPackageManager.queryIntentActivitiesAsUser(mMockIntent, - PackageManager.MATCH_DEFAULT_ONLY, mSystemUser)).thenReturn( - mMockResolveInfoList); - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); - - // UserManagerState#mUserId and UserManagerState#mCanForwardToProfileIdMap will empty + final List<ResolveInfo> mMockResolveInfoList = + Lists.newArrayList(mMockInfoManagedUser, mMockInfoPrivateUser); + when(mMockPackageManager.queryIntentActivitiesAsUser( + mMockIntent, PackageManager.MATCH_DEFAULT_ONLY, mSystemUser)) + .thenReturn(mMockResolveInfoList); + initializeUserManagerState( + currentUser, Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); + + // UserManagerState#mUserId and UserManagerState#mCanForwardToProfileIdMap will + // empty // by default if the getters of these member variables have not been called List<UserId> userIdsBeforeIntent = new ArrayList<>(mUserManagerState.getUserIds()); - Map<UserId, Boolean> canForwardToProfileIdMapBeforeIntent = new HashMap<>( - mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)); + Map<UserId, Boolean> canForwardToProfileIdMapBeforeIntent = + new HashMap<>(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)); List<UserId> expectedUserIdsAfterIntent = Lists.newArrayList(currentUser, managedUser); @@ -631,14 +678,18 @@ public class UserManagerStateTest { mUserManagerState.onProfileActionStatusChange(action, privateUser); assertWithMessage( - "UserIds list should not be same before and after receiving intent: " + action) - .that(mUserManagerState.getUserIds()).isNotEqualTo(userIdsBeforeIntent); + "UserIds list should not be same before and after receiving intent: " + + action) + .that(mUserManagerState.getUserIds()) + .isNotEqualTo(userIdsBeforeIntent); assertWithMessage("Unexpected changes to user id list on receiving intent: " + action) - .that(mUserManagerState.getUserIds()).isEqualTo(expectedUserIdsAfterIntent); - assertWithMessage("CanForwardToLabelMap should be same before and after receiving intent: " - + action) - .that(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)).isEqualTo( - canForwardToProfileIdMapBeforeIntent); + .that(mUserManagerState.getUserIds()) + .isEqualTo(expectedUserIdsAfterIntent); + assertWithMessage( + "CanForwardToLabelMap should be same before and after receiving intent: " + + action) + .that(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)) + .isEqualTo(canForwardToProfileIdMapBeforeIntent); } @Test @@ -647,40 +698,84 @@ public class UserManagerStateTest { UserId currentUser = UserId.of(mSystemUser); UserId managedUser = UserId.of(mManagedUser); UserId privateUser = UserId.of(mPrivateUser); - final List<ResolveInfo> mMockResolveInfoList = Lists.newArrayList(mMockInfo1, mMockInfo2); - when(mMockPackageManager.queryIntentActivitiesAsUser(mMockIntent, - PackageManager.MATCH_DEFAULT_ONLY, mSystemUser)).thenReturn( - mMockResolveInfoList); - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); + final List<ResolveInfo> mMockResolveInfoList = + Lists.newArrayList(mMockInfoManagedUser, mMockInfoPrivateUser); + when(mMockPackageManager.queryIntentActivitiesAsUser( + mMockIntent, PackageManager.MATCH_DEFAULT_ONLY, mSystemUser)) + .thenReturn(mMockResolveInfoList); + initializeUserManagerState( + currentUser, Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); // initialising the userIds list and canForwardToProfileIdMap mUserManagerState.getUserIds(); mUserManagerState.getCanForwardToProfileIdMap(mMockIntent); // Making the private profile unavailable after it has been initialised - mUserManagerState.onProfileActionStatusChange(Intent.ACTION_PROFILE_UNAVAILABLE, - privateUser); + mUserManagerState.onProfileActionStatusChange( + Intent.ACTION_PROFILE_UNAVAILABLE, privateUser); List<UserId> userIdsBeforeIntent = new ArrayList<>(mUserManagerState.getUserIds()); - Map<UserId, Boolean> canForwardToProfileIdMapBeforeIntent = new HashMap<>( - mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)); + Map<UserId, Boolean> canForwardToProfileIdMapBeforeIntent = + new HashMap<>(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)); - List<UserId> expectedUserIdsAfterIntent = Lists.newArrayList(currentUser, managedUser, - privateUser); + List<UserId> expectedUserIdsAfterIntent = + Lists.newArrayList(currentUser, managedUser, privateUser); String action = Intent.ACTION_PROFILE_AVAILABLE; mUserManagerState.onProfileActionStatusChange(action, privateUser); assertWithMessage( - "UserIds list should not be same before and after receiving intent: " + action) - .that(mUserManagerState.getUserIds()).isNotEqualTo(userIdsBeforeIntent); + "UserIds list should not be same before and after receiving intent: " + + action) + .that(mUserManagerState.getUserIds()) + .isNotEqualTo(userIdsBeforeIntent); assertWithMessage("Unexpected changes to user id list on receiving intent: " + action) - .that(mUserManagerState.getUserIds()).isEqualTo(expectedUserIdsAfterIntent); - assertWithMessage("CanForwardToLabelMap should be same before and after receiving intent: " - + action) - .that(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)).isEqualTo( - canForwardToProfileIdMapBeforeIntent); + .that(mUserManagerState.getUserIds()) + .isEqualTo(expectedUserIdsAfterIntent); + assertWithMessage( + "CanForwardToLabelMap should be same before and after receiving intent: " + + action) + .that(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)) + .isEqualTo(canForwardToProfileIdMapBeforeIntent); + } + + @Test + public void testOnProfileStatusChange_actionProfileAdded() { + assumeTrue(SdkLevel.isAtLeastV()); + UserId currentUser = UserId.of(mSystemUser); + UserId managedUser = UserId.of(mManagedUser); + UserId privateUser = UserId.of(mPrivateUser); + + final List<ResolveInfo> mMockResolveInfoList = Lists.newArrayList(mMockInfoManagedUser); + + when(mMockPackageManager.queryIntentActivities(any(Intent.class), anyInt())) + .thenReturn(mMockResolveInfoList); + + initializeUserManagerState(currentUser, Lists.newArrayList(mSystemUser, mManagedUser)); + + mUserManagerState.setCurrentStateIntent(new Intent()); + + // initialising the userIds list and canForwardToProfileIdMap + mUserManagerState.getUserIds(); + mUserManagerState.getCanForwardToProfileIdMap(mMockIntent); + + String action = Intent.ACTION_PROFILE_ADDED; + mUserManagerState.onProfileActionStatusChange(action, privateUser); + + assertWithMessage( + "UserIds list should not be same before and after receiving intent: " + + action) + .that(mUserManagerState.getUserIds()) + .containsExactly(currentUser, managedUser, privateUser); + assertWithMessage( + "CanForwardToLabelMap should be same before and after receiving intent: " + + action) + .that(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)) + .isEqualTo( + Map.ofEntries( + Map.entry(currentUser, true), + Map.entry(managedUser, true), + Map.entry(privateUser, true))); } @Test @@ -689,24 +784,27 @@ public class UserManagerStateTest { UserId currentUser = UserId.of(mSystemUser); UserId managedUser = UserId.of(mManagedUser); UserId privateUser = UserId.of(mPrivateUser); - final List<ResolveInfo> mMockResolveInfoList = Lists.newArrayList(mMockInfo1, mMockInfo2); - when(mMockPackageManager.queryIntentActivitiesAsUser(mMockIntent, - PackageManager.MATCH_DEFAULT_ONLY, mSystemUser)).thenReturn( - mMockResolveInfoList); + final List<ResolveInfo> mMockResolveInfoList = + Lists.newArrayList(mMockInfoManagedUser, mMockInfoPrivateUser); + when(mMockPackageManager.queryIntentActivities(any(Intent.class), anyInt())) + .thenReturn(mMockResolveInfoList); + + when(mMockUserManager.getProfileParent(UserHandle.of(privateUser.getIdentifier()))) + .thenReturn(mPrimaryUser); // Private user will not be initialised if it is in quiet mode when(mMockUserManager.isQuietModeEnabled(mPrivateUser)).thenReturn(true); - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); - - // UserManagerState#mUserId and UserManagerState#mCanForwardToProfileIdMap will be empty - // by default if the getters of these member variables have not been called + initializeUserManagerState( + currentUser, Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); + mUserManagerState.setCurrentStateIntent(new Intent()); + // UserManagerState#mUserId and UserManagerState#mCanForwardToProfileIdMap will + // be empty by default if the getters of these member variables have not been called List<UserId> userIdsBeforeIntent = new ArrayList<>(mUserManagerState.getUserIds()); - Map<UserId, Boolean> canForwardToProfileIdMapBeforeIntent = new HashMap<>( - mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)); + Map<UserId, Boolean> canForwardToProfileIdMapBeforeIntent = + new HashMap<>(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)); - List<UserId> expectedUserIdsAfterIntent = Lists.newArrayList(currentUser, managedUser, - privateUser); + List<UserId> expectedUserIdsAfterIntent = + Lists.newArrayList(currentUser, managedUser, privateUser); Map<UserId, Boolean> expectedCanForwardToProfileIdMapAfterIntent = new HashMap<>(); expectedCanForwardToProfileIdMapAfterIntent.put(currentUser, true); expectedCanForwardToProfileIdMapAfterIntent.put(managedUser, true); @@ -716,56 +814,62 @@ public class UserManagerStateTest { mUserManagerState.onProfileActionStatusChange(action, privateUser); assertWithMessage( - "UserIds list should not be same before and after receiving intent: " + action) - .that(mUserManagerState.getUserIds()).isNotEqualTo(userIdsBeforeIntent); + "UserIds list should not be same before and after receiving intent: " + + action) + .that(mUserManagerState.getUserIds()) + .isNotEqualTo(userIdsBeforeIntent); assertWithMessage("Unexpected changes to user id list on receiving intent: " + action) - .that(mUserManagerState.getUserIds()).isEqualTo(expectedUserIdsAfterIntent); + .that(mUserManagerState.getUserIds()) + .isEqualTo(expectedUserIdsAfterIntent); assertWithMessage( - "CanForwardToLabelMap should not be same before and after receiving intent: " - + action) - .that(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)).isNotEqualTo( - canForwardToProfileIdMapBeforeIntent); + "CanForwardToLabelMap should not be same before and after receiving intent:" + + " " + + action) + .that(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)) + .isNotEqualTo(canForwardToProfileIdMapBeforeIntent); assertWithMessage( - "Unexpected changes to canForwardToProfileIdMap on receiving intent: " + action) - .that(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)).isEqualTo( - expectedCanForwardToProfileIdMapAfterIntent); + "Unexpected changes to canForwardToProfileIdMap on receiving intent: " + + action) + .that(mUserManagerState.getCanForwardToProfileIdMap(mMockIntent)) + .isEqualTo(expectedCanForwardToProfileIdMapAfterIntent); } @Test public void testGetUserIdToLabelMap_systemUserAndManagedUser_PreV() { if (SdkLevel.isAtLeastV()) return; UserId currentUser = UserId.of(mSystemUser); - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser)); + initializeUserManagerState(currentUser, Lists.newArrayList(mSystemUser, mManagedUser)); if (SdkLevel.isAtLeastT()) { - DevicePolicyResourcesManager devicePolicyResourcesManager = mock( - DevicePolicyResourcesManager.class); + DevicePolicyResourcesManager devicePolicyResourcesManager = + mock(DevicePolicyResourcesManager.class); when(mDevicePolicyManager.getResources()).thenReturn(devicePolicyResourcesManager); - when(devicePolicyResourcesManager.getString(eq(PERSONAL_TAB), any())).thenReturn( - PERSONAL); + when(devicePolicyResourcesManager.getString(eq(PERSONAL_TAB), any())) + .thenReturn(PERSONAL); when(devicePolicyResourcesManager.getString(eq(WORK_TAB), any())).thenReturn(WORK); } Map<UserId, String> userIdToLabelMap = mUserManagerState.getUserIdToLabelMap(); assertWithMessage("Incorrect label returned for user id " + mSystemUser) - .that(userIdToLabelMap.get(UserId.of(mSystemUser))).isEqualTo(PERSONAL); + .that(userIdToLabelMap.get(UserId.of(mSystemUser))) + .isEqualTo(PERSONAL); assertWithMessage("Incorrect label returned for user id " + mManagedUser) - .that(userIdToLabelMap.get(UserId.of(mManagedUser))).isEqualTo(WORK); + .that(userIdToLabelMap.get(UserId.of(mManagedUser))) + .isEqualTo(WORK); } @Test public void testGetUserIdToLabelMap_systemUserManagedUserPrivateUser_PostV() { if (!SdkLevel.isAtLeastV()) return; UserId currentUser = UserId.of(mSystemUser); - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); + initializeUserManagerState( + currentUser, Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); if (SdkLevel.isAtLeastT()) { - DevicePolicyResourcesManager devicePolicyResourcesManager = mock( - DevicePolicyResourcesManager.class); + DevicePolicyResourcesManager devicePolicyResourcesManager = + mock(DevicePolicyResourcesManager.class); when(mDevicePolicyManager.getResources()).thenReturn(devicePolicyResourcesManager); - when(devicePolicyResourcesManager.getString(eq(PERSONAL_TAB), any())).thenReturn( - PERSONAL); + when(devicePolicyResourcesManager.getString(eq(PERSONAL_TAB), any())) + .thenReturn(PERSONAL); } UserManager managedUserManager = getUserManagerForManagedUser(); UserManager privateUserManager = getUserManagerForPrivateUser(); @@ -775,45 +879,50 @@ public class UserManagerStateTest { Map<UserId, String> userIdToLabelMap = mUserManagerState.getUserIdToLabelMap(); assertWithMessage("Incorrect label returned for user id " + mSystemUser) - .that(userIdToLabelMap.get(UserId.of(mSystemUser))).isEqualTo(PERSONAL); + .that(userIdToLabelMap.get(UserId.of(mSystemUser))) + .isEqualTo(PERSONAL); assertWithMessage("Incorrect label returned for user id " + mManagedUser) - .that(userIdToLabelMap.get(UserId.of(mManagedUser))).isEqualTo(WORK); + .that(userIdToLabelMap.get(UserId.of(mManagedUser))) + .isEqualTo(WORK); assertWithMessage("Incorrect label returned for user id " + mPrivateUser) - .that(userIdToLabelMap.get(UserId.of(mPrivateUser))).isEqualTo(PRIVATE); + .that(userIdToLabelMap.get(UserId.of(mPrivateUser))) + .isEqualTo(PRIVATE); } @Test public void testGetUserIdToBadgeMap_systemUserManagedUser_PreV() { if (SdkLevel.isAtLeastV()) return; UserId currentUser = UserId.of(mSystemUser); - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser)); + initializeUserManagerState(currentUser, Lists.newArrayList(mSystemUser, mManagedUser)); Drawable workBadge = mock(Drawable.class); Resources resources = mock(Resources.class); when(mMockContext.getResources()).thenReturn(resources); when(mMockContext.getDrawable(R.drawable.ic_briefcase)).thenReturn(workBadge); if (SdkLevel.isAtLeastT()) { - DevicePolicyResourcesManager devicePolicyResourcesManager = mock( - DevicePolicyResourcesManager.class); + DevicePolicyResourcesManager devicePolicyResourcesManager = + mock(DevicePolicyResourcesManager.class); when(mDevicePolicyManager.getResources()).thenReturn(devicePolicyResourcesManager); - when(devicePolicyResourcesManager.getDrawable(eq(WORK_PROFILE_ICON), eq(SOLID_COLORED), - any())).thenReturn(workBadge); + when(devicePolicyResourcesManager.getDrawable( + eq(WORK_PROFILE_ICON), eq(SOLID_COLORED), any())) + .thenReturn(workBadge); } Map<UserId, Drawable> userIdToBadgeMap = mUserManagerState.getUserIdToBadgeMap(); assertWithMessage("There should be no badge present for personal user") - .that(userIdToBadgeMap.containsKey(UserId.of(mSystemUser))).isFalse(); + .that(userIdToBadgeMap.containsKey(UserId.of(mSystemUser))) + .isFalse(); assertWithMessage("Incorrect badge returned for user id " + mManagedUser) - .that(userIdToBadgeMap.get(UserId.of(mManagedUser))).isEqualTo(workBadge); + .that(userIdToBadgeMap.get(UserId.of(mManagedUser))) + .isEqualTo(workBadge); } @Test public void testGetUserIdToBadgeMap_systemUserManagedUserPrivateUser_PostV() { if (!SdkLevel.isAtLeastV()) return; UserId currentUser = UserId.of(mSystemUser); - initializeUserManagerState(currentUser, - Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); + initializeUserManagerState( + currentUser, Lists.newArrayList(mSystemUser, mManagedUser, mPrivateUser)); Drawable workBadge = mock(Drawable.class); Drawable privateBadge = mock(Drawable.class); UserManager managedUserManager = getUserManagerForManagedUser(); @@ -824,19 +933,23 @@ public class UserManagerStateTest { Map<UserId, Drawable> userIdToBadgeMap = mUserManagerState.getUserIdToBadgeMap(); assertWithMessage("There should be no badge present for personal user") - .that(userIdToBadgeMap.get(UserId.of(mSystemUser))).isNull(); + .that(userIdToBadgeMap.get(UserId.of(mSystemUser))) + .isNull(); assertWithMessage("Incorrect badge returned for user id " + mManagedUser) - .that(userIdToBadgeMap.get(UserId.of(mManagedUser))).isEqualTo(workBadge); + .that(userIdToBadgeMap.get(UserId.of(mManagedUser))) + .isEqualTo(workBadge); assertWithMessage("Incorrect badge returned for user id " + mPrivateUser) - .that(userIdToBadgeMap.get(UserId.of(mPrivateUser))).isEqualTo(privateBadge); + .that(userIdToBadgeMap.get(UserId.of(mPrivateUser))) + .isEqualTo(privateBadge); } private void initializeUserManagerState(UserId current, List<UserHandle> usersOnDevice) { when(mMockUserManager.getUserProfiles()).thenReturn(usersOnDevice); TestConfigStore testConfigStore = new TestConfigStore(); testConfigStore.enablePrivateSpaceInPhotoPicker(); - mUserManagerState = new UserManagerState.RuntimeUserManagerState(mMockContext, current, - true, testConfigStore); + mUserManagerState = + new UserManagerState.RuntimeUserManagerState( + mMockContext, current, true, testConfigStore); } private UserManager getUserManagerForManagedUser() { diff --git a/tests/unit/com/android/documentsui/dirlist/MessageTest.java b/tests/unit/com/android/documentsui/dirlist/MessageTest.java index f7f8fe0e6..17ca61eeb 100644 --- a/tests/unit/com/android/documentsui/dirlist/MessageTest.java +++ b/tests/unit/com/android/documentsui/dirlist/MessageTest.java @@ -24,6 +24,7 @@ import static com.android.documentsui.DevicePolicyResources.Strings.WORK_PROFILE import static com.android.documentsui.DevicePolicyResources.Strings.WORK_PROFILE_OFF_ERROR_TITLE; import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.TruthJUnit.assume; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -139,6 +140,7 @@ public final class MessageTest { @Test public void testInflateMessage_updateToCrossProfileNoPermission() { // Make sure this test is running on system user. + assume().that(UserId.CURRENT_USER.isSystem()).isTrue(); Preconditions.checkArgument(UserId.CURRENT_USER.isSystem()); Model.Update error = new Model.Update( new CrossProfileNoPermissionException(), diff --git a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java index 68bb18867..1d6ef1fd6 100644 --- a/tests/unit/com/android/documentsui/files/ActionHandlerTest.java +++ b/tests/unit/com/android/documentsui/files/ActionHandlerTest.java @@ -16,6 +16,7 @@ package com.android.documentsui.files; +import static com.android.documentsui.util.FlagUtils.isUseMaterial3FlagEnabled; import static com.android.documentsui.testing.IntentAsserts.assertHasAction; import static com.android.documentsui.testing.IntentAsserts.assertHasData; import static com.android.documentsui.testing.IntentAsserts.assertHasExtra; @@ -31,6 +32,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertSame; import static org.junit.Assert.assertTrue; import static org.junit.Assume.assumeTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import android.app.Activity; import android.app.DownloadManager; @@ -39,6 +42,10 @@ import android.content.ClipData; import android.content.Intent; import android.net.Uri; import android.os.Parcelable; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.DocumentsContract; import android.provider.DocumentsContract.Path; import android.util.Pair; @@ -59,6 +66,7 @@ import com.android.documentsui.base.DocumentInfo; import com.android.documentsui.base.DocumentStack; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.Shared; +import com.android.documentsui.flags.Flags; import com.android.documentsui.inspector.InspectorActivity; import com.android.documentsui.testing.ClipDatas; import com.android.documentsui.testing.DocumentStackAsserts; @@ -78,11 +86,14 @@ import com.google.common.collect.Lists; import org.junit.Before; import org.junit.Ignore; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; import java.util.ArrayList; import java.util.Arrays; @@ -102,6 +113,10 @@ public class ActionHandlerTest { private TestFeatures mFeatures; private TestConfigStore mTestConfigStore; private boolean refreshAnswer = false; + @Mock private Runnable mMockCloseSelectionBar; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); @Parameter(0) public boolean isPrivateSpaceEnabled; @@ -117,6 +132,7 @@ public class ActionHandlerTest { @Before public void setUp() { + MockitoAnnotations.initMocks(this); mFeatures = new TestFeatures(); mEnv = TestEnv.create(mFeatures); mActivity = TestActivity.create(mEnv); @@ -143,6 +159,14 @@ public class ActionHandlerTest { mEnv.selectDocument(TestEnv.FILE_GIF); } + private void assertSelectionContainerClosed() { + if (isUseMaterial3FlagEnabled()) { + verify(mMockCloseSelectionBar, times(1)).run(); + } else { + assertTrue(mActionModeAddons.finishActionModeCalled); + } + } + @Test public void testOpenSelectedInNewWindow() { mHandler.openSelectedInNewWindow(); @@ -157,9 +181,36 @@ public class ActionHandlerTest { } @Test + @RequiresFlagsDisabled({Flags.FLAG_DESKTOP_FILE_HANDLING_RO}) + public void testOpenFileFlags() { + mHandler.onDocumentOpened(TestEnv.FILE_GIF, + com.android.documentsui.files.ActionHandler.VIEW_TYPE_PREVIEW, + com.android.documentsui.files.ActionHandler.VIEW_TYPE_REGULAR, false); + + int expectedFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_SINGLE_TOP + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION; + Intent actual = mActivity.startActivity.getLastValue(); + assertEquals(expectedFlags, actual.getFlags()); + } + + @Test + @RequiresFlagsEnabled({Flags.FLAG_DESKTOP_FILE_HANDLING_RO}) + public void testOpenFileFlagsDesktop() { + mHandler.onDocumentOpened(TestEnv.FILE_GIF, + com.android.documentsui.files.ActionHandler.VIEW_TYPE_PREVIEW, + com.android.documentsui.files.ActionHandler.VIEW_TYPE_REGULAR, false); + + int expectedFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_ACTIVITY_SINGLE_TOP + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_ACTIVITY_NEW_DOCUMENT + | Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_TASK; + Intent actual = mActivity.startActivity.getLastValue(); + assertEquals(expectedFlags, actual.getFlags()); + } + + @Test public void testSpringOpenDirectory() { mHandler.springOpenDirectory(TestEnv.FOLDER_0); - assertTrue(mActionModeAddons.finishActionModeCalled); + assertSelectionContainerClosed(); assertEquals(TestEnv.FOLDER_0, mEnv.state.stack.peek()); } @@ -214,7 +265,7 @@ public class ActionHandlerTest { mHandler.deleteSelectedDocuments(docs, mEnv.state.stack.peek()); mActivity.startService.assertCalled(); - assertTrue(mActionModeAddons.finishActionModeCalled); + assertSelectionContainerClosed(); } @Test @@ -424,7 +475,26 @@ public class ActionHandlerTest { assertEquals(false, result); } + // Require desktop file handling flag because when it's disabled proguard strips the + // openDocumentViewOnly function because it's not used anywhere reachable by production code. @Test + @RequiresFlagsEnabled({Flags.FLAG_DESKTOP_FILE_HANDLING_RO}) + public void testDocumentContextMenuOpen() throws Exception { + mActivity.resources.setQuickViewerPackage("corptropolis.viewer"); + mActivity.currentRoot = TestProvidersAccess.HOME; + + // Test normal picking (i.e. double click) behaviour will quick view + mHandler.openDocument(TestEnv.FILE_GIF, ActionHandler.VIEW_TYPE_PREVIEW, + ActionHandler.VIEW_TYPE_REGULAR); + mActivity.assertActivityStarted(Intent.ACTION_QUICK_VIEW); + + // And verify open via context menu will view instead + mHandler.openDocumentViewOnly(TestEnv.FILE_GIF); + mActivity.assertActivityStarted(Intent.ACTION_VIEW); + } + + @Test + @RequiresFlagsDisabled({Flags.FLAG_DESKTOP_FILE_HANDLING_RO}) public void testShowChooser() throws Exception { mActivity.currentRoot = TestProvidersAccess.DOWNLOADS; @@ -433,6 +503,18 @@ public class ActionHandlerTest { } @Test + @RequiresFlagsEnabled({Flags.FLAG_DESKTOP_FILE_HANDLING_RO}) + public void testShowChooserDesktop() throws Exception { + mActivity.currentRoot = TestProvidersAccess.DOWNLOADS; + + mHandler.showChooserForDoc(TestEnv.FILE_PDF); + Intent actual = mActivity.startActivity.getLastValue(); + assertEquals(Intent.ACTION_VIEW, actual.getAction()); + assertEquals("ComponentInfo{android/com.android.internal.app.ResolverActivity}", + actual.getComponent().toString()); + } + + @Test public void testInitLocation_LaunchToStackLocation() { DocumentStack path = new DocumentStack(Roots.create("123"), mEnv.model.getDocument("1")); @@ -546,8 +628,8 @@ public class ActionHandlerTest { public void testDragAndDrop_OnReadOnlyRoot() throws Exception { assumeTrue(VersionUtils.isAtLeastS()); RootInfo root = new RootInfo(); // root by default has no SUPPORT_CREATE flag - DragEvent event = DragEvent.obtain(DragEvent.ACTION_DROP, 1, 1, 0, 0, 0, null, null, null, - null, null, true); + DragEvent event = DragEvent.obtain(DragEvent.ACTION_DROP, 1, 1, 0, 0, 0, 0, null, null, + null, null, null, true); assertFalse(mHandler.dropOn(event, root)); } @@ -558,8 +640,8 @@ public class ActionHandlerTest { @Test public void testDragAndDrop_OnLibraryRoot() throws Exception { assumeTrue(VersionUtils.isAtLeastS()); - DragEvent event = DragEvent.obtain(DragEvent.ACTION_DROP, 1, 1, 0, 0, 0, null, null, null, - null, null, true); + DragEvent event = DragEvent.obtain(DragEvent.ACTION_DROP, 1, 1, 0, 0, 0, 0, null, null, + null, null, null, true); assertFalse(mHandler.dropOn(event, TestProvidersAccess.RECENTS)); } @@ -575,8 +657,8 @@ public class ActionHandlerTest { // our Clipper is getting the original CipData passed in. Object localState = new Object(); ClipData clipData = ClipDatas.createTestClipData(); - DragEvent event = DragEvent.obtain(DragEvent.ACTION_DROP, 1, 1, 0, 0, 0, localState, null, - clipData, null, null, true); + DragEvent event = DragEvent.obtain(DragEvent.ACTION_DROP, 1, 1, 0, 0, 0, 0, localState, + null, clipData, null, null, true); mHandler.dropOn(event, TestProvidersAccess.DOWNLOADS); event.recycle(); @@ -657,8 +739,17 @@ public class ActionHandlerTest { } @Test + @RequiresFlagsEnabled({Flags.FLAG_USE_MATERIAL3, Flags.FLAG_USE_PEEK_PREVIEW_RO}) + public void testShowPeek() throws Exception { + mHandler.showPreview(TestEnv.FILE_GIF); + // The inspector activity is not called. + mActivity.startActivity.assertNotCalled(); + } + + @Test + @RequiresFlagsDisabled({Flags.FLAG_USE_PEEK_PREVIEW_RO}) public void testShowInspector() throws Exception { - mHandler.showInspector(TestEnv.FILE_GIF); + mHandler.showPreview(TestEnv.FILE_GIF); mActivity.startActivity.assertCalled(); Intent intent = mActivity.startActivity.getLastValue(); @@ -670,10 +761,11 @@ public class ActionHandlerTest { } @Test + @RequiresFlagsDisabled({Flags.FLAG_USE_PEEK_PREVIEW_RO}) public void testShowInspector_DebugDisabled() throws Exception { mFeatures.debugSupport = false; - mHandler.showInspector(TestEnv.FILE_GIF); + mHandler.showPreview(TestEnv.FILE_GIF); Intent intent = mActivity.startActivity.getLastValue(); assertHasExtra(intent, Shared.EXTRA_SHOW_DEBUG); @@ -681,11 +773,12 @@ public class ActionHandlerTest { } @Test + @RequiresFlagsDisabled({Flags.FLAG_USE_PEEK_PREVIEW_RO}) public void testShowInspector_DebugEnabled() throws Exception { mFeatures.debugSupport = true; DebugFlags.setDocumentDetailsEnabled(true); - mHandler.showInspector(TestEnv.FILE_GIF); + mHandler.showPreview(TestEnv.FILE_GIF); Intent intent = mActivity.startActivity.getLastValue(); assertHasExtra(intent, Shared.EXTRA_SHOW_DEBUG); @@ -694,6 +787,7 @@ public class ActionHandlerTest { } @Test + @RequiresFlagsDisabled({Flags.FLAG_USE_PEEK_PREVIEW_RO}) public void testShowInspector_OverridesRootDocumentName() throws Exception { mActivity.currentRoot = TestProvidersAccess.PICKLES; mEnv.populateStack(); @@ -705,7 +799,7 @@ public class ActionHandlerTest { DocumentInfo rootDoc = mEnv.state.stack.peek(); rootDoc.displayName = "poodles"; - mHandler.showInspector(rootDoc); + mHandler.showPreview(rootDoc); Intent intent = mActivity.startActivity.getLastValue(); assertEquals( TestProvidersAccess.PICKLES.title, @@ -713,6 +807,7 @@ public class ActionHandlerTest { } @Test + @RequiresFlagsDisabled({Flags.FLAG_USE_PEEK_PREVIEW_RO}) public void testShowInspector_OverridesRootDocumentNameX() throws Exception { mActivity.currentRoot = TestProvidersAccess.PICKLES; mEnv.populateStack(); @@ -725,11 +820,28 @@ public class ActionHandlerTest { DocumentInfo rootDoc = mEnv.state.stack.peek(); rootDoc.displayName = "poodles"; - mHandler.showInspector(rootDoc); + mHandler.showPreview(rootDoc); Intent intent = mActivity.startActivity.getLastValue(); assertFalse(intent.getExtras().containsKey(Intent.EXTRA_TITLE)); } + @Test + public void testViewInOwner() { + mEnv.populateStack(); + + mEnv.selectionMgr.clearSelection(); + mEnv.selectDocument(TestEnv.FILE_PNG); + + mHandler.viewInOwner(); + mActivity.assertActivityStarted(DocumentsContract.ACTION_DOCUMENT_SETTINGS); + } + + @Test + public void testOpenSettings() { + mHandler.openSettings(TestProvidersAccess.HAMMY); + mActivity.assertActivityStarted(DocumentsContract.ACTION_DOCUMENT_ROOT_SETTINGS); + } + private void assertRootPicked(Uri expectedUri) throws Exception { mEnv.beforeAsserts(); @@ -748,10 +860,10 @@ public class ActionHandlerTest { mEnv.searchViewManager, mEnv::lookupExecutor, mActionModeAddons, + mMockCloseSelectionBar, mClipper, - null, // clip storage, not utilized unless we venture into *jumbo* clip territory. + null, // clip storage, not utilized unless we venture into *jumbo* clip territory. mDragAndDropManager, - mEnv.injector - ); + mEnv.injector); } } diff --git a/tests/unit/com/android/documentsui/files/MenuManagerTest.java b/tests/unit/com/android/documentsui/files/MenuManagerTest.java index 02988d62f..ac7b1c4f7 100644 --- a/tests/unit/com/android/documentsui/files/MenuManagerTest.java +++ b/tests/unit/com/android/documentsui/files/MenuManagerTest.java @@ -16,11 +16,17 @@ package com.android.documentsui.files; +import static com.android.documentsui.util.FlagUtils.isZipNgFlagEnabled; + import static junit.framework.Assert.assertEquals; import static org.junit.Assert.assertTrue; import android.net.Uri; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.DocumentsContract.Document; import android.provider.DocumentsContract.Root; @@ -35,6 +41,7 @@ import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.State; import com.android.documentsui.base.UserId; import com.android.documentsui.dirlist.TestData; +import com.android.documentsui.flags.Flags; import com.android.documentsui.testing.TestDirectoryDetails; import com.android.documentsui.testing.TestEnv; import com.android.documentsui.testing.TestFeatures; @@ -45,6 +52,7 @@ import com.android.documentsui.testing.TestSearchViewManager; import com.android.documentsui.testing.TestSelectionDetails; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -105,6 +113,7 @@ public final class MenuManagerTest { private TestMenuItem optionSort; private TestMenuItem mOptionLauncher; private TestMenuItem mOptionShowHiddenFiles; + private TestMenuItem mOptionExtractAll; /* Sub Option Menu items */ private TestMenuItem subOptionGrid; @@ -123,6 +132,9 @@ public final class MenuManagerTest { private int mFilesCount; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() { testMenu = TestMenu.create(); @@ -176,6 +188,7 @@ public final class MenuManagerTest { optionSort = testMenu.findItem(R.id.option_menu_sort); mOptionLauncher = testMenu.findItem(R.id.option_menu_launcher); mOptionShowHiddenFiles = testMenu.findItem(R.id.option_menu_show_hidden_files); + mOptionExtractAll = testMenu.findItem(R.id.option_menu_extract_all); // Menu actions on root title row. subOptionGrid = testMenu.findItem(R.id.sub_menu_grid); @@ -244,6 +257,7 @@ public final class MenuManagerTest { actionModeSort.assertEnabledAndVisible(); actionModeSelectAll.assertEnabledAndVisible(); mActionModeDeselectAll.assertDisabledAndInvisible(); + mOptionExtractAll.assertDisabledAndInvisible(); } @Test @@ -259,6 +273,7 @@ public final class MenuManagerTest { actionModeExtractTo.assertDisabledAndInvisible(); actionModeMoveTo.assertDisabledAndInvisible(); actionModeViewInOwner.assertDisabledAndInvisible(); + mOptionExtractAll.assertDisabledAndInvisible(); } @Test @@ -388,7 +403,7 @@ public final class MenuManagerTest { @Test public void testActionMenu_CanOpenWith() { - selectionDetails.canOpenWith = true; + selectionDetails.canOpen = true; mgr.updateActionMenu(testMenu, selectionDetails); actionModeOpenWith.assertEnabledAndVisible(); @@ -396,7 +411,7 @@ public final class MenuManagerTest { @Test public void testActionMenu_NoOpenWith() { - selectionDetails.canOpenWith = false; + selectionDetails.canOpen = false; mgr.updateActionMenu(testMenu, selectionDetails); actionModeOpenWith.assertDisabledAndInvisible(); @@ -476,6 +491,20 @@ public final class MenuManagerTest { } @Test + public void testOptionMenu_ExtractAll() { + dirDetails.isInArchive = true; + mgr.updateOptionMenu(testMenu); + if (isZipNgFlagEnabled()) { + mOptionExtractAll.assertEnabledAndVisible(); + } else { + mOptionExtractAll.assertDisabledAndInvisible(); + } + dirDetails.isInArchive = false; + mgr.updateOptionMenu(testMenu); + mOptionExtractAll.assertDisabledAndInvisible(); + } + + @Test public void testInflateContextMenu_Files() { TestMenuInflater inflater = new TestMenuInflater(); @@ -590,16 +619,28 @@ public final class MenuManagerTest { } @Test - public void testContextMenu_OnFile_CanOpenWith() { - selectionDetails.canOpenWith = true; + @RequiresFlagsDisabled({Flags.FLAG_DESKTOP_FILE_HANDLING_RO}) + public void testContextMenu_OnFile_CanOpen() { + selectionDetails.canOpen = true; mgr.updateContextMenuForFiles(testMenu, selectionDetails); + dirOpen.assertDisabledAndInvisible(); dirOpenWith.assertEnabledAndVisible(); } @Test - public void testContextMenu_OnFile_NoOpenWith() { - selectionDetails.canOpenWith = false; + @RequiresFlagsEnabled({Flags.FLAG_DESKTOP_FILE_HANDLING_RO}) + public void testContextMenu_OnFile_CanOpenDesktop() { + selectionDetails.canOpen = true; mgr.updateContextMenuForFiles(testMenu, selectionDetails); + dirOpen.assertEnabledAndVisible(); + dirOpenWith.assertEnabledAndVisible(); + } + + @Test + public void testContextMenu_OnFile_NoOpen() { + selectionDetails.canOpen = false; + mgr.updateContextMenuForFiles(testMenu, selectionDetails); + dirOpen.assertDisabledAndInvisible(); dirOpenWith.assertDisabledAndInvisible(); } diff --git a/tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java b/tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java index 14c86e69f..5adaa4d65 100644 --- a/tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java +++ b/tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java @@ -142,7 +142,7 @@ public class QuickViewIntentBuilderTest { public void testBuild_twoProfiles_containsOnlyPreviewDocument() { mEnv.model.reset(); mEnv.model.createDocumentForUser("a.txt", "text/plain", 0, - TestProvidersAccess.OtherUser.USER_ID); + System.currentTimeMillis(), TestProvidersAccess.OtherUser.USER_ID); DocumentInfo previewDoc = mEnv.model.createFile("b.png", 0); mEnv.model.createFile("c.png", 0); mEnv.model.update(); diff --git a/tests/unit/com/android/documentsui/loaders/BaseLoaderTest.kt b/tests/unit/com/android/documentsui/loaders/BaseLoaderTest.kt new file mode 100644 index 000000000..55f83bfea --- /dev/null +++ b/tests/unit/com/android/documentsui/loaders/BaseLoaderTest.kt @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2024 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.documentsui.loaders + +import android.os.Bundle +import android.os.Parcel +import android.provider.DocumentsContract +import com.android.documentsui.DirectoryResult +import com.android.documentsui.TestActivity +import com.android.documentsui.TestConfigStore +import com.android.documentsui.base.DocumentInfo +import com.android.documentsui.base.UserId +import com.android.documentsui.sorting.SortModel +import com.android.documentsui.testing.ActivityManagers +import com.android.documentsui.testing.TestEnv +import com.android.documentsui.testing.TestModel +import com.android.documentsui.testing.UserManagers +import java.time.Duration +import java.util.Locale +import kotlin.time.Duration.Companion.hours +import org.junit.Before + +/** + * Returns the number of matched files, or -1. + */ +fun getFileCount(result: DirectoryResult?) = result?.cursor?.count ?: -1 + +/** + * A data class that holds parameters that can be varied for the loader test. The last + * value, expectedCount, can be used for simple tests that check that the number of + * returned files matches the expectations. + */ +data class LoaderTestParams( + // A query, matched against file names. May be empty. + val query: String, + // The delta from now that indicates maximum age of matched files. + val lastModifiedDelta: Duration?, + // The extra arguments typically supplied by search view manager. + val otherArgs: Bundle, + // The number of files that are expected, for the above parameters, to be found by a loader. + val expectedCount: Int, +) + +/** + * Common base class for search and folder loaders. + */ +open class BaseLoaderTest { + lateinit var mEnv: TestEnv + lateinit var mActivity: TestActivity + lateinit var mTestConfigStore: TestConfigStore + + @Before + open fun setUp() { + mEnv = TestEnv.create() + mTestConfigStore = TestConfigStore() + mEnv.state.configStore = mTestConfigStore + mEnv.state.showHiddenFiles = false + val parcel = Parcel.obtain() + mEnv.state.sortModel = SortModel.CREATOR.createFromParcel(parcel) + + mActivity = TestActivity.create(mEnv) + mActivity.activityManager = ActivityManagers.create(false) + mActivity.userManager = UserManagers.create() + } + + /** + * Creates a text, PNG, MP4 and MPG files named sample-000x, for x in 0 .. count - 1. + * Each file gets a matching extension. The 0th file is modified 1h, 1st 2 hours, .. etc., ago. + */ + fun createDocuments(count: Int): Array<DocumentInfo> { + val extensionList = arrayOf("txt", "png", "mp4", "mpg") + val now = System.currentTimeMillis() + val flags = (DocumentsContract.Document.FLAG_SUPPORTS_WRITE + or DocumentsContract.Document.FLAG_SUPPORTS_DELETE + or DocumentsContract.Document.FLAG_SUPPORTS_RENAME) + return Array<DocumentInfo>(count) { i -> + val id = String.format(Locale.US, "%05d", i) + val name = "sample-$id.${extensionList[i % extensionList.size]}" + mEnv.model.createDocumentForUser( + name, + TestModel.guessMimeType(name), + flags, + now - 1.hours.inWholeMilliseconds * i, + UserId.DEFAULT_USER + ) + } + } +} diff --git a/tests/unit/com/android/documentsui/loaders/FolderLoaderTest.kt b/tests/unit/com/android/documentsui/loaders/FolderLoaderTest.kt new file mode 100644 index 000000000..44c410eff --- /dev/null +++ b/tests/unit/com/android/documentsui/loaders/FolderLoaderTest.kt @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2024 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.documentsui.loaders + +import android.os.Bundle +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import androidx.test.filters.SmallTest +import com.android.documentsui.ContentLock +import com.android.documentsui.base.DocumentInfo +import com.android.documentsui.flags.Flags.FLAG_USE_SEARCH_V2_RW +import com.android.documentsui.testing.TestFileTypeLookup +import com.android.documentsui.testing.TestProvidersAccess +import java.time.Duration +import junit.framework.Assert.assertEquals +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +private const val TOTAL_FILE_COUNT = 10 + +@RunWith(Parameterized::class) +@SmallTest +class FolderLoaderTest(private val testParams: LoaderTestParams) : BaseLoaderTest() { + companion object { + @JvmStatic + @Parameters(name = "with parameters {0}") + fun data() = listOf( + LoaderTestParams("", null, Bundle(), TOTAL_FILE_COUNT), + // The first file is at NOW, the second at NOW - 1h, etc. + LoaderTestParams("", Duration.ofMinutes(1L), Bundle(), 1), + LoaderTestParams("", Duration.ofMinutes(60L + 1), Bundle(), 2), + LoaderTestParams( + "", + Duration.ofMinutes(TOTAL_FILE_COUNT * 60L + 1), + Bundle(), + TOTAL_FILE_COUNT + ), + ) + } + + @get:Rule + val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + @Test + @RequiresFlagsEnabled(FLAG_USE_SEARCH_V2_RW) + fun testLoadInBackground() { + val mockProvider = mEnv.mockProviders[TestProvidersAccess.DOWNLOADS.authority] + val docs = createDocuments(TOTAL_FILE_COUNT) + mockProvider!!.setNextChildDocumentsReturns(*docs) + val userIds = listOf(TestProvidersAccess.DOWNLOADS.userId) + val queryOptions = + QueryOptions( + TOTAL_FILE_COUNT, + testParams.lastModifiedDelta, + null, + true, + arrayOf<String>("*/*"), + testParams.otherArgs, + ) + val contentLock = ContentLock() + // TODO(majewski): Is there a better way to create Downloads root folder DocumentInfo? + val rootFolderInfo = DocumentInfo() + rootFolderInfo.authority = TestProvidersAccess.DOWNLOADS.authority + rootFolderInfo.userId = userIds[0] + + val loader = + FolderLoader( + mActivity, + userIds, + TestFileTypeLookup(), + contentLock, + TestProvidersAccess.DOWNLOADS, + rootFolderInfo, + queryOptions, + mEnv.state.sortModel + ) + val directoryResult = loader.loadInBackground() + assertEquals(testParams.expectedCount, getFileCount(directoryResult)) + } +} diff --git a/tests/unit/com/android/documentsui/loaders/SearchLoaderTest.kt b/tests/unit/com/android/documentsui/loaders/SearchLoaderTest.kt new file mode 100644 index 000000000..e480337ab --- /dev/null +++ b/tests/unit/com/android/documentsui/loaders/SearchLoaderTest.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2024 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.documentsui.loaders + +import android.os.Bundle +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.provider.DocumentsContract +import androidx.test.filters.SmallTest +import com.android.documentsui.ContentLock +import com.android.documentsui.LockingContentObserver +import com.android.documentsui.base.DocumentInfo +import com.android.documentsui.flags.Flags.FLAG_USE_SEARCH_V2_RW +import com.android.documentsui.testing.TestFileTypeLookup +import com.android.documentsui.testing.TestProvidersAccess +import java.time.Duration +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import junit.framework.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameters + +private const val TOTAL_FILE_COUNT = 8 + +fun createQueryArgs(vararg mimeTypes: String): Bundle { + val args = Bundle() + args.putStringArray(DocumentsContract.QUERY_ARG_MIME_TYPES, arrayOf<String>(*mimeTypes)) + return args +} + +@RunWith(Parameterized::class) +@SmallTest +class SearchLoaderTest(private val testParams: LoaderTestParams) : BaseLoaderTest() { + lateinit var mExecutor: ExecutorService + val mContentLock = ContentLock() + val mContentObserver = LockingContentObserver(mContentLock) {} + + companion object { + @JvmStatic + @Parameters(name = "with parameters {0}") + fun data() = listOf( + LoaderTestParams("sample", null, Bundle(), TOTAL_FILE_COUNT), + LoaderTestParams("txt", null, Bundle(), 2), + LoaderTestParams("foozig", null, Bundle(), 0), + // The first file is at NOW, the second at NOW - 1h; expect 2. + LoaderTestParams("sample", Duration.ofMinutes(60 + 1), Bundle(), 2), + LoaderTestParams("sample", null, createQueryArgs("image/*"), 2), + LoaderTestParams("sample", null, createQueryArgs("image/*", "video/*"), 6), + LoaderTestParams("sample", null, createQueryArgs("application/pdf"), 0), + ) + } + + @get:Rule + val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + @Before + override fun setUp() { + super.setUp() + mExecutor = Executors.newSingleThreadExecutor() + } + + @Test + @RequiresFlagsEnabled(FLAG_USE_SEARCH_V2_RW) + fun testLoadInBackground() { + val mockProvider = mEnv.mockProviders[TestProvidersAccess.DOWNLOADS.authority] + val docs = createDocuments(TOTAL_FILE_COUNT) + mockProvider!!.setNextChildDocumentsReturns(*docs) + val userIds = listOf(TestProvidersAccess.DOWNLOADS.userId) + val queryOptions = + QueryOptions( + TOTAL_FILE_COUNT + 1, + testParams.lastModifiedDelta, + null, + true, + arrayOf("*/*"), + testParams.otherArgs, + ) + val rootIds = listOf(TestProvidersAccess.DOWNLOADS) + + // TODO(majewski): Is there a better way to create Downloads root folder DocumentInfo? + val rootFolderInfo = DocumentInfo() + rootFolderInfo.authority = TestProvidersAccess.DOWNLOADS.authority + rootFolderInfo.userId = userIds[0] + + val loader = + SearchLoader( + mActivity, + userIds, + TestFileTypeLookup(), + mContentObserver, + rootIds, + testParams.query, + queryOptions, + mEnv.state.sortModel, + mExecutor, + ) + val directoryResult = loader.loadInBackground() + assertEquals(testParams.expectedCount, getFileCount(directoryResult)) + } + + @Test + @RequiresFlagsEnabled(FLAG_USE_SEARCH_V2_RW) + @Ignore("b/397095797") + fun testBlankQueryAndRecency() { + val userIds = listOf(TestProvidersAccess.DOWNLOADS.userId) + val rootIds = listOf(TestProvidersAccess.DOWNLOADS) + val noLastModifiedQueryOptions = + QueryOptions(10, null, null, true, arrayOf("*/*"), Bundle()) + + // Blank query and no last modified duration is invalid. + assertThrows(IllegalArgumentException::class.java) { + SearchLoader( + mActivity, + userIds, + TestFileTypeLookup(), + mContentObserver, + rootIds, + "", + noLastModifiedQueryOptions, + mEnv.state.sortModel, + mExecutor, + ) + } + + // Null query and no last modified duration is invalid. + assertThrows(IllegalArgumentException::class.java) { + SearchLoader( + mActivity, + userIds, + TestFileTypeLookup(), + mContentObserver, + rootIds, + null, + noLastModifiedQueryOptions, + mEnv.state.sortModel, + mExecutor, + ) + } + } +} diff --git a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java index 166e48e7e..d3e6e90d0 100644 --- a/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java +++ b/tests/unit/com/android/documentsui/picker/ActionHandlerTest.java @@ -223,6 +223,7 @@ public class ActionHandlerTest { @Test public void testInitLocation_RestoresLastAccessedStack() throws Exception { + if (!SdkLevel.isAtLeastS()) return; final DocumentStack stack = new DocumentStack(TestProvidersAccess.HAMMY, TestEnv.FOLDER_0, TestEnv.FOLDER_1); mLastAccessed.setLastAccessed(mActivity, stack); @@ -644,7 +645,8 @@ public class ActionHandlerTest { mActivity.currentRoot = TestProvidersAccess.OtherUser.DOWNLOADS; mEnv.model.reset(); DocumentInfo otherUserDoc = mEnv.model.createDocumentForUser("a.png", - "image/png", /* flags= */ 0, TestProvidersAccess.OtherUser.USER_ID); + "image/png", /* flags= */ 0, System.currentTimeMillis(), + TestProvidersAccess.OtherUser.USER_ID); mEnv.model.update(); mHandler.onDocumentOpened(otherUserDoc, ActionHandler.VIEW_TYPE_PREVIEW, diff --git a/tests/unit/com/android/documentsui/picker/MenuManagerTest.java b/tests/unit/com/android/documentsui/picker/MenuManagerTest.java index cf9699a3b..266489d61 100644 --- a/tests/unit/com/android/documentsui/picker/MenuManagerTest.java +++ b/tests/unit/com/android/documentsui/picker/MenuManagerTest.java @@ -61,6 +61,7 @@ public final class MenuManagerTest { private TestMenuItem dirCutToClipboard; private TestMenuItem dirCopyToClipboard; private TestMenuItem dirPasteFromClipboard; + private TestMenuItem mDirCompress; private TestMenuItem dirCreateDir; private TestMenuItem dirSelectAll; private TestMenuItem mDirDeselectAll; @@ -102,6 +103,7 @@ public final class MenuManagerTest { private TestMenuItem optionSort; private TestMenuItem mOptionLauncher; private TestMenuItem mOptionShowHiddenFiles; + private TestMenuItem mOptionExtractAll; private TestMenuItem subOptionGrid; private TestMenuItem subOptionList; @@ -124,6 +126,7 @@ public final class MenuManagerTest { dirOpenWith = testMenu.findItem(R.id.dir_menu_open_with); dirCutToClipboard = testMenu.findItem(R.id.dir_menu_cut_to_clipboard); dirCopyToClipboard = testMenu.findItem(R.id.dir_menu_copy_to_clipboard); + mDirCompress = testMenu.findItem(R.id.dir_menu_compress); dirPasteFromClipboard = testMenu.findItem(R.id.dir_menu_paste_from_clipboard); dirCreateDir = testMenu.findItem(R.id.dir_menu_create_dir); dirSelectAll = testMenu.findItem(R.id.dir_menu_select_all); @@ -162,6 +165,7 @@ public final class MenuManagerTest { optionSort = testMenu.findItem(R.id.option_menu_sort); mOptionLauncher = testMenu.findItem(R.id.option_menu_launcher); mOptionShowHiddenFiles = testMenu.findItem(R.id.option_menu_show_hidden_files); + mOptionExtractAll = testMenu.findItem(R.id.option_menu_extract_all); // Menu actions on root title row. subOptionGrid = testMenu.findItem(R.id.sub_menu_grid); @@ -195,6 +199,7 @@ public final class MenuManagerTest { mActionModeDeselectAll.assertDisabledAndInvisible(); actionModeViewInOwner.assertDisabledAndInvisible(); actionModeSort.assertEnabledAndVisible(); + mOptionExtractAll.assertDisabledAndInvisible(); } @Test @@ -268,10 +273,21 @@ public final class MenuManagerTest { optionSort.assertEnabledAndVisible(); mOptionLauncher.assertDisabledAndInvisible(); mOptionShowHiddenFiles.assertEnabledAndVisible(); + mOptionExtractAll.assertDisabledAndInvisible(); assertTrue(testSearchManager.showMenuCalled()); } @Test + public void testOptionMenu_ExtractAll() { + dirDetails.isInArchive = true; + mgr.updateOptionMenu(testMenu); + mOptionExtractAll.assertDisabledAndInvisible(); + dirDetails.isInArchive = false; + mgr.updateOptionMenu(testMenu); + mOptionExtractAll.assertDisabledAndInvisible(); + } + + @Test public void testOptionMenu_notPicking() { state.action = ACTION_OPEN; state.derivedMode = State.MODE_LIST; @@ -281,6 +297,7 @@ public final class MenuManagerTest { optionCreateDir.assertDisabledAndInvisible(); subOptionGrid.assertEnabledAndVisible(); subOptionList.assertDisabledAndInvisible(); + mOptionExtractAll.assertDisabledAndInvisible(); assertFalse(testSearchManager.showMenuCalled()); } @@ -300,6 +317,7 @@ public final class MenuManagerTest { subOptionGrid.assertDisabledAndInvisible(); subOptionList.assertDisabledAndInvisible(); + mOptionExtractAll.assertDisabledAndInvisible(); } @@ -402,6 +420,7 @@ public final class MenuManagerTest { dirOpenWith.assertDisabledAndInvisible(); dirCutToClipboard.assertDisabledAndInvisible(); dirCopyToClipboard.assertEnabledAndVisible(); + mDirCompress.assertDisabledAndInvisible(); dirRename.assertDisabledAndInvisible(); dirDelete.assertDisabledAndInvisible(); } @@ -414,6 +433,7 @@ public final class MenuManagerTest { dirOpenInNewWindow.assertDisabledAndInvisible(); dirCutToClipboard.assertDisabledAndInvisible(); dirCopyToClipboard.assertEnabledAndVisible(); + mDirCompress.assertDisabledAndInvisible(); // Doesn't matter if directory is selected, we don't want pasteInto for PickerActivity dirPasteIntoFolder.assertDisabledAndInvisible(); dirRename.assertDisabledAndInvisible(); @@ -429,6 +449,7 @@ public final class MenuManagerTest { mgr.updateContextMenu(testMenu, selectionDetails); dirCutToClipboard.assertEnabledAndVisible(); dirCopyToClipboard.assertEnabledAndVisible(); + mDirCompress.assertDisabledAndInvisible(); dirDelete.assertEnabledAndVisible(); } @@ -442,6 +463,7 @@ public final class MenuManagerTest { mgr.updateContextMenu(testMenu, selectionDetails); dirCutToClipboard.assertDisabledAndInvisible(); dirCopyToClipboard.assertDisabledAndInvisible(); + mDirCompress.assertDisabledAndInvisible(); dirDelete.assertEnabledAndVisible(); } @@ -454,6 +476,7 @@ public final class MenuManagerTest { mgr.updateContextMenu(testMenu, selectionDetails); dirCutToClipboard.assertDisabledAndInvisible(); dirCopyToClipboard.assertEnabledAndVisible(); + mDirCompress.assertDisabledAndInvisible(); dirDelete.assertDisabledAndInvisible(); } diff --git a/tests/unit/com/android/documentsui/queries/SearchChipViewManagerTest.java b/tests/unit/com/android/documentsui/queries/SearchChipViewManagerTest.java index da83c38fa..6d20447dd 100644 --- a/tests/unit/com/android/documentsui/queries/SearchChipViewManagerTest.java +++ b/tests/unit/com/android/documentsui/queries/SearchChipViewManagerTest.java @@ -18,25 +18,40 @@ package com.android.documentsui.queries; import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertTrue; import static org.mockito.Mockito.spy; +import static java.util.Objects.requireNonNull; + import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; import android.os.Bundle; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.DocumentsContract; import android.view.View; import android.view.ViewGroup; import android.widget.LinearLayout; +import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; +import com.android.documentsui.IconUtils; import com.android.documentsui.R; import com.android.documentsui.base.MimeTypes; import com.android.documentsui.base.Shared; +import com.android.documentsui.flags.Flags; import com.android.documentsui.util.VersionUtils; +import com.google.android.material.chip.Chip; + import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -62,6 +77,9 @@ public final class SearchChipViewManagerTest { private SearchChipViewManager mSearchChipViewManager; private LinearLayout mChipGroup; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() { mContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); @@ -83,6 +101,56 @@ public final class SearchChipViewManagerTest { } @Test + @RequiresFlagsEnabled({Flags.FLAG_USE_MATERIAL3}) + public void testChipIcon() { + mSearchChipViewManager.initChipSets( + new String[] {"image/*", "audio/*", "video/*", "text/*"}); + mSearchChipViewManager.updateChips(new String[] {"*/*"}); + + Chip imageChip = (Chip) mChipGroup.getChildAt(0); + assertDrawablesEqual( + requireNonNull(imageChip.getChipIcon()), + requireNonNull(mContext.getDrawable(R.drawable.ic_chip_image))); + Chip audioChip = (Chip) mChipGroup.getChildAt(1); + assertDrawablesEqual( + requireNonNull(audioChip.getChipIcon()), + requireNonNull(mContext.getDrawable(R.drawable.ic_chip_audio))); + Chip videoChip = (Chip) mChipGroup.getChildAt(2); + assertDrawablesEqual( + requireNonNull(videoChip.getChipIcon()), + requireNonNull(mContext.getDrawable(R.drawable.ic_chip_video))); + Chip documentChip = (Chip) mChipGroup.getChildAt(3); + assertDrawablesEqual( + requireNonNull(documentChip.getChipIcon()), + requireNonNull(mContext.getDrawable(R.drawable.ic_chip_document))); + } + + @Test + @RequiresFlagsDisabled({Flags.FLAG_USE_MATERIAL3}) + public void testChipIcon_M3Disabled() { + mSearchChipViewManager.initChipSets( + new String[] {"image/*", "audio/*", "video/*", "text/*"}); + mSearchChipViewManager.updateChips(new String[] {"*/*"}); + + Chip imageChip = (Chip) mChipGroup.getChildAt(0); + assertDrawablesEqual( + requireNonNull(imageChip.getChipIcon()), + requireNonNull(IconUtils.loadMimeIcon(mContext, "image/*"))); + Chip audioChip = (Chip) mChipGroup.getChildAt(1); + assertDrawablesEqual( + requireNonNull(audioChip.getChipIcon()), + requireNonNull(IconUtils.loadMimeIcon(mContext, "audio/*"))); + Chip videoChip = (Chip) mChipGroup.getChildAt(2); + assertDrawablesEqual( + requireNonNull(videoChip.getChipIcon()), + requireNonNull(IconUtils.loadMimeIcon(mContext, "video/*"))); + Chip documentChip = (Chip) mChipGroup.getChildAt(3); + assertDrawablesEqual( + requireNonNull(documentChip.getChipIcon()), + requireNonNull(IconUtils.loadMimeIcon(mContext, MimeTypes.GENERIC_TYPE))); + } + + @Test public void testUpdateChips_hasCorrectChipCount() throws Exception { mSearchChipViewManager.updateChips(TEST_MIME_TYPES); @@ -172,4 +240,26 @@ public final class SearchChipViewManagerTest { chipDataList.add(new SearchChipData(CHIP_TYPE, 0 /* titleRes */, TEST_MIME_TYPES)); return chipDataList; } + + private void assertDrawablesEqual(Drawable actual, Drawable expected) { + Bitmap bitmap1 = + Bitmap.createBitmap( + actual.getIntrinsicWidth(), + actual.getIntrinsicHeight(), + Bitmap.Config.ARGB_8888); + Canvas canvas1 = new Canvas(bitmap1); + actual.setBounds(0, 0, actual.getIntrinsicWidth(), actual.getIntrinsicHeight()); + actual.draw(canvas1); + + Bitmap bitmap2 = + Bitmap.createBitmap( + expected.getIntrinsicWidth(), + expected.getIntrinsicHeight(), + Bitmap.Config.ARGB_8888); + Canvas canvas2 = new Canvas(bitmap2); + expected.setBounds(0, 0, expected.getIntrinsicWidth(), expected.getIntrinsicHeight()); + expected.draw(canvas2); + + assertTrue("Drawables are not equal", bitmap1.sameAs(bitmap2)); + } } diff --git a/tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java b/tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java index 6a7b86415..26512cc22 100644 --- a/tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java +++ b/tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java @@ -24,7 +24,12 @@ import static org.mockito.Mockito.when; import android.app.admin.DevicePolicyManager; import android.content.Context; +import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.test.filters.MediumTest; import androidx.test.platform.app.InstrumentationRegistry; @@ -33,6 +38,7 @@ import com.android.documentsui.TestConfigStore; import com.android.documentsui.TestUserManagerState; import com.android.documentsui.base.RootInfo; import com.android.documentsui.base.UserId; +import com.android.documentsui.flags.Flags; import com.android.documentsui.testing.TestEnv; import com.android.documentsui.testing.TestProvidersAccess; import com.android.documentsui.testing.TestResolveInfo; @@ -41,6 +47,7 @@ import com.android.modules.utils.build.SdkLevel; import com.google.android.collect.Lists; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -59,13 +66,14 @@ import java.util.List; public class RootsFragmentTest { private Context mContext; + private PackageManager mPackageManager; private DevicePolicyManager mDevicePolicyManager; private RootsFragment mRootsFragment; private TestEnv mEnv; private final TestConfigStore mTestConfigStore = new TestConfigStore(); private TestUserManagerState mTestUserManagerState; - private static final String[] EXPECTED_SORTED_RESULT = { + private static final String[] EXPECTED_SORTED_RESULT_FOR_NON_DESKTOP = { TestProvidersAccess.RECENTS.title, TestProvidersAccess.IMAGE.title, TestProvidersAccess.VIDEO.title, @@ -79,6 +87,19 @@ public class RootsFragmentTest { TestProvidersAccess.INSPECTOR.title, TestProvidersAccess.PICKLES.title}; + private static final String[] EXPECTED_SORTED_RESULT_FOR_DESKTOP = { + TestProvidersAccess.RECENTS.title, + TestProvidersAccess.DOWNLOADS.title, + "" /* SpacerItem */, + TestProvidersAccess.EXTERNALSTORAGE.title, + TestProvidersAccess.HAMMY.title, + "" /* SpacerItem */, + TestProvidersAccess.INSPECTOR.title, + TestProvidersAccess.PICKLES.title}; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Parameter(0) public boolean isPrivateSpaceEnabled; @@ -98,12 +119,14 @@ public class RootsFragmentTest { mContext = mock(Context.class); mDevicePolicyManager = mock(DevicePolicyManager.class); + mPackageManager = mock(PackageManager.class); when(mContext.getResources()).thenReturn( InstrumentationRegistry.getInstrumentation().getTargetContext().getResources()); when(mContext.getSystemService(Context.DEVICE_POLICY_SERVICE)) .thenReturn(mDevicePolicyManager); when(mContext.getApplicationContext()).thenReturn( InstrumentationRegistry.getInstrumentation().getTargetContext()); + when(mContext.getPackageManager()).thenReturn(mPackageManager); if (SdkLevel.isAtLeastS() && isPrivateSpaceEnabled) { mTestConfigStore.enablePrivateSpaceInPhotoPicker(); @@ -115,7 +138,38 @@ public class RootsFragmentTest { } @Test - public void testSortLoadResult_WithCorrectOrder() { + @RequiresFlagsDisabled({Flags.FLAG_HIDE_ROOTS_ON_DESKTOP_RO}) + public void testSortLoadResult_WithCorrectOrder_hideRootsOnDesktopFlagDisable() { + List<Item> items = mRootsFragment.sortLoadResult( + mContext, + mEnv.state, + createFakeRootInfoList(), + null /* excludePackage */, null /* handlerAppIntent */, new TestProvidersAccess(), + UserId.DEFAULT_USER, + Collections.singletonList(UserId.DEFAULT_USER), + /* maybeShowBadge */ false, mTestUserManagerState); + assertTrue(assertSortedResult(items, EXPECTED_SORTED_RESULT_FOR_NON_DESKTOP)); + } + + @Test + @RequiresFlagsEnabled({Flags.FLAG_HIDE_ROOTS_ON_DESKTOP_RO}) + public void testSortLoadResult_WithCorrectOrder_onNonDesktop() { + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC)).thenReturn(false); + List<Item> items = mRootsFragment.sortLoadResult( + mContext, + mEnv.state, + createFakeRootInfoList(), + null /* excludePackage */, null /* handlerAppIntent */, new TestProvidersAccess(), + UserId.DEFAULT_USER, + Collections.singletonList(UserId.DEFAULT_USER), + /* maybeShowBadge */ false, mTestUserManagerState); + assertTrue(assertSortedResult(items, EXPECTED_SORTED_RESULT_FOR_NON_DESKTOP)); + } + + @Test + @RequiresFlagsEnabled({Flags.FLAG_HIDE_ROOTS_ON_DESKTOP_RO}) + public void testSortLoadResult_WithCorrectOrder_onDesktop() { + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_PC)).thenReturn(true); List<Item> items = mRootsFragment.sortLoadResult( mContext, mEnv.state, @@ -124,7 +178,7 @@ public class RootsFragmentTest { UserId.DEFAULT_USER, Collections.singletonList(UserId.DEFAULT_USER), /* maybeShowBadge */ false, mTestUserManagerState); - assertTrue(assertSortedResult(items)); + assertTrue(assertSortedResult(items, EXPECTED_SORTED_RESULT_FOR_DESKTOP)); } @Test @@ -169,13 +223,13 @@ public class RootsFragmentTest { assertEquals(rootList.get(2).title, TestProvidersAccess.PICKLES.title); } - private boolean assertSortedResult(List<Item> items) { + private boolean assertSortedResult(List<Item> items, String[] expectedSortedResult) { for (int i = 0; i < items.size(); i++) { Item item = items.get(i); if (item instanceof RootItem) { - assertEquals(EXPECTED_SORTED_RESULT[i], ((RootItem) item).root.title); + assertEquals(expectedSortedResult[i], ((RootItem) item).root.title); } else if (item instanceof SpacerItem) { - assertTrue(EXPECTED_SORTED_RESULT[i].isEmpty()); + assertTrue(expectedSortedResult[i].isEmpty()); } else { return false; } diff --git a/tests/unit/com/android/documentsui/ui/MessageBuilderTest.kt b/tests/unit/com/android/documentsui/ui/MessageBuilderTest.kt new file mode 100644 index 000000000..63ff80dad --- /dev/null +++ b/tests/unit/com/android/documentsui/ui/MessageBuilderTest.kt @@ -0,0 +1,234 @@ +/* + * Copyright (C) 2025 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.documentsui.ui + +import android.content.Context +import android.content.res.Resources +import android.net.Uri +import android.provider.DocumentsContract.Document.MIME_TYPE_DIR +import androidx.test.filters.SmallTest +import com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_CONVERTED +import com.android.documentsui.OperationDialogFragment.DIALOG_TYPE_FAILURE +import com.android.documentsui.R +import com.android.documentsui.base.DocumentInfo +import com.android.documentsui.services.FileOperationService.OPERATION_COMPRESS +import com.android.documentsui.services.FileOperationService.OPERATION_COPY +import com.android.documentsui.services.FileOperationService.OPERATION_DELETE +import com.android.documentsui.services.FileOperationService.OPERATION_EXTRACT +import com.android.documentsui.services.FileOperationService.OPERATION_MOVE +import com.android.documentsui.services.FileOperationService.OPERATION_UNKNOWN +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Suite +import org.junit.runners.Suite.SuiteClasses +import org.mockito.ArgumentMatchers.anyString +import org.mockito.Mock +import org.mockito.Mockito.eq +import org.mockito.Mockito.`when` as whenever +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(Suite::class) +@SuiteClasses( + MessageBuilderTest.GenerateDeleteMessage::class, + MessageBuilderTest.GenerateListMessage::class +) +open class MessageBuilderTest() { + companion object { + const val EXPECTED_MESSAGE = "Delete message" + } + + class GenerateDeleteMessage() { + private lateinit var messageBuilder: MessageBuilder + + @Mock + private lateinit var resources: Resources + + @Mock + private lateinit var context: Context + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + whenever(context.resources).thenReturn(resources) + messageBuilder = MessageBuilder(context) + } + + private fun assertDeleteMessage(docInfo: DocumentInfo, resId: Int) { + whenever( + resources.getString( + eq(resId), + eq(docInfo.displayName) + ) + ).thenReturn(EXPECTED_MESSAGE) + assertEquals(messageBuilder.generateDeleteMessage(listOf(docInfo)), EXPECTED_MESSAGE) + } + + private fun assertQuantityDeleteMessage(docInfos: List<DocumentInfo>, resId: Int) { + whenever( + resources.getQuantityString( + eq(resId), + eq(docInfos.size), + eq(docInfos.size) + ) + ).thenReturn(EXPECTED_MESSAGE) + assertEquals(messageBuilder.generateDeleteMessage(docInfos), EXPECTED_MESSAGE) + } + + @Test + fun testGenerateDeleteMessage_singleFile() { + assertDeleteMessage( + createFile("Test doc"), + R.string.delete_filename_confirmation_message + ) + } + + @Test + fun testGenerateDeleteMessage_singleDirectory() { + assertDeleteMessage( + createDirectory("Test doc"), + R.string.delete_foldername_confirmation_message + ) + } + + @Test + fun testGenerateDeleteMessage_multipleFiles() { + assertQuantityDeleteMessage( + listOf(createFile("File 1"), createFile("File 2")), + R.plurals.delete_files_confirmation_message + ) + } + + @Test + fun testGenerateDeleteMessage_multipleDirectories() { + assertQuantityDeleteMessage( + listOf( + createDirectory("Directory 1"), + createDirectory("Directory 2") + ), + R.plurals.delete_folders_confirmation_message + ) + } + + @Test + fun testGenerateDeleteMessage_mixedFilesAndDirectories() { + assertQuantityDeleteMessage( + listOf(createFile("File 1"), createDirectory("Directory 1")), + R.plurals.delete_items_confirmation_message + ) + } + } + + @RunWith(Parameterized::class) + class GenerateListMessage() { + private lateinit var messageBuilder: MessageBuilder + + @Mock + private lateinit var resources: Resources + + @Mock + private lateinit var context: Context + + @Before + fun setUp() { + MockitoAnnotations.openMocks(this) + whenever(context.resources).thenReturn(resources) + messageBuilder = MessageBuilder(context) + } + + data class ListMessageData( + val dialogType: Int, + val opType: Int = OPERATION_UNKNOWN, + val resId: Int + ) + + companion object { + @Parameterized.Parameters(name = "{0}") + @JvmStatic + fun parameters() = + listOf( + ListMessageData( + dialogType = DIALOG_TYPE_CONVERTED, + resId = R.plurals.copy_converted_warning_content, + ), + ListMessageData( + dialogType = DIALOG_TYPE_FAILURE, + opType = OPERATION_COPY, + resId = R.plurals.copy_failure_alert_content, + ), + ListMessageData( + dialogType = DIALOG_TYPE_FAILURE, + opType = OPERATION_COMPRESS, + resId = R.plurals.compress_failure_alert_content, + ), + ListMessageData( + dialogType = DIALOG_TYPE_FAILURE, + opType = OPERATION_EXTRACT, + resId = R.plurals.extract_failure_alert_content, + ), + ListMessageData( + dialogType = DIALOG_TYPE_FAILURE, + opType = OPERATION_DELETE, + resId = R.plurals.delete_failure_alert_content, + ), + ListMessageData( + dialogType = DIALOG_TYPE_FAILURE, + opType = OPERATION_MOVE, + resId = R.plurals.move_failure_alert_content, + ), + ) + } + + @Parameterized.Parameter(0) + lateinit var testData: ListMessageData + + @Test + fun testGenerateListMessage() { + whenever( + resources.getQuantityString( + eq(testData.resId), + eq(2), + anyString(), + ) + ).thenReturn(EXPECTED_MESSAGE) + assertEquals( + messageBuilder.generateListMessage( + testData.dialogType, + testData.opType, + listOf(createFile("File 1")), + listOf(Uri.parse("content://random-uri")), + ), + EXPECTED_MESSAGE + ) + } + } +} + +fun createFile(displayName: String): DocumentInfo { + val doc = DocumentInfo() + doc.displayName = displayName + return doc +} + +fun createDirectory(displayName: String): DocumentInfo { + val doc = DocumentInfo() + doc.displayName = displayName + doc.mimeType = MIME_TYPE_DIR + return doc +} |