summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp15
-rw-r--r--AndroidManifest.xml49
-rw-r--r--AndroidManifestLib.xml2
-rw-r--r--PREUPLOAD.cfg2
-rw-r--r--TEST_MAPPING8
-rw-r--r--compose/Android.bp4
-rw-r--r--compose/AndroidManifest.xml2
-rw-r--r--flags.aconfig52
-rw-r--r--proguard.flags8
-rw-r--r--res/flag(!com.android.documentsui.flags.use_material3)/menu/action_mode_menu.xml (renamed from res/menu/action_mode_menu.xml)0
-rw-r--r--res/flag(!com.android.documentsui.flags.use_material3)/menu/activity.xml (renamed from res/menu/activity.xml)7
-rw-r--r--res/flag(!com.android.documentsui.flags.use_material3)/menu/container_context_menu.xml (renamed from res/menu/container_context_menu.xml)0
-rw-r--r--res/flag(!com.android.documentsui.flags.use_material3)/menu/dir_context_menu.xml (renamed from res/menu/dir_context_menu.xml)0
-rw-r--r--res/flag(!com.android.documentsui.flags.use_material3)/menu/file_context_menu.xml (renamed from res/menu/file_context_menu.xml)0
-rw-r--r--res/flag(!com.android.documentsui.flags.use_material3)/menu/mixed_context_menu.xml (renamed from res/menu/mixed_context_menu.xml)0
-rw-r--r--res/flag(!com.android.documentsui.flags.use_material3)/menu/root_context_menu.xml (renamed from res/menu/root_context_menu.xml)0
-rw-r--r--res/flag(!com.android.documentsui.flags.use_material3)/menu/sub_menu.xml (renamed from res/menu/sub_menu.xml)0
-rw-r--r--res/flag(!com.android.documentsui.flags.use_material3)/values-sw720dp/config.xml (renamed from res/values-sw720dp/config.xml)0
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/color/doc_list_item_badge_icon_color.xml21
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/color/doc_list_item_label_color.xml23
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/color/doc_list_item_subtitle_color.xml7
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/color/item_action_icon.xml1
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/color/item_root_icon.xml7
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/color/item_root_primary_text.xml6
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/color/item_root_ripple_color.xml22
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/color/item_root_secondary_text.xml8
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/color/list_item_ripple_color.xml24
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/color/menu_item_ripple_color.xml20
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/color/nav_rail_burger_icon_ripple_color.xml20
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/color/nav_rail_item_text_color.xml21
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/color/profile_tab_ripple_color.xml22
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/color/search_chip_ripple_color.xml2
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/color/search_chip_text_color.xml2
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/band_select_overlay.xml4
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/bottom_sheet_dialog_background.xml9
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/grid_nameplate_background.xml (renamed from res/flag(com.android.documentsui.flags.use_material3)/color/profile_tab_selector.xml)14
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/grid_thumbnail_background.xml24
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_arrow_upward.xml14
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_cancel.xml24
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_check_circle.xml13
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_audio.xml24
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_document.xml25
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_from_this_week.xml2
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_image.xml24
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_large_files.xml2
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_chip_video.xml24
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_hamburger.xml2
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_sort_arrow.xml2
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/ic_sort_icon.xml98
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/list_item_background.xml137
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/list_item_mask.xml21
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_bottom_section_background.xml24
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_middle_section_background.xml (renamed from res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_background.xml)2
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/main_container_top_section_background.xml24
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/menu_dropdown_panel.xml1
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/menu_item_background.xml36
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/nav_rail_burger_icon_background.xml26
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/nav_rail_item_background.xml23
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/nav_rail_item_icon_background.xml145
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/nav_rail_item_icon_mask.xml21
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/profile_tab_mask.xml21
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/progress_indeterminate_horizontal_material_trimmed.xml1
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/root_item_background.xml108
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/root_list_selector.xml19
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/sort_widget_background.xml1
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/drawable/tab_border_rounded.xml137
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout-w720dp/directory_app_bar.xml55
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout-w900dp/column_headers.xml (renamed from res/flag(com.android.documentsui.flags.use_material3)/layout-w720dp/column_headers.xml)45
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout-w900dp/item_doc_list.xml (renamed from res/flag(com.android.documentsui.flags.use_material3)/layout-w720dp/item_doc_list.xml)90
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout-w900dp/shared_cell_content.xml (renamed from res/flag(com.android.documentsui.flags.use_material3)/layout-w720dp/shared_cell_content.xml)7
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/apps_row.xml3
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/directory_app_bar.xml24
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/directory_header.xml50
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/drawer_layout.xml33
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/fixed_layout.xml93
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/fragment_directory.xml8
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/fragment_nav_rail_roots.xml25
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/fragment_roots.xml6
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/item_doc_grid.xml287
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/item_doc_list.xml44
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/item_root.xml37
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/item_root_spacer.xml9
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/nav_rail_item_root.xml55
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/nav_rail_layout.xml181
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/root_vertical_divider.xml1
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/layout/search_chip_row.xml6
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/menu/action_mode_menu.xml101
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/menu/activity.xml106
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/menu/container_context_menu.xml48
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/menu/dir_context_menu.xml60
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/menu/file_context_menu.xml74
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/menu/mixed_context_menu.xml47
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/menu/root_context_menu.xml30
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values-night-v31/colors.xml10
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values-night/colors.xml10
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values-night/themes.xml1
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values-v31/colors.xml11
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values-v31/dimens.xml7
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values-v31/styles.xml7
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values-w600dp/dimens.xml16
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values-w600dp/layouts.xml20
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values-w900dp/colors.xml (renamed from res/flag(com.android.documentsui.flags.use_material3)/values-w720dp/colors.xml)0
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values-w900dp/config.xml (renamed from res/flag(com.android.documentsui.flags.use_material3)/values-w720dp/dimens.xml)15
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values-w900dp/dimens.xml25
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values-w900dp/layouts.xml (renamed from res/flag(com.android.documentsui.flags.use_material3)/values-w720dp/layouts.xml)0
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values/colors.xml37
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values/dimens.xml101
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values/styles.xml101
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values/styles_text.xml33
-rw-r--r--res/flag(com.android.documentsui.flags.use_material3)/values/themes.xml16
-rw-r--r--res/values-af/strings.xml3
-rw-r--r--res/values-am/strings.xml3
-rw-r--r--res/values-ar/strings.xml3
-rw-r--r--res/values-as/strings.xml3
-rw-r--r--res/values-az/strings.xml3
-rw-r--r--res/values-b+sr+Latn/strings.xml3
-rw-r--r--res/values-be/strings.xml3
-rw-r--r--res/values-bg/strings.xml3
-rw-r--r--res/values-bn/strings.xml3
-rw-r--r--res/values-bs/strings.xml3
-rw-r--r--res/values-ca/strings.xml3
-rw-r--r--res/values-cs/strings.xml3
-rw-r--r--res/values-da/strings.xml3
-rw-r--r--res/values-de/strings.xml3
-rw-r--r--res/values-el/strings.xml3
-rw-r--r--res/values-en-rAU/strings.xml3
-rw-r--r--res/values-en-rCA/strings.xml2
-rw-r--r--res/values-en-rGB/strings.xml3
-rw-r--r--res/values-en-rIN/strings.xml3
-rw-r--r--res/values-es-rUS/strings.xml3
-rw-r--r--res/values-es/strings.xml3
-rw-r--r--res/values-et/strings.xml3
-rw-r--r--res/values-eu/strings.xml3
-rw-r--r--res/values-fa/strings.xml3
-rw-r--r--res/values-fi/strings.xml3
-rw-r--r--res/values-fr-rCA/strings.xml3
-rw-r--r--res/values-fr/strings.xml5
-rw-r--r--res/values-gl/strings.xml3
-rw-r--r--res/values-gu/strings.xml3
-rw-r--r--res/values-hi/strings.xml3
-rw-r--r--res/values-hr/strings.xml3
-rw-r--r--res/values-hu/strings.xml3
-rw-r--r--res/values-hy/strings.xml3
-rw-r--r--res/values-in/strings.xml3
-rw-r--r--res/values-is/strings.xml3
-rw-r--r--res/values-it/strings.xml3
-rw-r--r--res/values-iw/strings.xml3
-rw-r--r--res/values-ja/strings.xml3
-rw-r--r--res/values-ka/strings.xml3
-rw-r--r--res/values-kk/strings.xml3
-rw-r--r--res/values-km/strings.xml3
-rw-r--r--res/values-kn/strings.xml19
-rw-r--r--res/values-ko/strings.xml3
-rw-r--r--res/values-ky/strings.xml3
-rw-r--r--res/values-lo/strings.xml3
-rw-r--r--res/values-lt/strings.xml3
-rw-r--r--res/values-lv/strings.xml3
-rw-r--r--res/values-mk/strings.xml3
-rw-r--r--res/values-ml/strings.xml3
-rw-r--r--res/values-mn/strings.xml3
-rw-r--r--res/values-mr/strings.xml3
-rw-r--r--res/values-ms/strings.xml3
-rw-r--r--res/values-my/strings.xml3
-rw-r--r--res/values-nb/strings.xml3
-rw-r--r--res/values-ne/strings.xml3
-rw-r--r--res/values-nl/strings.xml3
-rw-r--r--res/values-or/strings.xml5
-rw-r--r--res/values-pa/strings.xml3
-rw-r--r--res/values-pl/strings.xml3
-rw-r--r--res/values-pt-rBR/strings.xml3
-rw-r--r--res/values-pt-rPT/strings.xml3
-rw-r--r--res/values-pt/strings.xml3
-rw-r--r--res/values-ro/strings.xml3
-rw-r--r--res/values-ru/strings.xml5
-rw-r--r--res/values-si/strings.xml3
-rw-r--r--res/values-sk/strings.xml3
-rw-r--r--res/values-sl/strings.xml3
-rw-r--r--res/values-sq/strings.xml3
-rw-r--r--res/values-sr/strings.xml3
-rw-r--r--res/values-sv/strings.xml3
-rw-r--r--res/values-sw/strings.xml3
-rw-r--r--res/values-ta/strings.xml3
-rw-r--r--res/values-te/strings.xml3
-rw-r--r--res/values-th/strings.xml3
-rw-r--r--res/values-tl/strings.xml3
-rw-r--r--res/values-tr/strings.xml3
-rw-r--r--res/values-uk/strings.xml3
-rw-r--r--res/values-ur/strings.xml3
-rw-r--r--res/values-uz/strings.xml3
-rw-r--r--res/values-vi/strings.xml3
-rw-r--r--res/values-zh-rCN/strings.xml3
-rw-r--r--res/values-zh-rHK/strings.xml3
-rw-r--r--res/values-zh-rTW/strings.xml3
-rw-r--r--res/values-zu/strings.xml3
-rw-r--r--res/values/strings.xml26
-rw-r--r--src/com/android/documentsui/AbstractActionHandler.java105
-rw-r--r--src/com/android/documentsui/ActionHandler.java8
-rw-r--r--src/com/android/documentsui/ActionModeController.java16
-rw-r--r--src/com/android/documentsui/BaseActivity.java163
-rw-r--r--src/com/android/documentsui/DrawerController.java12
-rw-r--r--src/com/android/documentsui/Injector.java5
-rw-r--r--src/com/android/documentsui/MenuManager.java37
-rw-r--r--src/com/android/documentsui/MultiRootDocumentsLoader.java2
-rw-r--r--src/com/android/documentsui/NavigationViewManager.java186
-rw-r--r--src/com/android/documentsui/ProfileTabs.java10
-rw-r--r--src/com/android/documentsui/RecentsLoader.java6
-rw-r--r--src/com/android/documentsui/UserManagerState.java457
-rw-r--r--src/com/android/documentsui/archives/Archive.java99
-rw-r--r--src/com/android/documentsui/archives/ReadableArchive.java6
-rw-r--r--src/com/android/documentsui/base/Menus.java4
-rw-r--r--src/com/android/documentsui/dirlist/AppsRowManager.java7
-rw-r--r--src/com/android/documentsui/dirlist/DirectoryFragment.java79
-rw-r--r--src/com/android/documentsui/dirlist/DocumentHolder.java5
-rw-r--r--src/com/android/documentsui/dirlist/DocumentsSwipeRefreshLayout.java38
-rw-r--r--src/com/android/documentsui/dirlist/GridDirectoryHolder.java1
-rw-r--r--src/com/android/documentsui/dirlist/GridDocumentHolder.java111
-rw-r--r--src/com/android/documentsui/dirlist/GridPhotoHolder.java7
-rw-r--r--src/com/android/documentsui/dirlist/IconHelper.java35
-rw-r--r--src/com/android/documentsui/dirlist/ListDocumentHolder.java40
-rw-r--r--src/com/android/documentsui/dirlist/ModelBackedDocumentsAdapter.java9
-rw-r--r--src/com/android/documentsui/dirlist/SelectionMetadata.java2
-rw-r--r--src/com/android/documentsui/files/ActionHandler.java68
-rw-r--r--src/com/android/documentsui/files/FilesActivity.java71
-rw-r--r--src/com/android/documentsui/files/MenuManager.java15
-rw-r--r--src/com/android/documentsui/loaders/BaseFileLoader.kt208
-rw-r--r--src/com/android/documentsui/loaders/FolderLoader.kt82
-rw-r--r--src/com/android/documentsui/loaders/QueryOptions.kt90
-rw-r--r--src/com/android/documentsui/loaders/SearchLoader.kt257
-rw-r--r--src/com/android/documentsui/picker/ActionHandler.java3
-rw-r--r--src/com/android/documentsui/picker/PickActivity.java25
-rw-r--r--src/com/android/documentsui/picker/TrampolineActivity.kt181
-rw-r--r--src/com/android/documentsui/queries/SearchChipViewManager.java56
-rw-r--r--src/com/android/documentsui/queries/SearchViewManager.java10
-rw-r--r--src/com/android/documentsui/services/CompressJob.java33
-rw-r--r--src/com/android/documentsui/services/CopyJob.java61
-rw-r--r--src/com/android/documentsui/services/DeleteJob.java33
-rw-r--r--src/com/android/documentsui/services/FileOperationService.java76
-rw-r--r--src/com/android/documentsui/services/Job.java2
-rw-r--r--src/com/android/documentsui/services/JobProgress.kt69
-rw-r--r--src/com/android/documentsui/services/MoveJob.java27
-rw-r--r--src/com/android/documentsui/services/ResolvedResourcesJob.java27
-rw-r--r--src/com/android/documentsui/sidebar/AppItem.java29
-rw-r--r--src/com/android/documentsui/sidebar/NavRailAppItem.java48
-rw-r--r--src/com/android/documentsui/sidebar/NavRailProfileItem.java47
-rw-r--r--src/com/android/documentsui/sidebar/NavRailRootAndAppItem.java40
-rw-r--r--src/com/android/documentsui/sidebar/NavRailRootItem.java52
-rw-r--r--src/com/android/documentsui/sidebar/ProfileItem.java7
-rw-r--r--src/com/android/documentsui/sidebar/RootAndAppItem.java17
-rw-r--r--src/com/android/documentsui/sidebar/RootItem.java74
-rw-r--r--src/com/android/documentsui/sidebar/RootsAdapter.java13
-rw-r--r--src/com/android/documentsui/sidebar/RootsFragment.java137
-rw-r--r--src/com/android/documentsui/sorting/HeaderCell.java51
-rw-r--r--src/com/android/documentsui/sorting/TableHeaderController.java56
-rw-r--r--src/com/android/documentsui/ui/DocumentDebugInfo.java59
-rw-r--r--src/com/android/documentsui/ui/Snackbars.java7
-rw-r--r--src/com/android/documentsui/util/FlagUtils.kt62
-rw-r--r--tests/Android.bp27
-rw-r--r--tests/AndroidManifest.xml2
-rw-r--r--tests/common/com/android/documentsui/bots/DirectoryListBot.java50
-rw-r--r--tests/common/com/android/documentsui/bots/SortBot.java37
-rw-r--r--tests/common/com/android/documentsui/bots/UiBot.java58
-rw-r--r--tests/common/com/android/documentsui/services/TestJob.java8
-rw-r--r--tests/common/com/android/documentsui/testing/TestDirectoryDetails.java6
-rw-r--r--tests/common/com/android/documentsui/testing/TestDocumentsProvider.java54
-rw-r--r--tests/common/com/android/documentsui/testing/TestMenu.java6
-rw-r--r--tests/common/com/android/documentsui/testing/TestModel.java18
-rw-r--r--tests/common/com/android/documentsui/testing/TestSelectionDetails.java6
-rw-r--r--tests/functional/com/android/documentsui/ActivityTestJunit4.kt223
-rw-r--r--tests/functional/com/android/documentsui/FileCopyUiTest.java2
-rw-r--r--tests/functional/com/android/documentsui/FileManagementUiTest.java15
-rw-r--r--tests/functional/com/android/documentsui/FilesActivityDefaultsUiTest.java62
-rw-r--r--tests/functional/com/android/documentsui/FilesActivityUiTest.java37
-rw-r--r--tests/functional/com/android/documentsui/SortDocumentUiTest.java132
-rw-r--r--tests/functional/com/android/documentsui/TrampolineActivityTest.kt260
-rw-r--r--tests/functional/com/android/documentsui/archives/ArchiveHandleTest.java45
-rw-r--r--tests/functional/com/android/documentsui/services/AbstractCopyJobTest.java60
-rw-r--r--tests/functional/com/android/documentsui/services/CopyJobTest.java10
-rw-r--r--tests/functional/com/android/documentsui/services/DeleteJobTest.java17
-rw-r--r--tests/functional/com/android/documentsui/services/FileOperationServiceTest.java2
-rw-r--r--tests/unit/com/android/documentsui/ProfileTabsTest.java65
-rw-r--r--tests/unit/com/android/documentsui/UserIdManagerTest.java21
-rw-r--r--tests/unit/com/android/documentsui/UserManagerStateTest.java651
-rw-r--r--tests/unit/com/android/documentsui/dirlist/MessageTest.java2
-rw-r--r--tests/unit/com/android/documentsui/files/ActionHandlerTest.java144
-rw-r--r--tests/unit/com/android/documentsui/files/MenuManagerTest.java53
-rw-r--r--tests/unit/com/android/documentsui/files/QuickViewIntentBuilderTest.java2
-rw-r--r--tests/unit/com/android/documentsui/loaders/BaseLoaderTest.kt101
-rw-r--r--tests/unit/com/android/documentsui/loaders/FolderLoaderTest.kt97
-rw-r--r--tests/unit/com/android/documentsui/loaders/SearchLoaderTest.kt160
-rw-r--r--tests/unit/com/android/documentsui/picker/ActionHandlerTest.java4
-rw-r--r--tests/unit/com/android/documentsui/picker/MenuManagerTest.java23
-rw-r--r--tests/unit/com/android/documentsui/queries/SearchChipViewManagerTest.java92
-rw-r--r--tests/unit/com/android/documentsui/sidebar/RootsFragmentTest.java66
-rw-r--r--tests/unit/com/android/documentsui/ui/MessageBuilderTest.kt234
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
+}