aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2024-01-17 22:14:31 -0800
committerXin Li <delphij@google.com>2024-01-17 22:14:31 -0800
commitefee97bcc526928fb7168072e0305f5a72324fbc (patch)
tree7edfc23366f90cdca5852209a6ac207b7de884a4
parent3e303554182e65402022ecd079d63b94ce80ffe4 (diff)
parent3007d9f481e92ed57ca9e3783719b3d84797ef2c (diff)
downloadIntentResolver-temp_319669529.tar.gz
Merge Android 24Q1 Release (ab/11220357)temp_319669529
Bug: 319669529 Merged-In: I95e383e2822917198425acf9ba8bfbea76fdf948 Change-Id: Ibd7bfe1c21d32e1d0cc3023971afb779ed14c3a9
-rw-r--r--.clang-format13
-rw-r--r--Android.bp85
-rw-r--r--AndroidManifest-app.xml37
-rw-r--r--AndroidManifest-lib.xml1
-rw-r--r--NOTICE202
-rw-r--r--PREUPLOAD.cfg9
-rw-r--r--README.md8
-rw-r--r--TEST_MAPPING10
-rw-r--r--aconfig/Android.bp30
-rw-r--r--aconfig/FeatureFlags.aconfig33
-rw-r--r--aconfig/README.md20
-rw-r--r--java/res/drawable/chooser_direct_share_label_placeholder.xml37
-rw-r--r--java/res/layout/chooser_grid_preview_file.xml8
-rw-r--r--java/res/layout/chooser_grid_preview_files_text.xml8
-rw-r--r--java/res/layout/chooser_grid_preview_image.xml8
-rw-r--r--java/res/layout/chooser_grid_preview_text.xml8
-rw-r--r--java/res/layout/chooser_grid_scrollable_preview.xml127
-rw-r--r--java/res/layout/chooser_list_per_profile_wrap.xml42
-rw-r--r--java/res/values-af/strings.xml26
-rw-r--r--java/res/values-am/strings.xml30
-rw-r--r--java/res/values-ar/strings.xml24
-rw-r--r--java/res/values-as/strings.xml24
-rw-r--r--java/res/values-az/strings.xml24
-rw-r--r--java/res/values-b+sr+Latn/strings.xml28
-rw-r--r--java/res/values-be/strings.xml24
-rw-r--r--java/res/values-bg/strings.xml24
-rw-r--r--java/res/values-bn/strings.xml26
-rw-r--r--java/res/values-bs/strings.xml24
-rw-r--r--java/res/values-ca/strings.xml26
-rw-r--r--java/res/values-cs/strings.xml24
-rw-r--r--java/res/values-da/strings.xml24
-rw-r--r--java/res/values-de/strings.xml24
-rw-r--r--java/res/values-el/strings.xml32
-rw-r--r--java/res/values-en-rAU/strings.xml24
-rw-r--r--java/res/values-en-rCA/strings.xml24
-rw-r--r--java/res/values-en-rGB/strings.xml24
-rw-r--r--java/res/values-en-rIN/strings.xml24
-rw-r--r--java/res/values-en-rXC/strings.xml24
-rw-r--r--java/res/values-es-rUS/strings.xml30
-rw-r--r--java/res/values-es/strings.xml26
-rw-r--r--java/res/values-et/strings.xml26
-rw-r--r--java/res/values-eu/strings.xml26
-rw-r--r--java/res/values-fa/strings.xml32
-rw-r--r--java/res/values-fi/strings.xml24
-rw-r--r--java/res/values-fr-rCA/strings.xml30
-rw-r--r--java/res/values-fr/strings.xml28
-rw-r--r--java/res/values-gl/strings.xml24
-rw-r--r--java/res/values-gu/strings.xml24
-rw-r--r--java/res/values-hi/strings.xml26
-rw-r--r--java/res/values-hr/strings.xml24
-rw-r--r--java/res/values-hu/strings.xml24
-rw-r--r--java/res/values-hy/strings.xml24
-rw-r--r--java/res/values-in/strings.xml32
-rw-r--r--java/res/values-is/strings.xml24
-rw-r--r--java/res/values-it/strings.xml30
-rw-r--r--java/res/values-iw/strings.xml30
-rw-r--r--java/res/values-ja/strings.xml30
-rw-r--r--java/res/values-ka/strings.xml24
-rw-r--r--java/res/values-kk/strings.xml30
-rw-r--r--java/res/values-km/strings.xml26
-rw-r--r--java/res/values-kn/strings.xml26
-rw-r--r--java/res/values-ko/strings.xml30
-rw-r--r--java/res/values-ky/strings.xml34
-rw-r--r--java/res/values-lo/strings.xml24
-rw-r--r--java/res/values-lt/strings.xml24
-rw-r--r--java/res/values-lv/strings.xml24
-rw-r--r--java/res/values-mk/strings.xml30
-rw-r--r--java/res/values-ml/strings.xml24
-rw-r--r--java/res/values-mn/strings.xml24
-rw-r--r--java/res/values-mr/strings.xml24
-rw-r--r--java/res/values-ms/strings.xml26
-rw-r--r--java/res/values-my/strings.xml26
-rw-r--r--java/res/values-nb/strings.xml24
-rw-r--r--java/res/values-ne/strings.xml24
-rw-r--r--java/res/values-night/styles.xml22
-rw-r--r--java/res/values-nl/strings.xml24
-rw-r--r--java/res/values-or/strings.xml28
-rw-r--r--java/res/values-pa/strings.xml28
-rw-r--r--java/res/values-pl/strings.xml24
-rw-r--r--java/res/values-pt-rBR/strings.xml24
-rw-r--r--java/res/values-pt-rPT/strings.xml30
-rw-r--r--java/res/values-pt/strings.xml24
-rw-r--r--java/res/values-ro/strings.xml26
-rw-r--r--java/res/values-ru/strings.xml28
-rw-r--r--java/res/values-si/strings.xml24
-rw-r--r--java/res/values-sk/strings.xml26
-rw-r--r--java/res/values-sl/strings.xml26
-rw-r--r--java/res/values-sq/strings.xml30
-rw-r--r--java/res/values-sr/strings.xml28
-rw-r--r--java/res/values-sv/strings.xml26
-rw-r--r--java/res/values-sw/strings.xml24
-rw-r--r--java/res/values-ta/strings.xml28
-rw-r--r--java/res/values-te/strings.xml30
-rw-r--r--java/res/values-th/strings.xml24
-rw-r--r--java/res/values-tl/strings.xml24
-rw-r--r--java/res/values-tr/strings.xml24
-rw-r--r--java/res/values-uk/strings.xml24
-rw-r--r--java/res/values-ur/strings.xml24
-rw-r--r--java/res/values-uz/strings.xml24
-rw-r--r--java/res/values-vi/strings.xml34
-rw-r--r--java/res/values-zh-rCN/strings.xml30
-rw-r--r--java/res/values-zh-rHK/strings.xml38
-rw-r--r--java/res/values-zh-rTW/strings.xml32
-rw-r--r--java/res/values-zu/strings.xml24
-rw-r--r--java/res/values/attrs.xml5
-rw-r--r--java/res/values/dimens.xml1
-rw-r--r--java/res/values/strings.xml4
-rw-r--r--java/src-debug/com/android/intentresolver/flags/DebugFeatureFlagRepository.kt81
-rw-r--r--java/src-debug/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt30
-rw-r--r--java/src-release/com/android/intentresolver/flags/ReleaseFeatureFlagRepository.kt31
-rw-r--r--java/src/com/android/intentresolver/AnnotatedUserHandles.java18
-rw-r--r--java/src/com/android/intentresolver/ChooserActionFactory.java43
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java270
-rw-r--r--java/src/com/android/intentresolver/ChooserGridLayoutManager.java2
-rw-r--r--java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java6
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java249
-rw-r--r--java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java37
-rw-r--r--java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java6
-rw-r--r--java/src/com/android/intentresolver/ChooserRefinementManager.java15
-rw-r--r--java/src/com/android/intentresolver/ChooserRequestParameters.java17
-rw-r--r--java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java2
-rw-r--r--java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java4
-rw-r--r--java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java235
-rw-r--r--java/src/com/android/intentresolver/IntentForwarderActivity.java5
-rw-r--r--java/src/com/android/intentresolver/MainApplication.kt (renamed from java/tests/src/com/android/intentresolver/RequireFeatureFlags.kt)9
-rw-r--r--java/src/com/android/intentresolver/MultiProfilePagerAdapter.java (renamed from java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java)457
-rw-r--r--java/src/com/android/intentresolver/ResolvedComponentInfo.java4
-rw-r--r--java/src/com/android/intentresolver/ResolverActivity.java231
-rw-r--r--java/src/com/android/intentresolver/ResolverInfoHelpers.kt34
-rw-r--r--java/src/com/android/intentresolver/ResolverListAdapter.java227
-rw-r--r--java/src/com/android/intentresolver/ResolverListController.java7
-rw-r--r--java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java22
-rw-r--r--java/src/com/android/intentresolver/ResolverViewPager.java2
-rw-r--r--java/src/com/android/intentresolver/ShortcutSelectionLogic.java3
-rw-r--r--java/src/com/android/intentresolver/SimpleIconFactory.java19
-rw-r--r--java/src/com/android/intentresolver/TargetPresentationGetter.java5
-rw-r--r--java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java2
-rw-r--r--java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java41
-rw-r--r--java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java19
-rw-r--r--java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java3
-rw-r--r--java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java3
-rw-r--r--java/src/com/android/intentresolver/chooser/TargetInfo.java19
-rw-r--r--java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt3
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java36
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java2
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java39
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java22
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java54
-rw-r--r--java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt45
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ImageLoader.kt4
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt6
-rw-r--r--java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt6
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt74
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt15
-rw-r--r--java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java54
-rw-r--r--java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java31
-rw-r--r--java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.java46
-rw-r--r--java/src/com/android/intentresolver/emptystate/CrossProfileIntentsChecker.java59
-rw-r--r--java/src/com/android/intentresolver/emptystate/EmptyState.java78
-rw-r--r--java/src/com/android/intentresolver/emptystate/EmptyStateProvider.java37
-rw-r--r--java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java63
-rw-r--r--java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java (renamed from java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java)29
-rw-r--r--java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java (renamed from java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java)23
-rw-r--r--java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java (renamed from java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java)16
-rw-r--r--java/src/com/android/intentresolver/flags/DeviceConfigProxy.kt33
-rw-r--r--java/src/com/android/intentresolver/flags/Flags.kt30
-rw-r--r--java/src/com/android/intentresolver/grid/ChooserGridAdapter.java39
-rw-r--r--java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt13
-rw-r--r--java/src/com/android/intentresolver/icons/LabelInfo.kt (renamed from java/src/com/android/intentresolver/flags/FeatureFlagRepository.kt)12
-rw-r--r--java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java2
-rw-r--r--java/src/com/android/intentresolver/icons/LoadLabelTask.java39
-rw-r--r--java/src/com/android/intentresolver/icons/TargetDataLoader.kt10
-rw-r--r--java/src/com/android/intentresolver/inject/ActivityModule.kt46
-rw-r--r--java/src/com/android/intentresolver/inject/ConcurrencyModule.kt43
-rw-r--r--java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt15
-rw-r--r--java/src/com/android/intentresolver/inject/FrameworkModule.kt76
-rw-r--r--java/src/com/android/intentresolver/inject/Qualifiers.kt39
-rw-r--r--java/src/com/android/intentresolver/inject/SingletonModule.kt22
-rw-r--r--java/src/com/android/intentresolver/logging/EventLog.kt74
-rw-r--r--java/src/com/android/intentresolver/logging/EventLogImpl.java (renamed from java/src/com/android/intentresolver/logging/EventLog.java)169
-rw-r--r--java/src/com/android/intentresolver/logging/EventLogModule.kt46
-rw-r--r--java/src/com/android/intentresolver/logging/FrameworkStatsLogger.kt75
-rw-r--r--java/src/com/android/intentresolver/model/AbstractResolverComparator.java15
-rw-r--r--java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java67
-rw-r--r--java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java16
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ScopedAppTargetListCallback.kt58
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt24
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java5
-rw-r--r--java/src/com/android/intentresolver/v2/ActivityLogic.kt156
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserActionFactory.java395
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserActivity.java1845
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt87
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java227
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserSelector.kt36
-rw-r--r--java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java666
-rw-r--r--java/src/com/android/intentresolver/v2/ResolverActivity.java2181
-rw-r--r--java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt81
-rw-r--r--java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java131
-rw-r--r--java/src/com/android/intentresolver/v2/data/BroadcastFlow.kt46
-rw-r--r--java/src/com/android/intentresolver/v2/data/model/User.kt50
-rw-r--r--java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt68
-rw-r--r--java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt29
-rw-r--r--java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt261
-rw-r--r--java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt34
-rw-r--r--java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt46
-rw-r--r--java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java141
-rw-r--r--java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java157
-rw-r--r--java/src/com/android/intentresolver/v2/emptystate/NoCrossProfileEmptyStateProvider.java138
-rw-r--r--java/src/com/android/intentresolver/v2/emptystate/WorkProfilePausedEmptyStateProvider.java116
-rw-r--r--java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt40
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt39
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt70
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt77
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/ListController.kt (renamed from java/tests/src/com/android/intentresolver/TestApplication.kt)16
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt34
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt39
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt69
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt121
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt108
-rw-r--r--java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt35
-rw-r--r--java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt32
-rw-r--r--java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt30
-rw-r--r--java/src/com/android/intentresolver/v2/platform/SecureSettings.kt25
-rw-r--r--java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt14
-rw-r--r--java/src/com/android/intentresolver/v2/ui/ActionTitle.java89
-rw-r--r--java/src/com/android/intentresolver/v2/util/MutableLazy.kt36
-rw-r--r--java/src/com/android/intentresolver/v2/validation/Findings.kt113
-rw-r--r--java/src/com/android/intentresolver/v2/validation/Validation.kt129
-rw-r--r--java/src/com/android/intentresolver/v2/validation/ValidationResult.kt39
-rw-r--r--java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt59
-rw-r--r--java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt83
-rw-r--r--java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt54
-rw-r--r--java/src/com/android/intentresolver/v2/validation/types/Validators.kt45
-rw-r--r--java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt90
-rw-r--r--java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java101
-rw-r--r--java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt137
-rw-r--r--java/tests/Android.bp47
-rw-r--r--java/tests/src/com/android/intentresolver/FeatureFlagRule.kt56
-rw-r--r--java/tests/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt225
-rw-r--r--java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt166
-rw-r--r--java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt482
-rw-r--r--tests/README.md33
-rw-r--r--tests/activity/Android.bp68
-rw-r--r--tests/activity/AndroidManifest.xml (renamed from java/tests/AndroidManifest.xml)14
-rw-r--r--tests/activity/AndroidTest.xml32
-rw-r--r--tests/activity/res/drawable/test320x240.png (renamed from java/tests/res/drawable/test320x240.png)bin39533 -> 39533 bytes
-rw-r--r--tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java (renamed from java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java)20
-rw-r--r--tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java (renamed from java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java)57
-rw-r--r--tests/activity/src/com/android/intentresolver/IChooserWrapper.java (renamed from java/tests/src/com/android/intentresolver/IChooserWrapper.java)14
-rw-r--r--tests/activity/src/com/android/intentresolver/ResolverActivityTest.java (renamed from java/tests/src/com/android/intentresolver/ResolverActivityTest.java)153
-rw-r--r--tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java (renamed from java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java)47
-rw-r--r--tests/activity/src/com/android/intentresolver/TestContentProvider.kt (renamed from java/tests/src/com/android/intentresolver/TestContentProvider.kt)0
-rw-r--r--tests/activity/src/com/android/intentresolver/UnbundledChooserActivityTest.java (renamed from java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java)641
-rw-r--r--tests/activity/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java (renamed from java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java)21
-rw-r--r--tests/activity/src/com/android/intentresolver/logging/TestEventLogModule.kt39
-rw-r--r--tests/activity/src/com/android/intentresolver/v2/ChooserActivityOverrideData.java131
-rw-r--r--tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java265
-rw-r--r--tests/activity/src/com/android/intentresolver/v2/ResolverActivityTest.java1105
-rw-r--r--tests/activity/src/com/android/intentresolver/v2/ResolverWrapperActivity.java289
-rw-r--r--tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt32
-rw-r--r--tests/activity/src/com/android/intentresolver/v2/TestResolverActivityLogic.kt22
-rw-r--r--tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityTest.java3147
-rw-r--r--tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityWorkProfileTest.java481
-rw-r--r--tests/integration/Android.bp44
-rw-r--r--tests/integration/AndroidManifest.xml24
-rw-r--r--tests/integration/AndroidTest.xml38
-rw-r--r--tests/integration/res/values/strings.xml18
-rw-r--r--tests/integration/src/com/android/intentresolver/v2/data/repository/PlaceholderTest.kt (renamed from java/src-release/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt)15
-rw-r--r--tests/shared/Android.bp37
-rw-r--r--tests/shared/src/com/android/intentresolver/MatcherUtils.java (renamed from java/tests/src/com/android/intentresolver/MatcherUtils.java)2
-rw-r--r--tests/shared/src/com/android/intentresolver/MockitoKotlinHelpers.kt (renamed from java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt)36
-rw-r--r--tests/shared/src/com/android/intentresolver/ResolverDataProvider.java (renamed from java/tests/src/com/android/intentresolver/ResolverDataProvider.java)24
-rw-r--r--tests/shared/src/com/android/intentresolver/TestContentPreviewViewModel.kt (renamed from java/tests/src/com/android/intentresolver/TestContentPreviewViewModel.kt)5
-rw-r--r--tests/shared/src/com/android/intentresolver/TestPreviewImageLoader.kt (renamed from java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt)6
-rw-r--r--tests/shared/src/com/android/intentresolver/logging/FakeEventLog.kt197
-rw-r--r--tests/shared/src/com/android/intentresolver/logging/FakeFrameworkStatsLogger.kt95
-rw-r--r--tests/shared/src/com/android/intentresolver/v2/platform/FakeSecureSettings.kt44
-rw-r--r--tests/shared/src/com/android/intentresolver/v2/platform/FakeUserManager.kt239
-rw-r--r--tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt22
-rw-r--r--tests/unit/Android.bp62
-rw-r--r--tests/unit/AndroidManifest.xml23
-rw-r--r--tests/unit/AndroidTest.xml (renamed from java/tests/AndroidTest.xml)8
-rw-r--r--tests/unit/res/values/strings.xml18
-rw-r--r--tests/unit/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt (renamed from java/tests/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt)0
-rw-r--r--tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt (renamed from java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt)14
-rw-r--r--tests/unit/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt (renamed from java/tests/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt)0
-rw-r--r--tests/unit/src/com/android/intentresolver/ChooserListAdapterDataTest.kt177
-rw-r--r--tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt (renamed from java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt)94
-rw-r--r--tests/unit/src/com/android/intentresolver/ChooserRefinementManagerTest.kt (renamed from java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.kt)0
-rw-r--r--tests/unit/src/com/android/intentresolver/ChooserRequestParametersTest.kt (renamed from java/tests/src/com/android/intentresolver/ChooserRequestParametersTest.kt)7
-rw-r--r--tests/unit/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt (renamed from java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt)0
-rw-r--r--tests/unit/src/com/android/intentresolver/FakeResolverListCommunicator.kt56
-rw-r--r--tests/unit/src/com/android/intentresolver/MultiProfilePagerAdapterTest.kt277
-rw-r--r--tests/unit/src/com/android/intentresolver/ResolverListAdapterTest.kt1048
-rw-r--r--tests/unit/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt (renamed from java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt)9
-rw-r--r--tests/unit/src/com/android/intentresolver/TargetPresentationGetterTest.kt (renamed from java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt)0
-rw-r--r--tests/unit/src/com/android/intentresolver/TestHelpers.kt (renamed from java/tests/src/com/android/intentresolver/TestHelpers.kt)0
-rw-r--r--tests/unit/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt (renamed from java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt)13
-rw-r--r--tests/unit/src/com/android/intentresolver/chooser/TargetInfoTest.kt (renamed from java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt)42
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt (renamed from java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt)30
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt (renamed from java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt)0
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/FileContentPreviewUiTest.kt99
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt423
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt (renamed from java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt)0
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt (renamed from java/tests/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt)3
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt (renamed from java/tests/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt)22
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/TextContentPreviewUiTest.kt108
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt348
-rw-r--r--tests/unit/src/com/android/intentresolver/emptystate/CompositeEmptyStateProviderTest.kt65
-rw-r--r--tests/unit/src/com/android/intentresolver/emptystate/CrossProfileIntentsCheckerTest.kt84
-rw-r--r--tests/unit/src/com/android/intentresolver/emptystate/EmptyStateUiHelperTest.kt113
-rw-r--r--tests/unit/src/com/android/intentresolver/logging/EventLogImplTest.java (renamed from java/tests/src/com/android/intentresolver/logging/EventLogTest.java)55
-rw-r--r--tests/unit/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java (renamed from java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java)6
-rw-r--r--tests/unit/src/com/android/intentresolver/shortcuts/ScopedAppTargetListCallbackTest.kt71
-rw-r--r--tests/unit/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt490
-rw-r--r--tests/unit/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt (renamed from java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt)0
-rw-r--r--tests/unit/src/com/android/intentresolver/util/TestExecutor.kt (renamed from java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt)31
-rw-r--r--tests/unit/src/com/android/intentresolver/util/UriFiltersTest.kt (renamed from java/tests/src/com/android/intentresolver/util/UriFiltersTest.kt)0
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/ChooserActionFactoryTest.kt244
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/MultiProfilePagerAdapterTest.kt285
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/coroutines/Flow.kt89
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt222
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelperTest.kt228
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/listcontroller/ChooserRequestFilteredComponentsTest.kt61
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/listcontroller/FakeResolverComparator.kt83
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/listcontroller/FilterableComponentsTest.kt77
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/listcontroller/IntentResolverTest.kt499
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/listcontroller/LastChosenManagerTest.kt111
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/listcontroller/PinnableComponentsTest.kt74
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduperTest.kt125
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFilteringTest.kt309
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSortingTest.kt197
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/listcontroller/SharedPreferencesPinnedComponentsTest.kt63
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/platform/FakeSecureSettingsTest.kt61
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/platform/FakeUserManagerTest.kt128
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/platform/NearbyShareModuleTest.kt83
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/validation/ValidationTest.kt99
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/validation/types/IntentOrUriTest.kt107
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/validation/types/ParceledArrayTest.kt93
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/validation/types/SimpleValueTest.kt52
-rw-r--r--tests/unit/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt (renamed from java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt)0
341 files changed, 26678 insertions, 4284 deletions
diff --git a/.clang-format b/.clang-format
deleted file mode 100644
index 03af56d..0000000
--- a/.clang-format
+++ /dev/null
@@ -1,13 +0,0 @@
-BasedOnStyle: Google
-
-AccessModifierOffset: -4
-AlignOperands: false
-AllowShortFunctionsOnASingleLine: Inline
-AlwaysBreakBeforeMultilineStrings: false
-ColumnLimit: 100
-CommentPragmas: NOLINT:.*
-ConstructorInitializerIndentWidth: 6
-ContinuationIndentWidth: 8
-IndentWidth: 4
-PenaltyBreakBeforeFirstCallParameter: 100000
-SpacesBeforeTrailingComments: 1
diff --git a/Android.bp b/Android.bp
index 9d0a8ee..2e67398 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1,5 +1,5 @@
//
-// Copyright (C) 2021 The Android Open Source Project
+// Copyright (C) 2023 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.
@@ -15,56 +15,31 @@
//
package {
- // See: http://go/android-license-faq
- // This was chosen for Sharesheet to match existing packages.
- default_applicable_licenses: ["packages_modules_IntentResolver_license"],
+ default_applicable_licenses: ["Android-Apache-2.0"],
+ default_visibility: [":__subpackages__"],
}
-license {
- name: "packages_modules_IntentResolver_license",
- visibility: [":__subpackages__"],
- license_kinds: [
- "SPDX-license-identifier-Apache-2.0",
- ],
- license_text: [
- "NOTICE",
- ],
-}
-
-filegroup {
- name: "ReleaseSources",
- srcs: [
- "java/src-release/**/*.kt",
- ],
-}
-
-filegroup {
- name: "DebugSources",
- srcs: [
- "java/src-debug/**/*.kt",
- ],
-}
-
-android_library {
- name: "IntentResolver-core",
- min_sdk_version: "current",
+java_defaults {
+ name: "Java_Defaults",
srcs: [
"java/src/**/*.java",
"java/src/**/*.kt",
- ":ReleaseSources",
],
- product_variables: {
- debuggable: {
- srcs: [":DebugSources"],
- exclude_srcs: [":ReleaseSources"],
- }
- },
resource_dirs: [
"java/res",
],
-
manifest: "AndroidManifest-lib.xml",
+ min_sdk_version: "current",
+ lint: {
+ strict_updatability_linting: false,
+ extra_check_modules: ["SystemUILintChecker"],
+ warning_checks: ["MissingApacheLicenseDetector"],
+ },
+}
+android_library {
+ name: "IntentResolver-core",
+ defaults: ["Java_Defaults"],
static_libs: [
"androidx.annotation_annotation",
"androidx.concurrent_concurrent-futures",
@@ -75,40 +50,44 @@ android_library {
"androidx.lifecycle_lifecycle-extensions",
"androidx.lifecycle_lifecycle-runtime-ktx",
"androidx.lifecycle_lifecycle-viewmodel-ktx",
+ "dagger2",
+ "hilt_android",
+ "IntentResolverFlagsLib",
+ "jsr330",
"kotlin-stdlib",
"kotlinx_coroutines",
"kotlinx-coroutines-android",
"//external/kotlinc:kotlin-annotations",
"guava",
- "SystemUIFlagsLib",
],
-
- lint: {
- strict_updatability_linting: false,
- },
-
- optimize: {
- proguard_flags_files: ["proguard.flags"],
- },
}
-android_app {
- name: "IntentResolver",
+java_defaults {
+ name: "App_Defaults",
min_sdk_version: "current",
+ platform_apis: true,
certificate: "platform",
privileged: true,
manifest: "AndroidManifest-app.xml",
required: [
"privapp_whitelist_com.android.intentresolver",
],
- srcs: ["src/**/*.java"],
- platform_apis: true,
+}
+
+android_app {
+ name: "IntentResolver",
+ defaults: ["App_Defaults"],
static_libs: [
"IntentResolver-core",
],
optimize: {
enabled: true,
+ optimize: true,
+ shrink: true,
+ shrink_resources: true,
+ proguard_flags_files: ["proguard.flags"],
},
+ visibility: ["//visibility:public"],
apex_available: [
"//apex_available:platform",
"com.android.intentresolver",
diff --git a/AndroidManifest-app.xml b/AndroidManifest-app.xml
index 57ea497..ec4fec8 100644
--- a/AndroidManifest-app.xml
+++ b/AndroidManifest-app.xml
@@ -17,12 +17,14 @@
*/
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:tools="http://schemas.android.com/tools"
package="com.android.intentresolver"
android:versionCode="0"
android:versionName="2021-11"
coreApp="true">
<application
+ android:name=".MainApplication"
android:hardwareAccelerated="true"
android:label="@string/app_label"
android:directBootAware="true"
@@ -58,6 +60,41 @@
android:visibleToInstantApps="true"
android:exported="false"/>
+ <receiver android:name="com.android.intentresolver.v2.ChooserSelector"
+ android:exported="true">
+ <intent-filter>
+ <action android:name="android.intent.action.BOOT_COMPLETED" />
+ </intent-filter>
+ </receiver>
+
+ <activity android:name="com.android.intentresolver.v2.ChooserActivity"
+ android:enabled="false"
+ android:theme="@style/Theme.DeviceDefault.Chooser"
+ android:finishOnCloseSystemDialogs="true"
+ android:excludeFromRecents="true"
+ android:documentLaunchMode="never"
+ android:relinquishTaskIdentity="true"
+ android:configChanges="screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
+ android:visibleToInstantApps="true"
+ android:exported="true">
+
+ <!-- This intent filter is assigned a priority greater than 500 so
+ that it will take precedence over the ChooserActivity
+ in the process of resolving implicit action.CHOOSER intents
+ whenever this activity is enabled by the experiment flag. -->
+ <intent-filter android:priority="501">
+ <action android:name="android.intent.action.CHOOSER" />
+ <category android:name="android.intent.category.DEFAULT" />
+ <category android:name="android.intent.category.VOICE" />
+ </intent-filter>
+
+ </activity>
+
+ <provider android:name="androidx.startup.InitializationProvider"
+ android:authorities="${applicationId}.androidx-startup"
+ tools:replace="android:authorities"
+ tools:node="remove" />
+
</application>
</manifest>
diff --git a/AndroidManifest-lib.xml b/AndroidManifest-lib.xml
index 509d46a..b3a43eb 100644
--- a/AndroidManifest-lib.xml
+++ b/AndroidManifest-lib.xml
@@ -31,4 +31,5 @@
<uses-permission android:name="android.permission.UNLIMITED_SHORTCUTS_API_CALLS" />
<uses-permission android:name="android.permission.QUERY_CLONED_APPS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+ <uses-permission android:name="android.permission.REPORT_USAGE_STATS" />
</manifest>
diff --git a/NOTICE b/NOTICE
deleted file mode 100644
index d645695..0000000
--- a/NOTICE
+++ /dev/null
@@ -1,202 +0,0 @@
-
- Apache License
- Version 2.0, January 2004
- http://www.apache.org/licenses/
-
- TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
-
- 1. Definitions.
-
- "License" shall mean the terms and conditions for use, reproduction,
- and distribution as defined by Sections 1 through 9 of this document.
-
- "Licensor" shall mean the copyright owner or entity authorized by
- the copyright owner that is granting the License.
-
- "Legal Entity" shall mean the union of the acting entity and all
- other entities that control, are controlled by, or are under common
- control with that entity. For the purposes of this definition,
- "control" means (i) the power, direct or indirect, to cause the
- direction or management of such entity, whether by contract or
- otherwise, or (ii) ownership of fifty percent (50%) or more of the
- outstanding shares, or (iii) beneficial ownership of such entity.
-
- "You" (or "Your") shall mean an individual or Legal Entity
- exercising permissions granted by this License.
-
- "Source" form shall mean the preferred form for making modifications,
- including but not limited to software source code, documentation
- source, and configuration files.
-
- "Object" form shall mean any form resulting from mechanical
- transformation or translation of a Source form, including but
- not limited to compiled object code, generated documentation,
- and conversions to other media types.
-
- "Work" shall mean the work of authorship, whether in Source or
- Object form, made available under the License, as indicated by a
- copyright notice that is included in or attached to the work
- (an example is provided in the Appendix below).
-
- "Derivative Works" shall mean any work, whether in Source or Object
- form, that is based on (or derived from) the Work and for which the
- editorial revisions, annotations, elaborations, or other modifications
- represent, as a whole, an original work of authorship. For the purposes
- of this License, Derivative Works shall not include works that remain
- separable from, or merely link (or bind by name) to the interfaces of,
- the Work and Derivative Works thereof.
-
- "Contribution" shall mean any work of authorship, including
- the original version of the Work and any modifications or additions
- to that Work or Derivative Works thereof, that is intentionally
- submitted to Licensor for inclusion in the Work by the copyright owner
- or by an individual or Legal Entity authorized to submit on behalf of
- the copyright owner. For the purposes of this definition, "submitted"
- means any form of electronic, verbal, or written communication sent
- to the Licensor or its representatives, including but not limited to
- communication on electronic mailing lists, source code control systems,
- and issue tracking systems that are managed by, or on behalf of, the
- Licensor for the purpose of discussing and improving the Work, but
- excluding communication that is conspicuously marked or otherwise
- designated in writing by the copyright owner as "Not a Contribution."
-
- "Contributor" shall mean Licensor and any individual or Legal Entity
- on behalf of whom a Contribution has been received by Licensor and
- subsequently incorporated within the Work.
-
- 2. Grant of Copyright License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- copyright license to reproduce, prepare Derivative Works of,
- publicly display, publicly perform, sublicense, and distribute the
- Work and such Derivative Works in Source or Object form.
-
- 3. Grant of Patent License. Subject to the terms and conditions of
- this License, each Contributor hereby grants to You a perpetual,
- worldwide, non-exclusive, no-charge, royalty-free, irrevocable
- (except as stated in this section) patent license to make, have made,
- use, offer to sell, sell, import, and otherwise transfer the Work,
- where such license applies only to those patent claims licensable
- by such Contributor that are necessarily infringed by their
- Contribution(s) alone or by combination of their Contribution(s)
- with the Work to which such Contribution(s) was submitted. If You
- institute patent litigation against any entity (including a
- cross-claim or counterclaim in a lawsuit) alleging that the Work
- or a Contribution incorporated within the Work constitutes direct
- or contributory patent infringement, then any patent licenses
- granted to You under this License for that Work shall terminate
- as of the date such litigation is filed.
-
- 4. Redistribution. You may reproduce and distribute copies of the
- Work or Derivative Works thereof in any medium, with or without
- modifications, and in Source or Object form, provided that You
- meet the following conditions:
-
- (a) You must give any other recipients of the Work or
- Derivative Works a copy of this License; and
-
- (b) You must cause any modified files to carry prominent notices
- stating that You changed the files; and
-
- (c) You must retain, in the Source form of any Derivative Works
- that You distribute, all copyright, patent, trademark, and
- attribution notices from the Source form of the Work,
- excluding those notices that do not pertain to any part of
- the Derivative Works; and
-
- (d) If the Work includes a "NOTICE" text file as part of its
- distribution, then any Derivative Works that You distribute must
- include a readable copy of the attribution notices contained
- within such NOTICE file, excluding those notices that do not
- pertain to any part of the Derivative Works, in at least one
- of the following places: within a NOTICE text file distributed
- as part of the Derivative Works; within the Source form or
- documentation, if provided along with the Derivative Works; or,
- within a display generated by the Derivative Works, if and
- wherever such third-party notices normally appear. The contents
- of the NOTICE file are for informational purposes only and
- do not modify the License. You may add Your own attribution
- notices within Derivative Works that You distribute, alongside
- or as an addendum to the NOTICE text from the Work, provided
- that such additional attribution notices cannot be construed
- as modifying the License.
-
- You may add Your own copyright statement to Your modifications and
- may provide additional or different license terms and conditions
- for use, reproduction, or distribution of Your modifications, or
- for any such Derivative Works as a whole, provided Your use,
- reproduction, and distribution of the Work otherwise complies with
- the conditions stated in this License.
-
- 5. Submission of Contributions. Unless You explicitly state otherwise,
- any Contribution intentionally submitted for inclusion in the Work
- by You to the Licensor shall be under the terms and conditions of
- this License, without any additional terms or conditions.
- Notwithstanding the above, nothing herein shall supersede or modify
- the terms of any separate license agreement you may have executed
- with Licensor regarding such Contributions.
-
- 6. Trademarks. This License does not grant permission to use the trade
- names, trademarks, service marks, or product names of the Licensor,
- except as required for reasonable and customary use in describing the
- origin of the Work and reproducing the content of the NOTICE file.
-
- 7. Disclaimer of Warranty. Unless required by applicable law or
- agreed to in writing, Licensor provides the Work (and each
- Contributor provides its Contributions) on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
- implied, including, without limitation, any warranties or conditions
- of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
- PARTICULAR PURPOSE. You are solely responsible for determining the
- appropriateness of using or redistributing the Work and assume any
- risks associated with Your exercise of permissions under this License.
-
- 8. Limitation of Liability. In no event and under no legal theory,
- whether in tort (including negligence), contract, or otherwise,
- unless required by applicable law (such as deliberate and grossly
- negligent acts) or agreed to in writing, shall any Contributor be
- liable to You for damages, including any direct, indirect, special,
- incidental, or consequential damages of any character arising as a
- result of this License or out of the use or inability to use the
- Work (including but not limited to damages for loss of goodwill,
- work stoppage, computer failure or malfunction, or any and all
- other commercial damages or losses), even if such Contributor
- has been advised of the possibility of such damages.
-
- 9. Accepting Warranty or Additional Liability. While redistributing
- the Work or Derivative Works thereof, You may choose to offer,
- and charge a fee for, acceptance of support, warranty, indemnity,
- or other liability obligations and/or rights consistent with this
- License. However, in accepting such obligations, You may act only
- on Your own behalf and on Your sole responsibility, not on behalf
- of any other Contributor, and only if You agree to indemnify,
- defend, and hold each Contributor harmless for any liability
- incurred by, or claims asserted against, such Contributor by reason
- of your accepting any such warranty or additional liability.
-
- END OF TERMS AND CONDITIONS
-
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "[]"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright [yyyy] [name of copyright owner]
-
- 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.
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg
index d8136fe..40f0527 100644
--- a/PREUPLOAD.cfg
+++ b/PREUPLOAD.cfg
@@ -1,12 +1,3 @@
-[Builtin Hooks]
-clang_format = true
-
-[Builtin Hooks Options]
-# Only turn on clang-format check for the following subfolders.
-clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp
- jni/
- native/
-
[Hook Scripts]
checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..192f5ca
--- /dev/null
+++ b/README.md
@@ -0,0 +1,8 @@
+# IntentResolver
+
+## About
+
+`IntentResolver` provides the implementation for Intent
+[ACTION_CHOOSER](https://developer.android.com/reference/android/content/Intent#ACTION_CHOOSER)
+
+See also: [ShareCompat.IntentBuilder](https://developer.android.com/reference/androidx/core/app/ShareCompat.IntentBuilder)
diff --git a/TEST_MAPPING b/TEST_MAPPING
index d142bb6..de28a49 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -1,7 +1,15 @@
{
"presubmit": [
{
- "name": "IntentResolverUnitTests"
+ "name": "IntentResolver-tests-unit"
+ }
+ ],
+ "postsubmit": [
+ {
+ "name": "IntentResolver-tests-activity"
+ },
+ {
+ "name": "IntentResolver-tests-integration"
}
]
}
diff --git a/aconfig/Android.bp b/aconfig/Android.bp
new file mode 100644
index 0000000..82267cd
--- /dev/null
+++ b/aconfig/Android.bp
@@ -0,0 +1,30 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+aconfig_declarations {
+ name: "IntentResolverFlags",
+ package: "com.android.intentresolver",
+ srcs: ["FeatureFlags.aconfig"],
+}
+
+java_aconfig_library {
+ name: "IntentResolverFlagsLib",
+ aconfig_declarations: "IntentResolverFlags",
+}
diff --git a/aconfig/FeatureFlags.aconfig b/aconfig/FeatureFlags.aconfig
new file mode 100644
index 0000000..ae83ca7
--- /dev/null
+++ b/aconfig/FeatureFlags.aconfig
@@ -0,0 +1,33 @@
+package: "com.android.intentresolver"
+
+# name: [a-z0-9][_a-z0-9]+
+# namespace: intentresolver
+# bug: "Feature_Bug_#" or "<none>"
+
+flag {
+ name: "example_new_sharing_method"
+ namespace: "intentresolver"
+ description: "Enables the example new sharing mechanism."
+ bug: "<none>"
+}
+
+flag {
+ name: "scrollable_preview"
+ namespace: "intentresolver"
+ description: "Makes preview scrollable with multiple profiles"
+ bug: "287102904"
+}
+
+flag {
+ name: "target_data_caching"
+ namespace: "intentresolver"
+ description: "Enables caching target icons and labels in a local DB"
+ bug: "285314844"
+}
+
+flag {
+ name: "modular_framework"
+ namespace: "intentresolver"
+ description: "Enables the new modular framework"
+ bug: "302113519"
+}
diff --git a/aconfig/README.md b/aconfig/README.md
new file mode 100644
index 0000000..87a6651
--- /dev/null
+++ b/aconfig/README.md
@@ -0,0 +1,20 @@
+# AConfig Flag libraries
+
+Generated java flag libraries.
+
+### FeatureFlagsLib
+
+__Flags__
+* Static singleton provider for FeatureFlags impl
+* Overridable with setFeatureFlags/unsetFeatureFlags
+
+* __FeatureFlags__
+* The generated flags interface, one boolean function per flag
+
+__FeatureFlagsImpl__
+* For production code
+* Real implementation using DeviceConfig
+
+__FakeFeatureFlagsImpl__
+* a configurable stateful fake (get/set/clear)
+* Use with Dagger to inject across multiple components for integration tests
diff --git a/java/res/drawable/chooser_direct_share_label_placeholder.xml b/java/res/drawable/chooser_direct_share_label_placeholder.xml
deleted file mode 100644
index b21444b..0000000
--- a/java/res/drawable/chooser_direct_share_label_placeholder.xml
+++ /dev/null
@@ -1,37 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
- ~ Copyright (C) 2019 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
- -->
-<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
-
- <!-- This drawable is intended to be used as the background of a two line TextView. We only
- want the height to be ~1 line. Do this cheaply by applying padding to the bottom. -->
- <item android:bottom="18dp">
- <shape android:shape="rectangle" >
-
- <!-- Size used for scaling should the container be different dimensions -->
- <size android:width="@dimen/chooser_direct_share_label_placeholder_max_width"
- android:height="18dp"/>
-
- <!-- Absurd corner radius to ensure pill shape -->
- <corners android:bottomLeftRadius="100dp"
- android:bottomRightRadius="100dp"
- android:topLeftRadius="100dp"
- android:topRightRadius="100dp" />
-
- <solid android:color="@color/chooser_gradient_background "/>
- </shape>
- </item>
-</layer-list> \ No newline at end of file
diff --git a/java/res/layout/chooser_grid_preview_file.xml b/java/res/layout/chooser_grid_preview_file.xml
index 3c836b4..90832d2 100644
--- a/java/res/layout/chooser_grid_preview_file.xml
+++ b/java/res/layout/chooser_grid_preview_file.xml
@@ -26,7 +26,13 @@
android:orientation="vertical"
android:background="?androidprv:attr/materialColorSurfaceContainer">
- <include layout="@layout/chooser_headline_row"/>
+ <ViewStub
+ android:id="@+id/chooser_headline_row_stub"
+ android:layout="@layout/chooser_headline_row"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="@dimen/chooser_edge_margin_normal"
+ android:layout_marginBottom="@dimen/chooser_view_spacing" />
<RelativeLayout
android:layout_width="match_parent"
diff --git a/java/res/layout/chooser_grid_preview_files_text.xml b/java/res/layout/chooser_grid_preview_files_text.xml
index c64d7dd..e774749 100644
--- a/java/res/layout/chooser_grid_preview_files_text.xml
+++ b/java/res/layout/chooser_grid_preview_files_text.xml
@@ -25,7 +25,13 @@
android:orientation="vertical"
android:background="?androidprv:attr/materialColorSurfaceContainer">
- <include layout="@layout/chooser_headline_row" />
+ <ViewStub
+ android:id="@+id/chooser_headline_row_stub"
+ android:layout="@layout/chooser_headline_row"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="@dimen/chooser_edge_margin_normal"
+ android:layout_marginBottom="@dimen/chooser_view_spacing" />
<LinearLayout
android:layout_width="match_parent"
diff --git a/java/res/layout/chooser_grid_preview_image.xml b/java/res/layout/chooser_grid_preview_image.xml
index 4a83232..4745e04 100644
--- a/java/res/layout/chooser_grid_preview_image.xml
+++ b/java/res/layout/chooser_grid_preview_image.xml
@@ -26,7 +26,13 @@
android:importantForAccessibility="no"
android:background="?androidprv:attr/materialColorSurfaceContainer">
- <include layout="@layout/chooser_headline_row"/>
+ <ViewStub
+ android:id="@+id/chooser_headline_row_stub"
+ android:layout="@layout/chooser_headline_row"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="@dimen/chooser_edge_margin_normal"
+ android:layout_marginBottom="@dimen/chooser_view_spacing" />
<com.android.intentresolver.widget.ScrollableImagePreviewView
android:id="@+id/scrollable_image_preview"
diff --git a/java/res/layout/chooser_grid_preview_text.xml b/java/res/layout/chooser_grid_preview_text.xml
index df906cc..f3045c3 100644
--- a/java/res/layout/chooser_grid_preview_text.xml
+++ b/java/res/layout/chooser_grid_preview_text.xml
@@ -27,7 +27,13 @@
android:orientation="vertical"
android:background="?androidprv:attr/materialColorSurfaceContainer">
- <include layout="@layout/chooser_headline_row" />
+ <ViewStub
+ android:id="@+id/chooser_headline_row_stub"
+ android:layout="@layout/chooser_headline_row"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="@dimen/chooser_edge_margin_normal"
+ android:layout_marginBottom="@dimen/chooser_view_spacing" />
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
diff --git a/java/res/layout/chooser_grid_scrollable_preview.xml b/java/res/layout/chooser_grid_scrollable_preview.xml
new file mode 100644
index 0000000..c1bcf91
--- /dev/null
+++ b/java/res/layout/chooser_grid_scrollable_preview.xml
@@ -0,0 +1,127 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright 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.
+*/
+-->
+<com.android.intentresolver.widget.ResolverDrawerLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ app:maxCollapsedHeight="0dp"
+ app:maxCollapsedHeightSmall="56dp"
+ app:useScrollablePreviewNestedFlingLogic="true"
+ android:maxWidth="@dimen/chooser_width"
+ android:id="@androidprv:id/contentPanel">
+
+ <RelativeLayout
+ android:id="@androidprv:id/chooser_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_alwaysShow="true"
+ android:elevation="0dp"
+ android:background="@drawable/bottomsheet_background">
+
+ <View
+ android:id="@androidprv:id/drag"
+ android:layout_width="64dp"
+ android:layout_height="4dp"
+ android:background="@drawable/ic_drag_handle"
+ android:layout_marginTop="@dimen/chooser_edge_margin_thin"
+ android:layout_marginBottom="@dimen/chooser_edge_margin_thin"
+ android:layout_centerHorizontal="true"
+ android:layout_alignParentTop="true" />
+
+ <TextView android:id="@android:id/title"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:textAppearance="@android:style/TextAppearance.DeviceDefault.WindowTitle"
+ android:gravity="center"
+ android:paddingBottom="@dimen/chooser_view_spacing"
+ android:paddingLeft="24dp"
+ android:paddingRight="24dp"
+ android:visibility="gone"
+ android:layout_below="@androidprv:id/drag"
+ android:layout_centerHorizontal="true"/>
+ </RelativeLayout>
+
+ <FrameLayout
+ android:id="@+id/chooser_headline_row_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layout_alwaysShow="true"
+ android:background="?androidprv:attr/materialColorSurfaceContainer">
+
+ <ViewStub
+ android:id="@+id/chooser_headline_row_stub"
+ android:inflatedId="@+id/chooser_headline_row"
+ android:layout="@layout/chooser_headline_row"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingHorizontal="@dimen/chooser_edge_margin_normal"
+ android:layout_marginBottom="@dimen/chooser_view_spacing" />
+ </FrameLayout>
+
+ <com.android.intentresolver.widget.ChooserNestedScrollView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical">
+
+ <FrameLayout
+ android:id="@androidprv:id/content_preview_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone" />
+
+ <TabHost
+ android:id="@androidprv:id/profile_tabhost"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:background="?androidprv:attr/materialColorSurfaceContainer">
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <TabWidget
+ android:id="@android:id/tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone">
+ </TabWidget>
+ <FrameLayout
+ android:id="@android:id/tabcontent"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <com.android.intentresolver.ResolverViewPager
+ android:id="@androidprv:id/profile_pager"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+ </FrameLayout>
+ </LinearLayout>
+ </TabHost>
+ </LinearLayout>
+
+ </com.android.intentresolver.widget.ChooserNestedScrollView>
+
+</com.android.intentresolver.widget.ResolverDrawerLayout>
diff --git a/java/res/layout/chooser_list_per_profile_wrap.xml b/java/res/layout/chooser_list_per_profile_wrap.xml
new file mode 100644
index 0000000..157fa75
--- /dev/null
+++ b/java/res/layout/chooser_list_per_profile_wrap.xml
@@ -0,0 +1,42 @@
+<!--
+ ~ Copyright (C) 2019 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.
+ -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:descendantFocusability="blocksDescendants">
+ <!-- ^^^ Block descendants from receiving focus to prevent NestedScrollView
+ (ChooserNestedScrollView) scrolling to the focused view when switching tabs. Without it, TabHost
+ view will request focus on the newly activated tab. The RecyclerView from this layout gets
+ focused and notifies its parents (including NestedScrollView) about it through
+ #requestChildFocus method call. NestedScrollView's view implementation of the method will
+ scroll to the focused view. -->
+
+ <androidx.recyclerview.widget.RecyclerView
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ app:layoutManager="com.android.intentresolver.ChooserGridLayoutManager"
+ android:id="@androidprv:id/resolver_list"
+ android:clipToPadding="false"
+ android:background="?androidprv:attr/materialColorSurfaceContainer"
+ android:scrollbars="none"
+ android:elevation="1dp"
+ android:nestedScrollingEnabled="true" />
+
+ <include layout="@layout/resolver_empty_states" />
+</RelativeLayout>
diff --git a/java/res/values-af/strings.xml b/java/res/values-af/strings.xml
index 91b9e04..e0a7383 100644
--- a/java/res/values-af/strings.xml
+++ b/java/res/values-af/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Speld <xliff:g id="LABEL">%1$s</xliff:g> vas"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Ontspeld <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Wysig"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # lêer}other{{file_name} + # lêers}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # lêer}other{+ # lêers}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ nog # lêer}other{+ nog # lêers}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Deel tans teks"</string>
<string name="sharing_link" msgid="2307694372813942916">"Deel tans skakel"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Deel tans prent}other{Deel tans # prente}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Deel tans video}other{Deel tans # video’s}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Deel tans # item}other{Deel tans # items}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Deel tans prent met teks"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Deel prent met skakel"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Deel tans # lêer}other{Deel tans # lêers}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Deel tans prent met teks}other{Deel tans # prente met teks}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Deel tans prent met skakel}other{Deel tans # prente met skakel}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Deel tans video met teks}other{Deel tans # video’s met teks}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Deel tans video met skakel}other{Deel tans # video’s met skakel}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Deel tans lêer met teks}other{Deel tans # lêers met teks}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Deel tans lêer met skakel}other{Deel tans # lêers met skakel}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Net prent}other{Net prente}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Net video}other{Net video’s}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Net lêer}other{Net lêers}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Prentvoorskouminiprent"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Videovoorskouminiprent"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Lêervoorskouminiprent"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Geen mense om mee te deel is aanbeveel nie"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Programmelys"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Opneemtoestemming is nie aan hierdie program verleen nie, maar dit kan oudio deur hierdie USB-toestel opneem."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Persoonlik"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Werk"</string>
@@ -72,10 +81,10 @@
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Deur jou IT-admin geblokkeer"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Hierdie inhoud kan nie met werkprogramme gedeel word nie"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Hierdie inhoud kan nie met werkprogramme oopgemaak word nie"</string>
- <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Hierdie inhoud kan nie met persoonlike programme gedeel word nie"</string>
+ <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Hierdie inhoud kan nie met persoonlike apps gedeel word nie"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Hierdie inhoud kan nie met persoonlike programme oopgemaak word nie"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Werkprofiel is onderbreek"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tik om aan te skakel"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Werkapps word onderbreek"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Hervat"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Geen werkprogramme nie"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Geen persoonlike programme nie"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Maak <xliff:g id="APP">%s</xliff:g> in jou persoonlike profiel oop?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Sluit teks in"</string>
<string name="exclude_link" msgid="1332778255031992228">"Sluit skakel uit"</string>
<string name="include_link" msgid="827855767220339802">"Sluit skakel in"</string>
+ <string name="pinned" msgid="7623664001331394139">"Vasgespeld"</string>
</resources>
diff --git a/java/res/values-am/strings.xml b/java/res/values-am/strings.xml
index 8145012..ba6409f 100644
--- a/java/res/values-am/strings.xml
+++ b/java/res/values-am/strings.xml
@@ -53,29 +53,38 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g>ን ፒን አድርግ"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> ንቀል"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"አርትዕ"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ፋይል}one{{file_name} + # ፋይል}other{{file_name} + # ፋይሎች}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ፋይል}one{+ # ፋይል}other{+ # ፋይሎች}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # ተጨማሪ ፋይል}one{+ # ተጨማሪ ፋይል}other{+ # ተጨማሪ ፋይሎች}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"ጽሑፍን በማጋራት ላይ"</string>
<string name="sharing_link" msgid="2307694372813942916">"አገናኝን በማጋራት ላይ"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{ምስልን በማጋራት ላይ}one{# ምስልን በማጋራት ላይ}other{# ምስሎችን በማጋራት ላይ}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{ቪድዮ በማጋራት ላይ}one{# ቪድዮ በማጋራት ላይ}other{# ቪድዮዎችን በማጋራት ላይ}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# ንጥልን በማጋራት ላይ}one{# ንጥልን በማጋራት ላይ}other{# ንጥሎችን በማጋራት ላይ}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"ምስልን ከጽሑፍ ጋር በማጋራት ላይ"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"ምስልን ከአገናኝ ጋር በማጋራት ላይ"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ፋይልን በማጋራት ላይ}one{# ፋይልን በማጋራት ላይ}other{# ፋይሎችን በማጋራት ላይ}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{ምስልን ከጽሑፍ ጋር በማጋራት ላይ}one{# ምስልን ከጽሑፍ ጋር በማጋራት ላይ}other{# ምስሎችን ከጽሑፍ ጋር በማጋራት ላይ}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{ምስልን ከአገናኝ ጋር በማጋራት ላይ}one{# ምስልን ከአገናኝ ጋር በማጋራት ላይ}other{# ምስሎችን ከአገናኝ ጋር በማጋራት ላይ}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ቪድዮ ከጽሑፍ ጋር በማጋራት ላይ}one{# ቪድዮ ከጽሑፍ ጋር በማጋራት ላይ}other{# ቪድዮዎችን ከጽሑፍ ጋር በማጋራት ላይ}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{ቪድዮ ከአገናኝ ጋር በማጋራት ላይ}one{# ቪድዮ ከአገናኝ ጋር በማጋራት ላይ}other{# ቪድዮዎችን ከአገናኝ ጋር በማጋራት ላይ}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ፋይልን ከጽሑፍ ጋር በማጋራት ላይ}one{# ፋይልን ከጽሑፍ ጋር በማጋራት ላይ}other{# ፋይሎችን ከጽሑፍ ጋር በማጋራት ላይ}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{ፋይልን ከአገናኝ ጋር በማጋራት ላይ}one{# ፋይልን ከአገናኝ ጋር በማጋራት ላይ}other{# ፋይሎችን ከአገናኝ ጋር በማጋራት ላይ}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{ምስል ብቻ}one{ምስል ብቻ}other{ምስሎች ብቻ}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{ቪድዮ ብቻ}one{ቪድዮ ብቻ}other{ቪድዮዎች ብቻ}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ፋይል ብቻ}one{ፋይል ብቻ}other{ፋይሎች ብቻ}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"የምስል ቅድመ ዕይታ ጥፍር አከል"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"የቪድዮ ቅድመ ዕይታ ጥፍር አከል"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"የፋይል ቅድመ ዕይታ ጥፍር አከል"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"የሚያጋሯቸው ምንም የሚመከሩ ሰዎች የሉም"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"የመተግበሪያዎች ዝርዝር"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ይህ መተግበሪያ የመቅረጽ ፈቃድ አልተሰጠውም፣ ነገር ግን በዚህ ዩኤስቢ መሣሪያ በኩል ኦዲዮን መቅረጽ ይችላል።"</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"የግል"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"ሥራ"</string>
- <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"የግል እይታ"</string>
- <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"የስራ እይታ"</string>
+ <string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"የግል ዕይታ"</string>
+ <string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"የስራ ዕይታ"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"በእርስዎ የአይቲ አስተዳዳሪ ታግዷል"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"ይህ ይዘት በሥራ መተግበሪያዎች መጋራት አይችልም"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ይህ ይዘት በሥራ መተግበሪያዎች መከፈት አይችልም"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ይህ ይዘት በግል መተግበሪያዎች መጋራት አይችልም"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ይህ ይዘት በግል መተግበሪያዎች መከፈት አይችልም"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"የሥራ መገለጫ ባለበት ቆሟል"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"ለማብራት መታ ያድርጉ"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"የሥራ መተግበሪያዎች ባሉበት ቆመዋል"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"ከቆመበት ቀጥል"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"ምንም የሥራ መተግበሪያዎች የሉም"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"ምንም የግል መተግበሪያዎች የሉም"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> በግል መገለጫዎ ውስጥ ይከፈት?"</string>
@@ -83,7 +92,8 @@
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"የግል አሳሽ ተጠቀም"</string>
<string name="miniresolver_use_work_browser" msgid="7892699758493230342">"የስራ አሳሽ ተጠቀም"</string>
<string name="exclude_text" msgid="5508128757025928034">"ጽሁፍን አታካትት"</string>
- <string name="include_text" msgid="642280283268536140">"ፅሁፍ ጨምር"</string>
+ <string name="include_text" msgid="642280283268536140">"ጽሁፍ ጨምር"</string>
<string name="exclude_link" msgid="1332778255031992228">"አገናኝን አታካትት"</string>
<string name="include_link" msgid="827855767220339802">"አገናኝ አካትት"</string>
+ <string name="pinned" msgid="7623664001331394139">"ፒን ተደርጓል"</string>
</resources>
diff --git a/java/res/values-ar/strings.xml b/java/res/values-ar/strings.xml
index 16bff5b..da8d4de 100644
--- a/java/res/values-ar/strings.xml
+++ b/java/res/values-ar/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"تثبيت <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"إزالة تثبيت <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"تعديل"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + ملف واحد}zero{{file_name} + # ملف}two{{file_name} + ملفان}few{{file_name} + # ملفات}many{{file_name} + # ملفًا}other{{file_name} + # ملف}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ ملف واحد}zero{+ # ملف}two{+ ملفان}few{+ # ملفات}many{+ # ملفًا}other{+ # ملف}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{وملف واحد آخر}zero{و# ملف آخر}two{وملفان آخران}few{و# ملفات أخرى}many{و# ملفًا آخر}other{و# ملف آخر}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"جارٍ مشاركة النص"</string>
<string name="sharing_link" msgid="2307694372813942916">"جارٍ مشاركة الرابط"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{جارٍ مشاركة صورة واحدة}zero{جارٍ مشاركة # صورة}two{جارٍ مشاركة صورتَين}few{جارٍ مشاركة # صور}many{جارٍ مشاركة # صورة}other{جارٍ مشاركة # صورة}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{جارٍ مشاركة فيديو واحد}zero{جارٍ مشاركة # فيديو}two{جارٍ مشاركة فيديوهَين}few{جارٍ مشاركة # فيديوهات}many{جارٍ مشاركة # فيديو}other{جارٍ مشاركة # فيديو}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{جارٍ مشاركة عنصر واحد}zero{جارٍ مشاركة # عنصر}two{جارٍ مشاركة عنصرَين}few{جارٍ مشاركة # عناصر}many{جارٍ مشاركة # عنصرًا}other{جارٍ مشاركة # عنصر}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"مشاركة صورة بنص"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"مشاركة صورة برابط"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{مشاركة ملف واحد}zero{مشاركة # ملف}two{مشاركة ملفَّين}few{مشاركة # ملفات}many{مشاركة # ملفًّا}other{مشاركة # ملف}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{مشاركة صورة واحدة ونص}zero{مشاركة # صورة ونص}two{مشاركة صورتَين ونص}few{مشاركة # صور ونص}many{مشاركة # صورة ونص}other{مشاركة # صورة ونص}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{مشاركة صورة واحدة ورابط}zero{مشاركة # صورة ورابط}two{مشاركة # صورتَين ورابط}few{مشاركة # صور ورابط}many{مشاركة # صورة ورابط}other{مشاركة # صورة ورابط}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{مشاركة فيديو واحد ونص}zero{مشاركة # فيديو ونص}two{مشاركة فيديوهَين ونص}few{مشاركة # فيديوهات ونص}many{مشاركة # فيديو ونص}other{مشاركة # فيديو ونص}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{مشاركة فيديو واحد ورابط}zero{مشاركة # فيديو ورابط}two{مشاركة فيديوهَين ورابط}few{مشاركة # فيديوهات ورابط}many{مشاركة # فيديو ورابط}other{مشاركة # فيديو ورابط}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{مشاركة ملف واحد ونص}zero{مشاركة # ملف ونص}two{مشاركة # ملفَّين ونص}few{مشاركة # ملفات ونص}many{مشاركة # ملفًا ونص}other{مشاركة # ملف ونص}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{مشاركة ملف واحد ورابط}zero{مشاركة # ملف ورابط}two{مشاركة ملفَّين ورابط}few{مشاركة # ملفات ورابط}many{مشاركة # ملفًا ورابط}other{مشاركة # ملف ورابط}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{الصورة فقط}zero{الصور فقط}two{الصورتان فقط}few{الصور فقط}many{الصور فقط}other{الصور فقط}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{الفيديو فقط}zero{الفيديوهات فقط}two{الفيديوهان فقط}few{الفيديوهات فقط}many{الفيديوهات فقط}other{الفيديوهات فقط}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{الملف فقط}zero{الملفات فقط}two{الملفان فقط}few{الملفات فقط}many{الملفات فقط}other{الملفات فقط}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"صورة مصغّرة لمعاينة صورة"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"صورة مصغّرة لمعاينة فيديو"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"صورة مصغّرة لمعاينة ملف"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ما مِن أشخاص مقترحين للمشاركة معهم."</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"قائمة التطبيقات"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"‏لم يتم منح هذا التطبيق إذن تسجيل، ولكن يمكنه تسجيل الصوت من خلال جهاز USB هذا."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"شخصي"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"للعمل"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"لا يمكن فتح هذا المحتوى باستخدام تطبيقات العمل."</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"لا يمكن مشاركة هذا المحتوى مع التطبيقات الشخصية."</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"لا يمكن فتح هذا المحتوى باستخدام التطبيقات الشخصية."</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"الملف الشخصي للعمل متوقف مؤقتًا."</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"انقر لتفعيل الميزة"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"تطبيقات العمل متوقفة مؤقتًا."</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"إلغاء الإيقاف المؤقت"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"ما مِن تطبيقات عمل."</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"ما مِن تطبيقات شخصية."</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"هل تريد فتح <xliff:g id="APP">%s</xliff:g> في ملفك الشخصي؟"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"تضمين النص"</string>
<string name="exclude_link" msgid="1332778255031992228">"استثناء الرابط"</string>
<string name="include_link" msgid="827855767220339802">"تضمين الرابط"</string>
+ <string name="pinned" msgid="7623664001331394139">"مثبَّت"</string>
</resources>
diff --git a/java/res/values-as/strings.xml b/java/res/values-as/strings.xml
index cd294ec..14bd864 100644
--- a/java/res/values-as/strings.xml
+++ b/java/res/values-as/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> পিন কৰক"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g>ক আনপিন কৰক"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"সম্পাদনা কৰক"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # টা ফাইল}one{{file_name} + # টা ফাইল}other{{file_name} + # টা ফাইল}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # টা ফাইল}one{+ # টা ফাইল}other{+ # টা ফাইল}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{আৰু # টা ফাইল}one{আৰু # টা ফাইল}other{আৰু # টা ফাইল}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"পাঠ শ্বেয়াৰ কৰি থকা হৈছে"</string>
<string name="sharing_link" msgid="2307694372813942916">"লিংক শ্বেয়াৰ কৰি থকা হৈছে"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}one{# খন প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}other{# খন প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}one{# টা ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}other{# টা ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# টা বস্তু শ্বেয়াৰ কৰি থকা হৈছে}one{# টা বস্তু শ্বেয়াৰ কৰি থকা হৈছে}other{# টা বস্তু শ্বেয়াৰ কৰি থকা হৈছে}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"পাঠেৰে প্ৰতিচ্ছবি শ্বেয়াৰ কৰি হৈছে"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"লিংকৰে প্ৰতিচ্ছবি শ্বেয়াৰ কৰি হৈছে"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# টা ফাইল শ্বেয়াৰ কৰি থকা হৈছে}one{# টা ফাইল শ্বেয়াৰ কৰি থকা হৈছে}other{# টা ফাইল শ্বেয়াৰ কৰি থকা হৈছে}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{পাঠৰ সৈতে প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}one{পাঠৰ সৈতে # টা প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}other{পাঠৰ সৈতে # টা প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{লিংকৰ সৈতে প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}one{লিংকৰ সৈতে # টা প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}other{লিংকৰ সৈতে # টা প্ৰতিচ্ছবি শ্বেয়াৰ কৰি থকা হৈছে}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{পাঠৰ সৈতে ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}one{পাঠৰ সৈতে # টা ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}other{পাঠৰ সৈতে # টা ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{লিংকৰ সৈতে ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}one{লিংকৰ সৈতে # টা ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}other{লিংকৰ সৈতে # টা ভিডিঅ’ শ্বেয়াৰ কৰি থকা হৈছে}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{পাঠৰ সৈতে ফাইল শ্বেয়াৰ কৰি থকা হৈছে}one{পাঠৰ সৈতে # টা ফাইল শ্বেয়াৰ কৰি থকা হৈছে}other{পাঠৰ সৈতে # টা ফাইল শ্বেয়াৰ কৰি থকা হৈছে}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{লিংকৰ সৈতে ফাইল শ্বেয়াৰ কৰি থকা হৈছে}one{লিংকৰ সৈতে # টা ফাইল শ্বেয়াৰ কৰি থকা হৈছে}other{লিংকৰ সৈতে # টা ফাইল শ্বেয়াৰ কৰি থকা হৈছে}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{কেৱল প্ৰতিচ্ছবি}one{কেৱল প্ৰতিচ্ছবিসমূহ}other{কেৱল প্ৰতিচ্ছবিসমূহ}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{কেৱল ভিডিঅ’}one{কেৱল ভিডিঅ’সমূহ}other{কেৱল ভিডিঅ’সমূহ}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{কেৱল ফাইল}one{কেৱল ফাইলসমূহ}other{কেৱল ফাইলসমূহ}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"প্ৰতিচ্ছবিৰ পূৰ্বদৰ্শনৰ থাম্বনেইল"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"ভিডিঅ’ৰ পূৰ্বদৰ্শনৰ থাম্বনেইল"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"ফাইলৰ পূৰ্বদৰ্শনৰ থাম্বনেইল"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"শ্বেয়াৰ কৰিবলৈ চুপাৰিছ কৰা কোনো লোক নাই"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"এপ্‌সমূহৰ সূচী"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"এই এপ্‌টোক ৰেকর্ড কৰাৰ অনুমতি দিয়া হোৱা নাই কিন্তু ই এই ইউএছবি ডিভাইচটোৰ জৰিয়তে অডিঅ\' ৰেকর্ড কৰিব পাৰে।"</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"ব্যক্তিগত"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"কৰ্মস্থান"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"এই সমল কৰ্মস্থানৰ এপৰ জৰিয়তে খুলিব নোৱাৰি"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"এই সমল ব্যক্তিগত এপৰ সৈতে শ্বেয়াৰ কৰিব নোৱাৰি"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"এই সমল ব্যক্তিগত এপৰ জৰিয়তে খুলিব নোৱাৰি"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"কৰ্মস্থানৰ প্ৰ\'ফাইলটো পজ কৰা আছে"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"অন কৰিবলৈ টিপক"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"কাম সম্পর্কীয় এপ্‌ পজ কৰা আছে"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"আনপজ কৰক"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"কোনো কৰ্মস্থানৰ এপ্‌ নাই"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"কোনো ব্যক্তিগত এপ্‌ নাই"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"আপোনাৰ ব্যক্তিগত প্ৰ’ফাইলত <xliff:g id="APP">%s</xliff:g> খুলিবনে?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"পাঠ অন্তৰ্ভুক্ত কৰক"</string>
<string name="exclude_link" msgid="1332778255031992228">"লিংক বহিৰ্ভূত কৰক"</string>
<string name="include_link" msgid="827855767220339802">"লিংক অন্তৰ্ভুক্ত কৰক"</string>
+ <string name="pinned" msgid="7623664001331394139">"পিন কৰা আছে"</string>
</resources>
diff --git a/java/res/values-az/strings.xml b/java/res/values-az/strings.xml
index 3c66f5c..a31df36 100644
--- a/java/res/values-az/strings.xml
+++ b/java/res/values-az/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Bərkidin: <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"İşarələməyin: <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Redaktə edin"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fayl}other{{file_name} + # fayl}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fayl}other{+ # fayl}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # fayl}other{+ # fayl}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Mətn paylaşılır"</string>
<string name="sharing_link" msgid="2307694372813942916">"Link paylaşılır"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Şəkil paylaşılır}other{# şəkil paylaşılır}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Video paylaşılır}other{# video paylaşılır}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# element paylaşılır}other{# element paylaşılır}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Şəkil mətn ilə paylaşılır"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Şəkil link ilə paylaşılır"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# fayl paylaşılır}other{# fayl paylaşılır}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Mətn olan şəkil paylaşılır}other{Mətn olan # şəkil paylaşılır}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Link olan şəkil paylaşılır}other{Link olan # şəkil paylaşılır}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Mətn olan video paylaşılır}other{Mətn olan # video paylaşılır}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Link olan video paylaşılır}other{Link olan # video paylaşılır}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Mətn olan fayl paylaşılır}other{Mətn olan # fayl paylaşılır}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Link olan fayl paylaşılır}other{Link olan # fayl paylaşılır}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Yalnız şəkil}other{Yalnız şəkillər}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Yalnız video}other{Yalnız videolar}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Yalnız fayl}other{Yalnız fayllar}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Şəkil önizləmə miniatürü"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Video önizləmə miniatürü"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Fayl önizləmə miniatürü"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Paylaşmaq üçün tövsiyə edilən bir kimsə yoxdur"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Tətbiq siyahısı"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Tətbiqə qeydə almaq icazəsi verilməsə də, bu USB vasitəsilə səsi qeydə ala bilər."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Şəxsi"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"İş"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Bu kontenti iş tətbiqləri ilə açmaq mümkün deyil"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Bu kontenti şəxsi tətbiqlər ilə paylaşmaq mümkün deyil"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Bu kontenti şəxsi tətbiqlər ilə açmaq mümkün deyil"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"İş profilinə fasilə verilib"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Aktiv etmək üçün toxunun"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"İş tətbiqləri durdurulub"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Pauzanı bitirin"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"İş tətbiqi yoxdur"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Şəxsi tətbiq yoxdur"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Şəxsi profilinizdə <xliff:g id="APP">%s</xliff:g> tətbiqi açılsın?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Mətn daxil edin"</string>
<string name="exclude_link" msgid="1332778255031992228">"Keçidi istisna edin"</string>
<string name="include_link" msgid="827855767220339802">"Keçid daxil edin"</string>
+ <string name="pinned" msgid="7623664001331394139">"Bərkidilib"</string>
</resources>
diff --git a/java/res/values-b+sr+Latn/strings.xml b/java/res/values-b+sr+Latn/strings.xml
index 83c55e2..ea0d87b 100644
--- a/java/res/values-b+sr+Latn/strings.xml
+++ b/java/res/values-b+sr+Latn/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Zakačite osobu <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Otkači aplikaciju <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Izmeni"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fajl}one{{file_name} + # fajl}few{{file_name} + # fajla}other{{file_name} + # fajlova}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{i još # fajl}one{i još # fajl}few{i još # fajla}other{i još # fajlova}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ još # fajl}one{+ još # fajl}few{+ još # fajla}other{+ još # fajlova}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Deli se tekst"</string>
<string name="sharing_link" msgid="2307694372813942916">"Deli se link"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Deli se slika}one{Deli se # slika}few{Dele se # slike}other{Deli se # slika}}"</string>
- <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Deli se video}one{Deli se # video}few{Dele se # video snimka}other{Deli se # video snimaka}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Deli se # stavka}one{Deli se # stavka}few{Dele se # stavke}other{Deli se # stavki}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Deli se slika sa tekstom"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Deli se slika sa linkom"</string>
+ <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Deli se video}one{Deli se # video}few{Dele se # video snimka}other{Deli se # videa}}"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Deli se # fajl}one{Deli se # fajl}few{Dele se # fajla}other{Deli se # fajlova}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Deli se slika sa tekstom}one{Deli se # slika sa tekstom}few{Dele se # slike sa tekstom}other{Deli se # slika sa tekstom}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Deli se slika sa linkom}one{Deli se # slika sa linkom}few{Dele se # slike sa linkom}other{Deli se # slika sa linkom}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Deli se video sa tekstom}one{Deli se # video sa tekstom}few{Dele se # video snimka sa tekstom}other{Deli se # videa sa tekstom}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Deli se video sa linkom}one{Deli se # video sa linkom}few{Dele se # video snimka sa linkom}other{Deli se # videa sa linkom}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Deli se fajl sa tekstom}one{Deli se # fajl sa tekstom}few{Dele se # fajla sa tekstom}other{Deli se # fajlova sa tekstom}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Deli se fajl sa linkom}one{Deli se # fajl sa linkom}few{Dele se # fajla sa linkom}other{Deli se # fajlova sa linkom}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Samo slika}one{Samo slike}few{Samo slike}other{Samo slike}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Samo video}one{Samo video snimci}few{Samo video snimci}other{Samo video snimci}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Samo fajl}one{Samo fajlovi}few{Samo fajlovi}other{Samo fajlovi}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Sličica za pregled slike"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Sličica za pregled videa"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Sličica za pregled fajla"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nema preporučenih ljudi za deljenje"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista aplikacija"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Ova aplikacija nema dozvolu za snimanje, ali bi mogla da snima zvuk pomoću ovog USB uređaja."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Lično"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Poslovno"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Ovaj sadržaj ne može da se otvara pomoću poslovnih aplikacija"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Ovaj sadržaj ne može da se deli pomoću ličnih aplikacija"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Ovaj sadržaj ne može da se otvara pomoću ličnih aplikacija"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Poslovni profil je pauziran"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Dodirnite da biste uključili"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Poslovne aplikacije su pauzirane"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Ponovo aktiviraj"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nema poslovnih aplikacija"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nema ličnih aplikacija"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Želite da na ličnom profilu otvorite: <xliff:g id="APP">%s</xliff:g>?"</string>
@@ -84,6 +93,7 @@
<string name="miniresolver_use_work_browser" msgid="7892699758493230342">"Koristi poslovni pregledač"</string>
<string name="exclude_text" msgid="5508128757025928034">"Isključi tekst"</string>
<string name="include_text" msgid="642280283268536140">"Uvrsti tekst"</string>
- <string name="exclude_link" msgid="1332778255031992228">"Isključi link"</string>
+ <string name="exclude_link" msgid="1332778255031992228">"Izuzmi link"</string>
<string name="include_link" msgid="827855767220339802">"Uvrsti link"</string>
+ <string name="pinned" msgid="7623664001331394139">"Zakačeno"</string>
</resources>
diff --git a/java/res/values-be/strings.xml b/java/res/values-be/strings.xml
index a24b4a3..aecc1cb 100644
--- a/java/res/values-be/strings.xml
+++ b/java/res/values-be/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Замацаваць праграму \"<xliff:g id="LABEL">%1$s</xliff:g>\""</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Адмацаваць праграму \"<xliff:g id="LABEL">%1$s</xliff:g>\""</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Рэдагаваць"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # файл}one{{file_name} + # файл}few{{file_name} + # файлы}many{{file_name} + # файлаў}other{{file_name} + # файла}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # файл}one{+ # файл}few{+ # файлы}many{+ # файлаў}other{+ # файла}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ яшчэ # файл}one{+ яшчэ # файл}few{+ яшчэ # файлы}many{+ яшчэ # файлаў}other{+ яшчэ # файла}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Абагульванне тэксту"</string>
<string name="sharing_link" msgid="2307694372813942916">"Абагульванне спасылкі"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Абагульванне відарыса}one{Абагульванне # відарыса}few{Абагульванне # відарысаў}many{Абагульванне # відарысаў}other{Абагульванне # відарыса}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Абагульванне відэа}one{Абагульванне # відэа}few{Абагульванне # відэа}many{Абагульванне # відэа}other{Абагульванне # відэа}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Абагульванне # аб\'екта}one{Абагульванне # аб\'екта}few{Абагульванне # аб\'ектаў}many{Абагульванне # аб\'ектаў}other{Абагульванне # аб\'екта}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Абагульванне відарыса з тэкстам"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Абагульванне відарыса са спасылкай"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Абагульваецца # файл}one{Абагульваецца # файл}few{Абагульваюцца # файлы}many{Абагульваюцца # файлаў}other{Абагульваюцца # файла}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Абагульванне відарыса з тэкстам}one{Абагульванне # відарыса з тэкстам}few{Абагульванне # відарысаў з тэкстам}many{Абагульванне # відарысаў з тэкстам}other{Абагульванне # відарыса з тэкстам}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Абагульванне відарыса са спасылкай}one{Абагульванне # відарыса са спасылкай}few{Абагульванне # відарысаў са спасылкай}many{Абагульванне # відарысаў са спасылкай}other{Абагульванне # відарыса са спасылкай}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Абагульванне відэа з тэкстам}one{Абагульванне # відэа з тэкстам}few{Абагульванне # відэа з тэкстам}many{Абагульванне # відэа з тэкстам}other{Абагульванне # відэа з тэкстам}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Абагульванне відэа са спасылкай}one{Абагульванне # відэа са спасылкай}few{Абагульванне # відэа са спасылкай}many{Абагульванне # відэа са спасылкай}other{Абагульванне # відэа са спасылкай}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Абагульванне файла з тэкстам}one{Абагульванне # файла з тэкстам}few{Абагульванне # файлаў з тэкстам}many{Абагульванне # файлаў з тэкстам}other{Абагульванне # файла з тэкстам}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Абагульванне файла са спасылкай}one{Абагульванне # файла са спасылкай}few{Абагульванне # файлаў са спасылкай}many{Абагульванне # файлаў са спасылкай}other{Абагульванне # файла са спасылкай}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Толькі відарыс}one{Толькі відарысы}few{Толькі відарысы}many{Толькі відарысы}other{Толькі відарысы}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Толькі відэа}one{Толькі відэа}few{Толькі відэа}many{Толькі відэа}other{Толькі відэа}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Толькі файл}one{Толькі файлы}few{Толькі файлы}many{Толькі файлы}other{Толькі файлы}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Мініяцюра перадпрагляду відарыса"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Мініяцюра перадпрагляду відэа"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Мініяцюра перадпрагляду файла"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Няма кантактаў, з якімі рэкамендуецца абагульваць змесціва"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Спіс праграм"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"У гэтай праграмы няма дазволу на запіс, аднак яна зможа запісваць аўдыя праз гэту USB-прыладу."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Асабісты"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Працоўны"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Не ўдалося адкрыць гэта змесціва з дапамогай працоўных праграм"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Не ўдалося абагуліць гэта змесціва з асабістымі праграмамі"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Не ўдалося адкрыць гэта змесціва з дапамогай асабістых праграм"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Працоўны профіль прыпынены"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Націсніце, каб уключыць"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Працоўныя праграмы прыпынены"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Уключыць"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Няма працоўных праграм"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Няма асабістых праграм"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Адкрыць праграму \"<xliff:g id="APP">%s</xliff:g>\" з выкарыстаннем асабістага профілю?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Дадаць тэкст"</string>
<string name="exclude_link" msgid="1332778255031992228">"Выдаліць спасылку"</string>
<string name="include_link" msgid="827855767220339802">"Дадаць спасылку"</string>
+ <string name="pinned" msgid="7623664001331394139">"Замацавана"</string>
</resources>
diff --git a/java/res/values-bg/strings.xml b/java/res/values-bg/strings.xml
index 4489205..5bc22d7 100644
--- a/java/res/values-bg/strings.xml
+++ b/java/res/values-bg/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Фиксиране на <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Премахване на фиксирането на <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Редактиране"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # файл}other{{file_name} + # файла}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # файл}other{+ # файла}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ още # файл}other{+ още # файла}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Текстът се споделя"</string>
<string name="sharing_link" msgid="2307694372813942916">"Връзката се споделя"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Изображението се споделя}other{# изображения се споделят}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Видеоклипът се споделя}other{# видеоклипа се споделят}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# елемент се споделя}other{# елемента се споделят}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Изобр. се споделя с текст"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Изобр. се споделя с връзка"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# файл се споделя}other{# файла се споделят}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Споделяне на изображението чрез SMS съобщение}other{Споделяне на # изображения чрез SMS съобщение}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Споделяне на изображението чрез връзка}other{Споделяне на # изображения чрез връзка}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Споделяне на видеоклипа чрез SMS съобщение}other{Споделяне на # видеоклипа чрез SMS съобщение}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Споделяне на видеоклипа чрез връзка}other{Споделяне на # видеоклипа чрез връзка}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Споделяне на файла чрез SMS съобщение}other{Споделяне на # файла чрез SMS съобщение}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Споделяне на файла чрез връзка}other{Споделяне на # файла чрез връзка}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Само изображение}other{Само изображения}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Само видеоклип}other{Само видеоклипове}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Само файл}other{Само файлове}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Миниизображение за визуализация на изображението"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Миниизображение за визуализация на видеоклипа"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Миниизображение за визуализация на файла"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Няма препоръки за хора, с които да споделяте"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Списък с приложения"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Приложението няма разрешение за записване, но може да записва звук чрез това USB устройство."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Лични"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Служебни"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Това съдържание не може да се отваря със служебни приложения"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Това съдържание не може да се споделя с лични приложения"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Това съдържание не може да се отваря с лични приложения"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Служебният потребителски профил е поставен на пауза"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Докоснете за включване"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Служебните приложения са поставени на пауза"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Отмяна на паузата"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Няма подходящи служебни приложения"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Няма подходящи лични приложения"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Искате ли да отворите <xliff:g id="APP">%s</xliff:g> в личния си потребителски профил?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Включване на текста"</string>
<string name="exclude_link" msgid="1332778255031992228">"Изключване на връзката"</string>
<string name="include_link" msgid="827855767220339802">"Включване на връзката"</string>
+ <string name="pinned" msgid="7623664001331394139">"Фиксирано"</string>
</resources>
diff --git a/java/res/values-bn/strings.xml b/java/res/values-bn/strings.xml
index 22438fb..0561cf9 100644
--- a/java/res/values-bn/strings.xml
+++ b/java/res/values-bn/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> অ্যাপ পিন করুন"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> অ্যাপ আনপিন করুন"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"এডিট করুন"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} ও আরও #টি ফাইল}one{{file_name} ও আরও #টি ফাইল}other{{file_name} ও আরও #টি ফাইল}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{আরও #টি ফাইল}one{আরও #টি ফাইল}other{আরও #টি ফাইল}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{আরও #টি ফাইল}one{আরও #টি ফাইল}other{আরও #টি ফাইল}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"টেক্সট শেয়ার করা হচ্ছে"</string>
- <string name="sharing_link" msgid="2307694372813942916">"লিঙ্ক শেয়ার করা হচ্ছে"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"শেয়ার করা লিঙ্ক"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{ছবি শেয়ার করা হচ্ছে}one{#টি ছবি শেয়ার করা হচ্ছে}other{#টি ছবি শেয়ার করা হচ্ছে}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{ভিডিও শেয়ার করা হচ্ছে}one{#টি ভিডিও শেয়ার করা হচ্ছে}other{#টি ভিডিও শেয়ার করা হচ্ছে}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{#টি আইটেম শেয়ার করা হচ্ছে}one{#টি আইটেম শেয়ার করা হচ্ছে}other{#টি আইটেম শেয়ার করা হচ্ছে}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"ছবি টেক্সটের মাধ্যমে শেয়ার করা হচ্ছে"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"ছবি লিঙ্কের মাধ্যমে শেয়ার করা হচ্ছে"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{#টি ফাইল শেয়ার করা হচ্ছে}one{#টি ফাইল শেয়ার করা হচ্ছে}other{#টি ফাইল শেয়ার করা হচ্ছে}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{টেক্সট সহ ছবি শেয়ার করা হচ্ছে}one{টেক্সট সহ #টি ছবি শেয়ার করা হচ্ছে}other{টেক্সট সহ #টি ছবি শেয়ার করা হচ্ছে}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{লিঙ্ক সহ ছবি শেয়ার করা হচ্ছে}one{লিঙ্ক সহ #টি ছবি শেয়ার করা হচ্ছে}other{লিঙ্ক সহ #টি ছবি শেয়ার করা হচ্ছে}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{টেক্সট সহ ভিডিও শেয়ার করা হচ্ছে}one{টেক্সট সহ #টি ভিডিও শেয়ার করা হচ্ছে}other{টেক্সট সহ #টি ভিডিও শেয়ার করা হচ্ছে}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{লিঙ্ক সহ ভিডিও শেয়ার করা হচ্ছে}one{লিঙ্ক সহ #টি ভিডিও শেয়ার করা হচ্ছে}other{লিঙ্ক সহ #টি ভিডিও শেয়ার করা হচ্ছে}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{টেক্সট সহ ফাইল শেয়ার করা হচ্ছে}one{টেক্সট সহ #টি ফাইল শেয়ার করা হচ্ছে}other{টেক্সট সহ #টি ফাইল শেয়ার করা হচ্ছে}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{লিঙ্ক সহ ফাইল শেয়ার করা হচ্ছে}one{লিঙ্ক সহ #টি ফাইল শেয়ার করা হচ্ছে}other{লিঙ্ক সহ #টি ফাইল শেয়ার করা হচ্ছে}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{শুধু ছবি}one{শুধু ছবি}other{শুধু ছবি}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{শুধু ভিডিও}one{শুধু ভিডিও}other{শুধু ভিডিও}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{শুধু ফাইল}one{শুধু ফাইল}other{শুধু ফাইল}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"ছবির প্রিভিউ থাম্বনেল"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"ভিডিওর প্রিভিউ থাম্বনেল"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"ফাইলের প্রিভিউ থাম্বনেল"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"শেয়ার করার জন্য সাজেস্ট করার মতো কেউ নেই"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"অ্যাপের তালিকা"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"এই অ্যাপকে রেকর্ড করার অনুমতি দেওয়া হয়নি কিন্তু USB ডিভাইসের মাধ্যমে সেটি অডিও রেকর্ড করতে পারে।"</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"ব্যক্তিগত"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"অফিস"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"অফিসের অ্যাপে এই খোলা যাবে না"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ব্যক্তিগত অ্যাপে এই কন্টেন্ট শেয়ার করা যাবে না"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ব্যক্তিগত অ্যাপে এই কন্টেন্ট খোলা যাবে না"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"অফিস প্রোফাইল বন্ধ করা আছে"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"চালু করতে ট্যাপ করুন"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"অফিসের অ্যাপ পজ করা আছে"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"আনপজ করুন"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"এর জন্য কোনও অফিস অ্যাপ নেই"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"ব্যক্তিগত অ্যাপে দেখা যাবে না"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"আপনার ব্যক্তিগত প্রোফাইল থেকে <xliff:g id="APP">%s</xliff:g> খুলবেন?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"টেক্সট যোগ করুন"</string>
<string name="exclude_link" msgid="1332778255031992228">"লিঙ্ক বাদ দিন"</string>
<string name="include_link" msgid="827855767220339802">"লিঙ্ক যোগ করুন"</string>
+ <string name="pinned" msgid="7623664001331394139">"পিন করা হয়েছে"</string>
</resources>
diff --git a/java/res/values-bs/strings.xml b/java/res/values-bs/strings.xml
index f4b54c7..3c88d9c 100644
--- a/java/res/values-bs/strings.xml
+++ b/java/res/values-bs/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Zakači aplikaciju <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Otkači aplikaciju <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Uredi"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} i # fajl}one{{file_name} i # fajl}few{{file_name} i # fajla}other{{file_name} i # fajlova}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{i još # fajl}one{i još # fajl}few{i još # fajla}other{i još # fajlova}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{i još # fajl}one{i još # fajl}few{i još # fajla}other{i još # fajlova}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Dijeljenje teksta"</string>
<string name="sharing_link" msgid="2307694372813942916">"Dijeljenje linka"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Dijeljenje slike}one{Dijeljenje # slike}few{Dijeljenje # slike}other{Dijeljenje # slika}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Dijeljenje videozapisa}one{Dijeljenje # videozapisa}few{Dijeljenje # videozapisa}other{Dijeljenje # videozapisa}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Dijeljenje # stavke}one{Dijeljenje # stavke}few{Dijeljenje # stavke}other{Dijeljenje # stavki}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Dijeljenje slike s tekstom"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Dijeljenje slike s linkom"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Dijeljenje # fajla}one{Dijeljenje # fajla}few{Dijeljenje # fajla}other{Dijeljenje # fajlova}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Dijeljenje slike putem poruke}one{Dijeljenje # slike putem poruke}few{Dijeljenje # slike putem poruke}other{Dijeljenje # slika putem poruke}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Dijeljenje slike putem linka}one{Dijeljenje # slike putem linka}few{Dijeljenje # slike putem linka}other{Dijeljenje # slika putem linka}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Dijeljenje videozapisa putem poruke}one{Dijeljenje # videozapisa putem poruke}few{Dijeljenje # videozapisa putem poruke}other{Dijeljenje # videozapisa putem poruke}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Dijeljenje videozapisa putem linka}one{Dijeljenje # videozapisa putem linka}few{Dijeljenje # videozapisa putem linka}other{Dijeljenje # videozapisa putem linka}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Dijeljenje fajla putem poruke}one{Dijeljenje # fajla putem poruke}few{Dijeljenje # fajla putem poruke}other{Dijeljenje # fajlova putem poruke}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Dijeljenje fajla putem linka}one{Dijeljenje # fajla putem linka}few{Dijeljenje # fajla putem linka}other{Dijeljenje # fajlova putem linka}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Samo slika}one{Samo slike}few{Samo slike}other{Samo slike}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Samo videozapis}one{Samo videozapisi}few{Samo videozapisi}other{Samo videozapisi}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Samo fajl}one{Samo fajlovi}few{Samo fajlovi}other{Samo fajlovi}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Sličica pregleda slike"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Sličica pregleda videozapisa"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Sličica pregleda fajla"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nema preporučenih osoba s kojima biste dijelili"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista aplikacija"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Ovoj aplikaciji nije dato odobrenje za snimanje, ali može snimati zvuk putem ovog USB uređaja."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Lično"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Posao"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Ovaj sadržaj nije moguće otvoriti pomoću poslovnih aplikacija"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Ovaj sadržaj nije moguće dijeliti pomoću ličnih aplikacija"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Ovaj sadržaj nije moguće otvoriti pomoću ličnih aplikacija"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Radni profil je pauziran"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Dodirnite da uključite"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Poslovne aplikacije su pauzirane"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Ponovo pokreni"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nema poslovnih aplikacija"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nema ličnih aplikacija"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Otvoriti aplikaciju <xliff:g id="APP">%s</xliff:g> na ličnom profilu?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Uključi tekst"</string>
<string name="exclude_link" msgid="1332778255031992228">"Izuzmi link"</string>
<string name="include_link" msgid="827855767220339802">"Uključi link"</string>
+ <string name="pinned" msgid="7623664001331394139">"Zakačeno"</string>
</resources>
diff --git a/java/res/values-ca/strings.xml b/java/res/values-ca/strings.xml
index 97aeedd..bd0416a 100644
--- a/java/res/values-ca/strings.xml
+++ b/java/res/values-ca/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Fixa <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"No fixis <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Edita"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} i # fitxer}many{{file_name} i # fitxers}other{{file_name} i # fitxers}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fitxer}many{+ # de fitxers}other{+ # fitxers}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{# fitxer més}many{# de fitxers més}other{# fitxers més}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"S\'està compartint text"</string>
- <string name="sharing_link" msgid="2307694372813942916">"S\'està compartint l\'enllaç"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"S\'està compartint un enllaç"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{S\'està compartint una imatge}many{S\'estan compartint # d\'imatges}other{S\'estan compartint # imatges}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{S\'està compartint un vídeo}many{S\'estan compartint # de vídeos}other{S\'estan compartint # vídeos}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{S\'està compartint # element}many{S\'estan compartint # d\'elements}other{S\'estan compartint # elements}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Compartint imatge amb text"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Compartint imatge i enllaç"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{S\'està compartint # fitxer}many{S\'estan compartint # de fitxers}other{S\'estan compartint # fitxers}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{S\'està compartint la imatge amb text}many{S\'estan compartint # d\'imatges amb text}other{S\'estan compartint # imatges amb text}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{S\'està compartint la imatge amb un enllaç}many{S\'estan compartint # d\'imatges amb un enllaç}other{S\'estan compartint # imatges amb un enllaç}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{S\'està compartint el vídeo amb un enllaç}many{S\'estan compartint # de vídeos amb un enllaç}other{S\'estan compartint # vídeos amb un enllaç}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{S\'està compartint el vídeo amb un enllaç}many{S\'estan compartint # de vídeos amb un enllaç}other{S\'estan compartint # vídeos amb un enllaç}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{S\'està compartint el fitxer amb text}many{S\'estan compartint # de fitxers amb text}other{S\'estan compartint # fitxers amb text}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{S\'està compartint el fitxer amb un enllaç}many{S\'estan compartint # de fitxers amb un enllaç}other{S\'estan compartint # fitxers amb un enllaç}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Només imatge}many{Només imatges}other{Només imatges}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Només vídeo}many{Només vídeos}other{Només vídeos}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Només fitxer}many{Només fitxers}other{Només fitxers}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura de previsualització de la imatge"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura de previsualització del vídeo"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura de previsualització del fitxer"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"No hi ha cap suggeriment de persones amb qui compartir"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Llista d\'aplicacions"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Aquesta aplicació no té permís de gravació, però pot capturar àudio a través d\'aquest dispositiu USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Feina"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"No es pot obrir aquest contingut amb aplicacions de treball"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"No es pot compartir aquest contingut amb aplicacions personals"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"No es pot obrir aquest contingut amb aplicacions personals"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"El perfil de treball està en pausa"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Toca per activar"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Les aplicacions de treball estan en pausa"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Reactiva"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Cap aplicació de treball"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Cap aplicació personal"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vols obrir <xliff:g id="APP">%s</xliff:g> al teu perfil personal?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Inclou text"</string>
<string name="exclude_link" msgid="1332778255031992228">"Exclou l\'enllaç"</string>
<string name="include_link" msgid="827855767220339802">"Inclou l\'enllaç"</string>
+ <string name="pinned" msgid="7623664001331394139">"Fixat"</string>
</resources>
diff --git a/java/res/values-cs/strings.xml b/java/res/values-cs/strings.xml
index e15b1b0..a5deed6 100644
--- a/java/res/values-cs/strings.xml
+++ b/java/res/values-cs/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Připnout <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Odepnout: <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Upravit"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # soubor}few{{file_name} + # soubory}many{{file_name} + # souboru}other{{file_name} + # souborů}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # soubor}few{+ # soubory}many{+ # souboru}other{+ # souborů}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{a # další soubor}few{a # další soubory}many{a # dalšího souboru}other{a # dalších souborů}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Sdílení textu"</string>
<string name="sharing_link" msgid="2307694372813942916">"Sdílení odkazu"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Sdílení obrázku}few{Sdílení # obrázků}many{Sdílení # obrázku}other{Sdílení # obrázků}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Sdílení videa}few{Sdílení # videí}many{Sdílení # videa}other{Sdílení # videí}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Sdílení # položky}few{Sdílení # položek}many{Sdílení # položky}other{Sdílení # položek}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Sdílení obrázku s textem"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Sdílení obrázku s odkazem"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Sdílení # souboru}few{Sdílení # souborů}many{Sdílení # souboru}other{Sdílení # souborů}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Sdílení obrázku s textem}few{Sdílení # obrázků s textem}many{Sdílení # obrázku s textem}other{Sdílení # obrázků s textem}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Sdílení obrázku s odkazem}few{Sdílení # obrázků s odkazem}many{Sdílení # obrázku s odkazem}other{Sdílení # obrázků s odkazem}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Sdílení videa s textem}few{Sdílení # videí s textem}many{Sdílení # videa s textem}other{Sdílení # videí s textem}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Sdílení videa s odkazem}few{Sdílení # videí s odkazem}many{Sdílení # videa s odkazem}other{Sdílení # videí s odkazem}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Sdílení souboru s textem}few{Sdílení # souborů s textem}many{Sdílení # souboru s textem}other{Sdílení # souborů s textem}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Sdílení souboru s odkazem}few{Sdílení # souborů s odkazem}many{Sdílení # souboru s odkazem}other{Sdílení # souborů s odkazem}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Pouze obrázek}few{Pouze obrázky}many{Pouze obrázky}other{Pouze obrázky}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Pouze video}few{Pouze videa}many{Pouze videa}other{Pouze videa}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Pouze soubor}few{Pouze soubory}many{Pouze soubory}other{Pouze soubory}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura náhledu obrázku"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura náhledu videa"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura náhledu souboru"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Žádní doporučení lidé, s nimiž můžete sdílet"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Seznam aplikací"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Tato aplikace nemá oprávnění k nahrávání, ale může zaznamenávat zvuk prostřednictvím tohoto zařízení USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Osobní"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Pracovní"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Tento obsah nelze otevřít pomocí pracovních aplikací"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Tento obsah nelze sdílet pomocí osobních aplikací"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Tento obsah nelze otevřít pomocí osobních aplikací"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Pracovní profil je pozastaven"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Klepnutím ho zapnete"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Pracovní aplikace jsou pozastaveny"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Zrušit pozastavení"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Žádné pracovní aplikace"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Žádné osobní aplikace"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Otevřít aplikaci <xliff:g id="APP">%s</xliff:g> v osobním profilu?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Zahrnout text"</string>
<string name="exclude_link" msgid="1332778255031992228">"Vyloučit odkaz"</string>
<string name="include_link" msgid="827855767220339802">"Zahrnout odkaz"</string>
+ <string name="pinned" msgid="7623664001331394139">"Připnuto"</string>
</resources>
diff --git a/java/res/values-da/strings.xml b/java/res/values-da/strings.xml
index ef66bae..8d226d4 100644
--- a/java/res/values-da/strings.xml
+++ b/java/res/values-da/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Fastgør <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Frigør <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Rediger"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fil}one{{file_name} + # fil}other{{file_name} + # filer}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fil}one{+ # fil}other{+ # filer}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # fil mere}one{+ # fil mere}other{+ # filer mere}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Deler tekst"</string>
<string name="sharing_link" msgid="2307694372813942916">"Deler link"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Deler billede}one{Deler # billede}other{Deler # billeder}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Deler video}one{Deler # video}other{Deler # videoer}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Deler # element}one{Deler # element}other{Deler # elementer}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Deler billede med tekst"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Deler billede med et link"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Deler # fil}one{Deler # fil}other{Deler # filer}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Deler billede med tekst}one{Deler # billede med tekst}other{Deler # billeder med tekst}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Deler billede med et link}one{Deler # billede med et link}other{Deler # billeder med et link}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Deler video med tekst}one{Deler # video med tekst}other{Deler # videoer med tekst}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Deler video med et link}one{Deler # video med et link}other{Deler # videoer med et link}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Deler fil med tekst}one{Deler # fil med tekst}other{Deler # filer med tekst}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Deler fil med et link}one{Deler # fil med et link}other{Deler # filer med et link}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Kun billedet}one{Kun billedet}other{Kun billeder}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Kun video}one{Kun video}other{Kun videoer}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Kun filen}one{Kun filen}other{Kun filer}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniaturepreview af billede"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniaturepreview af video"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniaturepreview af fil"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Der er ingen anbefalede personer at dele med"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Liste over apps"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Denne app har ikke fået tilladelse til at optage, men optager muligvis lyd via denne USB-enhed."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Personlig"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Arbejde"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Dette indhold kan ikke åbnes med arbejdsapps"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Dette indhold kan ikke deles med personlige apps"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Dette indhold kan ikke åbnes med personlige apps"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Arbejdsprofilen er sat på pause"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tryk for at aktivere"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Dine arbejdsapps er sat på pause"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Genoptag"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Der er ingen arbejdsapps"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Der er ingen personlige apps"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vil du åbne <xliff:g id="APP">%s</xliff:g> på din personlige profil?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Inkluder tekst"</string>
<string name="exclude_link" msgid="1332778255031992228">"Ekskluder link"</string>
<string name="include_link" msgid="827855767220339802">"Inkluder link"</string>
+ <string name="pinned" msgid="7623664001331394139">"Fastgjort"</string>
</resources>
diff --git a/java/res/values-de/strings.xml b/java/res/values-de/strings.xml
index a78310d..dc476fa 100644
--- a/java/res/values-de/strings.xml
+++ b/java/res/values-de/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> anpinnen"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> loslösen"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Bearbeiten"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # Datei}other{{file_name} + # Dateien}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # Datei}other{+ # Dateien}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # weitere Datei}other{+ # weitere Dateien}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Text wird geteilt"</string>
<string name="sharing_link" msgid="2307694372813942916">"Link wird geteilt"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Bild wird geteilt}other{# Bilder werden geteilt}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Video wird geteilt}other{# Videos werden geteilt}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# Element wird geteilt}other{# Elemente werden geteilt}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Bild mit Text geteilt"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Bild mit Link geteilt"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# Datei wird freigegeben}other{# Dateien werden freigegeben}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Bild wird mit Text geteilt}other{# Bilder werden mit Text geteilt}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Bild wird per Link geteilt}other{# Bilder werden per Link geteilt}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Video wird per SMS geteilt}other{# Videos werden per SMS geteilt}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Video wird per Link geteilt}other{# Videos werden per Link geteilt}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Datei wird per SMS geteilt}other{# Dateien werden per SMS geteilt}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Datei wird per Link geteilt}other{# Dateien werden per Link geteilt}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Nur Bild}other{Nur Bilder}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Nur Video}other{Nur Videos}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Nur Datei}other{Nur Dateien}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Vorschau-Miniaturansicht für Bild"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Vorschau-Miniaturansicht für Video"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Vorschau-Miniaturansicht für Datei"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Keine empfohlenen Empfänger"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Liste der Apps"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Diese App hat noch keine Berechtigung zum Aufnehmen erhalten, könnte aber Audioaufnahmen über dieses USB-Gerät machen."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Privat"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Geschäftlich"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Diese Art von Inhalt kann nicht mit geschäftlichen Apps geöffnet werden"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Diese Art von Inhalt kann nicht über private Apps geteilt werden"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Diese Art von Inhalt kann nicht mit privaten Apps geöffnet werden"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Arbeitsprofil pausiert"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Zum Aktivieren tippen"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Geschäftliche Apps sind pausiert"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Nicht mehr pausieren"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Keine geschäftlichen Apps"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Keine privaten Apps"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> in deinem privaten Profil öffnen?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Text einschließen"</string>
<string name="exclude_link" msgid="1332778255031992228">"Link ausschließen"</string>
<string name="include_link" msgid="827855767220339802">"Link einschließen"</string>
+ <string name="pinned" msgid="7623664001331394139">"Angepinnt"</string>
</resources>
diff --git a/java/res/values-el/strings.xml b/java/res/values-el/strings.xml
index 31e273a..e760e00 100644
--- a/java/res/values-el/strings.xml
+++ b/java/res/values-el/strings.xml
@@ -44,27 +44,36 @@
<string name="whichImageCaptureApplicationLabel" msgid="987153638235357094">"Λήψη εικόνας"</string>
<string name="use_a_different_app" msgid="2062380818535918975">"Χρήση άλλης εφαρμογής"</string>
<string name="chooseActivity" msgid="6659724877523973446">"Επιλέξτε μια ενέργεια"</string>
- <string name="noApplications" msgid="1139487441772284671">"Δεν υπάρχουν εφαρμογές, οι οποίες μπορούν να εκτελέσουν αυτήν την ενέργεια."</string>
- <string name="forward_intent_to_owner" msgid="6454987608971162379">"Χρησιμοποιείτε αυτήν την εφαρμογή εκτός του προφίλ εργασίας σας"</string>
- <string name="forward_intent_to_work" msgid="2906094223089139419">"Χρησιμοποιείτε αυτήν την εφαρμογή στο προφίλ εργασίας"</string>
+ <string name="noApplications" msgid="1139487441772284671">"Δεν υπάρχουν εφαρμογές, οι οποίες μπορούν να εκτελέσουν αυτή την ενέργεια."</string>
+ <string name="forward_intent_to_owner" msgid="6454987608971162379">"Χρησιμοποιείτε αυτή την εφαρμογή εκτός του προφίλ εργασίας σας"</string>
+ <string name="forward_intent_to_work" msgid="2906094223089139419">"Χρησιμοποιείτε αυτή την εφαρμογή στο προφίλ εργασίας"</string>
<string name="activity_resolver_use_always" msgid="8674194687637555245">"Πάντα"</string>
<string name="activity_resolver_use_once" msgid="594173435998892989">"Μόνο μία φορά"</string>
<string name="activity_resolver_work_profiles_support" msgid="8228711455685203580">"Η εφαρμογή <xliff:g id="APP">%1$s</xliff:g> δεν υποστηρίζει προφίλ εργασίας"</string>
<string name="pin_specific_target" msgid="5057063421361441406">"Καρφίτσωμα <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Ξεκαρφίτσωμα <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Επεξεργασία"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # αρχείο}other{{file_name} + # αρχεία}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # αρχείο}other{+ # αρχεία}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # ακόμη αρχείο}other{+ # ακόμη αρχεία}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Κοινοποίηση μηνύματος"</string>
<string name="sharing_link" msgid="2307694372813942916">"Κοινοποίηση συνδέσμου"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Κοινοποίηση εικόνας}other{Κοινοποίηση # εικόνων}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Κοινοποίηση βίντεο}other{Κοινοποίηση # βίντεο}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Κοινοποίηση # στοιχείου}other{Κοινοποίηση # στοιχείων}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Κοινοπ. εικόνας με κείμ."</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Κοινοπ. εικόνας με σύνδ."</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Κοινή χρήση # αρχείου}other{Κοινή χρήση # αρχείων}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Κοινοποίηση εικόνας με κείμενο}other{Κοινοποίηση # εικόνων με κείμενο}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Κοινοποίηση εικόνας με σύνδεσμο}other{Κοινοποίηση # εικόνων με σύνδεσμο}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Κοινοποίηση βίντεο με κείμενο}other{Κοινοποίηση # βίντεο με κείμενο}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Κοινοποίηση βίντεο με σύνδεσμο}other{Κοινοποίηση # βίντεο με σύνδεσμο}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Κοινοποίηση αρχείου με κείμενο}other{Κοινοποίηση # αρχείων με κείμενο}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Κοινοποίηση αρχείου με σύνδεσμο}other{Κοινοποίηση # αρχείων με σύνδεσμο}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Μόνο εικόνα}other{Μόνο εικόνες}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Μόνο βίντεο}other{Μόνο βίντεο}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Μόνο αρχείο}other{Μόνο αρχεία}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Μικρογραφία προεπισκόπησης εικόνας"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Μικρογραφία προεπισκόπησης βίντεο"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Μικρογραφία προεπισκόπησης αρχείου"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Δεν υπάρχουν προτεινόμενα άτομα για κοινοποίηση"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Λίστα εφαρμογών"</string>
- <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Δεν έχει εκχωρηθεί άδεια εγγραφής σε αυτήν την εφαρμογή, αλλά μέσω αυτής της συσκευής USB θα μπορεί να εγγράφει ήχο."</string>
+ <string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Δεν έχει εκχωρηθεί άδεια εγγραφής σε αυτή την εφαρμογή, αλλά μέσω αυτής της συσκευής USB θα μπορεί να εγγράφει ήχο."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Προσωπικό"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Εργασία"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Προσωπική προβολή"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Δεν είναι δυνατό το άνοιγμα αυτού του περιεχομένου με εφαρμογές εργασιών"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Δεν είναι δυνατή η κοινοποίηση αυτού του περιεχομένου με προσωπικές εφαρμογές"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Δεν είναι δυνατό το άνοιγμα αυτού του περιεχομένου με προσωπικές εφαρμογές"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Το προφίλ εργασίας σας έχει τεθεί σε παύση."</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Πατήστε για ενεργοποίηση"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Οι εφαρμογές εργασιών τέθηκαν σε παύση"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Αναίρεση παύσης"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Δεν υπάρχουν εφαρμογές εργασιών"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Δεν υπάρχουν προσωπικές εφαρμογές"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Θέλετε να ανοίξετε την εφαρμογή <xliff:g id="APP">%s</xliff:g> στο προσωπικό σας προφίλ;"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Συμπερίληψη κειμένου"</string>
<string name="exclude_link" msgid="1332778255031992228">"Εξαίρεση συνδέσμου"</string>
<string name="include_link" msgid="827855767220339802">"Συμπερίληψη συνδέσμου"</string>
+ <string name="pinned" msgid="7623664001331394139">"Καρφιτσωμένο"</string>
</resources>
diff --git a/java/res/values-en-rAU/strings.xml b/java/res/values-en-rAU/strings.xml
index 29707f2..a1438ed 100644
--- a/java/res/values-en-rAU/strings.xml
+++ b/java/res/values-en-rAU/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Pin <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Unpin <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Edit"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # file}other{{file_name} + # files}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # file}other{+ # files}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # more file}other{+ # more files}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Sharing text"</string>
<string name="sharing_link" msgid="2307694372813942916">"Sharing link"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Sharing image}other{Sharing # images}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Sharing video}other{Sharing # videos}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Sharing # item}other{Sharing # items}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Sharing image with text"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Sharing image with link"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Sharing # file}other{Sharing # files}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Sharing image with text}other{Sharing # images with text}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Sharing image with link}other{Sharing # images with link}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Sharing video with text}other{Sharing # videos with text}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Sharing video with link}other{Sharing # videos with link}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Sharing file with text}other{Sharing # files with text}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Sharing file with link}other{Sharing # files with link}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Image only}other{Images only}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Video only}other{Videos only}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{File only}other{Files only}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Image preview thumbnail"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Video preview thumbnail"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"File preview thumbnail"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"No recommended people to share with"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Apps list"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"This app has not been granted record permission but could capture audio through this USB device."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Work"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"This content can’t be shared with personal apps"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"This content can’t be opened with personal apps"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Work profile is paused"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tap to turn on"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Work apps are paused"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Unpause"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"No work apps"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"No personal apps"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Open <xliff:g id="APP">%s</xliff:g> in your personal profile?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Include text"</string>
<string name="exclude_link" msgid="1332778255031992228">"Exclude link"</string>
<string name="include_link" msgid="827855767220339802">"Include link"</string>
+ <string name="pinned" msgid="7623664001331394139">"Pinned"</string>
</resources>
diff --git a/java/res/values-en-rCA/strings.xml b/java/res/values-en-rCA/strings.xml
index 29707f2..a1438ed 100644
--- a/java/res/values-en-rCA/strings.xml
+++ b/java/res/values-en-rCA/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Pin <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Unpin <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Edit"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # file}other{{file_name} + # files}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # file}other{+ # files}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # more file}other{+ # more files}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Sharing text"</string>
<string name="sharing_link" msgid="2307694372813942916">"Sharing link"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Sharing image}other{Sharing # images}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Sharing video}other{Sharing # videos}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Sharing # item}other{Sharing # items}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Sharing image with text"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Sharing image with link"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Sharing # file}other{Sharing # files}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Sharing image with text}other{Sharing # images with text}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Sharing image with link}other{Sharing # images with link}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Sharing video with text}other{Sharing # videos with text}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Sharing video with link}other{Sharing # videos with link}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Sharing file with text}other{Sharing # files with text}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Sharing file with link}other{Sharing # files with link}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Image only}other{Images only}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Video only}other{Videos only}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{File only}other{Files only}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Image preview thumbnail"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Video preview thumbnail"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"File preview thumbnail"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"No recommended people to share with"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Apps list"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"This app has not been granted record permission but could capture audio through this USB device."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Work"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"This content can’t be shared with personal apps"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"This content can’t be opened with personal apps"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Work profile is paused"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tap to turn on"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Work apps are paused"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Unpause"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"No work apps"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"No personal apps"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Open <xliff:g id="APP">%s</xliff:g> in your personal profile?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Include text"</string>
<string name="exclude_link" msgid="1332778255031992228">"Exclude link"</string>
<string name="include_link" msgid="827855767220339802">"Include link"</string>
+ <string name="pinned" msgid="7623664001331394139">"Pinned"</string>
</resources>
diff --git a/java/res/values-en-rGB/strings.xml b/java/res/values-en-rGB/strings.xml
index 29707f2..a1438ed 100644
--- a/java/res/values-en-rGB/strings.xml
+++ b/java/res/values-en-rGB/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Pin <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Unpin <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Edit"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # file}other{{file_name} + # files}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # file}other{+ # files}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # more file}other{+ # more files}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Sharing text"</string>
<string name="sharing_link" msgid="2307694372813942916">"Sharing link"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Sharing image}other{Sharing # images}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Sharing video}other{Sharing # videos}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Sharing # item}other{Sharing # items}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Sharing image with text"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Sharing image with link"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Sharing # file}other{Sharing # files}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Sharing image with text}other{Sharing # images with text}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Sharing image with link}other{Sharing # images with link}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Sharing video with text}other{Sharing # videos with text}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Sharing video with link}other{Sharing # videos with link}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Sharing file with text}other{Sharing # files with text}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Sharing file with link}other{Sharing # files with link}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Image only}other{Images only}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Video only}other{Videos only}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{File only}other{Files only}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Image preview thumbnail"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Video preview thumbnail"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"File preview thumbnail"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"No recommended people to share with"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Apps list"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"This app has not been granted record permission but could capture audio through this USB device."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Work"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"This content can’t be shared with personal apps"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"This content can’t be opened with personal apps"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Work profile is paused"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tap to turn on"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Work apps are paused"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Unpause"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"No work apps"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"No personal apps"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Open <xliff:g id="APP">%s</xliff:g> in your personal profile?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Include text"</string>
<string name="exclude_link" msgid="1332778255031992228">"Exclude link"</string>
<string name="include_link" msgid="827855767220339802">"Include link"</string>
+ <string name="pinned" msgid="7623664001331394139">"Pinned"</string>
</resources>
diff --git a/java/res/values-en-rIN/strings.xml b/java/res/values-en-rIN/strings.xml
index 29707f2..a1438ed 100644
--- a/java/res/values-en-rIN/strings.xml
+++ b/java/res/values-en-rIN/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Pin <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Unpin <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Edit"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # file}other{{file_name} + # files}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # file}other{+ # files}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # more file}other{+ # more files}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Sharing text"</string>
<string name="sharing_link" msgid="2307694372813942916">"Sharing link"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Sharing image}other{Sharing # images}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Sharing video}other{Sharing # videos}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Sharing # item}other{Sharing # items}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Sharing image with text"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Sharing image with link"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Sharing # file}other{Sharing # files}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Sharing image with text}other{Sharing # images with text}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Sharing image with link}other{Sharing # images with link}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Sharing video with text}other{Sharing # videos with text}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Sharing video with link}other{Sharing # videos with link}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Sharing file with text}other{Sharing # files with text}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Sharing file with link}other{Sharing # files with link}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Image only}other{Images only}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Video only}other{Videos only}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{File only}other{Files only}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Image preview thumbnail"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Video preview thumbnail"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"File preview thumbnail"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"No recommended people to share with"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Apps list"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"This app has not been granted record permission but could capture audio through this USB device."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Work"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"This content can’t be shared with personal apps"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"This content can’t be opened with personal apps"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Work profile is paused"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tap to turn on"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Work apps are paused"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Unpause"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"No work apps"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"No personal apps"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Open <xliff:g id="APP">%s</xliff:g> in your personal profile?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Include text"</string>
<string name="exclude_link" msgid="1332778255031992228">"Exclude link"</string>
<string name="include_link" msgid="827855767220339802">"Include link"</string>
+ <string name="pinned" msgid="7623664001331394139">"Pinned"</string>
</resources>
diff --git a/java/res/values-en-rXC/strings.xml b/java/res/values-en-rXC/strings.xml
index 5811516..56574b6 100644
--- a/java/res/values-en-rXC/strings.xml
+++ b/java/res/values-en-rXC/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‏‏‎‎‎‏‎‏‏‏‎‎‏‎‎‏‏‎‎‎‏‏‎‎‎‏‏‎‏‏‎‎‏‎‎‏‏‎‏‏‏‏‎‏‏‎‎‏‎‏‎‎‏‏‏‏‏‏‎‎Pin ‎‏‎‎‏‏‎<xliff:g id="LABEL">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‏‏‎‎‏‏‏‎‏‏‎‏‎‎‎‏‎‎‏‎‎‎‎‏‎‏‏‎‏‎‏‎‏‏‏‏‎‎‎‎‏‏‎‎‎‏‎‏‎‎‎‏‏‏‎‎‎‏‎Unpin ‎‏‎‎‏‏‎<xliff:g id="LABEL">%1$s</xliff:g>‎‏‎‎‏‏‏‎‎‏‎‎‏‎"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‏‏‎‎‎‎‏‏‏‎‏‏‏‏‎‎‎‎‎‎‏‏‎‏‎‎‏‎‎‎‎‏‎‏‎‎‏‎‎‏‏‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‎‎Edit‎‏‎‎‏‎"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‏‏‎‏‏‎‎‎‏‏‏‎‎‎‏‏‏‎‎‎‏‏‎‎‎‎‏‎‏‏‎‏‏‏‎‎‎‏‏‎‏‏‎‎‎‎‎‎‎‎‎‎‎‎‏‏‎‎‎‎‏‎‎‏‏‎{file_name}‎‏‎‎‏‏‏‎ + # file‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‏‏‎‏‏‎‎‎‏‏‏‎‎‎‏‏‏‎‎‎‏‏‎‎‎‎‏‎‏‏‎‏‏‏‎‎‎‏‏‎‏‏‎‎‎‎‎‎‎‎‎‎‎‎‏‏‎‎‎‎‏‎‎‏‏‎{file_name}‎‏‎‎‏‏‏‎ + # files‎‏‎‎‏‎}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‏‏‎‎‏‏‏‎‏‏‏‎‏‏‎‏‏‎‎‏‎‏‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‎‏‎‏‎‎‏‏‎+ # file‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‏‏‎‎‏‏‏‎‏‏‏‎‏‏‎‏‏‎‎‏‎‏‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‎‏‎‏‎‎‏‏‎+ # files‎‏‎‎‏‎}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‎‎‏‏‏‏‏‎‎‏‎‎‏‎‏‏‏‎‏‏‏‏‎‎‎‎‏‎‎‏‏‏‎‏‎‎‎‎‏‎‏‎‏‎‎‏‎‎‎‏‎‎‎‎‎‏‎‎+ # more file‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‏‏‎‎‏‏‏‏‏‎‎‏‎‎‏‎‏‏‏‎‏‏‏‏‎‎‎‎‏‎‎‏‏‏‎‏‎‎‎‎‏‎‏‎‏‎‎‏‎‎‎‏‎‎‎‎‎‏‎‎+ # more files‎‏‎‎‏‎}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‏‎‏‏‏‎‎‏‎‏‎‏‏‏‎‎‏‏‎‎‎‎‏‏‎‎‎‎‎‎‎‏‏‎‏‎‏‎‎‏‎‎‏‏‏‎‎‏‏‏‏‏‏‎‎Sharing text‎‏‎‎‏‎"</string>
<string name="sharing_link" msgid="2307694372813942916">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‎‎‎‎‎‎‎‏‏‎‏‎‎‏‎‎‏‏‏‏‎‎‏‏‏‎‎‎‏‎‏‏‎‏‏‎‏‏‏‏‏‎‎‎‏‏‎‎‎‎‏‎‎‎‎‏‎‎‎Sharing link‎‏‎‎‏‎"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‎‏‏‏‎‎‎‎‎‏‏‏‎‎‎‎‎‎‎‏‏‏‏‎‏‏‏‏‏‎‎‏‎‏‏‏‎‏‎‎‎‏‎‏‏‎‏‎‎‎‏‎‏‎‏‏‎‎Sharing image‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‎‏‏‏‎‎‎‎‎‏‏‏‎‎‎‎‎‎‎‏‏‏‏‎‏‏‏‏‏‎‎‏‎‏‏‏‎‏‎‎‎‏‎‏‏‎‏‎‎‎‏‎‏‎‏‏‎‎Sharing # images‎‏‎‎‏‎}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‎‏‏‎‏‏‏‎‏‎‏‏‏‎‎‎‎‎‎‏‏‎‎‏‏‏‏‏‎‏‏‎‏‎‏‎‏‏‏‏‏‏‎‏‎‏‏‎‎‎‏‏‏‏‏‎‏‎‎Sharing video‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‎‏‏‎‏‏‏‎‏‎‏‏‏‎‎‎‎‎‎‏‏‎‎‏‏‏‏‏‎‏‏‎‏‎‏‎‏‏‏‏‏‏‎‏‎‏‏‎‎‎‏‏‏‏‏‎‏‎‎Sharing # videos‎‏‎‎‏‎}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‏‎‎‎‏‎‏‏‎‏‎‎‎‎‏‎‏‏‏‎‎‎‏‎‎‎‏‎‏‎‏‎‎‎‎‎‏‎‏‏‏‏‏‏‎‎‏‎‏‏‏‏‏‏‎‏‏‎Sharing # item‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‏‎‎‎‏‎‏‏‎‏‎‎‎‎‏‎‏‏‏‎‎‎‏‎‎‎‏‎‏‎‏‎‎‎‎‎‏‎‏‏‏‏‏‏‎‎‏‎‏‏‏‏‏‏‎‏‏‎Sharing # items‎‏‎‎‏‎}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‏‎‏‎‏‎‏‏‎‏‎‎‎‏‏‎‎‎‎‏‎‎‎‏‏‎‏‏‎‎‎‎‎‎‏‏‎‎‏‎‏‎‎‏‎‎‏‏‏‎‏‏‎‎‎‎‎‎‏‎Sharing image with text‎‏‎‎‏‎"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‎‎‏‏‏‎‎‏‏‏‎‎‏‏‏‎‏‏‎‏‏‏‏‏‎‏‎‎‎‎‏‎‎‏‎‎‎‎‎‏‎‏‎‏‎‎‏‏‏‎‎‎‎‎‎‏‎‏‏‎Sharing image with link‎‏‎‎‏‎"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‎‏‏‎‏‏‎‏‎‎‎‎‎‎‎‎‎‏‏‏‎‎‎‏‎‏‏‎‎‎‎‎‎‏‏‎‎‎‎‏‏‏‏‎‏‎‎‏‏‎‎‎‎‏‎‏‏‏‎Sharing # file‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‎‏‏‎‏‏‎‏‎‎‎‎‎‎‎‎‎‏‏‏‎‎‎‏‎‏‏‎‎‎‎‎‎‏‏‎‎‎‎‏‏‏‏‎‏‎‎‏‏‎‎‎‎‏‎‏‏‏‎Sharing # files‎‏‎‎‏‎}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‏‏‎‏‎‏‎‏‏‏‏‎‎‎‏‎‎‏‎‏‎‏‏‎‏‎‏‎‎‏‎‏‎‎‎‏‎‎‎‏‏‎‏‎‏‏‏‎‎‎‎‏‎‎Sharing image with text‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‏‎‎‏‏‏‏‏‎‏‎‏‎‏‏‏‏‎‎‎‏‎‎‏‎‏‎‏‏‎‏‎‏‎‎‏‎‏‎‎‎‏‎‎‎‏‏‎‏‎‏‏‏‎‎‎‎‏‎‎Sharing # images with text‎‏‎‎‏‎}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‏‏‏‎‎‏‏‏‏‏‎‎‏‏‎‎‎‏‏‎‏‏‏‎‏‎‏‏‎‏‎‎‎‎‎‎‏‎‎‏‎‎‏‎‏‏‏‏‏‏‎‏‏‎‎‏‎‏‎Sharing image with link‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‏‏‏‎‎‏‏‏‏‏‎‎‏‏‎‎‎‏‏‎‏‏‏‎‏‎‏‏‎‏‎‎‎‎‎‎‏‎‎‏‎‎‏‎‏‏‏‏‏‏‎‏‏‎‎‏‎‏‎Sharing # images with link‎‏‎‎‏‎}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‎‏‏‏‎‏‏‏‏‎‎‏‏‏‎‏‎‎‏‎‎‎‏‎‏‎‎‏‎‏‎‏‎‏‏‎‎‏‏‎‎‏‏‏‏‎‏‏‏‎‎‎‎‎‎‎‏‎‎Sharing video with text‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‎‏‏‏‎‏‏‏‏‎‎‏‏‏‎‏‎‎‏‎‎‎‏‎‏‎‎‏‎‏‎‏‎‏‏‎‎‏‏‎‎‏‏‏‏‎‏‏‏‎‎‎‎‎‎‎‏‎‎Sharing # videos with text‎‏‎‎‏‎}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‎‎‏‎‏‏‎‎‎‎‎‎‎‎‎‏‏‎‏‎‏‏‎‎‎‎‏‏‎‎‏‏‏‏‏‏‎‎‏‏‎‎‎‏‎‏‎‎‎‎‏‎‎‎‏‎Sharing video with link‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‎‎‏‎‏‏‎‎‎‎‎‎‎‎‎‏‏‎‏‎‏‏‎‎‎‎‏‏‎‎‏‏‏‏‏‏‎‎‏‏‎‎‎‏‎‏‎‎‎‎‏‎‎‎‏‎Sharing # videos with link‎‏‎‎‏‎}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‎‏‏‏‏‎‏‏‎‎‏‏‎‏‏‏‏‏‏‏‎‎‎‏‎‎‏‎‏‏‎‎‏‎‎‏‏‏‏‎‏‏‎‏‏‎‏‏‏‏‎‎‎‎‎‎Sharing file with text‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‏‎‏‏‎‏‏‏‏‎‏‏‎‎‏‏‎‏‏‏‏‏‏‏‎‎‎‏‎‎‏‎‏‏‎‎‏‎‎‏‏‏‏‎‏‏‎‏‏‎‏‏‏‏‎‎‎‎‎‎Sharing # files with text‎‏‎‎‏‎}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‎‏‎‏‏‏‎‏‎‎‏‏‏‏‎‎‏‏‏‏‎‏‏‎‏‎‎‎‏‎‏‏‎‎‎‏‏‏‎‎‏‏‏‎Sharing file with link‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‏‏‏‏‏‏‏‏‏‎‏‏‎‏‎‏‏‏‎‏‎‎‏‏‏‏‎‎‏‏‏‏‎‏‏‎‏‎‎‎‏‎‏‏‎‎‎‏‏‏‎‎‏‏‏‎Sharing # files with link‎‏‎‎‏‎}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‏‏‏‎‏‏‏‎‏‎‎‏‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‎‏‎‎‏‏‏‎‎‎‎‏‎‎‎‎‎‏‏‎‏‏‎‏‏‏‎‎Image only‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‏‏‏‎‏‏‏‎‏‎‎‏‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‎‏‎‎‏‏‏‎‎‎‎‏‎‎‎‎‎‏‏‎‏‏‎‏‏‏‎‎Images only‎‏‎‎‏‎}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‎‏‎‎‎‎‎‏‎‎‏‎‎‏‏‎‎‏‎‏‎‏‏‎‎‏‏‏‏‎‏‏‏‎‏‏‎‎‏‏‏‎‏‏‏‎‎‎‏‎‏‎‎‏‏‏‏‎‎Video only‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‎‏‎‎‎‎‎‏‎‎‏‎‎‏‏‎‎‏‎‏‎‏‏‎‎‏‏‏‏‎‏‏‏‎‏‏‎‎‏‏‏‎‏‏‏‎‎‎‏‎‏‎‎‏‏‏‏‎‎Videos only‎‏‎‎‏‎}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‏‏‏‎‏‎‎‏‎‎‏‏‏‎‏‏‏‏‏‎‏‏‏‎‎‏‏‎‎‎‏‎‎‎‎‏‎‏‎‎‎‎‏‎‏‎‎‏‏‎‎‎‏‎‎‎‎‎‎File only‎‏‎‎‏‎}other{‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‏‏‏‎‏‎‎‏‎‎‏‏‏‎‏‏‏‏‏‎‏‏‏‎‎‏‏‎‎‎‏‎‎‎‎‏‎‏‎‎‎‎‏‎‏‎‎‏‏‎‎‎‏‎‎‎‎‎‎Files only‎‏‎‎‏‎}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‏‏‎‎‎‎‎‏‏‏‏‏‏‎‎‎‎‏‎‏‎‏‎‎‎‏‏‏‏‏‎‏‎‎‎‏‎‏‏‎‏‏‎‎‏‏‏‏‎‎‎‎‎‎‎‏‎‏‎‏‎Image preview thumbnail‎‏‎‎‏‎"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‎‏‎‏‏‏‏‏‎‎‎‎‎‏‎‎‎‏‏‏‎‏‎‏‏‏‎‎‎‎‏‏‏‎‎‏‏‎‏‏‎‎‎‏‎‏‏‎‏‏‏‎‏‏‎‏‏‎‎Video preview thumbnail‎‏‎‎‏‎"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‏‏‎‏‎‏‎‏‎‎‎‎‎‏‏‏‎‎‎‏‏‎‎‏‎‎‏‏‏‏‎‏‎‎‎‏‏‎‏‏‏‎‎‏‏‎‏‏‏‏‎‎‏‏‎‏‎‏‎‎File preview thumbnail‎‏‎‎‏‎"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‏‎‏‏‎‎‎‎‎‎‎‎‎‏‏‏‏‎‎‎‎‎‎‏‎‏‎‏‎‎‎‏‏‏‏‎‏‎‏‎‏‎‎‎‎‎‏‎‎‏‎‏‎‏‎‏‎‎‎No recommended people to share with‎‏‎‎‏‎"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‏‏‏‎‎‏‏‏‏‎‏‎‏‎‏‏‎‎‎‏‎‎‏‏‎‏‏‎‎‏‏‏‎‏‏‏‏‏‏‏‎‎‎‏‏‎‎‏‎‏‏‎‎‎‏‏‏‎‎‎‎Apps list‎‏‎‎‏‎"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‏‏‎‎‎‎‏‎‏‎‏‏‏‏‏‏‏‏‏‎‏‏‎‏‏‎‏‎‎‎‏‏‏‎‏‎‏‏‎‏‎‎‎‏‎‏‏‎‎‏‏‎‎‏‎‏‎‎‎This app has not been granted record permission but could capture audio through this USB device.‎‏‎‎‏‎"</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‏‎‎‏‎‏‎‏‎‎‏‏‏‏‏‎‎‎‎‏‎‎‏‎‏‎‏‎‎‏‎‎‏‎‎‎‏‎‏‏‎‎‏‏‏‎‎‏‏‎‎‏‏‎‏‎‏‎Personal‎‏‎‎‏‎"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‎‏‏‏‎‎‏‏‎‎‎‏‎‎‏‎‏‏‎‎‏‏‏‎‏‎‎‏‏‎‏‏‏‏‎‏‎‏‎‎‎‏‎‎‎‏‏‏‏‏‎‎‎‏‏‏‎‎‎Work‎‏‎‎‏‎"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‎‎‎‏‎‎‏‏‎‏‏‏‏‏‎‏‎‎‎‎‎‎‏‎‎‎‏‏‏‏‏‎‎‏‏‎‎‎‏‏‎‏‎‏‏‎‎‏‎‎‏‏‎‏‏‎‏‎‎This content can’t be opened with work apps‎‏‎‎‏‎"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‎‏‏‏‎‏‎‏‎‎‎‏‎‎‎‏‎‎‏‏‏‏‏‏‏‎‎‎‎‏‎‏‏‎‎‎‎‏‎‏‏‏‏‎‎‏‎‎‎‏‏‏‏‏‎‎‏‎This content can’t be shared with personal apps‎‏‎‎‏‎"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‏‎‎‎‏‎‏‏‎‎‏‎‏‏‏‎‏‏‎‎‎‏‏‏‎‎‎‏‎‎‎‎‏‎‏‎‏‏‎‏‏‎‎‏‎‎‏‎‏‎‏‏‎‎‎‎‏‎‎This content can’t be opened with personal apps‎‏‎‎‏‎"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‏‎‎‏‏‎‏‏‎‏‎‏‏‎‎‎‏‎‏‎‎‏‏‏‏‏‎‏‎‏‎‏‎‎‎‎‏‎‏‎‏‎‎‎‏‎‎‎‏‏‎‏‎‏‎‏‏‎‎‏‎Work profile is paused‎‏‎‎‏‎"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‎‎‎‎‎‎‎‎‎‏‏‎‏‏‎‎‏‎‎‏‎‎‎‏‏‎‎‏‏‎‏‎‏‎‎‎‎‏‏‏‎‎‎‏‎‎‎‏‏‏‎‏‎‏‏‏‏‎‏‎‏‎Tap to turn on‎‏‎‎‏‎"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‎‎‏‎‏‎‏‏‏‏‏‎‎‏‏‏‏‎‏‏‏‏‎‎‏‎‏‏‎‎‏‏‎‏‎‎‎‎‏‎‏‎‎‎‏‎‏‏‏‎‏‏‎‏‎‎‎‏‎‎‎Work apps are paused‎‏‎‎‏‎"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‏‎‎‎‎‏‏‏‎‎‎‏‏‎‎‏‏‏‏‏‎‏‏‎‏‏‏‏‏‏‎‎‎‏‏‎‏‏‎‎‎‎‎‎‎‎‎‏‎‎‎‏‎‏‎‏‎‏‏‏‎Unpause‎‏‎‎‏‎"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‎‏‎‏‎‏‎‎‏‏‎‏‎‏‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‎‎‎‏‎‏‏‎‎‏‏‎‎‏‎‏‏‎‏‎‏‎‎‎‎‎‎‎‎‏‏‏‏‎No work apps‎‏‎‎‏‎"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‏‏‎‏‎‏‎‏‏‏‎‎‏‎‎‏‏‏‏‏‎‎‏‏‏‎‎‏‏‎‏‎‏‏‎‎‏‏‎‎‏‎‎‏‎‏‏‏‏‏‎‎‎‏‏‏‏‎No personal apps‎‏‎‎‏‎"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‎‏‎‎‏‎‏‏‏‏‎‏‎‎‎‎‎‎‎‏‏‏‏‏‎‏‎‏‏‏‎‎‏‎‏‏‎‏‏‎‎‎‎‎‎‏‏‏‏‏‏‏‏‎Open ‎‏‎‎‏‏‎<xliff:g id="APP">%s</xliff:g>‎‏‎‎‏‏‏‎ in your personal profile?‎‏‎‎‏‎"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‎‎‏‏‏‎‏‎‎‏‏‏‎‏‎‏‏‎‏‎‎‎‎‎‏‏‏‏‎‎‎‎‏‎‎‏‏‏‎‏‎‏‎‏‎‏‎‏‏‏‎‏‎‎‏‏‎‎‎Include text‎‏‎‎‏‎"</string>
<string name="exclude_link" msgid="1332778255031992228">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‎‎‏‏‏‏‏‏‎‏‏‏‏‏‎‏‎‏‏‎‎‎‎‏‎‏‏‏‏‎‏‏‏‏‎‎‎‏‎‏‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‎Exclude link‎‏‎‎‏‎"</string>
<string name="include_link" msgid="827855767220339802">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‎‏‎‏‏‎‏‏‏‏‏‎‏‎‎‏‎‎‎‏‎‎‏‏‎‏‏‏‏‎‎‏‎‎‎‏‎‎‏‏‎‏‏‎‏‎‏‎‎‎‏‎‎‎‏‎‏‏‎‏‎‎Include link‎‏‎‎‏‎"</string>
+ <string name="pinned" msgid="7623664001331394139">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‏‏‎‎‏‏‎‎‏‎‏‏‎‎‏‎‎‎‎‎‎‏‎‎‎‏‏‎‏‏‏‎‎‏‎‎‏‎‏‎‎‏‏‏‎‏‏‎‎‏‎‏‏‎‏‏‎Pinned‎‏‎‎‏‎"</string>
</resources>
diff --git a/java/res/values-es-rUS/strings.xml b/java/res/values-es-rUS/strings.xml
index 5393889..97ae9a6 100644
--- a/java/res/values-es-rUS/strings.xml
+++ b/java/res/values-es-rUS/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Dejar de fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Editar"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} y # archivo más}many{{file_name} y # archivos más}other{{file_name} y # archivos más}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # archivo}many{+ # de archivos}other{+ # archivos}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"Compartiendo texto"</string>
- <string name="sharing_link" msgid="2307694372813942916">"Compartiendo vínculo"</string>
- <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Compartiendo imagen}many{Compartiendo # de imág.}other{Compartiendo # imágenes}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{# archivo más}many{# de archivos más}other{# archivos más}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"Compartir texto"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"Compartir vínculo"</string>
+ <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Compartir la imagen}many{Compartir # de imágenes}other{Compartir # imágenes}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Compartiendo video}many{Compartiendo # de videos}other{Compartiendo # videos}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Compartiendo # elemento}many{Compartiendo # de elem.}other{Compartiendo # elementos}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Compartiendo con texto"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Compartiendo con vínculo"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Se compartirá # archivo}many{Se compartirán # de archivos}other{Se compartirán # archivos}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Compartir imagen con texto}many{Compartir # de imágenes con texto}other{Compartir # imágenes con texto}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Compartir imagen con vínculo}many{Compartir # de imágenes con vínculo}other{Compartir # imágenes con vínculo}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Compartir video con texto}many{Compartir # de videos con texto}other{Compartir # videos con texto}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Compartir video con vínculo}many{Compartir # de videos con vínculo}other{Compartir # videos con vínculo}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Compartir archivo con texto}many{Compartir # de archivos con texto}other{Compartir # archivos con texto}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Compartir archivo con vínculo}many{Compartir # de archivos con vínculo}other{Compartir # archivos con vínculo}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Solo imagen}many{Solo imágenes}other{Solo imágenes}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Solo video}many{Solo videos}other{Solo videos}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Solo archivo}many{Solo archivos}other{Solo archivos}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura de vista previa de la imagen"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura de vista previa del video"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura de vista previa del archivo"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"No hay personas recomendadas con quienes compartir"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista de apps"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Aunque no se le otorgó permiso de grabación a esta app, puede capturar audio con este dispositivo USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Trabajo"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"No se puede abrir este contenido con apps de trabajo"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"No se pueden usar apps personales para compartir este contenido"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"No se puede abrir este contenido con apps personales"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"El perfil de trabajo está en pausa"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Presionar para activar"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Se pausaron las apps de trabajo"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Reanudar"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"El contenido no es compatible con apps de trabajo"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"El contenido no es compatible con apps personales"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"¿Quieres abrir <xliff:g id="APP">%s</xliff:g> en tu perfil personal?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Incluir texto"</string>
<string name="exclude_link" msgid="1332778255031992228">"Excluir vínculo"</string>
<string name="include_link" msgid="827855767220339802">"Incluir vínculo"</string>
+ <string name="pinned" msgid="7623664001331394139">"Fijado"</string>
</resources>
diff --git a/java/res/values-es/strings.xml b/java/res/values-es/strings.xml
index 5be4c35..0c42bb8 100644
--- a/java/res/values-es/strings.xml
+++ b/java/res/values-es/strings.xml
@@ -51,19 +51,28 @@
<string name="activity_resolver_use_once" msgid="594173435998892989">"Solo una vez"</string>
<string name="activity_resolver_work_profiles_support" msgid="8228711455685203580">"<xliff:g id="APP">%1$s</xliff:g> no admite perfiles de trabajo"</string>
<string name="pin_specific_target" msgid="5057063421361441406">"Fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
- <string name="unpin_specific_target" msgid="3115158908159857777">"No fijar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
+ <string name="unpin_specific_target" msgid="3115158908159857777">"Desfijar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Editar"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} y # archivo más}many{{file_name} y # archivos más}other{{file_name} y # archivos más}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # archivo}many{+ # archivos}other{+ # archivos}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{y # archivo más}many{y # archivos más}other{y # archivos más}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Compartiendo texto"</string>
<string name="sharing_link" msgid="2307694372813942916">"Compartiendo enlace"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Compartiendo imagen}many{Compartiendo # imágenes}other{Compartiendo # imágenes}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Compartiendo vídeo}many{Compartiendo # vídeos}other{Compartiendo # vídeos}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Compartiendo # elemento}many{Compartiendo # elementos}other{Compartiendo # elementos}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Compartiendo imagen con texto"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Compartiendo imagen con enlace"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Compartiendo # archivo}many{Compartiendo # archivos}other{Compartiendo # archivos}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Compartiendo imagen con texto}many{Compartiendo # imágenes con texto}other{Compartiendo # imágenes con texto}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Compartiendo imagen con enlace}many{Compartiendo # imágenes con enlace}other{Compartiendo # imágenes con enlace}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Compartiendo vídeo con texto}many{Compartiendo # vídeos con texto}other{Compartiendo # vídeos con texto}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Compartiendo vídeo con enlace}many{Compartiendo # vídeos con enlace}other{Compartiendo # vídeos con enlace}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Compartiendo archivo con mensaje de texto}many{Compartiendo # archivos con mensaje de texto}other{Compartiendo # archivos con mensaje de texto}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Compartiendo archivo con enlace}many{Compartiendo # archivos con enlace}other{Compartiendo # archivos con enlace}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Solo imagen}many{Solo imágenes}other{Solo imágenes}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Solo vídeo}many{Solo vídeos}other{Solo vídeos}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Solo archivo}many{Solo archivos}other{Solo archivos}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura de previsualización de la imagen"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura de previsualización del vídeo"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura de previsualización del archivo"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"No hay sugerencias de personas con las que compartir"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista de aplicaciones"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Esta aplicación no tiene permiso para grabar, pero podría capturar audio con este dispositivo USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Trabajo"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Este contenido no se puede abrir con aplicaciones de trabajo"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Este contenido no se puede compartir con aplicaciones personales"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Este contenido no se puede abrir con aplicaciones personales"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"El perfil de trabajo está en pausa"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Toca para activar"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Las aplicaciones de trabajo están en pausa"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Reactivar"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Ninguna aplicación de trabajo"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Ninguna aplicación personal"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"¿Abrir <xliff:g id="APP">%s</xliff:g> en tu perfil personal?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Incluir texto"</string>
<string name="exclude_link" msgid="1332778255031992228">"Excluir enlace"</string>
<string name="include_link" msgid="827855767220339802">"Incluir enlace"</string>
+ <string name="pinned" msgid="7623664001331394139">"Fijada"</string>
</resources>
diff --git a/java/res/values-et/strings.xml b/java/res/values-et/strings.xml
index 60ba3db..bc96069 100644
--- a/java/res/values-et/strings.xml
+++ b/java/res/values-et/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Kinnita <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Vabasta <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Muuda"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fail}other{{file_name} + # faili}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fail}other{+ # faili}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Veel # fail}other{Veel # faili}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Teksti jagamine"</string>
<string name="sharing_link" msgid="2307694372813942916">"Lingi jagamine"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Pildi jagamine}other{# pildi jagamine}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Video jagamine}other{# video jagamine}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# üksuse jagamine}other{# üksuse jagamine}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Pildi jagamine tekstiga"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Pildi jagamine lingiga"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# faili jagamine}other{# faili jagamine}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Teksti sisaldava pildi jagamine}other{# teksti sisaldava pildi jagamine}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Linki sisaldava pildi jagamine}other{# linki sisaldava pildi jagamine}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Teksti sisaldava video jagamine}other{# teksti sisaldava video jagamine}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Linki sisaldava video jagamine}other{# linki sisaldava video jagamine}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Teksti sisaldava faili jagamine}other{# teksti sisaldava faili jagamine}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Linki sisaldava faili jagamine}other{# linki sisaldava faili jagamine}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Ainult pilt}other{Ainult pildid}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Ainult video}other{Ainult videod}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Ainult fail}other{Ainult failid}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Pildi eelvaate pisipilt"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Video eelvaate pisipilt"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Faili eelvaate pisipilt"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Ei ole ühtki soovitatud inimest, kellega jagada"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Rakenduste loend"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Sellele rakendusele pole antud salvestamise luba, kuid see saab heli jäädvustada selle USB-seadme kaudu."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Isiklik"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Töö"</string>
@@ -72,10 +81,10 @@
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokeeris teie IT-administraator"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Seda sisu ei saa töörakendustega jagada"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Seda sisu ei saa töörakendustega avada"</string>
- <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Seda sisu ei saa isiklike rakendustega jagada"</string>
+ <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Seda sisu ei saa isiklike rakendustega jagada."</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Seda sisu ei saa isiklike rakendustega avada"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Tööprofiil on peatatud"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Puudutage sisselülitamiseks"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Töörakendused on peatatud"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Jätka"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Töörakendusi pole"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Isiklikke rakendusi pole"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Kas avada <xliff:g id="APP">%s</xliff:g> teie isiklikul profiilil?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Kaasa tekst"</string>
<string name="exclude_link" msgid="1332778255031992228">"Välista link"</string>
<string name="include_link" msgid="827855767220339802">"Kaasa link"</string>
+ <string name="pinned" msgid="7623664001331394139">"Kinnitatud"</string>
</resources>
diff --git a/java/res/values-eu/strings.xml b/java/res/values-eu/strings.xml
index 1a613e7..1cc7576 100644
--- a/java/res/values-eu/strings.xml
+++ b/java/res/values-eu/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Ainguratu <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Kendu aingura <xliff:g id="LABEL">%1$s</xliff:g> aplikazioari"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Editatu"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} eta beste # fitxategi}other{{file_name} eta beste # fitxategi}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{eta beste # fitxategi}other{eta beste # fitxategi}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"Testua partekatzen"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{eta beste # fitxategi}other{eta beste # fitxategi}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"Partekatuko den testua"</string>
<string name="sharing_link" msgid="2307694372813942916">"Esteka partekatzen"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Irudia partekatzen}other{# irudi partekatzen}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Bideoa partekatzen}other{# bideo partekatzen}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# elementu partekatzen}other{# elementu partekatzen}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Irudi testuduna partekatzen"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Irudi estekaduna partekatzen"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# fitxategi partekatuko da}other{# fitxategi partekatuko dira}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Irudi testudun bat partekatuko da}other{# irudi testudun partekatuko dira}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Irudi estekadun bat partekatuko da}other{# irudi estekadun partekatuko dira}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Bideo testudun bat partekatuko da}other{# bideo testudun partekatuko dira}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Bideo estekadun bat partekatuko da}other{# bideo estekadun partekatuko dira}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Fitxategi testudun bat partekatuko da}other{# fitxategi testudun partekatuko dira}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Fitxategi estekadun bat partekatuko da}other{# fitxategi estekadun partekatuko dira}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Irudia soilik}other{Irudiak bakarrik}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Bideoa soilik}other{Bideoak soilik}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Fitxategia soilik}other{Fitxategiak soilik}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Irudiaren aurrebista gisako irudi txikia"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Bideoaren aurrebista gisako irudi txikia"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Fitxategiaren aurrebista gisako irudi txikia"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Ez dago edukia partekatzeko pertsona gomendaturik"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Aplikazioen zerrenda"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Aplikazioak ez du grabatzeko baimenik, baina baliteke audioa grabatzea USB bidezko gailu horren bidez."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Pertsonala"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Lanekoa"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Eduki hau ezin da laneko aplikazioekin ireki"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Eduki hau ezin da aplikazio pertsonalekin partekatu"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Eduki hau ezin da aplikazio pertsonalekin ireki"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Laneko profila pausatuta dago"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Sakatu aktibatzeko"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Pausatuta daude laneko aplikazioak"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Berraktibatu"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Ez dago laneko aplikaziorik"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Ez dago aplikazio pertsonalik"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Profil pertsonalean ireki nahi duzu <xliff:g id="APP">%s</xliff:g>?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Sartu testua"</string>
<string name="exclude_link" msgid="1332778255031992228">"Utzi kanpoan esteka"</string>
<string name="include_link" msgid="827855767220339802">"Sartu esteka"</string>
+ <string name="pinned" msgid="7623664001331394139">"Ainguratuta"</string>
</resources>
diff --git a/java/res/values-fa/strings.xml b/java/res/values-fa/strings.xml
index bb4a1a6..58313f7 100644
--- a/java/res/values-fa/strings.xml
+++ b/java/res/values-fa/strings.xml
@@ -32,7 +32,7 @@
<string name="whichEditApplicationLabel" msgid="5992662938338600364">"ویرایش"</string>
<string name="whichSendApplication" msgid="59510564281035884">"هم‌رسانی"</string>
<string name="whichSendApplicationNamed" msgid="495577664218765855">"هم‌رسانی با <xliff:g id="APP">%1$s</xliff:g>"</string>
- <string name="whichSendApplicationLabel" msgid="2391198069286568035">"اشتراک‌گذاری"</string>
+ <string name="whichSendApplicationLabel" msgid="2391198069286568035">"هم‌رسانی"</string>
<string name="whichSendToApplication" msgid="2724450540348806267">"ارسال با استفاده از"</string>
<string name="whichSendToApplicationNamed" msgid="1996548940365954543">"ارسال بااستفاده از <xliff:g id="APP">%1$s</xliff:g>"</string>
<string name="whichSendToApplicationLabel" msgid="6909037198280591110">"ارسال"</string>
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"سنجاق کردن <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"برداشتن سنجاق <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"ویرایش"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # فایل}one{{file_name} + # فایل}other{{file_name} + # فایل}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{بیش‌از # فایل}one{بیش‌از # فایل}other{بیش‌از # فایل}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"درحال هم‌رسانی نوشتار"</string>
- <string name="sharing_link" msgid="2307694372813942916">"درحال هم‌رسانی پیوند"</string>
- <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{درحال هم‌رسانی تصویر}one{درحال هم‌رسانی ‍# تصویر}other{درحال هم‌رسانی ‍# تصویر}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{بیش‌از # فایل دیگر}one{بیش‌از # فایل دیگر}other{بیش‌از # فایل دیگر}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"هم‌رسانی نوشتار"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"هم‌رسانی پیوند"</string>
+ <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{هم‌رسانی تصویر}one{هم‌رسانی ‍# تصویر}other{هم‌رسانی ‍# تصویر}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{درحال هم‌رسانی ویدیو}one{درحال هم‌رسانی # ویدیو}other{درحال هم‌رسانی # ویدیو}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{درحال هم‌رسانی # مورد}one{درحال هم‌رسانی # مورد}other{درحال هم‌رسانی # مورد}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"هم‌رسانی تصویر با نوشتار"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"هم‌رسانی تصویر با پیوند"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{هم‌رسانی # فایل}one{هم‌رسانی # فایل}other{هم‌رسانی # فایل}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{درحال هم‌رسانی تصویر با نوشتار}one{درحال هم‌رسانی # تصویر با نوشتار}other{درحال هم‌رسانی # تصویر با نوشتار}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{درحال هم‌رسانی تصویر با پیوند}one{درحال هم‌رسانی # تصویر با پیوند}other{درحال هم‌رسانی # تصویر با پیوند}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{درحال هم‌رسانی ویدیو با نوشتار}one{درحال هم‌رسانی # ویدیو با نوشتار}other{درحال هم‌رسانی # ویدیو با نوشتار}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{درحال هم‌رسانی ویدیو با پیوند}one{درحال هم‌رسانی # ویدیو با پیوند}other{درحال هم‌رسانی # ویدیو با پیوند}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{درحال هم‌رسانی فایل با نوشتار}one{درحال هم‌رسانی # فایل با نوشتار}other{درحال هم‌رسانی # فایل با نوشتار}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{درحال هم‌رسانی فایل با پیوند}one{درحال هم‌رسانی # فایل با پیوند}other{درحال هم‌رسانی # فایل با پیوند}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{فقط تصویر}one{فقط تصویر}other{فقط تصویر}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{فقط ویدیو}one{فقط ویدیو}other{فقط ویدیو}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{فقط فایل}one{فقط فایل}other{فقط فایل}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"تصویر کوچک پیش‌نمای تصویر"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"تصویر کوچک پیش‌نمای ویدیو"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"تصویر کوچک پیش‌نمای فایل"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"هیچ فردی توصیه نشده است که با او هم‌رسانی کنید"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"فهرست برنامه‌ها"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"‏مجوز ضبط به این برنامه داده نشده است اما می‌تواند صدا را ازطریق این دستگاه USB ضبط کند."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"شخصی"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"کاری"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"نمی‌توان این محتوا را با برنامه‌های کاری باز کرد"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"نمی‌توان این محتوا را با برنامه‌های شخصی هم‌رسانی کرد"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"نمی‌توان این محتوا را با برنامه‌های شخصی باز کرد"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"نمایه کاری موقتاً متوقف شده است"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"برای روشن کردن، ضربه بزنید"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"برنامه‌های کاری موقتاً متوقف شده‌اند"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"لغو مکث"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"برنامه کاری‌ای وجود ندارد"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"برنامه شخصی‌ای وجود ندارد"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> در نمایه شخصی باز شود؟"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"لحاظ کردن نوشتار"</string>
<string name="exclude_link" msgid="1332778255031992228">"مستثنی کردن پیوند"</string>
<string name="include_link" msgid="827855767220339802">"لحاظ کردن پیوند"</string>
+ <string name="pinned" msgid="7623664001331394139">"سنجاق‌شده"</string>
</resources>
diff --git a/java/res/values-fi/strings.xml b/java/res/values-fi/strings.xml
index 3c60b38..53537e6 100644
--- a/java/res/values-fi/strings.xml
+++ b/java/res/values-fi/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Kiinnitä <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Irrota <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Muokkaa"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # tiedosto}other{{file_name} + # tiedostoa}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{yli # tiedosto}other{yli # tiedostoa}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # muu tiedosto}other{+ # muuta tiedostoa}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Jaetaan tekstiä"</string>
<string name="sharing_link" msgid="2307694372813942916">"Jaetaan linkkiä"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Jaetaan kuvaa}other{Jaetaan # kuvaa}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Jaetaan videota}other{Jaetaan # videota}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Jaetaan # kohdetta}other{Jaetaan # kohdetta}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Kuvaa ja tekstiä jaetaan"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Kuvaa ja linkkiä jaetaan"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Jaetaan # tiedosto}other{Jaetaan # tiedostoa}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Kuvaa ja tekstiä jaetaan}other{# kuvaa ja tekstiä jaetaan}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Kuvaa ja linkkiä jaetaan}other{# kuvaa ja linkkiä jaetaan}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Videota ja tekstiä jaetaan}other{# videota ja tekstiä jaetaan}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Videota ja linkkiä jaetaan}other{# videota ja linkkiä jaetaan}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Tiedostoa ja tekstiä jaetaan}other{# tiedostoa ja tekstiä jaetaan}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Tiedostoa ja linkkiä jaetaan}other{# tiedostoa ja linkkiä jaetaan}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Vain kuva}other{Vain kuvat}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Vain video}other{Vain videot}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Vain tiedostot}other{Vain tiedostot}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Kuvan esikatselun pikkukuva"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Videon esikatselun pikkukuva"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Tiedoston esikatselun pikkukuva"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Ei suosituksia kenelle jakaa"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Sovellusluettelo"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Sovellus ei ole saanut tallennuslupaa mutta voi tallentaa ääntä tämän USB-laitteen avulla."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Henkilökohtainen"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Työ"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Tätä sisältöä ei voi avata työsovelluksilla"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Tätä sisältöä ei voi jakaa henkilökohtaisilla sovelluksilla"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Tätä sisältöä ei voi avata henkilökohtaisilla sovelluksilla"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Työprofiilin käyttö on keskeytetty"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Laita päälle napauttamalla"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Työsovellukset on keskeytetty"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Jatka käyttöä"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Ei työsovelluksia"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Ei henkilökohtaisia sovelluksia"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Avataanko <xliff:g id="APP">%s</xliff:g> henkilökohtaisessa profiilissa?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Liitä teksti mukaan"</string>
<string name="exclude_link" msgid="1332778255031992228">"Jätä linkki pois"</string>
<string name="include_link" msgid="827855767220339802">"Liitä linkki mukaan"</string>
+ <string name="pinned" msgid="7623664001331394139">"Kiinnitetty"</string>
</resources>
diff --git a/java/res/values-fr-rCA/strings.xml b/java/res/values-fr-rCA/strings.xml
index 47bea8a..5595b6c 100644
--- a/java/res/values-fr-rCA/strings.xml
+++ b/java/res/values-fr-rCA/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Épingler <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Annuler l\'épinglage de <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Modifier"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fichier}one{{file_name} + # fichier}many{{file_name} + # fichiers}other{{file_name} + # fichiers}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fichier}one{+ # fichier}many{+ # de fichiers}other{+ # fichiers}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"Partage du message texte…"</string>
- <string name="sharing_link" msgid="2307694372813942916">"Partage du lien en cours…"</string>
- <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Partage de l\'image…}one{Partage de # image…}many{Partage de # d\'images…}other{Partage de # images…}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{et # fichier supplémentaire}one{et # fichier supplémentaire}many{et # de fichiers supplémentaires}other{et # fichiers supplémentaires}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"Partage de texte"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"Partage d\'un lien"</string>
+ <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Partage d\'une image}one{Partage de # image}many{Partage de # d\'images}other{Partage de # images}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Partage de la vidéo…}one{Partage de # vidéo…}many{Partage de # de vidéos…}other{Partage de # vidéos…}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Partage de # élément…}one{Partage de # élément…}many{Partage de # d\'éléments}other{Partage de # éléments…}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Partage d\'image avec texte…"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Partage d\'image avec lien…"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Partage de # fichier en cours…}one{Partage de # fichier en cours…}many{Partage de # de fichiers en cours…}other{Partage de # fichiers en cours…}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Partage d\'une image avec du texte}one{Partage de # image avec du texte}many{Partage de # d\'images avec du texte}other{Partage de # images avec du texte}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Partage d\'une image avec un lien}one{Partage de # image avec un lien}many{Partage de # d\'images avec un lien}other{Partage de # images avec un lien}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Partage d\'une vidéo avec du texte}one{Partage de # vidéo avec du texte}many{Partage de # de vidéos avec du texte}other{Partage de # vidéos avec du texte}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Partage d\'une vidéo avec un lien}one{Partage de # vidéo avec un lien}many{Partage de # de vidéos avec un lien}other{Partage de # vidéos avec un lien}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Partage d\'un fichier avec du texte}one{Partage de # fichier avec du texte}many{Partage de # de fichiers avec du texte}other{Partage de # fichiers avec du texte}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Partage d\'un fichier avec un lien}one{Partage de # fichier avec un lien}many{Partage de # de fichiers avec un lien}other{Partage de # fichiers avec un lien}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Image uniquement}one{Image uniquement}many{Images uniquement}other{Images uniquement}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Vidéo uniquement}one{Vidéo uniquement}many{Vidéos uniquement}other{Vidéos uniquement}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Fichier uniquement}one{Fichier uniquement}many{Fichiers uniquement}other{Fichiers uniquement}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniature d\'aperçu de l\'image"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniature d\'aperçu de la vidéo"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniature d\'aperçu du fichier"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Aucune recommandation de personnes avec lesquelles effectuer un partage"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Liste des applications"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Cette application n\'a pas été autorisée à effectuer des enregistrements, mais elle pourrait capturer du contenu audio par l\'intermédiaire de cet appareil USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Personnel"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Professionnel"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Impossible d\'ouvrir ce contenu avec des applications professionnelles"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Impossible de partager ce contenu avec des applications personnelles"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Impossible d\'ouvrir ce contenu avec des applications personnelles"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Le profil professionnel est interrompu"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Touchez pour activer"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Les applications professionnelles sont interrompues"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Réactiver"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Aucune application professionnelle"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Aucune application personnelle"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Ouvrir <xliff:g id="APP">%s</xliff:g> dans votre profil personnel?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Inclure le texte"</string>
<string name="exclude_link" msgid="1332778255031992228">"Exclure le lien"</string>
<string name="include_link" msgid="827855767220339802">"Inclure le lien"</string>
+ <string name="pinned" msgid="7623664001331394139">"Épinglée"</string>
</resources>
diff --git a/java/res/values-fr/strings.xml b/java/res/values-fr/strings.xml
index fbdb3a1..5f0c85e 100644
--- a/java/res/values-fr/strings.xml
+++ b/java/res/values-fr/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Épingler <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Retirer <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Modifier"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fichier}one{{file_name} + # fichier}many{{file_name} + # fichiers}other{{file_name} + # fichiers}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fichier}one{+ # fichier}many{+ # fichiers}other{+ # fichiers}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # autre fichier}one{+ # autre fichier}many{+ # autres fichiers}other{+ # autres fichiers}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Partage du texte…"</string>
- <string name="sharing_link" msgid="2307694372813942916">"Partage du lien…"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"Partager le lien"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Partage de l\'image…}one{Partage de # image…}many{Partage de # d\'images…}other{Partage de # images…}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Partage de la vidéo…}one{Partage de # vidéo…}many{Partage de # de vidéos…}other{Partage de # vidéos…}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Partage de # élément…}one{Partage de # élément…}many{Partage de # d\'éléments…}other{Partage de # éléments…}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Partage de l\'image (texte)"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Partage de l\'image (lien)"</string>
- <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Aucune recommandation de personnes avec lesquelles effectuer un partage"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Liste des applications"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Partage de # fichier}one{Partage de # fichier}many{Partage de # fichiers}other{Partage de # fichiers}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Partager 1 image avec du texte}one{Partager # image avec du texte}many{Partager # images avec du texte}other{Partager # images avec du texte}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Partager 1 image avec un lien}one{Partager # image avec un lien}many{Partager # images avec un lien}other{Partager # images avec un lien}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Partager 1 vidéo avec du texte}one{Partager # vidéo avec du texte}many{Partager # vidéos avec du texte}other{Partager # vidéos avec du texte}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Partager 1 vidéo avec un lien}one{Partager # vidéo avec un lien}many{Partager # vidéos avec un lien}other{Partager # vidéos avec un lien}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Partager 1 fichier avec du texte}one{Partager # fichier avec du texte}many{Partager # fichiers avec du texte}other{Partager # fichiers avec du texte}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Partager 1 fichier avec un lien}one{Partager # fichier avec un lien}many{Partager # fichiers avec un lien}other{Partager # fichiers avec un lien}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Image uniquement}one{Image uniquement}many{Images uniquement}other{Images uniquement}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Vidéo uniquement}one{Vidéo uniquement}many{Vidéos uniquement}other{Vidéos uniquement}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Fichier uniquement}one{Fichier uniquement}many{Fichiers uniquement}other{Fichiers uniquement}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Vignette d\'aperçu de l\'image"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Vignette d\'aperçu de la vidéo"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Vignette d\'aperçu du fichier"</string>
+ <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Aucun destinataire recommandé"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Cette application n\'a pas reçu l\'autorisation d\'enregistrer des contenus audio, mais peut le faire via ce périphérique USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Personnel"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Professionnel"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Impossible d\'ouvrir ce contenu avec des applis professionnelles"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Impossible de partager ce contenu avec des applis personnelles"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Impossible d\'ouvrir ce contenu avec des applis personnelles"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Profil professionnel en pause"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Appuyez pour l\'activer"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Les applis professionnelles sont en pause"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Réactiver"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Aucune appli professionnelle"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Aucune appli personnelle"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Ouvrir <xliff:g id="APP">%s</xliff:g> dans votre profil personnel ?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Inclure le texte"</string>
<string name="exclude_link" msgid="1332778255031992228">"Exclure le lien"</string>
<string name="include_link" msgid="827855767220339802">"Inclure le lien"</string>
+ <string name="pinned" msgid="7623664001331394139">"Épinglée"</string>
</resources>
diff --git a/java/res/values-gl/strings.xml b/java/res/values-gl/strings.xml
index f50f61b..60dc78d 100644
--- a/java/res/values-gl/strings.xml
+++ b/java/res/values-gl/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Fixar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Deixar de fixar a <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Editar"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ficheiro}other{{file_name} + # ficheiros}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+# ficheiro}other{+# ficheiros}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{# ficheiro máis}other{# ficheiros máis}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Compartindo texto"</string>
<string name="sharing_link" msgid="2307694372813942916">"Compartindo ligazón"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Compartindo imaxe}other{Compartindo # imaxes}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Compartindo vídeo}other{Compartindo # vídeos}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Compartindo # elemento}other{Compartindo # elementos}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Compartindo imaxe (texto)"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Compartindo imaxe (lig.)"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Compartindo # ficheiro}other{Compartindo # ficheiros}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Compartindo imaxe con texto}other{Compartindo # imaxes con texto}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Compartindo imaxe con ligazón}other{Compartindo # imaxes con ligazón}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Compartindo vídeo con texto}other{Compartindo # vídeos con texto}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Compartindo vídeo con ligazón}other{Compartindo # vídeos con ligazón}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Compartindo ficheiro con texto}other{Compartindo # ficheiros con texto}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Compartindo ficheiro con ligazón}other{Compartindo # ficheiros con ligazón}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Só a imaxe}other{Só as imaxes}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Só o vídeo}other{Só os vídeos}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Só o ficheiro}other{Só os ficheiros}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura de vista previa da imaxe"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura de vista previa do vídeo"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura de vista previa do ficheiro"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Non hai recomendacións de persoas coas que compartir contido"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista de aplicacións"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Esta aplicación non está autorizada a realizar gravacións, pero podería capturar audio a través deste dispositivo USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Persoal"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Traballo"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Este contido non pode abrirse con aplicacións do traballo"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Este contido non pode compartirse con aplicacións persoais"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Este contido non pode abrirse con aplicacións persoais"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"O perfil de traballo está en pausa"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tocar para activar o perfil"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"As aplicacións do traballo están en pausa"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Reactivar"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Non hai ningunha aplicación do traballo compatible"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Non hai ningunha aplicación persoal compatible"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Queres abrir <xliff:g id="APP">%s</xliff:g> no teu perfil persoal?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Incluír texto"</string>
<string name="exclude_link" msgid="1332778255031992228">"Excluír ligazón"</string>
<string name="include_link" msgid="827855767220339802">"Incluír ligazón"</string>
+ <string name="pinned" msgid="7623664001331394139">"Elemento fixado"</string>
</resources>
diff --git a/java/res/values-gu/strings.xml b/java/res/values-gu/strings.xml
index b9d846e..db3bd59 100644
--- a/java/res/values-gu/strings.xml
+++ b/java/res/values-gu/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g>ને પિન કરો"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g>ને અનપિન કરો"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"ફેરફાર કરો"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ફાઇલ}one{{file_name} + # ફાઇલ}other{{file_name} + # ફાઇલો}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ફાઇલ}one{+ # ફાઇલ}other{+ # ફાઇલ}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ વધુ # ફાઇલ}one{+ વધુ # ફાઇલ}other{+ વધુ # ફાઇલ}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"ટેક્સ્ટ શેર કરીએ છીએ"</string>
<string name="sharing_link" msgid="2307694372813942916">"લિંક શેર કરી રહ્યાં છીએ"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{છબી શેર કરી રહ્યાં છીએ}one{# છબી શેર કરી રહ્યાં છીએ}other{# છબી શેર કરી રહ્યાં છીએ}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{વીડિયો શેર કરીએ છીએ}one{# વીડિયો શેર કરીએ છીએ}other{# વીડિયો શેર કરીએ છીએ}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# આઇટમ શેર કરી રહ્યાં છીએ}one{# આઇટમ શેર કરી રહ્યાં છીએ}other{# આઇટમ શેર કરી રહ્યાં છીએ}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"ટેક્સ્ટ સાથે છબી શેર થશે"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"લિંક સાથે છબી શેર થાય છે"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ફાઇલ શેર કરી રહ્યાં છીએ}one{# ફાઇલ શેર કરી રહ્યાં છીએ}other{# ફાઇલ શેર કરી રહ્યાં છીએ}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{ટેક્સ્ટ સાથે છબી શેર કરી રહ્યાં છીએ}one{ટેક્સ્ટ સાથે # છબી શેર કરી રહ્યાં છીએ}other{ટેક્સ્ટ સાથે # છબી શેર કરી રહ્યાં છીએ}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{લિંક સાથે છબી શેર કરી રહ્યાં છીએ}one{લિંક સાથે # છબી શેર કરી રહ્યાં છીએ}other{લિંક સાથે # છબી શેર કરી રહ્યાં છીએ}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ટેક્સ્ટ સાથે વીડિયો શેર કરી રહ્યાં છીએ}one{ટેક્સ્ટ સાથે # વીડિયો શેર કરી રહ્યાં છીએ}other{ટેક્સ્ટ સાથે # વીડિયો શેર કરી રહ્યાં છીએ}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{લિંક સાથે વીડિયો શેર કરી રહ્યાં છીએ}one{લિંક સાથે # વીડિયો શેર કરી રહ્યાં છીએ}other{લિંક સાથે # વીડિયો શેર કરી રહ્યાં છીએ}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ટેક્સ્ટ સાથે ફાઇલ શેર કરી રહ્યાં છીએ}one{ટેક્સ્ટ સાથે # ફાઇલ શેર કરી રહ્યાં છીએ}other{ટેક્સ્ટ સાથે # ફાઇલ શેર કરી રહ્યાં છીએ}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{લિંક સાથે ફાઇલ શેર કરી રહ્યાં છીએ}one{લિંક સાથે # ફાઇલ શેર કરી રહ્યાં છીએ}other{લિંક સાથે # ફાઇલ શેર કરી રહ્યાં છીએ}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{માત્ર છબી}one{માત્ર છબી}other{માત્ર છબીઓ}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{ફક્ત વીડિયો}one{ફક્ત વીડિયો}other{ફક્ત વીડિયો}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ફક્ત ફાઇલ}one{ફક્ત ફાઇલ}other{ફક્ત ફાઇલો}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"છબીના પ્રીવ્યૂની થંબનેલ"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"વીડિયોના પ્રીવ્યૂની થંબનેલ"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"ફાઇલના પ્રીવ્યૂની થંબનેલ"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"શેર કરવા માટે સુઝાવ આપવામાં આવેલા કોઈ લોકો નથી"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ઍપની સૂચિ"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"આ ઍપને રેકૉર્ડ કરવાની પરવાનગી આપવામાં આવી નથી પરંતુ તે આ USB ડિવાઇસ મારફતે ઑડિયો કૅપ્ચર કરી શકે છે."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"વ્યક્તિગત"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"ઑફિસ"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"આ કન્ટેન્ટ ઑફિસ માટેની ઍપ વડે ખોલી શકાતું નથી"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"આ કન્ટેન્ટ વ્યક્તિગત ઍપ સાથે શેર કરી શકાતું નથી"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"આ કન્ટેન્ટ વ્યક્તિગત ઍપ વડે ખોલી શકાતું નથી"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"ઑફિસની પ્રોફાઇલ થોભાવી છે"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"ચાલુ કરવા માટે ટૅપ કરો"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"ઑફિસ માટેની ઍપ થોભાવવામાં આવી છે"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"ફરી ચાલુ કરો"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"કોઈ ઑફિસ માટેની ઍપ સપોર્ટ કરતી નથી"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"કોઈ વ્યક્તિગત ઍપ સપોર્ટ કરતી નથી"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"તમારી વ્યક્તિગત પ્રોફાઇલમાં <xliff:g id="APP">%s</xliff:g> ખોલીએ?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"ટેક્સ્ટ શામેલ કરો"</string>
<string name="exclude_link" msgid="1332778255031992228">"લિંકને બાકાત કરો"</string>
<string name="include_link" msgid="827855767220339802">"લિંક શામેલ કરો"</string>
+ <string name="pinned" msgid="7623664001331394139">"પિન કરેલી"</string>
</resources>
diff --git a/java/res/values-hi/strings.xml b/java/res/values-hi/strings.xml
index 538b11d..b722e0c 100644
--- a/java/res/values-hi/strings.xml
+++ b/java/res/values-hi/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> को पिन करें"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> को अनपिन करें"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"बदलाव करें"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # फ़ाइल}one{{file_name} + # फ़ाइल}other{{file_name} + # फ़ाइलें}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # फ़ाइल}one{+ # फ़ाइल}other{+ # फ़ाइलें}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{# और फ़ाइल}one{# और फ़ाइल}other{# और फ़ाइलें}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"टेक्स्ट शेयर किया जा रहा है"</string>
<string name="sharing_link" msgid="2307694372813942916">"लिंक शेयर किया जा रहा है"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{इमेज शेयर की जा रही है}one{# इमेज शेयर की जा रही है}other{# इमेज शेयर की जा रही हैं}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{वीडियो शेयर किया जा रहा है}one{# वीडियो शेयर किया जा रहा है}other{# वीडियो शेयर किए जा रहे हैं}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# आइटम शेयर किया जा रहा है}one{# आइटम शेयर किया जा रहा है}other{# आइटम शेयर किए जा रहे हैं}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"टेक्स्ट के साथ इमेज शेयर की जा रही है"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"लिंक के साथ इमेज शेयर की जा रही है"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# फ़ाइल शेयर की जा रही है}one{# फ़ाइल शेयर की जा रही है}other{# फ़ाइलें शेयर की जा रही हैं}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{टेक्स्ट के साथ इमेज शेयर की जा रही है}one{टेक्स्ट के साथ # इमेज शेयर की जा रही है}other{टेक्स्ट के साथ # इमेज शेयर की जा रही हैं}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{लिंक के साथ इमेज शेयर की जा रही है}one{लिंक के साथ # इमेज शेयर की जा रही है}other{लिंक के साथ # इमेज शेयर की जा रही हैं}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{टेक्स्ट के साथ वीडियो शेयर किया जा रहा है}one{टेक्स्ट के साथ # वीडियो शेयर किया जा रहा है}other{टेक्स्ट के साथ # वीडियो शेयर किए जा रहे हैं}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{लिंक के साथ वीडियो शेयर किया जा रहा है}one{लिंक के साथ # वीडियो शेयर किया जा रहा है}other{लिंक के साथ # वीडियो शेयर किए जा रहे हैं}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{टेक्स्ट के साथ फ़ाइल शेयर की जा रही है}one{टेक्स्ट के साथ # फ़ाइल शेयर की जा रही है}other{टेक्स्ट के साथ # फ़ाइलें शेयर की जा रही हैं}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{लिंक के साथ फ़ाइल शेयर की जा रही है}one{लिंक के साथ # फ़ाइल शेयर की जा रही है}other{लिंक के साथ # फ़ाइलें शेयर की जा रही हैं}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{सिर्फ़ इमेज}one{सिर्फ़ इमेज}other{सिर्फ़ इमेज}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{सिर्फ़ वीडियो}one{सिर्फ़ वीडियो}other{सिर्फ़ वीडियो}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{सिर्फ़ फ़ाइल}one{सिर्फ़ फ़ाइल}other{सिर्फ़ फ़ाइलें}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"इमेज के थंबनेल की झलक"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"वीडियो के थंबनेल की झलक"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"फ़ाइल के थंबनेल की झलक"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"शेयर करने के लिए, किसी व्यक्ति का सुझाव नहीं दिया गया है"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ऐप्लिकेशन की सूची"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"इस ऐप्लिकेशन को रिकॉर्ड करने की अनुमति नहीं दी गई है. हालांकि, ऐप्लिकेशन इस यूएसबी डिवाइस से ऐसा कर सकता है."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"निजी प्रोफ़ाइल"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"वर्क प्रोफ़ाइल"</string>
@@ -72,10 +81,10 @@
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"आपके आईटी एडमिन ने इस कॉन्टेंट को शेयर करने की सुविधा ब्लॉक कर रखी है"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"इस कॉन्टेंट को ऑफ़िस के काम से जुड़े ऐप्लिकेशन का इस्तेमाल करके, शेयर नहीं किया जा सकता"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"इस कॉन्टेंट को ऑफ़िस के काम से जुड़े ऐप्लिकेशन पर खोला नहीं जा सकता"</string>
- <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"इस कॉन्टेंट को निजी ऐप्लिकेशन का इस्तेमाल करके, शेयर नहीं किया जा सकता"</string>
+ <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"इस कॉन्टेंट को निजी ऐप्लिकेशन के ज़रिए शेयर नहीं किया जा सकता"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"इस कॉन्टेंट को निजी ऐप्लिकेशन पर खोला नहीं जा सकता"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"वर्क प्रोफ़ाइल रोक दी गई है"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"वर्क प्रोफ़ाइल चालू करने के लिए टैप करें"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"वर्क ऐप्लिकेशन बंद किए गए हैं"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"चालू करें"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"यह कॉन्टेंट, ऑफ़िस के काम से जुड़े आपके किसी भी ऐप्लिकेशन पर खोला नहीं जा सकता"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"यह कॉन्टेंट आपके किसी भी निजी ऐप्लिकेशन पर खोला नहीं जा सकता"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"क्या <xliff:g id="APP">%s</xliff:g> को निजी प्रोफ़ाइल में खोलना है?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"टेक्स्ट जोड़ें"</string>
<string name="exclude_link" msgid="1332778255031992228">"लिंक हटाएं"</string>
<string name="include_link" msgid="827855767220339802">"लिंक जोड़ें"</string>
+ <string name="pinned" msgid="7623664001331394139">"पिन किया गया"</string>
</resources>
diff --git a/java/res/values-hr/strings.xml b/java/res/values-hr/strings.xml
index ef08630..e2d71b3 100644
--- a/java/res/values-hr/strings.xml
+++ b/java/res/values-hr/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Prikvači aplikaciju <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Otkvači sudionika <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Uredi"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} i # datoteka}one{{file_name} i # datoteka}few{{file_name} i # datoteke}other{{file_name} i # datoteka}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # datoteka}one{+ # datoteka}few{+ # datoteke}other{+ # datoteka}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{i još # datoteka}one{i još # datoteka}few{i još # datoteke}other{i još # datoteka}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Dijeli se tekst"</string>
<string name="sharing_link" msgid="2307694372813942916">"Dijeli se veza"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Dijeli se slika}one{Dijeli se # slika}few{Dijele se # slike}other{Dijeli se # slika}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Dijeli se videozapis}one{Dijeli se # videozapis}few{Dijele se # videozapisa}other{Dijeli se # videozapisa}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Dijeli se # stavka}one{Dijeli se # stavka}few{Dijele se # stavke}other{Dijeli se # stavki}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Dijeli se slika s tekstom"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Dijeli se slika s vezom"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Dijeli se # datoteka}one{Dijeli se # datoteka}few{Dijele se # datoteke}other{Dijeli se # datoteka}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Dijeli se slika s tekstom}one{Dijeli se # slika s tekstom}few{Dijele se # slike s tekstom}other{Dijeli se # slika s tekstom}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Dijeli se slika s vezom}one{Dijeli se # slika s vezom}few{Dijele se # slike s vezom}other{Dijeli se # slika s vezom}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Dijeli se videozapis s tekstom}one{Dijeli se # videozapis s tekstom}few{Dijele se # videozapisa s tekstom}other{Dijeli se # videozapisa s tekstom}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Dijeli se videozapis s vezom}one{Dijeli se # videozapis s vezom}few{Dijele se # videozapisa s vezom}other{Dijeli se # videozapisa s vezom}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Dijeli se datoteka s tekstom}one{Dijeli se # datoteka s tekstom}few{Dijele se # datoteke s tekstom}other{Dijeli se # datoteka s tekstom}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Dijeli se datoteka s vezom}one{Dijeli se # datoteka s vezom}few{Dijele se # datoteke s vezom}other{Dijeli se # datoteka s vezom}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Samo slika}one{Samo slike}few{Samo slike}other{Samo slike}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Samo videozapis}one{Samo videozapisi}few{Samo videozapisi}other{Samo videozapisi}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Samo datoteka}one{Samo datoteke}few{Samo datoteke}other{Samo datoteke}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Minijatura pregleda slike"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Minijatura pregleda videozapisa"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Minijatura pregleda datoteke"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nema preporučenih osoba za dijeljenje"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Popis aplikacija"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Ta aplikacija nema dopuštenje za snimanje, no mogla bi primati zvuk putem ovog USB uređaja."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Osobno"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Posao"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Taj se sadržaj ne može otvoriti pomoću poslovnih aplikacija"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Taj se sadržaj ne može dijeliti pomoću osobnih aplikacija"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Taj se sadržaj ne može otvoriti pomoću osobnih aplikacija"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Poslovni profil je pauziran"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Dodirnite da biste uključili"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Poslovne aplikacije su pauzirane"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Ponovno pokreni"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Poslovne aplikacije nisu dostupne"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Osobne aplikacije nisu dostupne"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Želite li otvoriti aplikaciju <xliff:g id="APP">%s</xliff:g> na osobnom profilu?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Uključi tekst"</string>
<string name="exclude_link" msgid="1332778255031992228">"Isključi vezu"</string>
<string name="include_link" msgid="827855767220339802">"Uključi vezu"</string>
+ <string name="pinned" msgid="7623664001331394139">"Prikvačeno"</string>
</resources>
diff --git a/java/res/values-hu/strings.xml b/java/res/values-hu/strings.xml
index 15b79c6..53ddba7 100644
--- a/java/res/values-hu/strings.xml
+++ b/java/res/values-hu/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> kitűzése"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> rögzítésének feloldása"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Szerkesztés"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fájl}other{{file_name} + # fájl}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fájl}other{+ # fájl}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{További # fájl}other{További # fájl}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Szöveg megosztása"</string>
<string name="sharing_link" msgid="2307694372813942916">"Link megosztása"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Kép megosztása}other{# kép megosztása}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Videó megosztása}other{# videó megosztása}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# elem megosztása}other{# elem megosztása}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Kép megosztása szöveggel…"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Kép megosztása linkkel…"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# fájl megosztása}other{# fájl megosztása}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Kép megosztása szöveggel}other{# kép megosztása szöveggel}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Kép megosztása linkkel}other{# kép megosztása linkkel}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Videó megosztása szöveggel}other{# videó megosztása szöveggel}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Videó megosztása linkkel}other{# videó megosztása linkkel}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Fájl megosztása szöveggel}other{# fájl megosztása szöveggel}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Fájl megosztása linkkel}other{# fájl megosztása linkkel}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Csak kép}other{Csak képek}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Csak videó}other{Csak videók}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Csak fájl}other{Csak fájlok}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Kép előnézeti indexképe"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Videó előnézeti indexképe"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Fájl előnézeti indexképe"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nincsenek ajánlott személyek a megosztáshoz"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Alkalmazások listája"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Ez az alkalmazás nem rendelkezik rögzítési engedéllyel, de ezzel az USB-eszközzel képes a hangfelvételre."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Személyes"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Munka"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Ez a tartalom nem nyitható meg munkahelyi alkalmazásokkal"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Ez a tartalom nem osztható meg személyes alkalmazásokkal"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Ez a tartalom nem nyitható meg személyes alkalmazásokkal"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"A munkaprofil használata szünetel"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Koppintson a bekapcsoláshoz"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"A munkahelyi alkalmazások szüneteltetve vannak"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Szüneteltetés feloldása"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nincs munkahelyi alkalmazás"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nincs személyes alkalmazás"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Megnyitja a(z) <xliff:g id="APP">%s</xliff:g> alkalmazást a személyes profil használatával?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Szöveggel együtt"</string>
<string name="exclude_link" msgid="1332778255031992228">"Link eltávolítása"</string>
<string name="include_link" msgid="827855767220339802">"Linkkel együtt"</string>
+ <string name="pinned" msgid="7623664001331394139">"Kitűzve"</string>
</resources>
diff --git a/java/res/values-hy/strings.xml b/java/res/values-hy/strings.xml
index 64f1b7f..6a83cda 100644
--- a/java/res/values-hy/strings.xml
+++ b/java/res/values-hy/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Ամրացնել <xliff:g id="LABEL">%1$s</xliff:g> հավելվածը"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Ապամրացնել <xliff:g id="LABEL">%1$s</xliff:g> հավելվածը"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Փոփոխել"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} ու ևս # ֆայլ}one{{file_name} ու ևս # ֆայլ}other{{file_name} ու ևս # ֆայլ}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ֆայլ}one{# ֆայլ}other{# ֆայլ}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Ու ևս # ֆայլ}one{Ու ևս # ֆայլ}other{Ու ևս # ֆայլ}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Տեքստի ուղարկում"</string>
<string name="sharing_link" msgid="2307694372813942916">"Հղման ուղարկում"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Պատկերի ուղարկում}one{# պատկերի ուղարկում}other{# պատկերի ուղարկում}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Տեսանյութի ուղարկում}one{# տեսանյութի ուղարկում}other{# տեսանյութի ուղարկում}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# տարրի ուղարկում}one{# տարրի ուղարկում}other{# տարրի ուղարկում}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Պատկերի+տեքստի ուղարկում"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Պատկերի և հղման ուղարկում"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Ուղարկվում է # ֆայլ}one{Ուղարկվում է # ֆայլ}other{Ուղարկվում է # ֆայլ}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Պատկերի ուղարկում տեքստային հաղորդագրության միջոցով}one{# պատկերի ուղարկում տեքստային հաղորդագրության միջոցով}other{# պատկերի ուղարկում տեքստային հաղորդագրության միջոցով}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Պատկերի ուղարկում հղման միջոցով}one{# պատկերի ուղարկում հղման միջոցով}other{# պատկերի ուղարկում հղման միջոցով}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Տեսանյութի ուղարկում տեքստային հաղորդագրության միջոցով}one{# տեսանյութի ուղարկում տեքստային հաղորդագրության միջոցով}other{# տեսանյութի ուղարկում տեքստային հաղորդագրության միջոցով}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Տեսանյութի ուղարկում հղման միջոցով}one{# տեսանյութի ուղարկում հղման միջոցով}other{# տեսանյութի ուղարկում հղման միջոցով}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Ֆայլի ուղարկում տեքստային հաղորդագրության միջոցով}one{# ֆայլի ուղարկում տեքստային հաղորդագրության միջոցով}other{# ֆայլի ուղարկում տեքստային հաղորդագրության միջոցով}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Ֆայլի ուղարկում հղման միջոցով}one{# ֆայլի ուղարկում հղման միջոցով}other{# ֆայլի ուղարկում հղման միջոցով}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Միայն պատկերը}one{Միայն պատկերը}other{Միայն պատկերները}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Միայն տեսանյութը}one{Միայն տեսանյութը}other{Միայն տեսանյութերը}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Միայն ֆայլը}one{Միայն ֆայլը}other{Միայն ֆայլերը}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Պատկերի նախադիտման մանրապատկեր"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Տեսանյութի նախադիտման մանրապատկեր"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Ֆայլի նախադիտման մանրապատկեր"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Չկան օգտատերեր, որոնց հետ կարող եք կիսվել"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Հավելվածների ցանկ"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Հավելվածը ձայնագրելու թույլտվություն չունի, սակայն կկարողանա գրանցել ձայնն այս USB սարքի միջոցով։"</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Անձնական"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Աշխատանքային"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Այս բովանդակությունը հնարավոր չէ բացել աշխատանքային հավելվածներով"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Այս բովանդակությունը հնարավոր չէ ուղարկել անձնական հավելվածներով"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Այս բովանդակությունը հնարավոր չէ բացել անձնական հավելվածներով"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Աշխատանքային պրոֆիլի ծառայությունը դադարեցված է"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Հպեք միացնելու համար"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Աշխատանքային հավելվածները դադարեցված են"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Նորից միացնել"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Աշխատանքային հավելվածներ չկան"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Անձնական հավելվածներ չկան"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Բացե՞լ <xliff:g id="APP">%s</xliff:g> հավելվածը ձեր անձնական պրոֆիլում"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Ներառել տեքստը"</string>
<string name="exclude_link" msgid="1332778255031992228">"Բացառել հղումը"</string>
<string name="include_link" msgid="827855767220339802">"Ներառել հղումը"</string>
+ <string name="pinned" msgid="7623664001331394139">"Ամրացված է"</string>
</resources>
diff --git a/java/res/values-in/strings.xml b/java/res/values-in/strings.xml
index 5c5ba63..d7400b8 100644
--- a/java/res/values-in/strings.xml
+++ b/java/res/values-in/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Sematkan <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Lepas sematan <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Edit"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # file}other{{file_name} + # file}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # file}other{+ # file}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"Membagikan teks"</string>
- <string name="sharing_link" msgid="2307694372813942916">"Membagikan link"</string>
- <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Membagikan gambar}other{Membagikan # gambar}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # file lainnya}other{+ # file lainnya}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"Berbagi teks"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"Berbagi link"</string>
+ <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Berbagi gambar}other{Berbagi # gambar}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Membagikan video}other{Membagikan # video}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Membagikan # item}other{Membagikan # item}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Membagikan gambar dengan teks"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Membagikan gambar dengan link"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Membagikan # file}other{Membagikan # file}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Membagikan gambar dengan teks}other{Membagikan # gambar dengan teks}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Membagikan gambar dengan link}other{Membagikan # gambar dengan link}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Membagikan video dengan teks}other{Membagikan # video dengan teks}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Membagikan video dengan link}other{Membagikan # video dengan link}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Membagikan file dengan teks}other{Membagikan # file dengan teks}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Membagikan file dengan link}other{Membagikan # file dengan link}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Khusus gambar}other{Khusus gambar}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Khusus video}other{Khusus video}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Khusus file}other{Khusus file}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Thumbnail pratinjau gambar"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Thumbnail pratinjau video"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Thumbnail pratinjau file"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Tidak ada rekomendasi kontak untuk berbagi"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Daftar aplikasi"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Aplikasi ini tidak diberi izin merekam, tetapi dapat merekam audio melalui perangkat USB ini."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Pribadi"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Kerja"</string>
@@ -72,10 +81,10 @@
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Diblokir oleh admin IT Anda"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Konten ini tidak dapat dibagikan dengan aplikasi kerja"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Konten ini tidak dapat dibuka dengan aplikasi kerja"</string>
- <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Konten ini tidak dapat dibagikan dengan aplikasi pribadi"</string>
+ <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Konten ini tidak dapat dibagikan ke aplikasi pribadi"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Konten ini tidak dapat dibuka dengan aplikasi pribadi"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Profil kerja dijeda"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Ketuk untuk mengaktifkan"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Aplikasi kerja dijeda"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Batalkan jeda"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Tidak ada aplikasi kerja"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Tidak ada aplikasi pribadi"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Buka <xliff:g id="APP">%s</xliff:g> di profil pribadi?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Sertakan teks"</string>
<string name="exclude_link" msgid="1332778255031992228">"Kecualikan link"</string>
<string name="include_link" msgid="827855767220339802">"Sertakan link"</string>
+ <string name="pinned" msgid="7623664001331394139">"Disematkan"</string>
</resources>
diff --git a/java/res/values-is/strings.xml b/java/res/values-is/strings.xml
index 04a99b7..8e0a9f4 100644
--- a/java/res/values-is/strings.xml
+++ b/java/res/values-is/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Festa <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Losa <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Breyta"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # skrá}one{{file_name} + # skrá}other{{file_name} + # skrár}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # skrá}one{+ # skrá}other{+ # skrár}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # skrá í viðbót}one{+ # skrá í viðbót}other{+ # skrár í viðbót}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Deilir texta"</string>
<string name="sharing_link" msgid="2307694372813942916">"Deilir tengli"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Deilir mynd}one{Deilir # mynd}other{Deilir # myndum}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Deilir myndskeiði}one{Deilir # myndskeiði}other{Deilir # myndskeiðum}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Deilir # atriði}one{Deilir # atriði}other{Deilir # atriðum}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Deilir mynd með texta"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Deilir mynd með tengli"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Deilir # skrá}one{Deilir # skrá}other{Deilir # skrám}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Deilir mynd með texta}one{Deilir # mynd með texta}other{Deilir # myndum með texta}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Deilir mynd með tengli}one{Deilir # mynd með tengli}other{Deilir # myndum með tengli}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Deilir myndskeiði með texta}one{Deilir # myndskeiði með texta}other{Deilir # myndskeiðum með texta}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Deilir myndskeiði með tengli}one{Deilir # myndskeiði með tengli}other{Deilir # myndskeiðum með tengli}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Deilir skrá með texta}one{Deilir # skrá með texta}other{Deilir # skrám með texta}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Deilir skrá með tengli}one{Deilir # skrá með tengli}other{Deilir # skrám með tengli}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Eingöngu mynd}one{Eingöngu myndir}other{Eingöngu myndir}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Eingöngu myndskeið}one{Eingöngu myndskeið}other{Eingöngu myndskeið}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Eingöngu skrá}one{Eingöngu skrár}other{Eingöngu skrár}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Forskoðunarsmámynd myndar"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Forskoðunarsmámynd myndskeiðs"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Forskoðunarsmámynd skráar"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Engar tillögur um fólk til að deila með"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Forritalisti"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Þetta forrit hefur ekki fengið heimild fyrir upptöku en gæti tekið upp hljóð í gegnum þetta USB-tæki."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Persónulegt"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Vinna"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Ekki er hægt að opna þetta efni með vinnuforritum"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Ekki er hægt að deila þessu efni með forritum til einkanota"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Ekki er hægt að opna þetta efni með forritum til einkanota"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Hlé gert á vinnusniði"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Ýttu til að kveikja"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Hlé gert á vinnuforritum"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Ljúka hléi"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Engin vinnuforrit"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Engin forrit til einkanota"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Opna <xliff:g id="APP">%s</xliff:g> í þínu eigin sniði?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Hafa texta með"</string>
<string name="exclude_link" msgid="1332778255031992228">"Útiloka tengil"</string>
<string name="include_link" msgid="827855767220339802">"Hafa tengil með"</string>
+ <string name="pinned" msgid="7623664001331394139">"Fest"</string>
</resources>
diff --git a/java/res/values-it/strings.xml b/java/res/values-it/strings.xml
index dc23c62..38aba0c 100644
--- a/java/res/values-it/strings.xml
+++ b/java/res/values-it/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Fissa <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Sblocca <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Modifica"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # file}many{{file_name} + # file}other{{file_name} + # file}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # file}many{+ # file}other{+ # file}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"Condivisione del testo…"</string>
- <string name="sharing_link" msgid="2307694372813942916">"Condivisione del link…"</string>
- <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Condivisione immagine…}many{Condivis. di # immagini…}other{Condivis. di # immagini…}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # altro file}many{+ altri # file}other{+ altri # file}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"Condivisione del testo"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"Condivisione del link"</string>
+ <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Condivisione dell\'immagine}many{Condivisione di # immagini}other{Condivisione di # immagini}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Condivisione del video…}many{Condivisione di # video…}other{Condivisione di # video…}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Condivis. di # elemento…}many{Condivis. di # elementi…}other{Condivis. di # elementi…}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Condivis. img con testo…"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Condivis. img con link…"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Condivisione di # file in corso…}many{Condivisione di # file in corso…}other{Condivisione di # file in corso…}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Condivisione immagine con testo in corso…}many{Condivisione # immagini con testo in corso…}other{Condivisione # immagini con testo in corso…}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Condivisione immagine con link}many{Condivisione # immagini con link}other{Condivisione # immagini con link}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Condivisione video con messaggio in corso…}many{Condivisione # video con messaggio in corso…}other{Condivisione # video con messaggio in corso…}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Condivisione video con link in corso…}many{Condivisione # video con link in corso…}other{Condivisione # video con link in corso…}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Condivisione file con messaggio in corso…}many{Condivisione # file con messaggio in corso…}other{Condivisione # file con messaggio in corso…}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Condivisione file con link in corso…}many{Condivisione # file con link in corso…}other{Condivisione # file con link in corso…}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Soltanto l\'immagine}many{Soltanto le immagini}other{Soltanto le immagini}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Soltanto il video}many{Soltanto i video}other{Soltanto i video}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Soltanto il file}many{Soltanto i file}other{Soltanto i file}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura di anteprima dell\'immagine"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura di anteprima del video"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura di anteprima del file"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nessuna persona consigliata per la condivisione"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Elenco di app"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"A questa app non è stata concessa l\'autorizzazione di registrazione, ma l\'app potrebbe acquisire l\'audio tramite questo dispositivo USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Personale"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Lavoro"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Questi contenuti non possono essere aperti con app di lavoro"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Questi contenuti non possono essere condivisi con app personali"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Questi contenuti non possono essere aperti con app personali"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Profilo di lavoro in pausa"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tocca per attivare"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Le app di lavoro sono in pausa"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Riattiva"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nessuna app di lavoro"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nessuna app personale"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Aprire <xliff:g id="APP">%s</xliff:g> nel tuo profilo personale?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Includi testo"</string>
<string name="exclude_link" msgid="1332778255031992228">"Escludi link"</string>
<string name="include_link" msgid="827855767220339802">"Includi link"</string>
+ <string name="pinned" msgid="7623664001331394139">"Elemento fissato"</string>
</resources>
diff --git a/java/res/values-iw/strings.xml b/java/res/values-iw/strings.xml
index 62ff1d8..c79425d 100644
--- a/java/res/values-iw/strings.xml
+++ b/java/res/values-iw/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"הצמדה של <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"ביטול ההצמדה של <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"עריכה"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} ועוד קובץ אחד}one{{file_name} ועוד # קבצים}two{{file_name} ועוד # קבצים}other{{file_name} ועוד # קבצים}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ קובץ אחד}one{+ # קבצים}two{+ # קבצים}other{+ # קבצים}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"שיתוף הטקסט מתבצע"</string>
- <string name="sharing_link" msgid="2307694372813942916">"שיתוף הקישור מתבצע"</string>
- <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{מתבצע שיתוף של תמונה}one{מתבצע שיתוף של # תמונות}two{מתבצע שיתוף של # תמונות}other{מתבצע שיתוף של # תמונות}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{קובץ אחד נוסף}one{# קבצים נוספים}two{# קבצים נוספים}other{# קבצים נוספים}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"שיתוף טקסט"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"שיתוף קישור"</string>
+ <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{שיתוף של תמונה}one{שיתוף של # תמונות}two{שיתוף של # תמונות}other{שיתוף של # תמונות}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{מתבצע שיתוף של סרטון}one{מתבצע שיתוף של # סרטונים}two{מתבצע שיתוף של # סרטונים}other{מתבצע שיתוף של # סרטונים}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{מתבצע שיתוף של פריט אחד (#)}one{מתבצע שיתוף של # פריטים}two{מתבצע שיתוף של # פריטים}other{מתבצע שיתוף של # פריטים}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"שיתוף תמונה עם טקסט"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"שיתוף תמונה עם קישור"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{מתבצע שיתוף של קובץ אחד}one{מתבצע שיתוף של # קבצים}two{מתבצע שיתוף של # קבצים}other{מתבצע שיתוף של # קבצים}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{שיתוף תמונה עם טקסט}one{שיתוף # תמונות עם טקסט}two{שיתוף # תמונות עם טקסט}other{שיתוף # תמונות עם טקסט}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{שיתוף תמונה עם קישור}one{שיתוף # תמונות עם קישור}two{שיתוף # תמונות עם קישור}other{שיתוף # תמונות עם קישור}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{שיתוף סרטון עם טקסט}one{שיתוף # סרטונים עם טקסט}two{שיתוף # סרטונים עם טקסט}other{שיתוף # סרטונים עם טקסט}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{שיתוף סרטון עם קישור}one{שיתוף # סרטונים עם קישור}two{שיתוף # סרטונים עם קישור}other{שיתוף # סרטונים עם קישור}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{שיתוף קובץ עם טקסט}one{שיתוף # קבצים עם טקסט}two{שיתוף # קבצים עם טקסט}other{שיתוף # קבצים עם טקסט}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{שיתוף תמונה עם קישור}one{שיתוף # תמונות עם קישור}two{שיתוף # תמונות עם קישור}other{שיתוף # תמונות עם קישור}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{תמונה בלבד}one{תמונות בלבד}two{תמונות בלבד}other{תמונות בלבד}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{סרטון בלבד}one{סרטונים בלבד}two{סרטונים בלבד}other{סרטונים בלבד}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{קובץ בלבד}one{קבצים בלבד}two{קבצים בלבד}other{קבצים בלבד}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"תמונה ממוזערת של תצוגה מקדימה של תמונה"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"תמונה ממוזערת של תצוגה מקדימה של סרטון"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"תמונה ממוזערת של תצוגה מקדימה של קובץ"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"אין אנשים שניתן לשתף איתם"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"רשימת האפליקציות"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"‏לאפליקציה זו לא ניתנה הרשאת הקלטה, אבל אפשר להקליט אודיו באמצעות התקן ה-USB הזה."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"אישי"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"עבודה"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"אי אפשר לפתוח את התוכן הזה באמצעות אפליקציות לעבודה"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"אי אפשר לשתף את התוכן הזה עם אפליקציות לשימוש אישי"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"אי אפשר לפתוח את התוכן הזה באמצעות אפליקציות לשימוש אישי"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"פרופיל העבודה מושהה"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"יש להקיש כדי להפעיל את פרופיל העבודה"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"האפליקציות לעבודה מושהות"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"ביטול ההשהיה"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"אין אפליקציות לעבודה"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"אין אפליקציות לשימוש אישי"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"לפתוח את <xliff:g id="APP">%s</xliff:g> בפרופיל האישי?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"הכללת הטקסט"</string>
<string name="exclude_link" msgid="1332778255031992228">"החרגת הקישור"</string>
<string name="include_link" msgid="827855767220339802">"הכללת הקישור"</string>
+ <string name="pinned" msgid="7623664001331394139">"מוצמד"</string>
</resources>
diff --git a/java/res/values-ja/strings.xml b/java/res/values-ja/strings.xml
index 0e0751d..15c2277 100644
--- a/java/res/values-ja/strings.xml
+++ b/java/res/values-ja/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> を固定"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> の固定を解除"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"編集"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name}、他 # ファイル}other{{file_name}、他 # ファイル}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{他 # 件のファイル}other{他 # 件のファイル}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"テキストを共有しています"</string>
- <string name="sharing_link" msgid="2307694372813942916">"リンクを共有しています"</string>
- <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{画像を共有中}other{# 枚の画像を共有中}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{その他 # ファイル}other{その他 # ファイル}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"テキストを共有中"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"リンクを共有中"</string>
+ <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{画像を共有しています}other{# 枚の画像を共有しています}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{動画を共有中}other{# 個の動画を共有中}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# 個のアイテムを共有中}other{# 個のアイテムを共有中}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"テキスト付き画像を共有中"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"リンク付き画像を共有中"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# 個のファイルを共有中}other{# 個のファイルを共有中}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{テキスト付き画像を共有しています}other{テキスト付き画像を # 件共有しています}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{リンク付き画像を共有しています}other{リンク付き画像を # 件共有しています}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{テキスト付き動画を共有中}other{テキスト付き動画を # 件共有中}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{リンク付き動画を共有中}other{リンク付き動画を # 件共有中}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{テキスト付きファイルを共有中}other{テキスト付きファイルを # 件共有中}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{リンク付きファイルを共有中}other{リンク付きファイルを # 件共有中}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{画像のみ}other{画像のみ}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{動画のみ}other{動画のみ}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ファイルのみ}other{ファイルのみ}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"画像のプレビュー サムネイル"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"動画のプレビュー サムネイル"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"ファイルのプレビュー サムネイル"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"おすすめの共有相手はいません"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"アプリのリスト"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"このアプリに録音権限は付与されていませんが、この USB デバイスから音声を収集できるようになります。"</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"個人用"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"仕事用"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"このコンテンツを仕事用アプリで開くことはできません"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"このコンテンツを個人用アプリと共有することはできません"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"このコンテンツを個人用アプリで開くことはできません"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"仕事用プロファイルが一時停止しています"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"タップして ON にする"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"仕事用アプリ一時停止中"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"一時停止を解除"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"仕事用アプリはありません"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"個人用アプリはありません"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"個人用プロファイルで <xliff:g id="APP">%s</xliff:g> を開きますか?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"テキストを含める"</string>
<string name="exclude_link" msgid="1332778255031992228">"リンクを除外"</string>
<string name="include_link" msgid="827855767220339802">"リンクを含める"</string>
+ <string name="pinned" msgid="7623664001331394139">"固定されています"</string>
</resources>
diff --git a/java/res/values-ka/strings.xml b/java/res/values-ka/strings.xml
index 5c6e046..88bc15a 100644
--- a/java/res/values-ka/strings.xml
+++ b/java/res/values-ka/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g>-ის ჩამაგრება"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g>-ის ჩამაგრების მოხსნა"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"რედაქტირება"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ფაილი}other{{file_name} + # ფაილი}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ფაილი}other{+ # ფაილი}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{კიდევ # ფაილი}other{კიდევ # ფაილი}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"ზიარდება ტექსტი"</string>
<string name="sharing_link" msgid="2307694372813942916">"ზიარდება ბმული"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{ზიარდება სურათი}other{ზიარდება # სურათი}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{ზიარდება ვიდეო}other{ზიარდება # ვიდეო}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{ზიარდება # ერთეული}other{ზიარდება # ერთეული}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"სურათი ზიარდება ტექსტით"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"სურათი ზიარდება ბმულით"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{ზიარდება # ფაილი}other{ზიარდება # ფაილი}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{სურათი ზიარდება ტექსტით}other{# სურათი ზიარდება ტექსტით}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{სურათი ზიარდება ბმულით}other{# სურათი ზიარდება ბმულით}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ვიდეო ზიარდება ტექსტით}other{# ვიდეო ზიარდება ტექსტით}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{ვიდეო ზიარდება ბმულით}other{# ვიდეო ზიარდება ბმულით}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ფაილი ზიარდება ტექსტით}other{# ფაილი ზიარდება ტექსტით}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{ფაილი ზიარდება ბმულით}other{# ფაილი ზიარდება ბმულით}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{მხოლოდ სურათი}other{მხოლოდ სურათები}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{მხოლოდ ვიდეო}other{მხოლოდ ვიდეოები}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{მხოლოდ ფაილი}other{მხოლოდ ფაილები}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"სურათის წინასწარი ვერსიის მინიატურა"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"ვიდეოს წინასწარი ვერსიის მინიატურა"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"ფაილის წინასწარი ვერსიის მინიატურა"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ვერ იძებნება რეკომენდებული ადამიანები, რომლებთანაც გაზიარება შეიძლება"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"აპების სია"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ამ აპს არ აქვს მინიჭებული ჩაწერის ნებართვა, მაგრამ შეუძლია ჩაიწეროს აუდიო ამ USB მოწყობილობის მეშვეობით."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"პირადი"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"სამსახური"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ამ კონტენტის სამსახურის აპებით გახსნა შეუძლებელია"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ამ კონტენტის პირადი აპებისთვის გაზიარება შეუძლებელია"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ამ კონტენტის პირადი აპებით გახსნა შეუძლებელია"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"სამსახურის პროფილი დაპაუზებულია"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"შეეხეთ ჩასართავად"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"სამსახურის აპები დაპაუზებულია"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"პაუზის გაუქმება"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"სამსახურის აპები არ არის"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"პირადი აპები არ არის"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"გსურთ <xliff:g id="APP">%s</xliff:g>-ის გახსნა თქვენს პირად პროფილში?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"ტექსტის ჩასმა"</string>
<string name="exclude_link" msgid="1332778255031992228">"ბმულის ამოღება"</string>
<string name="include_link" msgid="827855767220339802">"ბმულის დართვა"</string>
+ <string name="pinned" msgid="7623664001331394139">"ჩამაგრებული"</string>
</resources>
diff --git a/java/res/values-kk/strings.xml b/java/res/values-kk/strings.xml
index 94ff258..7b19579 100644
--- a/java/res/values-kk/strings.xml
+++ b/java/res/values-kk/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> бекіту"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> босату"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Өзгерту"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # файл}other{{file_name} + # файл}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # файл}other{+ # файл}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"Мәтін бөлісіліп жатыр"</string>
- <string name="sharing_link" msgid="2307694372813942916">"Сілтеме бөлісіліп жатыр"</string>
- <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Сурет бөлісіліп жатыр}other{# сурет бөлісіліп жатыр}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Тағы # файл}other{Тағы # файл}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"Мәтінді бөлісіп жатыр"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"Сілтемені бөлісіп жатыр"</string>
+ <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Сурет бөлісіп жатырсыз}other{# сурет бөлісіп жатырсыз}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Бейне бөлісіліп жатыр}other{# бейне бөлісіліп жатыр}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# элемент бөлісіліп жатыр}other{# элемент бөлісіліп жатыр}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Сурет мәтінімен бөлісіліп жатыр"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Сурет сілтемесімен бөлісіліп жатыр"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# файлды бөлісіп жатыр}other{# файлды бөлісіп жатыр}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Мәтіні бар сурет жіберу}other{Мәтіні бар # сурет жіберу}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Сілтемесі бар сурет жіберу}other{Сілтемесі бар # сурет жіберу}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Мәтіні бар бейне жіберу}other{Мәтіні бар # бейне жіберу}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Сілтемесі бар бейне жіберу}other{Сілтемесі бар # бейне жіберу}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Мәтіні бар файл жіберу}other{Мәтіні бар # файл жіберу}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Сілтемесі бар файл жіберу}other{Сілтемесі бар # файл жіберу}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Тек сурет}other{Тек суреттер}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Тек бейне}other{Тек бейнелер}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Тек файл}other{Тек файлдар}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Алдын ала көрсетілген суреттің нобайы"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Алдын ала көрсетілген бейненің нобайы"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Алдын ала көрсетілген файлдың нобайы"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Бөлісу үшін ұсынылатын адамдар жоқ."</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Қолданбалар тізімі"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Қолданбаға жазу рұқсаты берілмеді, бірақ ол осы USB құрылғысы арқылы дыбыс жаза алады."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Жеке"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Жұмыс"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Бұл контентті жұмыс қолданбаларымен ашу мүмкін емес."</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Бұл контентті жеке қолданбалармен бөлісу мүмкін емес."</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Бұл контентті жеке қолданбалармен ашу мүмкін емес."</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Жұмыс профилі кідіртілді."</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Қосу үшін түртіңіз"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Жұмыс қолданбалары кідіртілген."</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Қайта қосу"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Жұмыс қолданбалары жоқ."</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Жеке қолданбалар жоқ."</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> қолданбасын жеке профиліңізде ашу керек пе?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Мәтін қосу"</string>
<string name="exclude_link" msgid="1332778255031992228">"Сілтемені шығару"</string>
<string name="include_link" msgid="827855767220339802">"Сілтеме қосу"</string>
+ <string name="pinned" msgid="7623664001331394139">"Бекітілген"</string>
</resources>
diff --git a/java/res/values-km/strings.xml b/java/res/values-km/strings.xml
index 9d069d8..ae956af 100644
--- a/java/res/values-km/strings.xml
+++ b/java/res/values-km/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"ខ្ទាស់ <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"ដកខ្ទាស់ <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"កែ"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + ឯកសារ #}other{{file_name} + ឯកសារ #}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ឯកសារ}other{+ # ឯកសារ}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{ឯកសារ + # ទៀត}other{ឯកសារ + # ទៀត}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"កំពុងចែករំលែក​សារជាអក្សរ"</string>
<string name="sharing_link" msgid="2307694372813942916">"កំពុងចែករំលែកតំណ"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{កំពុងចែក​រំលែករូបភាព}other{កំពុងចែក​រំលែករូបភាព #}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{កំពុងចែករំលែកវីដេអូ}other{កំពុងចែករំលែកវីដេអូ #}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{កំពុងចែករំលែកធាតុ #}other{កំពុងចែករំលែកធាតុ #}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"ចែករំលែករូបភាពជាមួយអក្សរ"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"ចែករំលែករូបភាពជាមួយតំណ"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{កំពុង​ចែករំលែកឯកសារ #}other{កំពុង​ចែករំលែកឯកសារ #}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{ចែករំលែករូបភាពជាមួយអក្សរ}other{ចែករំលែករូបភាព # ជាមួយអក្សរ}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{ចែករំលែករូបភាពជាមួយតំណ}other{ចែករំលែករូបភាព # ជាមួយតំណ}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ចែករំលែកវីដេអូជាមួយអក្សរ}other{ចែករំលែក # វីដេអូជាមួយអក្សរ}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{ចែករំលែកវីដេអូជាមួយតំណ}other{ចែករំលែក # វីដេអូជាមួយតំណ}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ចែករំលែកឯកសារជាមួយអក្សរ}other{ចែករំលែក # ឯកសារជាមួយអក្សរ}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{ចែករំលែកឯកសារជាមួយតំណ}other{ចែករំលែកឯកសារ # ជាមួយតំណ}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{រូបភាព​តែប៉ុណ្ណោះ}other{រូបភាពតែប៉ុណ្ណោះ}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{​វីដេអូតែប៉ុណ្ណោះ}other{វីដេអូតែប៉ុណ្ណោះ}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ឯកសារតែប៉ុណ្ណោះ}other{ឯកសារតែប៉ុណ្ណោះ}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"រូបក្របតំណាងការមើលរូបភាពសាកល្បង"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"រូបក្របតំណាងការមើលវីដេអូសាកល្បង"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"រូបក្របតំណាងការមើលឯកសារសាកល្បង"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"មិនមាន​មនុស្សដែល​បានណែនាំ​សម្រាប់​ចែករំលែក​ជាមួយទេ"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"បញ្ជីកម្មវិធី"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"កម្មវិធីនេះ​មិនទាន់បាន​ទទួលសិទ្ធិ​ថតសំឡេង​នៅឡើយទេ ប៉ុន្តែអាច​ថតសំឡេង​តាមរយៈ​ឧបករណ៍ USB នេះបាន។"</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"ផ្ទាល់ខ្លួន"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"ការងារ"</string>
@@ -72,10 +81,10 @@
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"បានទប់ស្កាត់ដោយ​អ្នកគ្រប់គ្រង​ផ្នែកព័ត៌មានវិទ្យា​របស់អ្នក"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"ខ្លឹមសារនេះ​មិនអាចចែករំលែក​តាមរយៈ​កម្មវិធី​ការងារ​បានទេ"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ខ្លឹមសារនេះ​មិនអាចបើក​តាមរយៈ​កម្មវិធី​ការងារ​បានទេ"</string>
- <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ខ្លឹមសារនេះ​មិនអាចចែករំលែក​តាមរយៈ​កម្មវិធី​ផ្ទាល់ខ្លួន​បានទេ"</string>
+ <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"មិនអាចចែករំលែកខ្លឹមសារនេះ​ជាមួយ​កម្មវិធី​ផ្ទាល់ខ្លួន​បានទេ"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ខ្លឹមសារនេះ​មិនអាចបើក​តាមរយៈ​កម្មវិធី​ផ្ទាល់ខ្លួន​បានទេ"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"កម្រងព័ត៌មានការងារត្រូវបាន​ផ្អាក"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"ចុច​ដើម្បី​បើក"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"កម្មវិធី​ការងារ​ត្រូវបានផ្អាក"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"ឈប់ផ្អាក"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"គ្មាន​កម្មវិធី​ការងារ​ទេ"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"គ្មាន​កម្មវិធី​ផ្ទាល់ខ្លួន​ទេ"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"បើក <xliff:g id="APP">%s</xliff:g> នៅក្នុងកម្រង​ព័ត៌មាន​ផ្ទាល់​ខ្លួនរបស់អ្នកឬ?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"រួមបញ្ចូលអក្សរ"</string>
<string name="exclude_link" msgid="1332778255031992228">"មិនរួមបញ្ចូលតំណ"</string>
<string name="include_link" msgid="827855767220339802">"រួមបញ្ចូល​តំណ"</string>
+ <string name="pinned" msgid="7623664001331394139">"បាន​ខ្ទាស់"</string>
</resources>
diff --git a/java/res/values-kn/strings.xml b/java/res/values-kn/strings.xml
index 2e6c0fa..505277c 100644
--- a/java/res/values-kn/strings.xml
+++ b/java/res/values-kn/strings.xml
@@ -36,7 +36,7 @@
<string name="whichSendToApplication" msgid="2724450540348806267">"ಇದನ್ನು ಬಳಸಿಕೊಂಡು ಕಳುಹಿಸಿ"</string>
<string name="whichSendToApplicationNamed" msgid="1996548940365954543">"<xliff:g id="APP">%1$s</xliff:g> ಬಳಸಿ ಕಳುಹಿಸಿ"</string>
<string name="whichSendToApplicationLabel" msgid="6909037198280591110">"ಕಳುಹಿಸು"</string>
- <string name="whichHomeApplication" msgid="8797832422254564739">"ಮುಖಪುಟ‌ ಅಪ್ಲಿಕೇಶನ್‌ ಆಯ್ಕೆಮಾಡಿ"</string>
+ <string name="whichHomeApplication" msgid="8797832422254564739">"Home ಆ್ಯಪ್ ಆಯ್ಕೆಮಾಡಿ"</string>
<string name="whichHomeApplicationNamed" msgid="3943122502791761387">"<xliff:g id="APP">%1$s</xliff:g> ಅನ್ನು ಹೋಮ್ ಆಗಿ ಬಳಸಿ"</string>
<string name="whichHomeApplicationLabel" msgid="2066319585322981524">"ಚಿತ್ರ ಕ್ಯಾಪ್ಚರ್ ಮಾಡಿ"</string>
<string name="whichImageCaptureApplication" msgid="7830965894804399333">"ಇದರ ಜೊತೆಗೆ ಚಿತ್ರ ಕ್ಯಾಪ್ಚರ್ ಮಾಡಿ"</string>
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> ಪಿನ್ ಮಾಡಿ"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> ಅನ್ನು ಅನ್‌ಪಿನ್ ಮಾಡಿ"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"ಎಡಿಟ್"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ಫೈಲ್}one{{file_name} + # ಫೈಲ್‌ಗಳು}other{{file_name} + # ಫೈಲ್‌ಗಳು}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ಫೈಲ್‌}one{+ # ಫೈಲ್‌ಗಳು}other{+ # ಫೈಲ್‌ಗಳು}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # ಇನ್ನಷ್ಟು ಫೈಲ್}one{+ # ಇನ್ನಷ್ಟು ಫೈಲ್‌ಗಳು}other{+ # ಇನ್ನಷ್ಟು ಫೈಲ್‌ಗಳು}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"ಪಠ್ಯ ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ"</string>
<string name="sharing_link" msgid="2307694372813942916">"ಲಿಂಕ್ ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{ಚಿತ್ರವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{# ಚಿತ್ರಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{# ಚಿತ್ರಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{ವೀಡಿಯೊವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{# ವೀಡಿಯೊಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{# ವೀಡಿಯೊಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# ಐಟಂ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{# ಐಟಂಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{# ಐಟಂಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"ಪಠ್ಯದೊಂದಿಗೆ ಚಿತ್ರವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"ಲಿಂಕ್‌ನೊಂದಿಗೆ ಚಿತ್ರವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ಫೈಲ್ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{# ಫೈಲ್‌ಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{# ಫೈಲ್‌ಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{ಪಠ್ಯದೊಂದಿಗೆ ಚಿತ್ರವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{ಪಠ್ಯದೊಂದಿಗೆ # ಚಿತ್ರಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{ಪಠ್ಯದೊಂದಿಗೆ # ಚಿತ್ರಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{ಲಿಂಕ್‌ನೊಂದಿಗೆ ಚಿತ್ರವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{ಲಿಂಕ್‌ನೊಂದಿಗೆ # ಚಿತ್ರಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{ಲಿಂಕ್‌ನೊಂದಿಗೆ # ಚಿತ್ರಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ಪಠ್ಯದೊಂದಿಗೆ ವೀಡಿಯೊವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{ಪಠ್ಯದೊಂದಿಗೆ # ವೀಡಿಯೊಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{ಪಠ್ಯದೊಂದಿಗೆ # ವೀಡಿಯೊಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{ಲಿಂಕ್‌ನೊಂದಿಗೆ ವೀಡಿಯೊವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{ಲಿಂಕ್‌ನೊಂದಿಗೆ # ವೀಡಿಯೊಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{ಲಿಂಕ್‌ನೊಂದಿಗೆ # ವೀಡಿಯೊಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ಪಠ್ಯದೊಂದಿಗೆ ಫೈಲ್ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{ಪಠ್ಯದೊಂದಿಗೆ # ಫೈಲ್‌ಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{ಪಠ್ಯದೊಂದಿಗೆ # ಫೈಲ್‌ಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{ಲಿಂಕ್‌ನೊಂದಿಗೆ ಫೈಲ್ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}one{ಲಿಂಕ್‌ನೊಂದಿಗೆ # ಫೈಲ್‌ಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}other{ಲಿಂಕ್‌ನೊಂದಿಗೆ # ಫೈಲ್‌ಗಳನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{ಚಿತ್ರ ಮಾತ್ರ}one{ಚಿತ್ರಗಳು ಮಾತ್ರ}other{ಚಿತ್ರಗಳು ಮಾತ್ರ}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{ವೀಡಿಯೊ ಮಾತ್ರ}one{ವೀಡಿಯೊಗಳು ಮಾತ್ರ}other{ವೀಡಿಯೊಗಳು ಮಾತ್ರ}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ಫೈಲ್ ಮಾತ್ರ}one{ಫೈಲ್‌ಗಳು ಮಾತ್ರ}other{ಫೈಲ್‌ಗಳು ಮಾತ್ರ}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"ಚಿತ್ರ ಪೂರ್ವವೀಕ್ಷಣೆಯ ಥಂಬ್‌ನೇಲ್"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"ವೀಡಿಯೊ ಪೂರ್ವವೀಕ್ಷಣೆಯ ಥಂಬ್‌ನೇಲ್"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"ಫೈಲ್ ಪೂರ್ವವೀಕ್ಷಣೆಯ ಥಂಬ್‌ನೇಲ್"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ಹಂಚಿಕೊಳ್ಳಲು ಶಿಫಾರಸು ಮಾಡಲಾದವರು ಯಾರೂ ಇಲ್ಲ"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ಆ್ಯಪ್‌ಗಳ ಪಟ್ಟಿ"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ಈ ಆ್ಯಪ್‌ಗೆ ರೆಕಾರ್ಡ್ ಅನುಮತಿಯನ್ನು ನೀಡಲಾಗಿಲ್ಲ, ಆದರೆ ಈ USB ಸಾಧನದ ಮೂಲಕ ಆಡಿಯೊವನ್ನು ಸೆರೆಹಿಡಿಯಬಲ್ಲದು."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"ವೈಯಕ್ತಿಕ"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"ಕೆಲಸ"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ಕೆಲಸಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಆ್ಯಪ್‌ಗಳ ಈ ವಿಷಯವನ್ನು ತೆರೆಯಲಾಗುವುದಿಲ್ಲ"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ವೈಯಕ್ತಿಕ ಆ್ಯಪ್‌ಗಳ ಮೂಲಕ ಈ ವಿಷಯವನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುವುದಿಲ್ಲ"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ವೈಯಕ್ತಿಕ ಆ್ಯಪ್‌ಗಳ ಮೂಲಕ ಈ ವಿಷಯವನ್ನು ತೆರೆಯಲಾಗುವುದಿಲ್ಲ"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"ಕೆಲಸಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಪ್ರೊಫೈಲ್ ಅನ್ನು ವಿರಾಮಗೊಳಿಸಲಾಗಿದೆ"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"ಆನ್‌‌‌ ಮಾಡಲು ಟ್ಯಾಪ್‌ ಮಾಡಿ"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"ಕೆಲಸಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಆ್ಯಪ್‌ಗಳನ್ನು ವಿರಾಮಗೊಳಿಸಲಾಗಿದೆ"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"ವಿರಾಮವನ್ನು ರದ್ದುಗೊಳಿಸಿ"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"ಯಾವುದೇ ಕೆಲಸಕ್ಕೆ ಸಂಬಂಧಿಸಿದ ಆ್ಯಪ್‌ಗಳಿಲ್ಲ"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"ಯಾವುದೇ ವೈಯಕ್ತಿಕ ಆ್ಯಪ್‌ಗಳಿಲ್ಲ"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"ನಿಮ್ಮ ವೈಯಕ್ತಿಕ ಪ್ರೊಫೈಲ್‌ನಲ್ಲಿ <xliff:g id="APP">%s</xliff:g> ಅನ್ನು ತೆರೆಯಬೇಕೆ?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"ಪಠ್ಯವನ್ನು ಸೇರಿಸಿ"</string>
<string name="exclude_link" msgid="1332778255031992228">"ಲಿಂಕ್ ಹೊರತುಪಡಿಸಿ"</string>
<string name="include_link" msgid="827855767220339802">"ಲಿಂಕ್ ಸೇರಿಸಿ"</string>
+ <string name="pinned" msgid="7623664001331394139">"ಪಿನ್‌ ಮಾಡಲಾಗಿದೆ"</string>
</resources>
diff --git a/java/res/values-ko/strings.xml b/java/res/values-ko/strings.xml
index 4df2adf..e9e908b 100644
--- a/java/res/values-ko/strings.xml
+++ b/java/res/values-ko/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> 고정"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> 고정 해제"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"수정"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + 파일 #개}other{{file_name} + 파일 #개}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{외 파일 #개}other{외 파일 #개}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"텍스트 공유 중"</string>
- <string name="sharing_link" msgid="2307694372813942916">"링크 공유 중"</string>
- <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{이미지 공유 중}other{이미지 #개 공유 중}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{추가 파일 #개}other{추가 파일 #개}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"텍스트 공유"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"링크 공유"</string>
+ <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{이미지 공유}other{이미지 #개 공유}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{동영상 1개 공유 중}other{동영상 #개 공유 중}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{항목 1개 공유 중}other{항목 #개 공유 중}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"텍스트로 이미지 공유 중"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"링크로 이미지 공유 중"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{파일 #개 공유 중}other{파일 #개 공유 중}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{텍스트로 이미지 공유 중}other{텍스트로 이미지 #개 공유 중}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{링크로 이미지 공유 중}other{링크로 이미지 #개 공유 중}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{텍스트로 동영상 공유 중}other{텍스트로 동영상 #개 공유 중}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{링크로 동영상 공유 중}other{링크로 동영상 #개 공유 중}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{텍스트로 파일 공유 중}other{텍스트로 파일 #개 공유 중}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{링크로 파일 공유 중}other{링크로 파일 #개 공유 중}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{이미지만}other{이미지만}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{동영상만}other{동영상만}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{파일만}other{파일만}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"이미지 미리보기 썸네일"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"동영상 미리보기 썸네일"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"파일 미리보기 썸네일"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"공유할 추천 사용자가 없음"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"앱 목록"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"이 앱에는 녹음 권한이 부여되지 않았지만, 이 USB 기기를 통해 오디오를 녹음할 수 있습니다."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"개인"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"직장"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"이 콘텐츠는 직장 앱으로 열 수 없습니다."</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"이 콘텐츠는 개인 앱을 통해 공유할 수 없습니다."</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"이 콘텐츠는 개인 앱으로 열 수 없습니다."</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"직장 프로필이 일시중지됨"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"탭하여 사용 설정"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"직장 앱이 일시중지됨"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"일시중지 해제"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"직장 앱 없음"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"개인 앱 없음"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"개인 프로필에서 <xliff:g id="APP">%s</xliff:g> 앱을 여시겠습니까?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"텍스트 포함"</string>
<string name="exclude_link" msgid="1332778255031992228">"링크 제외"</string>
<string name="include_link" msgid="827855767220339802">"링크 포함"</string>
+ <string name="pinned" msgid="7623664001331394139">"고정됨"</string>
</resources>
diff --git a/java/res/values-ky/strings.xml b/java/res/values-ky/strings.xml
index c438a92..311a216 100644
--- a/java/res/values-ky/strings.xml
+++ b/java/res/values-ky/strings.xml
@@ -53,29 +53,38 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Кадап коюу: <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> бошотуу"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Түзөтүү"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # файл}other{{file_name} + # файл}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # файл}other{+ # файл}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ дагы # файл}other{+ дагы # файл}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Текст бөлүшүлүүдө"</string>
<string name="sharing_link" msgid="2307694372813942916">"Шилтеме бөлүшүлүүдө"</string>
- <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Сүрөт бөлүшүлүүдө}other{# сүрөт бөлүшүлүүдө}}"</string>
+ <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Сүрөт бөлүшүү}other{# сүрөт бөлүшүлүүдө}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Видео бөлүшүлүүдө}other{# видео бөлүшүлүүдө}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# нерсе бөлүшүлүүдө}other{# нерсе бөлүшүлүүдө}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Сүрөттү текст менен жөнөтүү"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Сүрөттү шилтеме менен жөнөтүү"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# файл бөлүшүлүүдө}other{# файл бөлүшүлүүдө}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Сүрөттү текст менен жөнөтүү}other{# cүрөттү текст менен жөнөтүү}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Сүрөттү шилтеме менен жөнөтүү}other{# сүрөттү шилтеме менен жөнөтүү}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Видеону текст менен жөнөтүү}other{# видеону текст менен жөнөтүү}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Видеону шилтеме менен жөнөтүү}other{# видеону шилтеме менен жөнөтүү}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Файлды текст менен жөнөтүү}other{# файлды текст менен жөнөтүү}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Файлды шилтеме менен жөнөтүү}other{# файлды шилтеме менен жөнөтүү}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Сүрөт гана}other{Сүрөттөр гана}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Видео гана}other{Видеолор гана}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Файл гана}other{Файлдар гана}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Сүрөттүн алдын ала эскизи"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Видеонун алдын ала эскизи"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Файлдын алдын ала эскизи"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Бөлүшкөнгө эч ким сунушталган жок"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Колдонмолордун тизмеси"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Бул колдонмого жаздырууга уруксат берилген эмес, бирок ушул USB түзмөгү аркылуу үндөрдү жаза алат."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Жеке"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Жумуш"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Жеке көрүнүш"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Жумуш көрүнүшү"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"IT администраторуңуз бөгөттөп койгон"</string>
- <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Бул мазмунду жумуш колдонмолору менен бөлүшүү мүмкүн эмес"</string>
- <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Бул мазмунду жумуш колдонмолору менен ачуу мүмкүн эмес"</string>
- <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Бул мазмунду жеке колдонмолор менен бөлүшүү мүмкүн эмес"</string>
- <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Бул мазмунду жеке колдонмолор менен ачуу мүмкүн эмес"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Жумуш профили тындырылган"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Күйгүзүү үчүн таптап коюңуз"</string>
+ <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Бул нерсени жумуш колдонмолору менен бөлүшө албайсыз"</string>
+ <string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Бул нерсени жумуш колдонмолору менен ача албайсыз"</string>
+ <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Бул нерсени жеке колдонмолор менен бөлүшө албайсыз"</string>
+ <string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Бул нерсени жеке колдонмолор менен ача албайсыз"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Жумуш колдонмолору тындырылды"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Иштетүү"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Жумуш колдонмолору жок"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Жеке колдонмолор жок"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> колдонмосу жеке профилде ачылсынбы?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Текст кошуу"</string>
<string name="exclude_link" msgid="1332778255031992228">"Шилтемени чыгарып салуу"</string>
<string name="include_link" msgid="827855767220339802">"Шилтеме кошуу"</string>
+ <string name="pinned" msgid="7623664001331394139">"Кадалган"</string>
</resources>
diff --git a/java/res/values-lo/strings.xml b/java/res/values-lo/strings.xml
index debe9c2..48e9a07 100644
--- a/java/res/values-lo/strings.xml
+++ b/java/res/values-lo/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"ປັກໝຸດ <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"ຖອດປັກມຸດ <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"ແກ້ໄຂ"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ໄຟລ໌}other{{file_name} + # ໄຟລ໌}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{ອີກ # ໄຟລ໌}other{ອີກ # ໄຟລ໌}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{ອີກ # ໄຟລ໌}other{ອີກ # ໄຟລ໌}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"ກຳລັງແບ່ງປັນຂໍ້ຄວາມ"</string>
<string name="sharing_link" msgid="2307694372813942916">"ກຳລັງແບ່ງປັນລິ້ງ"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{ກຳລັງແບ່ງປັນຮູບ}other{ກຳລັງແບ່ງປັນ # ຮູບ}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{ກຳລັງແບ່ງປັນວິດີໂອ}other{ກຳລັງແບ່ງປັນ # ວິດີໂອ}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{ກຳລັງແບ່ງປັນ # ລາຍການ}other{ກຳລັງແບ່ງປັນ # ລາຍການ}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"ກຳລັງແບ່ງປັນຮູບດ້ວຍຂໍ້ຄວາມ"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"ກຳລັງແບ່ງປັນຮູບດ້ວຍລິ້ງ"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{ກຳລັງຈະແບ່ງປັນ # ໄຟລ໌}other{ກຳລັງຈະແບ່ງປັນ # ໄຟລ໌}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{ກຳລັງແບ່ງປັນຮູບພ້ອມຂໍ້ຄວາມ}other{ກຳລັງແບ່ງປັນ # ຮູບພ້ອມຂໍ້ຄວາມ}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{ກຳລັງແບ່ງປັນຮູບພ້ອມລິ້ງ}other{ກຳລັງແບ່ງປັນ # ຮູບພ້ອມລິ້ງ}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ກຳລັງແບ່ງປັນວິດີໂອພ້ອມຂໍ້ຄວາມ}other{ກຳລັງແບ່ງປັນ # ວິດີໂອພ້ອມຂໍ້ຄວາມ}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{ກຳລັງແບ່ງປັນວິດີໂອພ້ອມລິ້ງ}other{ກຳລັງແບ່ງປັນ # ວິດີໂອພ້ອມລິ້ງ}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ກຳລັງແບ່ງປັນໄຟລ໌ພ້ອມຂໍ້ຄວາມ}other{ກຳລັງແບ່ງປັນ # ໄຟລ໌ພ້ອມຂໍ້ຄວາມ}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{ກຳລັງແບ່ງປັນໄຟລ໌ພ້ອມລິ້ງ}other{ກຳລັງແບ່ງປັນ # ໄຟລ໌ພ້ອມລິ້ງ}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{ຮູບເທົ່ານັ້ນ}other{ຮູບເທົ່ານັ້ນ}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{ວິດີໂອເທົ່ານັ້ນ}other{ວິດີໂອເທົ່ານັ້ນ}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ໄຟລ໌ເທົ່ານັ້ນ}other{ໄຟລ໌ເທົ່ານັ້ນ}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"ຮູບຕົວຢ່າງຂອງຮູບພາບ"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"ຮູບຕົວຢ່າງຂອງວິດີໂອ"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"ຮູບຕົວຢ່າງຂອງໄຟລ໌"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ບໍ່ມີຄົນທີ່ແນະນຳໃຫ້ແບ່ງປັນນຳ"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ລາຍຊື່ແອັບ"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ແອັບນີ້ບໍ່ໄດ້ຮັບສິດອະນຸຍາດໃນການບັນທຶກ ແຕ່ສາມາດບັນທຶກສຽງໄດ້ຜ່ານອຸປະກອນ USB ນີ້."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"ສ່ວນຕົວ"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"ວຽກ"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ເນື້ອຫານີ້ບໍ່ສາມາດຖືກເປີດໄດ້ດ້ວຍແອັບບ່ອນເຮັດວຽກ"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ເນື້ອຫານີ້ບໍ່ສາມາດຖືກແບ່ງປັນກັບແອັບສ່ວນຕົວໄດ້"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ເນື້ອຫານີ້ບໍ່ສາມາດຖືກເປີດໄດ້ດ້ວຍແອັບສ່ວນຕົວ"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"ຢຸດໂປຣໄຟລ໌ວຽກໄວ້ຊົ່ວຄາວແລ້ວ"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"ແຕະເພື່ອເປີດໃຊ້"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"ຢຸດແອັບບ່ອນເຮັດວຽກໄວ້ຊົ່ວຄາວແລ້ວ"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"ຍົກເລີກການຢຸດຊົ່ວຄາວ"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"ບໍ່ມີແອັບບ່ອນເຮັດວຽກ"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"ບໍ່ມີແອັບສ່ວນຕົວ"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"ເປີດ <xliff:g id="APP">%s</xliff:g> ໃນໂປຣໄຟລ໌ສ່ວນຕົວຂອງທ່ານບໍ?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"ຮວມຂໍ້ຄວາມ"</string>
<string name="exclude_link" msgid="1332778255031992228">"ບໍ່ຮວມລິ້ງ"</string>
<string name="include_link" msgid="827855767220339802">"ຮວມລິ້ງ"</string>
+ <string name="pinned" msgid="7623664001331394139">"ປັກໝຸດແລ້ວ"</string>
</resources>
diff --git a/java/res/values-lt/strings.xml b/java/res/values-lt/strings.xml
index 77ae0a4..51ffbbf 100644
--- a/java/res/values-lt/strings.xml
+++ b/java/res/values-lt/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Prisegti <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Atsegti <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Redaguoti"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{„{file_name}“ ir dar # failas}one{„{file_name}“ ir dar # failas}few{„{file_name}“ ir dar # failai}many{„{file_name}“ ir dar # failo}other{„{file_name}“ ir dar # failų}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{Dar # failas}one{Dar # failas}few{Dar # failai}many{Dar # failo}other{Dar # failų}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Dar # failas}one{Dar # failas}few{Dar # failai}many{Dar # failo}other{Dar # failų}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Bendrinamas tekstas"</string>
<string name="sharing_link" msgid="2307694372813942916">"Bendrinama nuoroda"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Bendrinamas vaizdas}one{Bendrinamas # vaizdas}few{Bendrinami # vaizdai}many{Bendrinama # vaizdo}other{Bendrinama # vaizdų}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Bendrinamas vaizdo įrašas}one{Bendrinamas # vaizdo įrašas}few{Bendrinami # vaizdo įrašai}many{Bendrinama # vaizdo įrašo}other{Bendrinama # vaizdo įrašų}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Bendrinamas # elementas}one{Bendrinamas # elementas}few{Bendrinami # elementai}many{Bendrinama # elemento}other{Bendrinama # elementų}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Bendrinamas vaizdas su tekstu"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Bendrinamas vaizdas su nuoroda"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Bendrinamas # failas}one{Bendrinamas # failas}few{Bendrinami # failai}many{Bendrinama # failo}other{Bendrinama # failų}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Bendrinamas vaizdas su tekstu}one{Bendrinamas # vaizdas su tekstu}few{Bendrinami # vaizdai su tekstu}many{Bendrinama # vaizdo su tekstu}other{Bendrinama # vaizdų su tekstu}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Bendrinamas vaizdas su nuoroda}one{Bendrinamas # vaizdas su nuoroda}few{Bendrinami # vaizdai su nuoroda}many{Bendrinama # vaizdo su nuoroda}other{Bendrinama # vaizdų su nuoroda}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Bendrinamas vaizdo įrašas su tekstu}one{Bendrinamas # vaizdo įrašas su tekstu}few{Bendrinami # vaizdo įrašai su tekstu}many{Bendrinama # vaizdo įrašo su tekstu}other{Bendrinama # vaizdo įrašų su tekstu}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Bendrinamas vaizdo įrašas su nuoroda}one{Bendrinamas # vaizdo įrašas su nuoroda}few{Bendrinami # vaizdo įrašai su nuoroda}many{Bendrinamas # vaizdo įrašo su nuoroda}other{Bendrinama # vaizdo įrašų su nuoroda}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Bendrinamas failas su tekstu}one{Bendrinamas # failas su tekstu}few{Bendrinami # failai su tekstu}many{Bendrinama # failo su tekstu}other{Bendrinama # failų su tekstu}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Bendrinamas failas su nuoroda}one{Bendrinamas # failas su nuoroda}few{Bendrinami # failai su nuoroda}many{Bendrinama # failo su nuoroda}other{Bendrinama # failų su nuoroda}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Tik vaizdas}one{Tik vaizdai}few{Tik vaizdai}many{Tik vaizdai}other{Tik vaizdai}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Tik vaizdo įrašas}one{Tik vaizdo įrašai}few{Tik vaizdo įrašai}many{Tik vaizdo įrašai}other{Tik vaizdo įrašai}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Tik failas}one{Tik failai}few{Tik failai}many{Tik failai}other{Tik failai}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Vaizdo peržiūros miniatiūra"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Vaizdo įrašo peržiūros miniatiūra"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Failo peržiūros miniatiūra"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nėra rekomenduojamų žmonių, su kuriais būtų galima bendrinti"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Programų sąrašas"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Šiai programai nebuvo suteiktas leidimas įrašyti, bet ji gali užfiksuoti garsą per šį USB įrenginį."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Asmeninis"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Darbo"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Šio turinio negalima atidaryti naudojant darbo programas"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Šio turinio negalima bendrinti su asmeninėmis programomis"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Šio turinio negalima atidaryti naudojant asmenines programas"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Darbo profilis pristabdytas"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Paliesti, norint įjungti"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Darbo programos pristabdytos"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Atšaukti pristabdymą"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nėra darbo programų"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nėra asmeninių programų"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Atidaryti „<xliff:g id="APP">%s</xliff:g>“ asmeniniame profilyje?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Įtraukti tekstą"</string>
<string name="exclude_link" msgid="1332778255031992228">"Išskirti nuorodą"</string>
<string name="include_link" msgid="827855767220339802">"Įtraukti nuorodą"</string>
+ <string name="pinned" msgid="7623664001331394139">"Prisegta"</string>
</resources>
diff --git a/java/res/values-lv/strings.xml b/java/res/values-lv/strings.xml
index 6fb7fee..de5c352 100644
--- a/java/res/values-lv/strings.xml
+++ b/java/res/values-lv/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Piespraust lietotni <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Atspraust lietotni <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Rediģēt"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} un vēl # fails}zero{{file_name} un vēl # failu}one{{file_name} un vēl # fails}other{{file_name} un vēl # faili}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{un vēl # fails}zero{un vēl # faili}one{un vēl # fails}other{un vēl # faili}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Un vēl # fails}zero{Un vēl # failu}one{Un vēl # fails}other{Un vēl # faili}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Tiek kopīgots teksts"</string>
<string name="sharing_link" msgid="2307694372813942916">"Tiek kopīgota saite"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Tiek kopīgots attēls}zero{Tiek kopīgoti # attēli}one{Tiek kopīgots # attēls}other{Tiek kopīgoti # attēli}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Tiek kopīgots video}zero{Tiek kopīgoti # video}one{Tiek kopīgots # video}other{Tiek kopīgoti # video}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Tiek kopīgots # vienums}zero{Tiek kopīgoti # vienumi}one{Tiek kopīgots # vienums}other{Tiek kopīgoti # vienumi}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Tiek kopīgots attēls ar tekstu"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Tiek kopīgots attēls ar saiti"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Notiek # faila kopīgošana}zero{Notiek # failu kopīgošana}one{Notiek # faila kopīgošana}other{Notiek # failu kopīgošana}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Tiek kopīgots attēls ar tekstu}zero{Tiek kopīgoti # attēli ar tekstu}one{Tiek kopīgots # attēls ar tekstu}other{Tiek kopīgoti # attēli ar tekstu}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Tiek kopīgots attēls ar saiti}zero{Tiek kopīgoti # attēli ar saitēm}one{Tiek kopīgots # attēls ar saitēm}other{Tiek kopīgoti # attēli ar saitēm}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Tiek kopīgots videoklips ar tekstu}zero{Tiek kopīgoti # videoklipi ar tekstu}one{Tiek kopīgots # videoklips ar tekstu}other{Tiek kopīgoti # videoklipi ar tekstu}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Tiek kopīgots videoklips ar saiti}zero{Tiek kopīgoti # videoklipi ar saitēm}one{Tiek kopīgots # videoklips ar saitēm}other{Tiek kopīgoti # videoklipi ar saitēm}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Tiek kopīgots fails ar tekstu}zero{Tiek kopīgoti # faili ar tekstu}one{Tiek kopīgots # fails ar tekstu}other{Tiek kopīgoti # faili ar tekstu}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Tiek kopīgots fails ar saiti}zero{Tiek kopīgoti # faili ar saitēm}one{Tiek kopīgots # fails ar saitēm}other{Tiek kopīgoti # faili ar saitēm}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Tikai attēls}zero{Tikai attēli}one{Tikai attēli}other{Tikai attēli}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Tikai videoklips}zero{Tikai videoklipi}one{Tikai videoklipi}other{Tikai videoklipi}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Tikai fails}zero{Tikai faili}one{Tikai faili}other{Tikai faili}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Attēla priekšskatījuma sīktēls"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Videoklipa priekšskatījuma sīktēls"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Faila priekšskatījuma sīktēls"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nav ieteikta neviena persona, ar ko kopīgot"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lietotņu saraksts"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Šai lietotnei nav piešķirta ierakstīšanas atļauja, taču tā varētu tvert audio, izmantojot šo USB ierīci."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Privātais profils"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Darba profils"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Šo saturu nevar atvērt darba lietotnēs"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Šo saturu nevar kopīgot ar personīgajām lietotnēm"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Šo saturu nevar atvērt personīgajās lietotnēs"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Darba profila darbība ir apturēta."</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Lai ieslēgtu, pieskarieties"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Darba lietotnes ir apturētas."</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Aktivizēt"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nav darba lietotņu"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nav personīgu lietotņu"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vai atvērt lietotni <xliff:g id="APP">%s</xliff:g> jūsu personīgajā profilā?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Iekļaut tekstu"</string>
<string name="exclude_link" msgid="1332778255031992228">"Izslēgt saiti"</string>
<string name="include_link" msgid="827855767220339802">"Iekļaut saiti"</string>
+ <string name="pinned" msgid="7623664001331394139">"Piespraustās"</string>
</resources>
diff --git a/java/res/values-mk/strings.xml b/java/res/values-mk/strings.xml
index 001772f..7ef3a9c 100644
--- a/java/res/values-mk/strings.xml
+++ b/java/res/values-mk/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Закачи <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Откачи <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Измени"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # датотека}one{{file_name} + # датотека}other{{file_name} + # датотеки}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # датотека}one{+ # датотека}other{+ # датотеки}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"Се споделува текст"</string>
- <string name="sharing_link" msgid="2307694372813942916">"Се споделува линк"</string>
- <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Се споделува слика}one{Се споделува # слика}other{Се споделуваат # слики}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{и уште # датотека}one{и уште # датотека}other{и уште # датотеки}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"Споделување текст"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"Споделување линк"</string>
+ <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Споделување слика}one{Споделување # слика}other{Споделување # слики}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Се споделува видео}one{Се споделува # видео}other{Се споделуваат # видеа}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Се споделува # ставка}one{Се споделува # ставка}other{Се споделуваат # ставки}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Се споделува слика со текст"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Се споделува слика со линк"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Се споделува # датотека}one{Се споделуваат # датотека}other{Се споделуваат # датотеки}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Се споделува слика со SMS}one{Се споделуваат # слика со SMS}other{Се споделуваат # слики со SMS}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Се споделува слика со линк}one{Се споделуваат # слика со линк}other{Се споделуваат # слики со линк}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Се споделува видео со SMS}one{Се споделуваат # видео со SMS}other{Се споделуваат # видеа со SMS}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Се споделува видео со линк}one{Се споделуваат # видео со линк}other{Се споделуваat # видеa со линк}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Се споделува датотека со SMS}one{Се споделуваат # датотека со SMS}other{Се споделуваат # датотеки со SMS}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Се споделува датотека со линк}one{Се споделуваат # датотека со линк}other{Се споделуваат # датотеки со линк}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Само слика}one{Само слики}other{Само слики}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Само видео}one{Само видеа}other{Само видеа}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Само датотека}one{Само датотеки}other{Само датотеки}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Сликичка за преглед на сликата"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Сликичка за преглед на видеото"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Сликичка за преглед на датотеката"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Нема препорачани луѓе со кои може да се сподели"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Список со апликации"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"На апликацијава не ѝ е доделена дозвола за снимање, но може да снима аудио преку овој USB-уред."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Лични"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"За работа"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Овие содржини не може да се отвораат со работни апликации"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Овие содржини не може да се споделуваат со лични апликации"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Овие содржини не може да се отвораат со лични апликации"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Работниот профил е паузиран"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Допрете за да вклучите"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Работните апликации се паузирани"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Прекини ја паузата"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Нема работни апликации"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Нема лични апликации"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Да се отвори <xliff:g id="APP">%s</xliff:g> во личниот профил?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Опфати текст"</string>
<string name="exclude_link" msgid="1332778255031992228">"Исклучи линк"</string>
<string name="include_link" msgid="827855767220339802">"Вклучи линк"</string>
+ <string name="pinned" msgid="7623664001331394139">"Закачено"</string>
</resources>
diff --git a/java/res/values-ml/strings.xml b/java/res/values-ml/strings.xml
index b91adae..03b01db 100644
--- a/java/res/values-ml/strings.xml
+++ b/java/res/values-ml/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> പിൻ ചെയ്യുക"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> അൺപിൻ ചെയ്യുക"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"എഡിറ്റ് ചെയ്യുക"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ഫയൽ}other{{file_name} + # ഫയലുകൾ}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ഫയൽ}other{+ # ഫയലുകൾ}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # ഫയൽ കൂടി}other{+ # ഫയലുകൾ കൂടി}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"ടെക്‌സ്‌റ്റ് പങ്കിടുന്നു"</string>
<string name="sharing_link" msgid="2307694372813942916">"ലിങ്ക് പങ്കിടുന്നു"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{ചിത്രം പങ്കിടുന്നു}other{# ചിത്രങ്ങൾ പങ്കിടുന്നു}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{വീഡിയോ പങ്കിടുന്നു}other{# വീഡിയോകൾ പങ്കിടുന്നു}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# ഇനം പങ്കിടുന്നു}other{# ഇനങ്ങൾ പങ്കിടുന്നു}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"ടെക്സ്റ്റിനൊപ്പം ചിത്രം പങ്കിടുന്നു"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"ലിങ്കിനൊപ്പം ചിത്രം പങ്കിടുന്നു"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ഫയൽ പങ്കിടുന്നു}other{# ഫയലുകൾ പങ്കിടുന്നു}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{ടെക്സ്റ്റിനൊപ്പം ചിത്രം പങ്കിടുന്നു}other{ടെക്സ്റ്റിനൊപ്പം # ചിത്രം പങ്കിടുന്നു}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{ലിങ്കിനൊപ്പം ചിത്രം പങ്കിടുന്നു}other{ലിങ്കിനൊപ്പം # ചിത്രങ്ങൾ പങ്കിടുന്നു}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ടെക്സ്റ്റിനൊപ്പം വീഡിയോ പങ്കിടുന്നു}other{ടെക്സ്റ്റിനൊപ്പം # വീഡിയോകൾ പങ്കിടുന്നു}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{ലിങ്കിനൊപ്പം വീഡിയോ പങ്കിടുന്നു}other{ലിങ്കിനൊപ്പം # വീഡിയോകൾ പങ്കിടുന്നു}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ടെക്സ്റ്റിനൊപ്പം ഫയൽ പങ്കിടുന്നു}other{ടെക്സ്റ്റിനൊപ്പം # ഫയലുകൾ പങ്കിടുന്നു}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{ലിങ്കിനൊപ്പം ഫയൽ പങ്കിടുന്നു}other{ലിങ്കിനൊപ്പം # ഫയലുകൾ പങ്കിടുന്നു}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{ചിത്രം മാത്രം}other{ചിത്രങ്ങൾ മാത്രം}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{വീഡിയോ മാത്രം}other{വീഡിയോകൾ മാത്രം}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ഫയൽ മാത്രം}other{ഫയലുകൾ മാത്രം}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"ചിത്രത്തിന്റെ പ്രിവ്യൂ ലഘുചിത്രം"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"വീഡിയോയുടെ പ്രിവ്യൂ ലഘുചിത്രം"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"ഫയലിന്റെ പ്രിവ്യൂ ലഘുചിത്രം"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"പങ്കിടാൻ, നിർദ്ദേശിക്കപ്പെട്ട ആളുകളൊന്നുമില്ല"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ആപ്പുകളുടെ ലിസ്‌റ്റ്"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ഈ ആപ്പിന് റെക്കോർഡ് അനുമതി നൽകിയിട്ടില്ല, എന്നാൽ ഈ USB ഉപകരണത്തിലൂടെ ഓഡിയോ ക്യാപ്‌ചർ ചെയ്യാനാവും."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"വ്യക്തിപരം"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"ഔദ്യോഗികം"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ഔദ്യോഗിക ആപ്പുകൾ ഉപയോഗിച്ച് ഈ ഉള്ളടക്കം തുറക്കാനാകില്ല"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"വ്യക്തിപര ആപ്പുകൾ ഉപയോഗിച്ച് ഈ ഉള്ളടക്കം പങ്കിടാനാകില്ല"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"വ്യക്തിപര ആപ്പുകൾ ഉപയോഗിച്ച് ഈ ഉള്ളടക്കം തുറക്കാനാകില്ല"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"ഔദ്യോഗിക പ്രൊഫൈൽ തൽക്കാലം നിർത്തിയിരിക്കുന്നു"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"ഓണാക്കാൻ ടാപ്പ് ചെയ്യുക"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"ഔദ്യോഗിക ആപ്പുകൾ തൽക്കാലം നിർത്തിയിരിക്കുന്നു"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"താൽക്കാലികമായി നിർത്തിയത് മാറ്റുക"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"ഔദ്യോഗിക ആപ്പുകൾ ഇല്ല"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"വ്യക്തിപര ആപ്പുകൾ ഇല്ല"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g>, നിങ്ങളുടെ വ്യക്തിപരമായ പ്രൊഫൈലിൽ തുറക്കണോ?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"ടെക്‌സ്റ്റ് ഉൾപ്പെടുത്തുക"</string>
<string name="exclude_link" msgid="1332778255031992228">"ലിങ്ക് ഒഴിവാക്കുക"</string>
<string name="include_link" msgid="827855767220339802">"ലിങ്ക് ഉൾപ്പെടുത്തുക"</string>
+ <string name="pinned" msgid="7623664001331394139">"പിൻ ചെയ്‌തത്"</string>
</resources>
diff --git a/java/res/values-mn/strings.xml b/java/res/values-mn/strings.xml
index ad356c0..339ca5e 100644
--- a/java/res/values-mn/strings.xml
+++ b/java/res/values-mn/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g>-г бэхлэх"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g>-г тогтоосныг болиулах"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Засах"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # файл}other{{file_name} + # файл}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # файл}other{+ # файл}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Өөр + # файл}other{Өөр + # файл}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Текст хуваалцаж байна"</string>
<string name="sharing_link" msgid="2307694372813942916">"Холбоос хуваалцаж байна"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Зураг хуваалцаж байна}other{# зураг хуваалцаж байна}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Видео хуваалцаж байна}other{# видео хуваалцаж байна}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# зүйл хуваалцаж байна}other{# зүйл хуваалцаж байна}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Тексттэй зураг хуваалцаж байна"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Холбоостой зураг хуваалцаж байна"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# файл хуваалцаж байна}other{# файл хуваалцаж байна}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Тексттэй зураг хуваалцаж байна}other{Тексттэй # зураг хуваалцаж байна}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Холбоостой зураг хуваалцаж байна}other{Холбоостой # зураг хуваалцаж байна}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Тексттэй видео хуваалцаж байна}other{Тексттэй # видео хуваалцаж байна}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Холбоостой видео хуваалцаж байна}other{Холбоостой # видео хуваалцаж байна}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Тексттэй файл хуваалцаж байна}other{Тексттэй # файл хуваалцаж байна}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Холбоостой файл хуваалцаж байна}other{Холбоостой # файл хуваалцаж байна}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Зөвхөн зураг}other{Зөвхөн зургууд}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Зөвхөн видео}other{Зөвхөн видеонууд}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Зөвхөн файл}other{Зөвхөн файлууд}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Зургийн урьдчилан үзэх өнгөц зураг"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Видеоны урьдчилан үзэх өнгөц зураг"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Файлын урьдчилан үзэх өнгөц зураг"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Хуваалцахыг санал болгосон хүн байхгүй"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Аппын жагсаалт"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Энэ апликейшнд бичих зөвшөөрөл олгогдоогүй ч энэ USB төхөөрөмжөөр дамжуулан аудио бичиж чадсан."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Хувийн"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Ажил"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Энэ контентыг ажлын аппуудаар нээх боломжгүй"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Энэ контентыг хувийн аппуудаар хуваалцах боломжгүй"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Энэ контентыг хувийн аппуудаар нээх боломжгүй"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Ажлын профайлыг түр зогсоосон"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Асаахын тулд товших"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Ажлын аппуудыг түр зогсоосон"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Үргэлжлүүлэх"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Ямар ч ажлын апп байхгүй байна"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Ямар ч хувийн апп байхгүй байна"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Хувийн профайл дээрээ <xliff:g id="APP">%s</xliff:g>-г нээх үү?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Текстийг оруулах"</string>
<string name="exclude_link" msgid="1332778255031992228">"Холбоосыг хасах"</string>
<string name="include_link" msgid="827855767220339802">"Холбоосыг оруулах"</string>
+ <string name="pinned" msgid="7623664001331394139">"Бэхэлсэн"</string>
</resources>
diff --git a/java/res/values-mr/strings.xml b/java/res/values-mr/strings.xml
index 469adb4..5202a3b 100644
--- a/java/res/values-mr/strings.xml
+++ b/java/res/values-mr/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> पिन करा"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> ला अनपिन करा"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"संपादित करा"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # फाइल}other{{file_name} + # फाइल}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # फाइल}other{+ # फाइल}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{इतर आणखी # फाइल}other{इतर आणखी # फाइल}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"मजकूर शेअर करत आहे"</string>
<string name="sharing_link" msgid="2307694372813942916">"लिंक शेअर करत आहे"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{इमेज शेअर करत आहे}other{# इमेज शेअर करत आहे}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{व्हिडिओ शेअर करत आहे}other{# व्हिडिओ शेअर करत आहे}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# आयटम शेअर करत आहे}other{# आयटम शेअर करत आहे}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"मजकुरासह इमेज शेअर करत आहे"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"लिंकसह इमेज शेअर करत आहे"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# फाइल शेअर करत आहे}other{# फाइल शेअर करत आहे}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{मजकुरासह इमेज शेअर करत आहे}other{मजकुरासह # इमेज शेअर करत आहे}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{लिंकसह इमेज शेअर करत आहे}other{लिंकसह # इमेज शेअर करत आहे}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{मजकुरासह व्हिडिओ शेअर करत आहे}other{मजकुरासह # व्हिडिओ शेअर करत आहे}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{लिंकसह व्हिडिओ शेअर करत आहे}other{लिंकसह # व्हिडिओ शेअर करत आहे}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{मजकुरासह फाइल शेअर करत आहे}other{मजकुरासह # फाइल शेअर करत आहे}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{लिंकसह फाइल शेअर करत आहे}other{लिंकसह # फाइल शेअर करत आहे}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{फक्त इमेज}other{फक्त इमेज}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{फक्त व्हिडिओ}other{फक्त व्हिडिओ}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{फक्त फाइल}other{फक्त फाइल}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"इमेज पूर्वावलोकनाची थंबनेल"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"व्हिडिओ पूर्वावलोकनाची थंबनेल"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"फाइल पूर्वावलोकनाची थंबनेल"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"शेअर करण्यासाठी शिफारस केलेल्या कोणत्याही व्यक्ती नाहीत"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"अ‍ॅप्स सूची"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"या अ‍ॅपला रेकॉर्ड करण्याची परवानगी दिली गेली नाही पण हे USB डिव्हाइस वापरून ऑडिओ कॅप्चर केला जाऊ शकतो."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"वैयक्तिक"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"कार्य"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"हा आशय कार्य ॲप्स वापरून उघडला जाऊ शकत नाही"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"हा आशय वैयक्तिक ॲप्ससह शेअर केला जाऊ शकत नाही"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"हा आशय वैयक्तिक ॲप्स वापरून उघडला जाऊ शकत नाही"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"कार्य प्रोफाइल थांबवली आहे"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"सुरू करण्यासाठी टॅप करा"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"कामाशी संबंधित अ‍ॅप्स थांबवली आहेत"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"पुन्हा सुरू करा"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"कोणतीही कार्य ॲप्स सपोर्ट करत नाहीत"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"कोणतीही वैयक्तिक ॲप्स सपोर्ट करत नाहीत"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"तुमच्या वैयक्तिक प्रोफाइलमध्ये <xliff:g id="APP">%s</xliff:g> उघडायचे आहे का?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"मजकूर समाविष्ट करा"</string>
<string name="exclude_link" msgid="1332778255031992228">"लिंक वगळा"</string>
<string name="include_link" msgid="827855767220339802">"लिंक समाविष्ट करा"</string>
+ <string name="pinned" msgid="7623664001331394139">"पिन केलेली"</string>
</resources>
diff --git a/java/res/values-ms/strings.xml b/java/res/values-ms/strings.xml
index 4d6eb7c..f1ac4d1 100644
--- a/java/res/values-ms/strings.xml
+++ b/java/res/values-ms/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Sematkan <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Nyahsemat <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Edit"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fail}other{{file_name} + # fail}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fail}other{+ # fail}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # fail lagi}other{+ # fail lagi}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Berkongsi teks"</string>
<string name="sharing_link" msgid="2307694372813942916">"Berkongsi pautan"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Berkongsi imej}other{Berkongsi # imej}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Berkongsi video}other{Berkongsi # video}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Berkongsi # item}other{Berkongsi # item}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Berkongsi imej dengan teks"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Berkongsi imej dengan pautan"</string>
- <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Tiada orang yang disyorkan untuk berkongsi"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Senarai apl"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Berkongsi # fail}other{Berkongsi # fail}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Berkongsi imej dengan teks}other{Berkongsi # imej dengan teks}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Berkongsi imej dengan pautan}other{Berkongsi # imej dengan pautan}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Berkongsi video dengan teks}other{Berkongsi # video dengan teks}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Berkongsi video dengan pautan}other{Berkongsi # video dengan pautan}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Berkongsi fail dengan teks}other{Berkongsi # fail dengan teks}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Berkongsi fail dengan pautan}other{Berkongsi # fail dengan pautan}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Imej sahaja}other{Imej sahaja}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Video sahaja}other{Video sahaja}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Fail sahaja}other{Fail sahaja}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Lakaran kecil pratonton imej"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Lakaran kecil pratonton video"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Lakaran kecil pratonton fail"</string>
+ <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Tiada orang yang disyorkan untuk membuat perkongsian"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Apl ini belum diberikan kebenaran merakam tetapi dapat merakam audio melalui peranti USB ini."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Peribadi"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Kerja"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Kandungan ini tidak boleh dibuka dengan apl kerja"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Kandungan ini tidak boleh dikongsi dengan apl peribadi"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Kandungan ini tidak boleh dibuka dengan apl peribadi"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Profil kerja dijeda"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Ketik untuk menghidupkan profil"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Apl kerja dijeda"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Nyahjeda"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Tiada apl kerja"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Tiada apl peribadi"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Buka <xliff:g id="APP">%s</xliff:g> dalam profil peribadi anda?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Sertakan teks"</string>
<string name="exclude_link" msgid="1332778255031992228">"Kecualikan pautan"</string>
<string name="include_link" msgid="827855767220339802">"Sertakan pautan"</string>
+ <string name="pinned" msgid="7623664001331394139">"Disemat"</string>
</resources>
diff --git a/java/res/values-my/strings.xml b/java/res/values-my/strings.xml
index 0b17535..c3ab1ee 100644
--- a/java/res/values-my/strings.xml
+++ b/java/res/values-my/strings.xml
@@ -29,7 +29,7 @@
<string name="whichGiveAccessToApplicationLabel" msgid="5120142857844152131">"သုံးခွင့်ပေးရန်"</string>
<string name="whichEditApplication" msgid="5097563012157950614">"...နှင့် တည်းဖြတ်ရန်"</string>
<string name="whichEditApplicationNamed" msgid="3150137489226219100">"<xliff:g id="APP">%1$s</xliff:g> ဖြင့် တည်းဖြတ်ခြင်း"</string>
- <string name="whichEditApplicationLabel" msgid="5992662938338600364">"တည်းဖြတ်ပါ"</string>
+ <string name="whichEditApplicationLabel" msgid="5992662938338600364">"တည်းဖြတ်ရန်"</string>
<string name="whichSendApplication" msgid="59510564281035884">"မျှဝေပါ"</string>
<string name="whichSendApplicationNamed" msgid="495577664218765855">"<xliff:g id="APP">%1$s</xliff:g> ဖြင့် မျှဝေခြင်း"</string>
<string name="whichSendApplicationLabel" msgid="2391198069286568035">"မျှဝေပါ"</string>
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> ကို ပင်ထိုးရန်"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> ကို ပင်ဖြုတ်ရန်"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"တည်းဖြတ်ရန်"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ဖိုင်}other{{file_name} + # ဖိုင်}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ဖိုင်}other{+ # ဖိုင်}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+နောက်ထပ် # ဖိုင်}other{+နောက်ထပ် # ဖိုင်}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"စာသား မျှဝေနေသည်"</string>
<string name="sharing_link" msgid="2307694372813942916">"လင့်ခ် မျှဝေနေသည်"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{ပုံ မျှဝေနေသည်}other{ပုံ # ပုံ မျှဝေနေသည်}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{ဗီဒီယို မျှဝေနေသည်}other{ဗီဒီယို # ခု မျှဝေနေသည်}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# ခု မျှဝေနေသည်}other{# ခု မျှဝေနေသည်}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"စာပါသောပုံ မျှဝေနေသည်"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"လင့်ခ်ပါသောပုံ မျှဝေနေသည်"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ဖိုင် မျှဝေနေသည်}other{# ဖိုင် မျှဝေနေသည်}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{စာသားပါသောပုံကို မျှဝေနေသည်}other{စာသားပါသောပုံ # ပုံကို မျှဝေနေသည်}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{လင့်ခ်ပါသောပုံကို မျှဝေနေသည်}other{လင့်ခ်ပါသောပုံ # ပုံကို မျှဝေနေသည်}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{စာသားပါသောဗီဒီယိုကို မျှဝေနေသည်}other{စာသားပါသောဗီဒီယို # ခုကို မျှဝေနေသည်}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{လင့်ခ်ပါသောဗီဒီယိုကို မျှဝေနေသည်}other{လင့်ခ်ပါသောဗီဒီယို # ခုကို မျှဝေနေသည်}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{စာသားပါသောဖိုင်ကို မျှဝေနေသည်}other{စာသားပါသောဖိုင် # ခုကို မျှဝေနေသည်}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{လင့်ခ်ပါသောဖိုင်ကို မျှဝေနေသည်}other{လင့်ခ်ပါသောဖိုင် # ခုကို မျှဝေနေသည်}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{ပုံသာလျှင်}other{ပုံများသာလျှင်}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{ဗီဒီယိုသာလျှင်}other{ဗီဒီယိုများသာလျှင်}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ဖိုင်သာလျှင်}other{ဖိုင်များသာလျှင်}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"ပုံအစမ်းကြည့်ရှုမှု ပုံသေး"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"ဗီဒီယိုအစမ်းကြည့်ရှုမှု ပုံသေး"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"ဖိုင်အစမ်းကြည့်ရှုမှု ပုံသေး"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"မျှဝေရန် အကြံပြုထားသူများ မရှိပါ"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"အက်ပ်စာရင်း"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ဤအက်ပ်ကို အသံဖမ်းခွင့် ပေးမထားသော်လည်း ၎င်းသည် ဤ USB စက်ပစ္စည်းမှတစ်ဆင့် အသံများကို ဖမ်းယူနိုင်ပါသည်။"</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"ကိုယ်ပိုင်"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"အလုပ်"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ဤအကြောင်းအရာကို အလုပ်သုံးအက်ပ်များဖြင့် မဖွင့်နိုင်ပါ"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ဤအကြောင်းအရာကို ကိုယ်ပိုင်အက်ပ်များဖြင့် မမျှဝေနိုင်ပါ"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ဤအကြောင်းအရာကို ကိုယ်ပိုင်အက်ပ်များဖြင့် မဖွင့်နိုင်ပါ"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"အလုပ်ပရိုဖိုင် ခဏရပ်ထားသည်"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"ဖွင့်ရန်တို့ပါ"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"အလုပ်သုံးအက်ပ်များကို ခေတ္တရပ်ထားသည်"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"ပြန်ဖွင့်ရန်"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"အလုပ်သုံးအက်ပ်များ မရှိပါ"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"ကိုယ်ပိုင်အက်ပ်များ မရှိပါ"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> ကို သင့်ကိုယ်ပိုင်ပရိုဖိုင်တွင် ဖွင့်မလား။"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"စာသားထည့်သွင်းရန်"</string>
<string name="exclude_link" msgid="1332778255031992228">"လင့်ခ် ဖယ်ထုတ်ရန်"</string>
<string name="include_link" msgid="827855767220339802">"လင့်ခ်ထည့်သွင်းရန်"</string>
+ <string name="pinned" msgid="7623664001331394139">"ပင်ထိုးထားသည်"</string>
</resources>
diff --git a/java/res/values-nb/strings.xml b/java/res/values-nb/strings.xml
index b6e49cd..a2c6da6 100644
--- a/java/res/values-nb/strings.xml
+++ b/java/res/values-nb/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Fest <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Løsne <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Endre"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fil}other{{file_name} + # filer}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fil}other{+ # filer}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # fil til}other{+ # filer til}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Deler teksten"</string>
<string name="sharing_link" msgid="2307694372813942916">"Deler linken"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Deler bildet}other{Deler # bilder}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Deler videoen}other{Deler # videoer}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Deler # element}other{Deler # elementer}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Deler bildet med tekst"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Deler bildet med link"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Deler # fil}other{Deler # filer}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Deler bildet med tekst}other{Deler # bilder med tekst}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Deler bildet med link}other{Deler # bilder med link}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Deler videoen med tekst}other{Deler # videoer med tekst}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Deler videoen med link}other{Deler # videoer med link}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Deler filen med tekst}other{Deler # filer med tekst}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Deler filen med link}other{Deler # filer med link}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Bare bildet}other{Bare bildene}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Bare videoen}other{Bare videoene}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Bare filen}other{Bare filene}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatyrbilde for forhåndsvisning av bilde"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatyrbilde for forhåndsvisning av video"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatyrbilde for forhåndsvisning av fil"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Det finnes ingen anbefalte personer å dele med"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Appliste"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Denne appen har ikke fått tillatelse til å spille inn, men kan ta opp lyd med denne USB-enheten."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Personlig"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Jobb"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Dette innholdet kan ikke åpnes med jobbapper"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Dette innholdet kan ikke deles med personlige apper"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Dette innholdet kan ikke åpnes med personlige apper"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Jobbprofilen er satt på pause"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Trykk for å slå på"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Jobbapper er satt på pause"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Slå av pausen"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Ingen jobbapper"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Ingen personlige apper"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vil du åpne <xliff:g id="APP">%s</xliff:g> i den personlige profilen din?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Inkluder teksten"</string>
<string name="exclude_link" msgid="1332778255031992228">"Ekskluder linken"</string>
<string name="include_link" msgid="827855767220339802">"Inkluder linken"</string>
+ <string name="pinned" msgid="7623664001331394139">"Festet"</string>
</resources>
diff --git a/java/res/values-ne/strings.xml b/java/res/values-ne/strings.xml
index 9bf2051..176067f 100644
--- a/java/res/values-ne/strings.xml
+++ b/java/res/values-ne/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> पिन गर्नुहोस्"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> लाई अनपिन गर्नुहोस्"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"सम्पादन गर्नुहोस्"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # फाइल}other{{file_name} + # वटा फाइल}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{# भन्दा बढी फाइल}other{# भन्दा बढी फाइलहरू}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{थप + # वटा फाइल}other{थप + # वटा फाइल}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"टेक्स्ट सेयर गरिँदै छ"</string>
<string name="sharing_link" msgid="2307694372813942916">"लिंक सेयर गरिँदै छ"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{फोटो सेयर गरिँदै छ}other{# वटा फोटो सेयर गरिँदै छ}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{भिडियो सेयर गरिँदै छ}other{# वटा भिडियो सेयर गरिँदै छ}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# सामग्री सेयर गरिँदै छ}other{# वटा सामग्री सेयर गरिँदै छ}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"टेक्स्ट भएको फोटो सेयर गरिँदै छ"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"लिंक भएको फोटो सेयर गरिँदै छ"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# वटा फाइल सेयर गरिँदै छ}other{# वटा फाइल सेयर गरिँदै छ}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{टेक्स्ट भएको फोटो सेयर गरिँदै छ}other{टेक्स्ट भएका # वटा फोटो सेयर गरिँदै छन्}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{लिंक भएको फोटो सेयर गरिँदै छ}other{लिंक भएका # वटा फोटो सेयर गरिँदै छन्}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{टेक्स्ट भएको भिडियो सेयर गरिँदै छ}other{टेक्स्ट भएका # वटा भिडियो सेयर गरिँदै छन्}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{लिंक भएको भिडियो सेयर गरिँदै छ}other{लिंक भएका # वटा भिडियो सेयर गरिँदै छन्}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{टेक्स्ट भएको फाइल सेयर गरिँदै छ}other{टेक्स्ट भएका # वटा फाइल सेयर गरिँदै छन्}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{लिंक भएको फाइल सेयर गरिँदै छ}other{लिंक भएका # वटा फाइल सेयर गरिँदै छन्}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{फोटो मात्र}other{फोटोहरू मात्र}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{भिडियो मात्र}other{भिडियोहरू मात्र}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{फाइल मात्र}other{फाइलहरू मात्र}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"फोटो प्रिभ्यू थम्बनेल"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"भिडियो प्रिभ्यू थम्बनेल"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"फाइल प्रिभ्यू थम्बनेल"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"कुनै पनि व्यक्तिसँग सेयर गर्ने सिफारिस गरिएको छैन"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"अनुप्रयोगहरूको सूची"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"यो एपलाई रेकर्ड गर्ने अनुमति प्रदान गरिएको छैन तर यसले यो USB यन्त्रमार्फत अडियो क्याप्चर गर्न सक्छ।"</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"व्यक्तिगत"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"काम"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"यो सामग्री कामसम्बन्धी एपहरूमार्फत खोल्न मिल्दैन"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"यो सामग्री व्यक्तिगत एपहरूमार्फत सेयर गर्न मिल्दैन"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"यो सामग्री व्यक्तिगत एपहरूमार्फत खोल्न मिल्दैन"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"कार्य प्रोफाइल पज गरिएको छ"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"अन गर्न ट्याप गर्नुहोस्"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"कामसम्बन्धी एपहरू पज गरिएका छन्"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"अनपज गर्नुहोस्"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"यो सामग्री खोल्न मिल्ने कुनै पनि कामसम्बन्धी एप छैन"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"यो सामग्री खोल्न मिल्ने कुनै पनि व्यक्तिगत एप छैन"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> तपाईंको व्यक्तिगत प्रोफाइलमा खोल्ने हो?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"टेक्स्ट समावेश गर्नुहोस्"</string>
<string name="exclude_link" msgid="1332778255031992228">"लिंक हटाउनुहोस्"</string>
<string name="include_link" msgid="827855767220339802">"लिंक समावेश गर्नुहोस्"</string>
+ <string name="pinned" msgid="7623664001331394139">"पिन गरिएको"</string>
</resources>
diff --git a/java/res/values-night/styles.xml b/java/res/values-night/styles.xml
new file mode 100644
index 0000000..95071ba
--- /dev/null
+++ b/java/res/values-night/styles.xml
@@ -0,0 +1,22 @@
+<!--
+ ~ Copyright (C) 2022 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+
+ <style name="Theme.DeviceDefault.Resolver" parent="Theme.DeviceDefault.ResolverCommon">
+ <item name="android:windowLightNavigationBar">false</item>
+ </style>
+</resources>
diff --git a/java/res/values-nl/strings.xml b/java/res/values-nl/strings.xml
index a779bf6..7ef1513 100644
--- a/java/res/values-nl/strings.xml
+++ b/java/res/values-nl/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> vastzetten"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> losmaken"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Bewerken"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # bestand}other{{file_name} + # bestanden}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # bestand}other{+ # bestanden}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ nog # bestand}other{+ nog # bestanden}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Tekst delen"</string>
<string name="sharing_link" msgid="2307694372813942916">"Link delen"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Afbeelding delen}other{# afbeeldingen delen}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Video delen}other{# video\'s delen}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# item delen}other{# items delen}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Afbeelding delen met tekst"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Afbeelding delen met link"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# bestand delen}other{# bestanden delen}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Afbeelding met tekst wordt gedeeld}other{# afbeeldingen met tekst worden gedeeld}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Afbeelding delen via link}other{# afbeeldingen delen via link}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Video delen via tekstbericht}other{# video\'s delen via tekstbericht}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Video delen via link}other{# video\'s delen via link}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Bestand delen via tekstbericht}other{# bestanden delen via tekstbericht}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Bestand delen via link}other{# bestanden delen via link}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Alleen afbeelding}other{Alleen afbeeldingen}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Alleen video}other{Alleen video\'s}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Alleen bestand}other{Alleen bestanden}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Voorbeeldthumbnail voor afbeelding"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Voorbeeldthumbnail voor video"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Voorbeeldthumbnail voor bestand"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Geen aanbevolen mensen om mee te delen"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lijst met apps"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Deze app heeft geen opnamerechten gekregen, maar zou audio kunnen vastleggen via dit USB-apparaat."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Persoonlijk"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Werk"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Deze content kan niet worden geopend met werk-apps"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Deze content kan niet worden gedeeld met persoonlijke apps"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Deze content kan niet worden geopend met persoonlijke apps"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Werkprofiel is onderbroken"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tik om aan te zetten"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Werk-apps zijn onderbroken"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Hervatten"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Geen werk-apps"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Geen persoonlijke apps"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> openen in je persoonlijke profiel?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Tekst opnemen"</string>
<string name="exclude_link" msgid="1332778255031992228">"Link uitsluiten"</string>
<string name="include_link" msgid="827855767220339802">"Link opnemen"</string>
+ <string name="pinned" msgid="7623664001331394139">"Vastgezet"</string>
</resources>
diff --git a/java/res/values-or/strings.xml b/java/res/values-or/strings.xml
index 0ed8358..93c60db 100644
--- a/java/res/values-or/strings.xml
+++ b/java/res/values-or/strings.xml
@@ -30,7 +30,7 @@
<string name="whichEditApplication" msgid="5097563012157950614">"ସହିତ ଏଡିଟ କରନ୍ତୁ"</string>
<string name="whichEditApplicationNamed" msgid="3150137489226219100">"<xliff:g id="APP">%1$s</xliff:g> ମାଧ୍ୟମରେ ଏଡିଟ କରନ୍ତୁ"</string>
<string name="whichEditApplicationLabel" msgid="5992662938338600364">"ଏଡିଟ କରନ୍ତୁ"</string>
- <string name="whichSendApplication" msgid="59510564281035884">"ସେୟାର୍ କରନ୍ତୁ"</string>
+ <string name="whichSendApplication" msgid="59510564281035884">"ସେୟାର କରନ୍ତୁ"</string>
<string name="whichSendApplicationNamed" msgid="495577664218765855">"<xliff:g id="APP">%1$s</xliff:g> ସହ ସେୟାର କରନ୍ତୁ"</string>
<string name="whichSendApplicationLabel" msgid="2391198069286568035">"ସେୟାର୍‌ କରନ୍ତୁ"</string>
<string name="whichSendToApplication" msgid="2724450540348806267">"ଏହା ଜରିଆରେ ପଠାନ୍ତୁ"</string>
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g>କୁ ପିନ କରନ୍ତୁ"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g>ରେ ଅନ୍‌ପିନ୍ କରନ୍ତୁ"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"ଏଡିଟ କରନ୍ତୁ"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + #ଟି ଫାଇଲ}other{{file_name} + #ଟି ଫାଇଲ}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ #ଟି ଫାଇଲ}other{+ #ଟି ଫାଇଲ}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ #ଟି ଅଧିକ ଫାଇଲ}other{+ #ଟି ଅଧିକ ଫାଇଲ}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"ଟେକ୍ସଟ ସେୟାର କରାଯାଉଛି"</string>
<string name="sharing_link" msgid="2307694372813942916">"ଲିଙ୍କ ସେୟାର କରାଯାଉଛି"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{ଇମେଜ ସେୟାର କରାଯାଉଛି}other{#ଟିି ଇମେଜ ସେୟାର କରାଯାଉଛି}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{ଭିଡିଓ ସେୟାର କରାଯାଉଛି}other{#ଟି ଭିଡିଓ ସେୟାର କରାଯାଉଛି}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{#ଟି ଆଇଟମ ସେୟାର କରାଯାଉଛି}other{#ଟି ଆଇଟମ ସେୟାର କରାଯାଉଛି}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"ଟେକ୍ସଟରେ ଇମେଜ ସେୟାର ହେଉଛି"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"ଲିଙ୍କରେ ଇମେଜ ସେୟାର ହେଉଛି"</string>
- <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ଏହାକୁ ସେୟାର୍ କରିବା ପାଇଁ କୌଣସି ସୁପାରିଶ କରାଯାଇଥିବା ଲୋକ ନାହାଁନ୍ତି"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ଆପ୍ସ ତାଲିକା"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{#ଟି ଫାଇଲ ସେୟାର କରାଯାଉଛି}other{#ଟି ଫାଇଲ ସେୟାର କରାଯାଉଛି}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{ଟେକ୍ସଟ ସହ ଇମେଜ ସେୟାର କରାଯାଉଛି}other{ଟେକ୍ସଟ ସହ #ଟି ଇମେଜ ସେୟାର କରାଯାଉଛି}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{ଲିଙ୍କ ସହ ଇମେଜ ସେୟାର କରାଯାଉଛି}other{ଲିଙ୍କ ସହ #ଟି ଇମେଜ ସେୟାର କରାଯାଉଛି}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ଟେକ୍ସଟ ସହ ଭିଡିଓ ସେୟାର କରାଯାଉଛି}other{ଟେକ୍ସଟ ସହ #ଟି ଭିଡିଓ ସେୟାର କରାଯାଉଛି}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{ଲିଙ୍କ ସହ ଭିଡିଓ ସେୟାର କରାଯାଉଛି}other{ଲିଙ୍କ ସହ #ଟି ଭିଡିଓ ସେୟାର କରାଯାଉଛି}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ଟେକ୍ସଟ ସହ ଫାଇଲ ସେୟାର କରାଯାଉଛି}other{ଟେକ୍ସଟ ସହ #ଟି ଫାଇଲ ସେୟାର କରାଯାଉଛି}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{ଲିଙ୍କ ସହ ଫାଇଲ ସେୟାର କରାଯାଉଛି}other{ଲିଙ୍କ ସହ #ଟି ଫାଇଲ ସେୟାର କରାଯାଉଛି}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{କେବଳ ଇମେଜ}other{କେବଳ ଇମେଜଗୁଡ଼ିକ}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{କେବଳ ଭିଡିଓ}other{କେବଳ ଭିଡିଓଗୁଡ଼ିକ}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{କେବଳ ଫାଇଲ}other{କେବଳ ଫାଇଲଗୁଡ଼ିକ}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"ଇମେଜ ପ୍ରିଭ୍ୟୁର ଥମ୍ବନେଲ"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"ଭିଡିଓ ପ୍ରିଭ୍ୟୁର ଥମ୍ବନେଲ"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"ଫାଇଲ ପ୍ରିଭ୍ୟୁର ଥମ୍ବନେଲ"</string>
+ <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ଏହାକୁ ସେୟାର କରିବା ପାଇଁ କୌଣସି ସୁପାରିଶ କରାଯାଇଥିବା ଲୋକ ନାହାଁନ୍ତି"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ଏହି ଆପ୍‌କୁ ରେକର୍ଡ କରିବାକୁ ଅନୁମତି ଦିଆଯାଇ ନାହିଁ କିନ୍ତୁ ଏହି USB ଡିଭାଇସ୍ ଜରିଆରେ ଅଡିଓ କ୍ୟାପ୍‍ଚର୍‍ କରିପାରିବ।"</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"ବ୍ୟକ୍ତିଗତ"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"ୱାର୍କ"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ଏହି ବିଷୟବସ୍ତୁ ୱାର୍କ ଆପଗୁଡ଼ିକରେ ଖୋଲାଯାଇପାରିବ ନାହିଁ"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ଏହି ବିଷୟବସ୍ତୁ ବ୍ୟକ୍ତିଗତ ଆପଗୁଡ଼ିକରେ ସେୟାର୍ କରାଯାଇପାରିବ ନାହିଁ"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ଏହି ବିଷୟବସ୍ତୁ ବ୍ୟକ୍ତିଗତ ଆପଗୁଡ଼ିକରେ ଖୋଲାଯାଇପାରିବ ନାହିଁ"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"ୱାର୍କ ପ୍ରୋଫାଇଲକୁ ବିରତ କରାଯାଇଛି"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"ଚାଲୁ କରିବା ପାଇଁ ଟାପ୍ କରନ୍ତୁ"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"ୱାର୍କ ଆପ୍ସକୁ ବିରତ କରାଯାଇଛି"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"ପୁଣି ଚାଲୁ କରନ୍ତୁ"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"କୌଣସି ୱାର୍କ ଆପ୍ ନାହିଁ"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"କୌଣସି ବ୍ୟକ୍ତିଗତ ଆପ୍ ନାହିଁ"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g>କୁ ଆପଣଙ୍କ ବ୍ୟକ୍ତିଗତ ପ୍ରୋଫାଇଲରେ ଖୋଲିବେ?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"ଟେକ୍ସଟକୁ ଅନ୍ତର୍ଭୁକ୍ତ କରନ୍ତୁ"</string>
<string name="exclude_link" msgid="1332778255031992228">"ଲିଙ୍କକୁ ବାଦ ଦିଅନ୍ତୁ"</string>
<string name="include_link" msgid="827855767220339802">"ଲିଙ୍କକୁ ଅନ୍ତର୍ଭୁକ୍ତ କରନ୍ତୁ"</string>
+ <string name="pinned" msgid="7623664001331394139">"ପିନ କରାଯାଇଛି"</string>
</resources>
diff --git a/java/res/values-pa/strings.xml b/java/res/values-pa/strings.xml
index 3076880..872168d 100644
--- a/java/res/values-pa/strings.xml
+++ b/java/res/values-pa/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> ਨੂੰ ਪਿੰਨ ਕਰੋ"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> ਨੂੰ ਅਨਪਿੰਨ ਕਰੋ"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"ਸੰਪਾਦਨ ਕਰੋ"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ਫ਼ਾਈਲ}one{{file_name} + # ਫ਼ਾਈਲ}other{{file_name} + # ਫ਼ਾਈਲਾਂ}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ਫ਼ਾਈਲ}one{+ # ਫ਼ਾਈਲ}other{+ # ਫ਼ਾਈਲਾਂ}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"ਲਿਖਤ ਸੁਨੇਹਾ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # ਹੋਰ ਫ਼ਾਈਲ}one{+ # ਹੋਰ ਫ਼ਾਈਲ}other{+ # ਹੋਰ ਫ਼ਾਈਲਾਂ}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"ਲਿਖਤ ਸਾਂਝੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ"</string>
<string name="sharing_link" msgid="2307694372813942916">"ਲਿੰਕ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{ਚਿੱਤਰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}one{# ਚਿੱਤਰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}other{# ਚਿੱਤਰ ਸਾਂਝੇ ਕੀਤੇ ਜਾ ਰਹੇ ਹਨ}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{ਵੀਡੀਓ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}one{# ਵੀਡੀਓ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}other{# ਵੀਡੀਓ ਸਾਂਝੇ ਕੀਤੇ ਜਾ ਰਹੇ ਹਨ}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# ਆਈਟਮ ਸਾਂਝੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ}one{# ਆਈਟਮ ਸਾਂਝੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ}other{# ਆਈਟਮਾਂ ਸਾਂਝੀਆਂ ਕੀਤੀਆਂ ਜਾ ਰਹੀਆਂ ਹਨ}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ ਚਿੱਤਰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"ਲਿੰਕ ਨਾਲ ਚਿੱਤਰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</string>
- <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ਸਾਂਝਾ ਕਰਨ ਲਈ ਕੋਈ ਸਿਫ਼ਾਰਸ਼ ਕੀਤੇ ਲੋਕ ਨਹੀਂ"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ਐਪ ਸੂਚੀ"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ਫ਼ਾਈਲ ਸਾਂਝੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ}one{# ਫ਼ਾਈਲ ਸਾਂਝੀ ਕੀਤੀ ਜਾ ਰਹੀ ਹੈ}other{# ਫ਼ਾਈਲਾਂ ਸਾਂਝੀਆਂ ਕੀਤੀਆਂ ਜਾ ਰਹੀਆਂ ਹਨ}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ ਚਿੱਤਰ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}one{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ # ਚਿੱਤਰ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}other{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ # ਚਿੱਤਰਾਂ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{ਲਿੰਕ ਨਾਲ ਚਿੱਤਰ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}one{ਲਿੰਕ ਨਾਲ # ਚਿੱਤਰ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}other{ਲਿੰਕ ਨਾਲ # ਚਿੱਤਰਾਂ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ ਵੀਡੀਓ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}one{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ # ਵੀਡੀਓ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}other{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ # ਵੀਡੀਓ ਸਾਂਝੇ ਕੀਤੇ ਜਾ ਰਹੇ ਹਨ}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{ਲਿੰਕ ਨਾਲ ਵੀਡੀਓ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}one{ਲਿੰਕ ਨਾਲ # ਵੀਡੀਓ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}other{ਲਿੰਕ ਨਾਲ # ਵੀਡੀਓ ਸਾਂਝੇ ਕੀਤੇ ਜਾ ਰਹੇ ਹਨ}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ ਫ਼ਾਈਲ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}one{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ # ਫ਼ਾਈਲ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}other{ਲਿਖਤ ਸੁਨੇਹੇ ਨਾਲ # ਫ਼ਾਈਲਾਂ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{ਲਿੰਕ ਨਾਲ ਫ਼ਾਈਲ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}one{ਲਿੰਕ ਨਾਲ # ਫ਼ਾਈਲ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}other{ਲਿੰਕ ਨਾਲ # ਫ਼ਾਈਲਾਂ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{ਸਿਰਫ਼ ਚਿੱਤਰ}one{ਸਿਰਫ਼ ਚਿੱਤਰ}other{ਸਿਰਫ਼ ਚਿੱਤਰ}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{ਸਿਰਫ਼ ਵੀਡੀਓ}one{ਸਿਰਫ਼ ਵੀਡੀਓ}other{ਸਿਰਫ਼ ਵੀਡੀਓ}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ਸਿਰਫ਼ ਫ਼ਾਈਲ}one{ਸਿਰਫ਼ ਫ਼ਾਈਲ}other{ਸਿਰਫ਼ ਫ਼ਾਈਲਾਂ}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"ਚਿੱਤਰ ਦੀ ਪੂਰਵ-ਝਲਕ ਦਾ ਲਘੂ-ਚਿੱਤਰ"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"ਵੀਡੀਓ ਦੀ ਪੂਰਵ-ਝਲਕ ਦਾ ਲਘੂ-ਚਿੱਤਰ"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"ਫ਼ਾਈਲ ਦੀ ਪੂਰਵ-ਝਲਕ ਦਾ ਲਘੂ-ਚਿੱਤਰ"</string>
+ <string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ਸਾਂਝਾ ਕਰਨ ਲਈ ਕੋਈ ਸਿਫ਼ਾਰਸ਼ ਕੀਤਾ ਵਿਅਕਤੀ ਨਹੀਂ ਹੈ"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ਇਸ ਐਪ ਨੂੰ ਰਿਕਾਰਡ ਕਰਨ ਦੀ ਇਜਾਜ਼ਤ ਨਹੀਂ ਦਿੱਤੀ ਗਈ ਪਰ ਇਹ USB ਡੀਵਾਈਸ ਰਾਹੀਂ ਆਡੀਓ ਕੈਪਚਰ ਕਰ ਸਕਦੀ ਹੈ।"</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"ਨਿੱਜੀ"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"ਕੰਮ ਸੰਬੰਧੀ"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ਇਸ ਸਮੱਗਰੀ ਨੂੰ ਕੰਮ ਸੰਬੰਧੀ ਐਪਾਂ ਨਾਲ ਨਹੀਂ ਖੋਲ੍ਹਿਆ ਜਾ ਸਕਦਾ"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ਇਸ ਸਮੱਗਰੀ ਨੂੰ ਨਿੱਜੀ ਐਪਾਂ ਨਾਲ ਸਾਂਝਾ ਨਹੀਂ ਕੀਤਾ ਜਾ ਸਕਦਾ"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ਇਸ ਸਮੱਗਰੀ ਨੂੰ ਨਿੱਜੀ ਐਪਾਂ ਨਾਲ ਨਹੀਂ ਖੋਲ੍ਹਿਆ ਜਾ ਸਕਦਾ"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"ਕਾਰਜ ਪ੍ਰੋਫਾਈਲ ਨੂੰ ਰੋਕਿਆ ਗਿਆ ਹੈ"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"ਚਾਲੂ ਕਰਨ ਲਈ ਟੈਪ ਕਰੋ"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"ਕੰਮ ਸੰਬੰਧੀ ਐਪਾਂ ਨੂੰ ਰੋਕਿਆ ਗਿਆ ਹੈ"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"ਰੋਕ ਹਟਾਓ"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"ਕੋਈ ਕੰਮ ਸੰਬੰਧੀ ਐਪ ਨਹੀਂ"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"ਕੋਈ ਨਿੱਜੀ ਐਪ ਨਹੀਂ"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"ਕੀ ਆਪਣੇ ਨਿੱਜੀ ਪ੍ਰੋਫਾਈਲ ਵਿੱਚ <xliff:g id="APP">%s</xliff:g> ਨੂੰ ਖੋਲ੍ਹਣਾ ਹੈ?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"ਲਿਖਤ ਨੂੰ ਸ਼ਾਮਲ ਕਰੋ"</string>
<string name="exclude_link" msgid="1332778255031992228">"ਲਿੰਕ ਨੂੰ ਸ਼ਾਮਲ ਨਾ ਕਰੋ"</string>
<string name="include_link" msgid="827855767220339802">"ਲਿੰਕ ਸ਼ਾਮਲ ਕਰੋ"</string>
+ <string name="pinned" msgid="7623664001331394139">"ਪਿੰਨ ਕੀਤਾ ਗਿਆ"</string>
</resources>
diff --git a/java/res/values-pl/strings.xml b/java/res/values-pl/strings.xml
index 634a32d..40fe586 100644
--- a/java/res/values-pl/strings.xml
+++ b/java/res/values-pl/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Przypnij aplikację <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Odepnij: <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Edytuj"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # plik}few{{file_name} + # pliki}many{{file_name} + # plików}other{{file_name} + # pliku}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{i jeszcze # plik}few{i jeszcze # pliki}many{i jeszcze # plików}other{i jeszcze # pliku}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{I jeszcze # plik}few{I jeszcze # pliki}many{I jeszcze # plików}other{I jeszcze # pliku}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Udostępnianie tekstu"</string>
<string name="sharing_link" msgid="2307694372813942916">"Udostępnianie linku"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Udostępnianie obrazu}few{Udostępnianie # obrazów}many{Udostępnianie # obrazów}other{Udostępnianie # obrazu}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Udostępnianie filmu}few{Udostępnianie # filmów}many{Udostępnianie # filmów}other{Udostępnianie # filmu}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Udostępnianie # elementu}few{Udostępnianie # elementów}many{Udostępnianie # elementów}other{Udostępnianie # elementu}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Udostępnianie obrazu z tekstem"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Udostępnianie obrazu z linkiem"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Udostępnianie # pliku}few{Udostępnianie # plików}many{Udostępnianie # plików}other{Udostępnianie # pliku}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Udostępnianie obrazu przez SMS}few{Udostępnianie # obrazów przez SMS}many{Udostępnianie # obrazów przez SMS}other{Udostępnianie # obrazu przez SMS}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Udostępnianie obrazu przez link}few{Udostępnianie # obrazów przez link}many{Udostępnianie # obrazów przez link}other{Udostępnianie # obrazu przez link}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Udostępnianie filmu przez SMS}few{Udostępnianie # filmów przez SMS}many{Udostępnianie # filmów przez SMS}other{Udostępnianie # filmu przez SMS}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Udostępnianie filmu przez link}few{Udostępnianie # filmów przez link}many{Udostępnianie # filmów przez link}other{Udostępnianie # filmu przez link}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Udostępnianie pliku przez SMS}few{Udostępnianie # plików przez SMS}many{Udostępnianie # plików przez SMS}other{Udostępnianie # pliku przez SMS}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Udostępnianie pliku przez link}few{Udostępnianie # plików przez link}many{Udostępnianie # plików przez link}other{Udostępnianie # pliku przez link}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Tylko obraz}few{Tylko obrazy}many{Tylko obrazy}other{Tylko obrazy}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Tylko film}few{Tylko filmy}many{Tylko filmy}other{Tylko filmy}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Tylko plik}few{Tylko pliki}many{Tylko pliki}other{Tylko pliki}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura podglądu obrazu"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura podglądu filmu"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura podglądu pliku"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Brak polecanych osób, którym możesz udostępniać"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista aplikacji"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Ta aplikacja nie ma uprawnień do nagrywania, ale może rejestrować dźwięk za pomocą tego urządzenia USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Osobiste"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Służbowe"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Tych treści nie można otworzyć w aplikacjach służbowych"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Tych treści nie można udostępniać w aplikacjach osobistych"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Tych treści nie można otworzyć w aplikacjach osobistych"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Działanie profilu służbowego jest wstrzymane"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Kliknij, aby włączyć"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Aplikacje służbowe są wstrzymane"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Cofnij wstrzymanie"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Brak aplikacji służbowych"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Brak aplikacji osobistych"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Otworzyć aplikację <xliff:g id="APP">%s</xliff:g> w profilu osobistym?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Dołącz tekst"</string>
<string name="exclude_link" msgid="1332778255031992228">"Wyklucz link"</string>
<string name="include_link" msgid="827855767220339802">"Dołącz link"</string>
+ <string name="pinned" msgid="7623664001331394139">"Przypięte"</string>
</resources>
diff --git a/java/res/values-pt-rBR/strings.xml b/java/res/values-pt-rBR/strings.xml
index 8f1746f..ec52fd2 100644
--- a/java/res/values-pt-rBR/strings.xml
+++ b/java/res/values-pt-rBR/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Fixar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Liberar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Editar"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # arquivo}one{{file_name} + # arquivo}many{{file_name} + # arquivos}other{{file_name} + # arquivos}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{Mais # arquivo}one{Mais # arquivo}many{Mais # de arquivos}other{Mais # arquivos}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Mais # arquivo}one{Mais # arquivo}many{Mais # de arquivos}other{Mais # arquivos}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Compartilhando texto"</string>
<string name="sharing_link" msgid="2307694372813942916">"Compartilhando link"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Compartilhando imagem}one{Compartilhando # imagem}many{Compartilhando # de imagens}other{Compartilhando # imagens}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Compartilhando vídeo}one{Compartilhando # vídeo}many{Compartilhando # de vídeos}other{Compartilhando # vídeos}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Compartilhando # item}one{Compartilhando # item}many{Compartilhando # de itens}other{Compartilhando # itens}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Compartilhando imagem com texto"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Compartilhando imagem com link"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Compartilhando # arquivo}one{Compartilhando # arquivo}many{Compartilhando # de arquivos}other{Compartilhando # arquivos}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Compartilhando imagem com texto}one{Compartilhando # imagem com texto}many{Compartilhando # de imagens com texto}other{Compartilhando # imagens com texto}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Compartilhando imagem com link}one{Compartilhando # imagem com link}many{Compartilhando # de imagens com link}other{Compartilhando # imagens com link}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Compartilhando vídeo com texto}one{Compartilhando # vídeo com texto}many{Compartilhando # de vídeos com texto}other{Compartilhando # vídeos com texto}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Compartilhando vídeo com link}one{Compartilhando # vídeo com link}many{Compartilhando # de vídeos com link}other{Compartilhando # vídeos com link}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Compartilhando arquivo com texto}one{Compartilhando # arquivo com texto}many{Compartilhando # de arquivos com texto}other{Compartilhando # arquivos com texto}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Compartilhando arquivo com link}one{Compartilhando # arquivo com link}many{Compartilhando # de arquivos com link}other{Compartilhando # arquivos com link}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Somente imagem}one{Somente imagem}many{Somente imagens}other{Somente imagens}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Somente vídeo}one{Somente vídeo}many{Somente vídeos}other{Somente vídeos}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Somente arquivo}one{Somente arquivo}many{Somente arquivos}other{Somente arquivos}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura da prévia da imagem"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura da prévia do vídeo"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura da prévia do arquivo"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Não há sugestões de pessoas para compartilhar"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista de apps"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Este app não tem permissão de gravação, mas pode capturar áudio pelo dispositivo USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Pessoal"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Trabalho"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Não é possível abrir esse conteúdo com apps de trabalho"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Não é possível compartilhar esse conteúdo com apps pessoais"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Não é possível abrir esse conteúdo com apps pessoais"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"O perfil de trabalho está pausado"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Toque para ativar"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Os apps de trabalho foram pausados"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Reativar"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nenhum app de trabalho"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nenhum app pessoal"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Abrir o app <xliff:g id="APP">%s</xliff:g> no seu perfil pessoal?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Incluir texto"</string>
<string name="exclude_link" msgid="1332778255031992228">"Excluir link"</string>
<string name="include_link" msgid="827855767220339802">"Incluir link"</string>
+ <string name="pinned" msgid="7623664001331394139">"Fixada"</string>
</resources>
diff --git a/java/res/values-pt-rPT/strings.xml b/java/res/values-pt-rPT/strings.xml
index cc2bd47..c60b923 100644
--- a/java/res/values-pt-rPT/strings.xml
+++ b/java/res/values-pt-rPT/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Fixar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Soltar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Editar"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ficheiro}many{{file_name} + # ficheiros}other{{file_name} + # ficheiros}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ficheiro}many{+ # ficheiros}other{+ # ficheiros}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"A partilhar texto"</string>
- <string name="sharing_link" msgid="2307694372813942916">"A partilhar link"</string>
- <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{A partilhar imagem}many{A partilhar # imagens}other{A partilhar # imagens}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{E mais # ficheiro}many{E mais # ficheiros}other{E mais # ficheiros}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"Partilhar texto"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"Partilhar link"</string>
+ <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Partilhar imagem}many{Partilhar # imagens}other{Partilhar # imagens}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{A partilhar vídeo}many{A partilhar # vídeos}other{A partilhar # vídeos}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{A partilhar # item}many{A partilhar # itens}other{A partilhar # itens}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"A partilh. imag. c/ texto"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"A partilhar imag. c/ link"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{A partilhar # ficheiro}many{A partilhar # ficheiros}other{A partilhar # ficheiros}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{A partilhar imagem com texto}many{A partilhar # imagens com texto}other{A partilhar # imagens com texto}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{A partilhar imagem com link}many{A partilhar # imagens com link}other{A partilhar # imagens com link}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{A partilhar vídeo com texto}many{A partilhar # vídeos com texto}other{A partilhar # vídeos com texto}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{A partilhar vídeo com link}many{A partilhar # vídeos com link}other{A partilhar # vídeos com link}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{A partilhar ficheiro com texto}many{A partilhar # ficheiros com texto}other{A partilhar # ficheiros com texto}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{A partilhar ficheiro com link}many{A partilhar # ficheiros com link}other{A partilhar # ficheiros com link}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Apenas imagem}many{Apenas imagens}other{Apenas imagens}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Apenas vídeo}many{Apenas vídeos}other{Apenas vídeos}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Apenas ficheiro}many{Apenas ficheiros}other{Apenas ficheiros}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura de pré-visualização da imagem"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura de pré-visualização do vídeo"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura de pré-visualização do ficheiro"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Não existem pessoas recomendadas com quem partilhar"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista de aplicações"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Esta app não recebeu autorização de gravação, mas pode capturar áudio através deste dispositivo USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Pessoal"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Trabalho"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Não é possível abrir este conteúdo com apps de trabalho"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Não é possível partilhar este conteúdo com apps pessoais"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Não é possível abrir este conteúdo com apps pessoais"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Perfil de trabalho em pausa"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tocar para ativar"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"As apps de trabalho estão pausadas"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Retomar"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Sem apps de trabalho"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Sem apps pessoais"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Abrir a app <xliff:g id="APP">%s</xliff:g> no seu perfil pessoal?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Incluir texto"</string>
<string name="exclude_link" msgid="1332778255031992228">"Excluir link"</string>
<string name="include_link" msgid="827855767220339802">"Incluir link"</string>
+ <string name="pinned" msgid="7623664001331394139">"Afixada"</string>
</resources>
diff --git a/java/res/values-pt/strings.xml b/java/res/values-pt/strings.xml
index 8f1746f..ec52fd2 100644
--- a/java/res/values-pt/strings.xml
+++ b/java/res/values-pt/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Fixar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Liberar <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Editar"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # arquivo}one{{file_name} + # arquivo}many{{file_name} + # arquivos}other{{file_name} + # arquivos}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{Mais # arquivo}one{Mais # arquivo}many{Mais # de arquivos}other{Mais # arquivos}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Mais # arquivo}one{Mais # arquivo}many{Mais # de arquivos}other{Mais # arquivos}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Compartilhando texto"</string>
<string name="sharing_link" msgid="2307694372813942916">"Compartilhando link"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Compartilhando imagem}one{Compartilhando # imagem}many{Compartilhando # de imagens}other{Compartilhando # imagens}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Compartilhando vídeo}one{Compartilhando # vídeo}many{Compartilhando # de vídeos}other{Compartilhando # vídeos}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Compartilhando # item}one{Compartilhando # item}many{Compartilhando # de itens}other{Compartilhando # itens}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Compartilhando imagem com texto"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Compartilhando imagem com link"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Compartilhando # arquivo}one{Compartilhando # arquivo}many{Compartilhando # de arquivos}other{Compartilhando # arquivos}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Compartilhando imagem com texto}one{Compartilhando # imagem com texto}many{Compartilhando # de imagens com texto}other{Compartilhando # imagens com texto}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Compartilhando imagem com link}one{Compartilhando # imagem com link}many{Compartilhando # de imagens com link}other{Compartilhando # imagens com link}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Compartilhando vídeo com texto}one{Compartilhando # vídeo com texto}many{Compartilhando # de vídeos com texto}other{Compartilhando # vídeos com texto}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Compartilhando vídeo com link}one{Compartilhando # vídeo com link}many{Compartilhando # de vídeos com link}other{Compartilhando # vídeos com link}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Compartilhando arquivo com texto}one{Compartilhando # arquivo com texto}many{Compartilhando # de arquivos com texto}other{Compartilhando # arquivos com texto}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Compartilhando arquivo com link}one{Compartilhando # arquivo com link}many{Compartilhando # de arquivos com link}other{Compartilhando # arquivos com link}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Somente imagem}one{Somente imagem}many{Somente imagens}other{Somente imagens}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Somente vídeo}one{Somente vídeo}many{Somente vídeos}other{Somente vídeos}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Somente arquivo}one{Somente arquivo}many{Somente arquivos}other{Somente arquivos}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura da prévia da imagem"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura da prévia do vídeo"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura da prévia do arquivo"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Não há sugestões de pessoas para compartilhar"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista de apps"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Este app não tem permissão de gravação, mas pode capturar áudio pelo dispositivo USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Pessoal"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Trabalho"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Não é possível abrir esse conteúdo com apps de trabalho"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Não é possível compartilhar esse conteúdo com apps pessoais"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Não é possível abrir esse conteúdo com apps pessoais"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"O perfil de trabalho está pausado"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Toque para ativar"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Os apps de trabalho foram pausados"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Reativar"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nenhum app de trabalho"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nenhum app pessoal"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Abrir o app <xliff:g id="APP">%s</xliff:g> no seu perfil pessoal?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Incluir texto"</string>
<string name="exclude_link" msgid="1332778255031992228">"Excluir link"</string>
<string name="include_link" msgid="827855767220339802">"Incluir link"</string>
+ <string name="pinned" msgid="7623664001331394139">"Fixada"</string>
</resources>
diff --git a/java/res/values-ro/strings.xml b/java/res/values-ro/strings.xml
index 7296244..d6cae15 100644
--- a/java/res/values-ro/strings.xml
+++ b/java/res/values-ro/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Fixează <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Anulează fixarea pentru <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Editează"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fișier}few{{file_name} + # fișiere}other{{file_name} + # de fișiere}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fișier}few{+ # fișiere}other{+ # de fișiere}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Încă un fișier}few{Încă # fișiere}other{Încă # de fișiere}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Se trimite textul"</string>
<string name="sharing_link" msgid="2307694372813942916">"Se trimite linkul"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Se trimite imaginea}few{Se trimit # imagini}other{Se trimit # de imagini}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Se trimite videoclipul}few{Se trimit # videoclipuri}other{Se trimit # de videoclipuri}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Se trimite # element}few{Se trimit # elemente}other{Se trimit # de elemente}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Se trimite imaginea cu text"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Se trimite imaginea cu linkul"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Se trimite un fișier}few{Se trimit # fișiere}other{Se trimit # de fișiere}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Se trimite imaginea cu text}few{Se trimit # imagini cu text}other{Se trimit # de imagini cu text}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Se trimite imaginea cu linkul}few{Se trimit # imagini cu linkul}other{Se trimit # de imagini cu linkul}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Se trimite videoclipul cu text}few{Se trimit # videoclipuri cu text}other{Se trimit # de videoclipuri cu text}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Se trimite videoclipul cu linkul}few{Se trimit # videoclipuri cu linkul}other{Se trimit # de videoclipuri cu linkul}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Se trimite fișierul cu text}few{Se trimit # fișiere cu text}other{Se trimit # de fișiere cu text}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Se trimite fișierul cu linkul}few{Se trimit # fișiere cu linkul}other{Se trimit # de fișiere cu linkul}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Numai imaginea}few{Numai imaginile}other{Numai imaginile}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Numai videoclipul}few{Numai videoclipurile}other{Numai videoclipurile}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Numai fișierul}few{Numai fișierele}other{Numai fișierele}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatură pentru previzualizarea imaginii"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatură pentru previzualizarea videoclipului"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatură pentru previzualizarea fișierului"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nu există persoane recomandate pentru permiterea accesului"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista de aplicații"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Permisiunea de înregistrare nu a fost acordată aplicației, dar aceasta poate să înregistreze conținut audio prin intermediul acestui dispozitiv USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Serviciu"</string>
@@ -72,10 +81,10 @@
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blocat de administratorul IT"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Acest conținut nu poate fi trimis cu aplicații pentru lucru"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Acest conținut nu poate fi deschis cu aplicații pentru lucru"</string>
- <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Acest conținut nu poate fi trimis cu aplicații personale"</string>
+ <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Acest conținut nu poate fi trimis către aplicații personale"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Acest conținut nu poate fi deschis cu aplicații personale"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Profilul de serviciu este întrerupt"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Atinge pentru a activa"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Aplicațiile pentru lucru sunt întrerupte"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Reactivează"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nicio aplicație pentru lucru"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nicio aplicație personală"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Deschizi <xliff:g id="APP">%s</xliff:g> în profilul personal?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Include textul"</string>
<string name="exclude_link" msgid="1332778255031992228">"Exclude linkul"</string>
<string name="include_link" msgid="827855767220339802">"Include linkul"</string>
+ <string name="pinned" msgid="7623664001331394139">"Fixat"</string>
</resources>
diff --git a/java/res/values-ru/strings.xml b/java/res/values-ru/strings.xml
index 2db2c5e..618e0a6 100644
--- a/java/res/values-ru/strings.xml
+++ b/java/res/values-ru/strings.xml
@@ -53,29 +53,38 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Закрепить приложение \"<xliff:g id="LABEL">%1$s</xliff:g>\""</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Открепить приложение \"<xliff:g id="LABEL">%1$s</xliff:g>\""</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Изменить"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{\"{file_name}\" и ещё # файл}one{\"{file_name}\" и ещё # файл}few{\"{file_name}\" и ещё # файла}many{\"{file_name}\" и ещё # файлов}other{\"{file_name}\" и ещё # файла}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{и ещё # файл}one{и ещё # файл}few{и ещё # файла}many{и ещё # файлов}other{и ещё # файла}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ ещё # файл}one{+ ещё # файл}few{+ ещё # файла}many{+ ещё # файлов}other{+ ещё # файла}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Отправка сообщения"</string>
<string name="sharing_link" msgid="2307694372813942916">"Отправка ссылки"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Отправка изображения}one{Отправка # изображения}few{Отправка # изображений}many{Отправка # изображений}other{Отправка # изображения}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Отправка видео}one{Отправка # видео}few{Отправка # видео}many{Отправка # видео}other{Отправка # видео}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Отправка # объекта}one{Отправка # объекта}few{Отправка # объектов}many{Отправка # объектов}other{Отправка # объекта}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Сообщение с изображением"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Ссылка на изображение"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Предоставляется доступ к # файлу}one{Предоставляется доступ к # файлу}few{Предоставляется доступ к # файлам}many{Предоставляется доступ к # файлам}other{Предоставляется доступ к # файла}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Отправка изображения с текстом}one{Отправка # изображения с текстом}few{Отправка # изображений с текстом}many{Отправка # изображений с текстом}other{Отправка # изображения с текстом}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Отправка изображения со ссылкой}one{Отправка # изображения со ссылкой}few{Отправка # изображений со ссылкой}many{Отправка # изображений со ссылкой}other{Отправка # изображения со ссылкой}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Отправка видео с текстом}one{Отправка # видео с текстом}few{Отправка # видео с текстом}many{Отправка # видео с текстом}other{Отправка # видео с текстом}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Отправка видео со ссылкой}one{Отправка # видео со ссылкой}few{Отправка # видео со ссылкой}many{Отправка # видео со ссылкой}other{Отправка # видео со ссылкой}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Отправка файла с текстом}one{Отправка # файла с текстом}few{Отправка # файлов с текстом}many{Отправка # файлов с текстом}other{Отправка # файла с текстом}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Отправка файла со ссылкой}one{Отправка # файла со ссылкой}few{Отправка # файлов со ссылкой}many{Отправка # файлов со ссылкой}other{Отправка # файла со ссылкой}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Только изображение}one{Только изображения}few{Только изображения}many{Только изображения}other{Только изображения}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Только видео}one{Только видео}few{Только видео}many{Только видео}other{Только видео}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Только файл}one{Только файлы}few{Только файлы}many{Только файлы}other{Только файлы}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Значок предварительного просмотра изображения"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Значок предварительного просмотра видео"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Значок предварительного просмотра файла"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Рекомендованных получателей нет."</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Список приложений"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Приложению не разрешено записывать звук, однако оно может делать это с помощью этого USB-устройства."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Личное"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Рабочее"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Просмотр личных данных"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Просмотр рабочих данных"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Заблокировано вашим администратором"</string>
- <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Этот контент нельзя открывать через рабочие приложения."</string>
+ <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Этим контентом нельзя делиться с рабочими приложениями."</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Этот контент нельзя открыть в рабочем приложении."</string>
- <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Этот контент нельзя открывать через личные приложения."</string>
+ <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Этим контентом нельзя делиться с личными приложениями."</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Этот контент нельзя открыть в личном приложении."</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Действие рабочего профиля приостановлено."</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Нажмите, чтобы включить"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Рабочие приложения приостановлены"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Включить"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Не поддерживается рабочими приложениями."</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Не поддерживается личными приложениями."</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Открыть приложение \"<xliff:g id="APP">%s</xliff:g>\" в личном профиле?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Вернуть текст"</string>
<string name="exclude_link" msgid="1332778255031992228">"Исключить ссылку"</string>
<string name="include_link" msgid="827855767220339802">"Вернуть ссылку"</string>
+ <string name="pinned" msgid="7623664001331394139">"Закреплено"</string>
</resources>
diff --git a/java/res/values-si/strings.xml b/java/res/values-si/strings.xml
index bbb0107..176206e 100644
--- a/java/res/values-si/strings.xml
+++ b/java/res/values-si/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> අමුණන්න"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> ඇමුණුම ඉවත් කරන්න"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"සංස්කරණය"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + ගොනු #}one{{file_name} + ගොනු #}other{{file_name} + ගොනු #}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ගොනුවක්}one{ගොනු + #}other{ගොනු + #}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{තව + # ගොනුවක්}one{තව ගොනු + #}other{තව ගොනු + #}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"පෙළ බෙදා ගැනීම"</string>
<string name="sharing_link" msgid="2307694372813942916">"සබැඳිය බෙදා ගැනීම"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{රූපය බෙදා ගැනීම}one{රූප #ක් බෙදා ගැනීම}other{රූප #ක් බෙදා ගැනීම}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{වීඩියෝව බෙදා ගැනීම}one{වීඩියෝ #ක් බෙදා ගැනීම}other{වීඩියෝ #ක් බෙදා ගැනීම}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# අයිතමයක් බෙදා ගැනීම}one{අයිතම #ක් බෙදා ගැනීම}other{අයිතම #ක් බෙදා ගැනීම}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"පෙළ සමග රූපය බෙදා ගැනීම"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"සබැඳිය සමග රූපය බෙදාගැනීම"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ගොනුවක් බෙදා ගැනීම}one{ගොනු #ක් බෙදා ගැනීම}other{ගොනු #ක් බෙදා ගැනීම}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{පෙළ සමග රූපය බෙදා ගැනීම}one{පෙළ සමග රූප #ක් බෙදා ගැනීම}other{පෙළ සමග රූප #ක් බෙදා ගැනීම}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{සබැඳිය සමග රූපය බෙදා ගැනීම}one{සබැඳිය සමග රූප #ක් බෙදා ගැනීම}other{සබැඳිය සමග රූප #ක් බෙදා ගැනීම}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{පෙළ සමග වීඩියෝව බෙදා ගැනීම}one{පෙළ සමග වීඩියෝ #ක් බෙදා ගැනීම}other{පෙළ සමග වීඩියෝ #ක් බෙදා ගැනීම}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{සබැඳිය සමග වීඩියෝව බෙදා ගැනීම}one{සබැඳිය සමග වීඩියෝ #ක් බෙදා ගැනීම}other{සබැඳිය සමග වීඩියෝ #ක් බෙදා ගැනීම}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{පෙළ සමග ගොනුව බෙදා ගැනීම}one{පෙළ සමග ගොනු #ක් බෙදා ගැනීම}other{පෙළ සමග ගොනු #ක් බෙදා ගැනීම}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{සබැඳිය සමග ගොනුව බෙදා ගැනීම}one{සබැඳිය සමග ගොනු #ක් බෙදා ගැනීම}other{සබැඳිය සමග ගොනු #ක් බෙදා ගැනීම}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{රූපය පමණි}one{රූප පමණි}other{රූප පමණි}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{වීඩියෝව පමණි}one{වීඩියෝ පමණි}other{වීඩියෝ පමණි}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ගොනුව පමණි}one{ගොනු පමණි}other{ගොනු පමණි}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"රූප පෙරදසුන් සිඟිති රුව"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"වීඩියෝ පෙරදසුන් සිඟිති රුව"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"ගොනු පෙරදසුන් සිඟිති රුව"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"බෙදා ගැනීමට නිර්දේශිත පුද්ගලයන් නැත"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"යෙදුම් ලැයිස්තුව"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"මෙම යෙදුමට පටිගත කිරීම් අවසරයක් ලබා දී නොමැති නමුත් මෙම USB උපාංගය හරහා ශ්‍රව්‍ය ග්‍රහණය කර ගත හැකිය."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"පුද්ගලික"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"කාර්යාල"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"මෙම අන්තර්ගතය කාර්යාල යෙදුම් සමඟ විවෘත කළ නොහැකිය"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"මෙම අන්තර්ගතය පුද්ගලික යෙදුම් සමඟ බෙදා ගත නොහැකිය"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"මෙම අන්තර්ගතය පුද්ගලික යෙදුම් සමඟ විවෘත කළ නොහැකිය"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"කාර්යාල පැතිකඩ විරාම කර ඇත"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"ක්‍රියාත්මක කිරීමට තට්ටු කරන්න"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"කාර්යාල යෙදුම් විරාම කර ඇත"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"විරාම නොකරන්න"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"කාර්යාල යෙදුම් නැත"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"පුද්ගලික යෙදුම් නැත"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> ඔබගේ පුද්ගලික පැතිකඩ තුළ විවෘත කරන්නද?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"පාඨය ඇතළත් කරන්න"</string>
<string name="exclude_link" msgid="1332778255031992228">"සබැඳිය බැහැර කරන්න"</string>
<string name="include_link" msgid="827855767220339802">"සබැඳිය ඇතුළත් කරන්න"</string>
+ <string name="pinned" msgid="7623664001331394139">"අමුණා ඇත"</string>
</resources>
diff --git a/java/res/values-sk/strings.xml b/java/res/values-sk/strings.xml
index 7e96d4a..1ac43e6 100644
--- a/java/res/values-sk/strings.xml
+++ b/java/res/values-sk/strings.xml
@@ -53,20 +53,29 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Pripnúť aplikáciu <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Odopnúť <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Upraviť"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # súbor}few{{file_name} + # súbory}many{{file_name} + # files}other{{file_name} + # súborov}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # súbor}few{+ # súbory}many{+ # files}other{+ # súborov}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{a # ďalší súbor}few{a # ďalšie súbory}many{+ # more files}other{a # ďalších súborov}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Zdieľa sa textová správa"</string>
<string name="sharing_link" msgid="2307694372813942916">"Zdieľa sa odkaz"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Zdieľa sa obrázok}few{Zdieľajú sa # obrázky}many{Sharing # images}other{Zdieľa sa # obrázkov}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Zdieľa sa video}few{Zdieľajú sa # videá}many{Sharing # videos}other{Zdieľa sa # videí}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Zdieľa sa # položka}few{Zdieľajú sa # položky}many{Sharing # items}other{Zdieľa sa # položiek}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Zdieľa sa obr. s textom"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Zdieľa sa obr. s odkazom"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Zdieľa sa # súbor}few{Zdieľajú sa # súbory}many{Sharing # files}other{Zdieľa sa # súborov}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Zdieľa sa obrázok s textom}few{Zdieľajú sa # obrázky s textom}many{Sharing # images with text}other{Zdieľa sa # obrázkov s textom}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Zdieľa sa obrázok s odkazom}few{Zdieľajú sa # obrázky s odkazom}many{Sharing # images with link}other{Zdieľa sa # obrázkov s odkazom}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Zdieľa sa video s textom}few{Zdieľajú sa # videá s textom}many{Sharing # videos with text}other{Zdieľa sa # videí s textom}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Zdieľa sa video s odkazom}few{Zdieľajú sa # videá s odkazom}many{Sharing # videos with link}other{Zdieľa sa # videí s odkazom}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Zdieľa sa súbor s textom}few{Zdieľajú sa # súbory s textom}many{Sharing # files with text}other{Zdieľa sa # súborov s textom}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Zdieľa sa súbor s odkazom}few{Zdieľajú sa # súbory s odkazom}many{Sharing # files with link}other{Zdieľa sa # súborov s odkazom}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Iba obrázok}few{Iba obrázky}many{Iba obrázky}other{Iba obrázky}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Iba video}few{Iba videá}many{Iba videá}other{Iba videá}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Iba súbor}few{Iba súbory}many{Iba súbory}other{Iba súbory}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatúra ukážky obrázka"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatúra ukážky videa"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatúra ukážky súboru"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Žiadni odporúčaní príjemcovia"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Zoznam aplikácií"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Tejto aplikácii nebolo udelené povolenie na nahrávanie, ale môže nasnímať zvuk cez toto zariadenie USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Osobné"</string>
- <string name="resolver_work_tab" msgid="3588325717455216412">"Práca"</string>
+ <string name="resolver_work_tab" msgid="3588325717455216412">"Pracovné"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Osobné zobrazenie"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Pracovné zobrazenie"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokované vaším správcom IT"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Tento obsah sa nedá otvoriť pomocou pracovných aplikácií"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Tento obsah sa nedá zdieľať pomocou osobných aplikácií"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Tento obsah sa nedá otvoriť pomocou osobných aplikácií"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Pracovný profil je pozastavený"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Zapnúť klepnutím"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Pracovné aplikácie sú pozastavené"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Zrušiť pozastavenie"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Žiadne pracovné aplikácie"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Žiadne osobné aplikácie"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Chcete otvoriť <xliff:g id="APP">%s</xliff:g> v osobnom profile?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Zahrnúť text"</string>
<string name="exclude_link" msgid="1332778255031992228">"Vylúčiť odkaz"</string>
<string name="include_link" msgid="827855767220339802">"Zahrnúť odkaz"</string>
+ <string name="pinned" msgid="7623664001331394139">"Pripnuté"</string>
</resources>
diff --git a/java/res/values-sl/strings.xml b/java/res/values-sl/strings.xml
index b2aabdd..0ef8872 100644
--- a/java/res/values-sl/strings.xml
+++ b/java/res/values-sl/strings.xml
@@ -53,20 +53,29 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Pripni aplikacijo <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Odpni aplikacijo <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Uredi"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # datoteka}one{{file_name} + # datoteka}two{{file_name} + # datoteki}few{{file_name} + # datoteke}other{{file_name} + # datotek}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # datoteka}one{+ # datoteka}two{+ # datoteki}few{+ # datoteke}other{+ # datotek}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ še # datoteka}one{+ še # datoteka}two{+ še # datoteki}few{+ še # datoteke}other{+ še # datotek}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Deljenje besedila"</string>
<string name="sharing_link" msgid="2307694372813942916">"Deljenje povezave"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Deljenje slike}one{Deljenje # slike}two{Deljenje # slik}few{Deljenje # slik}other{Deljenje # slik}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Deljenje videoposnetka}one{Deljenje # videoposnetka}two{Deljenje # videoposnetkov}few{Deljenje # videoposnetkov}other{Deljenje # videoposnetkov}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Deljenje # elementa}one{Deljenje # elementa}two{Deljenje # elementov}few{Deljenje # elementov}other{Deljenje # elementov}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Deljenje slike z besedilom"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Deljenje slike s povezavo"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Deljenje # datoteke}one{Deljenje # datoteke}two{Deljenje # datotek}few{Deljenje # datotek}other{Deljenje # datotek}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Deljenje slike z besedilom}one{Deljenje # slike z besedilom}two{Deljenje # slik z besedilom}few{Deljenje # slik z besedilom}other{Deljenje # slik z besedilom}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Deljenje slike s povezavo}one{Deljenje # slike s povezavo}two{Deljenje # slik s povezavo}few{Deljenje # slik s povezavo}other{Deljenje # slik s povezavo}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Deljenje videoposnetka z besedilom}one{Deljenje # videoposnetka z besedilom}two{Deljenje # videoposnetkov z besedilom}few{Deljenje # videoposnetkov z besedilom}other{Deljenje # videoposnetkov z besedilom}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Deljenje videoposnetka s povezavo}one{Deljenje # videoposnetka s povezavo}two{Deljenje # videoposnetkov s povezavo}few{Deljenje # videoposnetkov s povezavo}other{Deljenje # videoposnetkov s povezavo}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Deljenje datoteke z besedilom}one{Deljenje # datoteke z besedilom}two{Deljenje # datotek z besedilom}few{Deljenje # datotek z besedilom}other{Deljenje # datotek z besedilom}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Deljenje datoteke s povezavo}one{Deljenje # datoteke s povezavo}two{Deljenje # datotek s povezavo}few{Deljenje # datotek s povezavo}other{Deljenje # datotek s povezavo}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Samo slika}one{Samo slike}two{Samo slike}few{Samo slike}other{Samo slike}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Samo videoposnetek}one{Samo videoposnetki}two{Samo videoposnetki}few{Samo videoposnetki}other{Samo videoposnetki}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Samo datoteka}one{Samo datoteke}two{Samo datoteke}few{Samo datoteke}other{Samo datoteke}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Sličica predogleda slike"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Sličica predogleda videoposnetka"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Sličica predogleda datoteke"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Ni priporočenih oseb za deljenje vsebine."</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Seznam aplikacij"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Ta aplikacija sicer nima dovoljenja za snemanje, vendar bi lahko zajemala zvok prek te naprave USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Osebno"</string>
- <string name="resolver_work_tab" msgid="3588325717455216412">"Služba"</string>
+ <string name="resolver_work_tab" msgid="3588325717455216412">"Delo"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Pogled osebnega profila"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Pogled delovnega profila"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokiral skrbnik za IT"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Te vsebine ni mogoče odpreti z delovnimi aplikacijami."</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Te vsebine ni mogoče deliti z osebnimi aplikacijami."</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Te vsebine ni mogoče odpreti z osebnimi aplikacijami."</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Delovni profil je začasno zaustavljen"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Dotaknite se za vklop"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Delovne aplikacije so začasno zaustavljene"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Znova aktiviraj"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nobena delovna aplikacija ni na voljo"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nobena osebna aplikacija"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Želite aplikacijo <xliff:g id="APP">%s</xliff:g> odpreti v osebnem profilu?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Vključi besedilo"</string>
<string name="exclude_link" msgid="1332778255031992228">"Izloči povezavo"</string>
<string name="include_link" msgid="827855767220339802">"Vključi povezavo"</string>
+ <string name="pinned" msgid="7623664001331394139">"Pripeto"</string>
</resources>
diff --git a/java/res/values-sq/strings.xml b/java/res/values-sq/strings.xml
index 37fb755..95c3e57 100644
--- a/java/res/values-sq/strings.xml
+++ b/java/res/values-sq/strings.xml
@@ -31,7 +31,7 @@
<string name="whichEditApplicationNamed" msgid="3150137489226219100">"Modifiko me <xliff:g id="APP">%1$s</xliff:g>"</string>
<string name="whichEditApplicationLabel" msgid="5992662938338600364">"Redakto"</string>
<string name="whichSendApplication" msgid="59510564281035884">"Ndaj"</string>
- <string name="whichSendApplicationNamed" msgid="495577664218765855">"Shpërndaj me <xliff:g id="APP">%1$s</xliff:g>"</string>
+ <string name="whichSendApplicationNamed" msgid="495577664218765855">"Ndaj me <xliff:g id="APP">%1$s</xliff:g>"</string>
<string name="whichSendApplicationLabel" msgid="2391198069286568035">"Ndaj"</string>
<string name="whichSendToApplication" msgid="2724450540348806267">"Dërgo me"</string>
<string name="whichSendToApplicationNamed" msgid="1996548940365954543">"Dërgo duke përdorur <xliff:g id="APP">%1$s</xliff:g>"</string>
@@ -53,29 +53,38 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Gozhdo \"<xliff:g id="LABEL">%1$s</xliff:g>\""</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Zhgozhdoje <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Modifiko"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # skedar}other{{file_name} + # skedarë}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # skedar}other{+ # skedarë}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # skedar tjetër}other{+ # skedarë të tjerë}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Po ndahet teksti"</string>
<string name="sharing_link" msgid="2307694372813942916">"Po ndahet lidhja"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Po ndahet imazh}other{Po ndahen # imazhe}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Po ndahet videoja}other{Po ndahen # video}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Po ndahet # artikull}other{Po ndahen # artikuj}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Po ndahet imazh me tekst"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Po ndahet imazh me lidhje"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Po ndahet # skedar}other{Po ndahen # skedarë}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Po ndahet një imazh me tekst}other{Po ndahen # imazhe me tekst}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Po ndahet një imazh me lidhje}other{Po ndahen # imazhe me lidhje}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Po ndahet një video me tekst}other{Po ndahen # video me tekst}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Po ndahet një video me lidhje}other{Po ndahen # video me lidhje}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Po ndahet një skedar me tekst}other{Po ndahen # skedarë me tekst}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Po ndahet një skedar me lidhje}other{Po ndahen # skedarë me lidhje}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Vetëm imazhi}other{Vetëm imazhet}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Vetëm videoja}other{Vetëm videot}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Vetëm skedari}other{Vetëm skedarët}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatura e pamjes paraprake të imazhit"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatura e pamjes paraprake të videos"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatura e pamjes paraprake të skedarit"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Nuk ka persona të rekomanduar për ta ndarë"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Lista e aplikacioneve"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Këtij aplikacioni nuk i është dhënë leje për regjistrim, por mund të regjistrojë audio përmes kësaj pajisjeje USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Puna"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Pamja personale"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Pamja e punës"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bllokuar nga administratori yt i teknologjisë së informacionit"</string>
- <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Kjo përmbajtje nuk mund të shpërndahet me aplikacione pune"</string>
+ <string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Kjo përmbajtje nuk mund të ndahet me aplikacione pune"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Kjo përmbajtje nuk mund të hapet me aplikacione pune"</string>
- <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Kjo përmbajtje nuk mund të shpërndahet me aplikacione personale"</string>
+ <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Kjo përmbajtje nuk mund të ndahet me aplikacione personale"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Kjo përmbajtje nuk mund të hapet me aplikacione personale"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Profili i punës është në pauzë"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Trokit për ta aktivizuar"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Aplikacionet e punës janë vendosur në pauzë"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Hiq nga pauza"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Nuk ka aplikacione pune"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Nuk ka aplikacione personale"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Të hapet <xliff:g id="APP">%s</xliff:g> në profilin tënd personal?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Përfshi tekstin"</string>
<string name="exclude_link" msgid="1332778255031992228">"Përjashto lidhjen"</string>
<string name="include_link" msgid="827855767220339802">"Përfshi lidhjen"</string>
+ <string name="pinned" msgid="7623664001331394139">"U gozhdua"</string>
</resources>
diff --git a/java/res/values-sr/strings.xml b/java/res/values-sr/strings.xml
index fb88164..511a129 100644
--- a/java/res/values-sr/strings.xml
+++ b/java/res/values-sr/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Закачите особу <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Откачи апликацију <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Измени"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # фајл}one{{file_name} + # фајл}few{{file_name} + # фајла}other{{file_name} + # фајлова}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{и још # фајл}one{и још # фајл}few{и још # фајла}other{и још # фајлова}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ још # фајл}one{+ још # фајл}few{+ још # фајла}other{+ још # фајлова}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Дели се текст"</string>
<string name="sharing_link" msgid="2307694372813942916">"Дели се линк"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Дели се слика}one{Дели се # слика}few{Деле се # слике}other{Дели се # слика}}"</string>
- <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Дели се видео}one{Дели се # видео}few{Деле се # видео снимка}other{Дели се # видео снимака}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Дели се # ставка}one{Дели се # ставка}few{Деле се # ставке}other{Дели се # ставки}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Дели се слика са текстом"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Дели се слика са линком"</string>
+ <string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Дели се видео}one{Дели се # видео}few{Деле се # видео снимка}other{Дели се # видеа}}"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Дели се # фајл}one{Дели се # фајл}few{Деле се # фајла}other{Дели се # фајлова}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Дели се слика са текстом}one{Дели се # слика са текстом}few{Деле се # слике са текстом}other{Дели се # слика са текстом}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Дели се слика са линком}one{Дели се # слика са линком}few{Деле се # слике са линком}other{Дели се # слика са линком}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Дели се видео са текстом}one{Дели се # видео са текстом}few{Деле се # видео снимка са текстом}other{Дели се # видеа са текстом}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Дели се видео са линком}one{Дели се # видео са линком}few{Деле се # видео снимка са линком}other{Дели се # видеа са линком}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Дели се фајл са текстом}one{Дели се # фајл са текстом}few{Деле се # фајла са текстом}other{Дели се # фајлова са текстом}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Дели се фајл са линком}one{Дели се # фајл са линком}few{Деле се # фајла са линком}other{Дели се # фајлова са линком}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Само слика}one{Само слике}few{Само слике}other{Само слике}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Само видео}one{Само видео снимци}few{Само видео снимци}other{Само видео снимци}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Само фајл}one{Само фајлови}few{Само фајлови}other{Само фајлови}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Сличица за преглед слике"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Сличица за преглед видеа"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Сличица за преглед фајла"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Нема препоручених људи за дељење"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Листа апликација"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Ова апликација нема дозволу за снимање, али би могла да снима звук помоћу овог USB уређаја."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Лично"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Пословно"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Овај садржај не може да се отвара помоћу пословних апликација"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Овај садржај не може да се дели помоћу личних апликација"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Овај садржај не може да се отвара помоћу личних апликација"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Пословни профил је паузиран"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Додирните да бисте укључили"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Пословне апликације су паузиране"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Поново активирај"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Нема пословних апликација"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Нема личних апликација"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Желите да на личном профилу отворите: <xliff:g id="APP">%s</xliff:g>?"</string>
@@ -84,6 +93,7 @@
<string name="miniresolver_use_work_browser" msgid="7892699758493230342">"Користи пословни прегледач"</string>
<string name="exclude_text" msgid="5508128757025928034">"Искључи текст"</string>
<string name="include_text" msgid="642280283268536140">"Уврсти текст"</string>
- <string name="exclude_link" msgid="1332778255031992228">"Искључи линк"</string>
+ <string name="exclude_link" msgid="1332778255031992228">"Изузми линк"</string>
<string name="include_link" msgid="827855767220339802">"Уврсти линк"</string>
+ <string name="pinned" msgid="7623664001331394139">"Закачено"</string>
</resources>
diff --git a/java/res/values-sv/strings.xml b/java/res/values-sv/strings.xml
index 37c7f68..7ed2d3f 100644
--- a/java/res/values-sv/strings.xml
+++ b/java/res/values-sv/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Fäst <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Lossa <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Redigera"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # fil}other{{file_name} + # filer}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # fil}other{+ # filer}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{och # fil till}other{och # filer till}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Delar text"</string>
- <string name="sharing_link" msgid="2307694372813942916">"Delar länk"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"Delningslänk"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Delar bild}other{Delar # bilder}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Delar video}other{Delar # videor}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Delar # objekt}other{Delar # objekt}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Delar bild med text"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Delar bild med länk"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Delar # fil}other{Delar # filer}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Delar bild med text}other{Delar # bilder med text}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Delar bild med länk}other{Delar # bilder med länk}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Delar video med text}other{Delar # videor med text}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Delar video med länk}other{Delar # videor med länk}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Delar fil med text}other{Delar # filer med text}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Delar fil med länk}other{Delar # filer med länk}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Endast bild}other{Endast bilder}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Endast video}other{Endast videor}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Endast fil}other{Endast filer}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Miniatyr av förhandsgranskning av bild"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Miniatyr av förhandsgranskning av video"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Miniatyr av förhandsgranskning av fil"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Inga rekommenderade personer att dela med"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Applista"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Appen har inte fått inspelningsbehörighet men kan spela in ljud via denna USB-enhet."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Privat"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Jobb"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Det här innehållet kan inte öppnas med jobbappar"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Det här innehållet kan inte delas med privata appar"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Det här innehållet kan inte öppnas med privata appar"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Jobbprofilen är pausad"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Tryck för att aktivera"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Jobbappar har pausats"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Återuppta"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Inga jobbappar"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Inga privata appar"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vill du öppna <xliff:g id="APP">%s</xliff:g> i din privata profil?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Inkludera text"</string>
<string name="exclude_link" msgid="1332778255031992228">"Uteslut länk"</string>
<string name="include_link" msgid="827855767220339802">"Inkludera länk"</string>
+ <string name="pinned" msgid="7623664001331394139">"Fäst"</string>
</resources>
diff --git a/java/res/values-sw/strings.xml b/java/res/values-sw/strings.xml
index f8aa1ea..de45a78 100644
--- a/java/res/values-sw/strings.xml
+++ b/java/res/values-sw/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Bandika <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Bandua <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Badilisha"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + faili #}other{{file_name} + faili #}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ faili #}other{+ faili #}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Faili nyingine #}other{Faili zingine #}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Inashiriki maandishi"</string>
<string name="sharing_link" msgid="2307694372813942916">"Inashiriki kiungo"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Inashiriki picha}other{Inashiriki picha #}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Inashiriki video}other{Inashiriki video #}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Inashiriki kipengee #}other{Inashiriki vipengee #}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Inashiriki picha na maandishi"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Inashiriki picha na kiungo"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Inashiriki faili #}other{Inashiriki faili #}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Inashiriki picha na maandishi}other{Inashiriki picha # na maandishi}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Inashiriki picha na kiungo}other{Inashiriki picha # na kiungo}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Inashiriki video na maandishi}other{Inashiriki video # na maandishi}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Inashiriki video na kiungo}other{Inashiriki video # na kiungo}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Inashiriki faili na maandishi}other{Inashiriki faili # na maandishi}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Inashiriki faili na kiungo}other{Inashiriki faili # na kiungo}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Picha pekee}other{Picha pekee}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Video pekee}other{Video pekee}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Faili pekee}other{Faili pekee}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Kijipicha cha onyesho la kukagua picha"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Kijipicha cha onyesho la kukagua video"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Kijipicha cha onyesho la kukagua faili"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Hujapendekezewa watu wa kushiriki nao"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Orodha ya programu"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Programu hii haijapewa ruhusa ya kurekodi lakini inaweza kurekodi sauti kupitia kifaa hiki cha USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Binafsi"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Kazini"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Huwezi kufungua maudhui haya ukitumia programu za kazini"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Huwezi kushiriki maudhui haya na programu za binafsi"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Huwezi kufungua maudhui haya ukitumia programu za binafsi"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Wasifu wa kazini umesimamishwa"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Gusa ili uwashe"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Programu za kazini zimesitishwa"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Acha kusitisha"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Hakuna programu za kazini"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Hakuna programu za binafsi"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Je, unataka kufungua <xliff:g id="APP">%s</xliff:g> katika wasifu wako binafsi?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Jumuisha maandishi"</string>
<string name="exclude_link" msgid="1332778255031992228">"Usijumuishe kiungo"</string>
<string name="include_link" msgid="827855767220339802">"Jumuisha kiungo"</string>
+ <string name="pinned" msgid="7623664001331394139">"Imebandikwa"</string>
</resources>
diff --git a/java/res/values-ta/strings.xml b/java/res/values-ta/strings.xml
index da13e7d..c95e5cb 100644
--- a/java/res/values-ta/strings.xml
+++ b/java/res/values-ta/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> ஆப்ஸைப் பின் செய்"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> ஐப் பின் நீக்கு"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"திருத்து"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ஃபைல்}other{{file_name} + # ஃபைல்கள்}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ஃபைல்}other{+ # ஃபைல்கள்}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"உரையைப் பகிர்கிறது"</string>
- <string name="sharing_link" msgid="2307694372813942916">"பகிர்வதற்கான இணைப்பு"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{மேலும் # ஃபைல்}other{மேலும் # ஃபைல்கள்}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"மெசேஜைப் பகிர்கிறது"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"இணைப்பைப் பகிர்கிறது"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{படத்தைப் பகிர்கிறது}other{# படங்களைப் பகிர்கிறது}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{வீடியோவைப் பகிர்கிறது}other{# வீடியோக்களை பகிர்கிறது}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# ஃபைலைப் பகிர்கிறது}other{# ஃபைல்களைப் பகிர்கிறது}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"உரையுடன் படம் பகிர்தல்"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"இணைப்புடன் படம் பகிர்தல்"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ஃபைலைப் பகிர்கிறது}other{# ஃபைல்களைப் பகிர்கிறது}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{வார்த்தைகளுடன் படத்தைப் பகிர்கிறது}other{வார்த்தைகளுடன் # படங்களைப் பகிர்கிறது}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{இணைப்பைக் கொண்ட படத்தைப் பகிர்கிறது}other{இணைப்பைக் கொண்ட # படங்களைப் பகிர்கிறது}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{வார்த்தைகளைக் கொண்ட வீடியோவைப் பகிர்கிறது}other{வார்த்தைகளைக் கொண்ட # வீடியோக்களைப் பகிர்கிறது}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{இணைப்புடன் வீடியோவைப் பகிர்கிறது}other{இணைப்புடன் # வீடியோக்களைப் பகிர்கிறது}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{வார்த்தைகளைக் கொண்ட ஃபைலைப் பகிர்கிறது}other{வார்த்தைகளைக் கொண்ட # ஃபைல்களைப் பகிர்கிறது}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{இணைப்பைக் கொண்ட ஃபைலைப் பகிர்கிறது}other{இணைப்பைக் கொண்ட # ஃபைல்களைப் பகிர்கிறது}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{படம் மட்டும்}other{படங்கள் மட்டும்}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{வீடியோ மட்டும்}other{வீடியோக்கள் மட்டும்}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ஃபைல் மட்டும்}other{ஃபைல்கள் மட்டும்}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"படத்தின் மாதிரிக்காட்சிச் சிறுபடம்"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"வீடியோவின் மாதிரிக்காட்சிச் சிறுபடம்"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"ஃபைலின் மாதிரிக்காட்சிச் சிறுபடம்"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"பகிர்வதற்கு எவரும் பரிந்துரைக்கப்படவில்லை"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ஆப்ஸ் பட்டியல்"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"இந்த ஆப்ஸிற்கு ரெக்கார்டு செய்வதற்கான அனுமதி வழங்கப்படவில்லை, எனினும் இந்த USB சாதனம் மூலம் ஆடியோவைப் பதிவுசெய்ய முடியும்."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"தனிப்பட்ட சுயவிவரம்"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"பணிச் சுயவிவரம்"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"பணி ஆப்ஸ் மூலம் இந்த உள்ளடக்கத்தைத் திறக்க முடியாது"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"தனிப்பட்ட ஆப்ஸுடன் இந்த உள்ளடக்கத்தைப் பகிர முடியாது"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"தனிப்பட்ட ஆப்ஸ் மூலம் இந்த உள்ளடக்கத்தைத் திறக்க முடியாது"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"பணிக் கணக்கு இடைநிறுத்தப்பட்டுள்ளது"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"ஆன் செய்யத் தட்டுக"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"பணி ஆப்ஸ் இடைநிறுத்தப்பட்டுள்ளன"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"மீண்டும் இயக்கு"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"பணி ஆப்ஸ் எதுவுமில்லை"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"தனிப்பட்ட ஆப்ஸ் எதுவுமில்லை"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"உங்கள் தனிப்பட்ட கணக்கில் <xliff:g id="APP">%s</xliff:g> ஆப்ஸைத் திறக்கவா?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"வார்த்தைகளைச் சேர்"</string>
<string name="exclude_link" msgid="1332778255031992228">"இணைப்பைத் தவிர்"</string>
<string name="include_link" msgid="827855767220339802">"இணைப்பைச் சேர்"</string>
+ <string name="pinned" msgid="7623664001331394139">"பின் செய்யப்பட்டுள்ளது"</string>
</resources>
diff --git a/java/res/values-te/strings.xml b/java/res/values-te/strings.xml
index 7f430eb..a8b9457 100644
--- a/java/res/values-te/strings.xml
+++ b/java/res/values-te/strings.xml
@@ -35,7 +35,7 @@
<string name="whichSendApplicationLabel" msgid="2391198069286568035">"షేర్ చేయి"</string>
<string name="whichSendToApplication" msgid="2724450540348806267">"దీన్ని ఉపయోగించి పంపండి"</string>
<string name="whichSendToApplicationNamed" msgid="1996548940365954543">"<xliff:g id="APP">%1$s</xliff:g> యాప్‌ను ఉపయోగించి పంపండి"</string>
- <string name="whichSendToApplicationLabel" msgid="6909037198280591110">"పంపు"</string>
+ <string name="whichSendToApplicationLabel" msgid="6909037198280591110">"పంపండి"</string>
<string name="whichHomeApplication" msgid="8797832422254564739">"హోమ్ యాప్‌ను ఎంచుకోండి"</string>
<string name="whichHomeApplicationNamed" msgid="3943122502791761387">"<xliff:g id="APP">%1$s</xliff:g> యాప్‌ను హోమ్ పేజీగా ఉపయోగించండి"</string>
<string name="whichHomeApplicationLabel" msgid="2066319585322981524">"చిత్రాన్ని క్యాప్చర్ చేయి"</string>
@@ -53,29 +53,38 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g>‌ను పిన్ చేయండి"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g>ను అన్‌పిన్ చేయి"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"ఎడిట్ చేయండి"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ఫైల్}other{{file_name} + # ఫైల్స్}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ఫైల్}other{+ # ఫైల్స్}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ మరో # ఫైల్}other{+ మరో # ఫైల్స్}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"టెక్స్ట్‌ను షేర్ చేయడం"</string>
<string name="sharing_link" msgid="2307694372813942916">"లింక్‌ను షేర్ చేయడం"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{ఇమేజ్‌ను షేర్ చేయడం}other{# ఇమేజ్‌లను షేర్ చేయడం}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{వీడియోను షేర్ చేయడం}other{# వీడియోలను షేర్ చేయడం}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# ఐటెమ్‌ను షేర్ చేయడం}other{# ఐటెమ్‌లను షేర్ చేయడం}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"టెక్స్ట్‌తో ఇమేజ్‌ను షేర్ చేయడం"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"లింక్‌తో ఇమేజ్‌ను షేర్ చేయడం"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ఫైల్‌ను షేర్ చేస్తోంది}other{# ఫైళ్లను షేర్ చేస్తోంది}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{టెక్స్ట్ మెసేజ్ పంపడం ద్వారా ఇమేజ్‌ను షేర్ చేయడం}other{టెక్స్ట్ మెసేజ్ పంపడం ద్వారా # ఇమేజ్‌లను షేర్ చేయడం}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{లింక్ చేయడం ద్వారా ఇమేజ్‌ను షేర్ చేయడం}other{లింక్ చేయడం ద్వారా # ఇమేజ్‌లను షేర్ చేయడం}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{టెక్స్ట్ మెసేజ్ పంపడం ద్వారా వీడియోను షేర్ చేయడం}other{టెక్స్ట్ మెసేజ్ పంపడం ద్వారా # వీడియోలను షేర్ చేయడం}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{లింక్ చేయడం ద్వారా వీడియోను షేర్ చేయడం}other{లింక్ చేయడం ద్వారా # వీడియోలను షేర్ చేయడం}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{టెక్స్ట్ మెసేజ్ పంపడం ద్వారా ఫైల్‌ను షేర్ చేయడం}other{టెక్స్ట్ మెసేజ్ పంపడం ద్వారా # ఫైల్స్‌ను షేర్ చేయడం}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{లింక్ చేయడం ద్వారా ఫైల్‌ను షేర్ చేయడం}other{లింక్ చేయడం ద్వారా # ఫైల్స్‌ను షేర్ చేయడం}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{ఇమేజ్ మాత్రమే}other{ఇమేజ్‌లు మాత్రమే}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{వీడియో మాత్రమే}other{వీడియోలు మాత్రమే}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ఫైల్ మాత్రమే}other{ఫైళ్లు మాత్రమే}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"ఇమేజ్ ప్రివ్యూ థంబ్‌నెయిల్"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"వీడియో ప్రివ్యూ థంబ్‌నెయిల్"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"ఫైల్ ప్రివ్యూ థంబ్‌నెయిల్"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ఎవరికి షేర్ చేయాలనే దానికి సంబంధించి సిఫార్సులేవీ లేవు"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"యాప్‌ల లిస్ట్‌"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"ఈ యాప్‌కు రికార్డ్ చేసే అనుమతి మంజూరు కాలేదు, అయినా ఈ USB పరికరం ద్వారా ఆడియోను క్యాప్చర్ చేయగలదు."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"వ్యక్తిగతం"</string>
- <string name="resolver_work_tab" msgid="3588325717455216412">"ఆఫీస్"</string>
+ <string name="resolver_work_tab" msgid="3588325717455216412">"వర్క్ ప్లేస్"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"వ్యక్తిగత వీక్షణ"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"పని వీక్షణ"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"మీ IT అడ్మిన్ ద్వారా బ్లాక్ చేయబడింది"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"ఈ కంటెంట్ వర్క్ యాప్‌తో షేర్ చేయడం సాధ్యం కాదు"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"ఈ కంటెంట్ వర్క్ యాప్‌తో తెరవడం సాధ్యం కాదు"</string>
- <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ఈ కంటెంట్ వ్యక్తిగత యాప్‌తో షేర్ చేయడం సాధ్యం కాదు"</string>
+ <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"ఈ కంటెంట్‌ను వ్యక్తిగత యాప్స్ లోకి షేర్ చేయడం సాధ్యం కాదు"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"ఈ కంటెంట్ వ్యక్తిగత యాప్‌తో తెరవడం సాధ్యం కాదు"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"వర్క్ ప్రొఫైల్ పాజ్ చేయబడింది"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"ఆన్ చేయడానికి ట్యాప్ చేయండి"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"వర్క్ యాప్‌లు పాజ్ చేయబడ్డాయి"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"అన్‌పాజ్ చేయండి"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"వర్క్ యాప్‌లు లేవు"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"వ్యక్తిగత యాప్‌లు లేవు"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g>ను మీ వ్యక్తిగత ప్రొఫైల్‌లో తెరవాలా?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"టెక్స్ట్‌ను చేర్చండి"</string>
<string name="exclude_link" msgid="1332778255031992228">"లింక్‌ను మినహాయించండి"</string>
<string name="include_link" msgid="827855767220339802">"లింక్‌ను చేర్చండి"</string>
+ <string name="pinned" msgid="7623664001331394139">"పిన్ చేయబడింది"</string>
</resources>
diff --git a/java/res/values-th/strings.xml b/java/res/values-th/strings.xml
index 7051984..af91064 100644
--- a/java/res/values-th/strings.xml
+++ b/java/res/values-th/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"ปักหมุด <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"เลิกปักหมุด <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"แก้ไข"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ไฟล์}other{{file_name} + # ไฟล์}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{อีก # ไฟล์}other{อีก # ไฟล์}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{อีก # ไฟล์}other{อีก # ไฟล์}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"กำลังแชร์ข้อความ"</string>
<string name="sharing_link" msgid="2307694372813942916">"กำลังแชร์ลิงก์"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{กำลังแชร์รูปภาพ}other{กำลังแชร์รูปภาพ # รายการ}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{กำลังแชร์วิดีโอ}other{กำลังแชร์วิดีโอ # รายการ}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{กำลังแชร์ # รายการ}other{กำลังแชร์ # รายการ}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"กำลังแชร์รูปภาพพร้อมข้อความ"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"กำลังแชร์รูปภาพพร้อมลิงก์"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{กำลังจะแชร์ # ไฟล์}other{กำลังจะแชร์ # ไฟล์}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{กำลังแชร์รูปภาพพร้อมข้อความ}other{กำลังแชร์รูปภาพ # รายการพร้อมข้อความ}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{กำลังแชร์รูปภาพพร้อมลิงก์}other{กำลังแชร์รูปภาพ # รายการพร้อมลิงก์}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{กำลังแชร์วิดีโอพร้อมข้อความ}other{กำลังแชร์วิดีโอ # รายการพร้อมข้อความ}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{กำลังแชร์วิดีโอพร้อมลิงก์}other{กำลังแชร์วิดีโอ # รายการพร้อมลิงก์}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{กำลังแชร์ไฟล์พร้อมข้อความ}other{กำลังแชร์ไฟล์ # รายการพร้อมข้อความ}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{กำลังแชร์ไฟล์พร้อมลิงก์}other{กำลังแชร์ไฟล์ # รายการพร้อมลิงก์}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{รูปภาพเท่านั้น}other{รูปภาพเท่านั้น}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{วิดีโอเท่านั้น}other{วิดีโอเท่านั้น}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{ไฟล์เท่านั้น}other{ไฟล์เท่านั้น}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"ภาพตัวอย่างขนาดย่อของรูปภาพ"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"ภาพตัวอย่างขนาดย่อของวิดีโอ"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"ภาพตัวอย่างขนาดย่อของไฟล์"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"ไม่พบใครที่แนะนำให้แชร์ด้วย"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"รายชื่อแอป"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"แอปนี้ไม่ได้รับอนุญาตให้บันทึกเสียงแต่อาจเก็บเสียงผ่านอุปกรณ์ USB นี้ได้"</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"ส่วนตัว"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"งาน"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"เปิดเนื้อหานี้โดยใช้แอปงานไม่ได้"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"แชร์เนื้อหานี้โดยใช้แอปส่วนตัวไม่ได้"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"เปิดเนื้อหานี้โดยใช้แอปส่วนตัวไม่ได้"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"โปรไฟล์งานหยุดชั่วคราว"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"แตะเพื่อเปิด"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"แอปงานหยุดชั่วคราว"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"ยกเลิกการหยุดชั่วคราว"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"ไม่มีแอปงาน"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"ไม่มีแอปส่วนตัว"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"เปิด <xliff:g id="APP">%s</xliff:g> ในโปรไฟล์ส่วนตัวไหม"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"รวมข้อความ"</string>
<string name="exclude_link" msgid="1332778255031992228">"ไม่รวมลิงก์"</string>
<string name="include_link" msgid="827855767220339802">"รวมลิงก์"</string>
+ <string name="pinned" msgid="7623664001331394139">"ปักหมุดไว้"</string>
</resources>
diff --git a/java/res/values-tl/strings.xml b/java/res/values-tl/strings.xml
index b7c50d4..cb4ff65 100644
--- a/java/res/values-tl/strings.xml
+++ b/java/res/values-tl/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"I-pin ang <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"I-unpin ang <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"I-edit"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # file}one{{file_name} + # file}other{{file_name} + # na file}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # file}one{+ # file}other{+ # na file}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # pang file}one{+ # pang file}other{+ # pang file}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Ibinabahagi ang text"</string>
<string name="sharing_link" msgid="2307694372813942916">"Ibinabahagi ang link"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Ibinabahagi ang larawan}one{Ibinabahagi ang # larawan}other{Ibinabahagi ang # na larawan}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Ibinabahagi ang video}one{Ibinabahagi ang # video}other{Ibinabahagi ang # na video}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Ibinabahagi ang # item}one{Ibinabahagi ang # item}other{Ibinabahagi ang # na item}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Larawang may text"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Larawang may link"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Nagshe-share ng # file}one{Nagshe-share ng # file}other{Nagshe-share ng # na file}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Nagbabahagi ng larawang may text}one{Nagbabahagi ng # larawang may text}other{Nagbabahagi ng # na larawang may text}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Nagbabahagi ng larawang may link}one{Nagbabahagi ng # larawang may link}other{Nagbabahagi ng # na larawang may link}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Nagbabahagi ng video na may text}one{Nagbabahagi ng # video na may text}other{Nagbabahagi ng # na video na may text}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Nagbabahagi ng video na may link}one{Nagbabahagi ng # video na may link}other{Nagbabahagi ng # na video na may link}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Nagbabahagi ng file na may text}one{Nagbabahagi ng # file na may text}other{Nagbabahagi ng # na file na may text}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Nagbabahagi ng file na may link}one{Nagbabahagi ng # file na may link}other{Nagbabahagi ng # na file na may link}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Larawan lang}one{Mga larawan lang}other{Mga larawan lang}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Video lang}one{Mga video lang}other{Mga video lang}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{File lang}one{Mga file lang}other{Mga file lang}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Thumbnail ng preview ng larawan"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Thumbnail ng preview ng video"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Thumbnail ng preview ng file"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Walang inirerekomendang taong mapagbabahagian"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Listahan ng mga app"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Hindi nabigyan ng pahintulot ang app na ito para mag-record pero nakakapag-capture ito ng audio sa pamamagitan ng USB device na ito."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Personal"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Trabaho"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Hindi puwedeng buksan sa mga app para sa trabaho ang content na ito"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Hindi puwedeng ibahagi sa mga personal na app ang content na ito"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Hindi puwedeng buksan sa mga personal na app ang content na ito"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Naka-pause ang profile sa trabaho"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"I-tap para i-on"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Naka-pause ang mga app para sa trabaho"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"I-unpause"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Walang app para sa trabaho"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Walang personal na app"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Buksan ang <xliff:g id="APP">%s</xliff:g> sa iyong personal na profile?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Isama ang text"</string>
<string name="exclude_link" msgid="1332778255031992228">"Huwag isama ang link"</string>
<string name="include_link" msgid="827855767220339802">"Isama ang link"</string>
+ <string name="pinned" msgid="7623664001331394139">"Naka-pin"</string>
</resources>
diff --git a/java/res/values-tr/strings.xml b/java/res/values-tr/strings.xml
index 7116871..53d74bb 100644
--- a/java/res/values-tr/strings.xml
+++ b/java/res/values-tr/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Şunu sabitle: <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> uygulamasının sabitlemesini kaldır"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Düzenle"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # dosya}other{{file_name} + # dosya}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # dosya}other{+ # dosya}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # dosya daha}other{+ # dosya daha}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Metin paylaşılıyor"</string>
<string name="sharing_link" msgid="2307694372813942916">"Bağlantı paylaşılıyor"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Resim paylaşılıyor}other{# resim paylaşılıyor}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Video paylaşılıyor}other{# video paylaşılıyor}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# öğe paylaşılıyor}other{# öğe paylaşılıyor}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Metin ekli resim paylaşılıyor"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Bağlantı ekli resim paylaşılıyor"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# dosya paylaşılıyor}other{# dosya paylaşılıyor}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Metin ekli resim paylaşılıyor}other{Metin ekli # resim paylaşılıyor}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Bağlantı ekli resim paylaşılıyor}other{Bağlantı ekli # resim paylaşılıyor}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Metin ekli video paylaşılıyor}other{Metin ekli # video paylaşılıyor}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Bağlantı ekli video paylaşılıyor}other{Bağlantı ekli # video paylaşılıyor}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Metin ekli dosya paylaşılıyor}other{Metin ekli # dosya paylaşılıyor}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Bağlantı ekli dosya paylaşılıyor}other{Bağlantı ekli # dosya paylaşılıyor}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Yalnızca resim}other{Yalnızca resimler}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Yalnızca video}other{Yalnızca videolar}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Yalnızca dosya}other{Yalnızca dosyalar}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Resim önizleme küçük resmi"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Video önizleme küçük resmi"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Dosya önizleme küçük resmi"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Paylaşmak için önerilen kullanıcı yok"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Uygulama listesi"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Bu uygulamaya ses kaydetme izni verilmedi ancak bu USB cihazı üzerinden sesleri yakalayabilir."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Kişisel"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"İş"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Bu içerik, iş uygulamalarıyla açılamaz"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Bu içerik, kişisel uygulamalarla paylaşılamaz"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Bu içerik, kişisel uygulamalarla açılamaz"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"İş profili duraklatıldı"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Açmak için dokunun"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"İş uygulamaları duraklatıldı"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Devam ettir"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"İş uygulaması yok"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Kişisel uygulama yok"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> uygulaması kişisel profilinizde açılsın mı?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Metni dahil et"</string>
<string name="exclude_link" msgid="1332778255031992228">"Bağlantıyı hariç tut"</string>
<string name="include_link" msgid="827855767220339802">"Bağlantıyı dahil et"</string>
+ <string name="pinned" msgid="7623664001331394139">"Sabitlendi"</string>
</resources>
diff --git a/java/res/values-uk/strings.xml b/java/res/values-uk/strings.xml
index 8a74466..f9d810a 100644
--- a/java/res/values-uk/strings.xml
+++ b/java/res/values-uk/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Закріпити додаток <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Відкріпити додаток <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Редагувати"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} і ще # файл}one{{file_name} і ще # файл}few{{file_name} і ще # файли}many{{file_name} і ще # файлів}other{{file_name} і ще # файлу}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{і ще # файл}one{і ще # файл}few{і ще # файли}many{і ще # файлів}other{і ще # файлу}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{і ще # файл}one{і ще # файл}few{і ще # файли}many{і ще # файлів}other{і ще # файлу}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Надсилається текст"</string>
<string name="sharing_link" msgid="2307694372813942916">"Надсилається посилання"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Надсилається зображення}one{Надсилається # зображення}few{Надсилаються # зображення}many{Надсилаються # зображень}other{Надсилається # зображення}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Надсилається відео}one{Надсилається # відео}few{Надсилаються # відео}many{Надсилаються # відео}other{Надсилається # відео}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Надсилається # об’єкт}one{Надсилається # об’єкт}few{Надсилаються # об’єкти}many{Надсилаються # об’єктів}other{Надсилається # об’єкта}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Надсил. зображ. з текстом"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Надсил. зображ. з посил."</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Надсилається # файл}one{Надсилається # файл}few{Надсилаються # файли}many{Надсилаються # файлів}other{Надсилається # файлу}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Надсилання зображення з текстом}one{Надсилання # зображення з текстом}few{Надсилання # зображень із текстом}many{Надсилання # зображень із текстом}other{Надсилання # зображення з текстом}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Надсилання зображення з посиланням}one{Надсилання # зображення з посиланням}few{Надсилання # зображень із посиланням}many{Надсилання # зображень із посиланням}other{Надсилання # зображення з посиланням}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Надсилання відео з текстом}one{Надсилання # відео з текстом}few{Надсилання # відео з текстом}many{Надсилання # відео з текстом}other{Надсилання # відео з текстом}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Надсилання відео з посиланням}one{Надсилання # відео з посиланням}few{Надсилання # відео з посиланням}many{Надсилання # відео з посиланням}other{Надсилання # відео з посиланням}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Надсилання файлу з текстом}one{Надсилання # файлу з текстом}few{Надсилання # файлів із текстом}many{Надсилання # файлів із текстом}other{Надсилання # файлу з текстом}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Надсилання файлу з посиланням}one{Надсилання # файлу з посиланням}few{Надсилання # файлів із посиланням}many{Надсилання # файлів із посиланням}other{Надсилання # файлу з посиланням}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Лише зображення}one{Лише зображення}few{Лише зображення}many{Лише зображення}other{Лише зображення}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Лише відео}one{Лише відео}few{Лише відео}many{Лише відео}other{Лише відео}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Лише файл}one{Лише файли}few{Лише файли}many{Лише файли}other{Лише файли}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Зображення для попереднього перегляду фото чи малюнка"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Зображення для попереднього перегляду відео"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Зображення для попереднього перегляду файлу"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Немає рекомендацій про те, з ким поділитися"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Список додатків"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Цей додаток не має дозволу на запис, але він може фіксувати звук через цей USB-пристрій."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Особисте"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Робоче"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Цей контент не можна відкривати в робочих додатках"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Цим контентом не можна ділитися в особистих додатках"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Цей контент не можна відкривати в особистих додатках"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Робочий профіль призупинено"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Торкніться, щоб увімкнути"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Робочі додатки призупинено"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Увімкнути знову"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Немає робочих додатків"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Немає особистих додатків"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Відкрити додаток <xliff:g id="APP">%s</xliff:g> в особистому профілі?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Додати текст"</string>
<string name="exclude_link" msgid="1332778255031992228">"Вилучити посилання"</string>
<string name="include_link" msgid="827855767220339802">"Додати посилання"</string>
+ <string name="pinned" msgid="7623664001331394139">"Закріплено"</string>
</resources>
diff --git a/java/res/values-ur/strings.xml b/java/res/values-ur/strings.xml
index 493ffef..6a101d9 100644
--- a/java/res/values-ur/strings.xml
+++ b/java/res/values-ur/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"<xliff:g id="LABEL">%1$s</xliff:g> کو پن کریں"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"<xliff:g id="LABEL">%1$s</xliff:g> سے پن ہٹائیں"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"ترمیم کریں"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # فائل}other{{file_name} + # فائلز}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # فائل}other{+ # فائلز}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ #‏1 مزید فائل}other{+ # مزید فائلز}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"ٹیکسٹ کا اشتراک کیا جا رہا ہے"</string>
<string name="sharing_link" msgid="2307694372813942916">"لنک کا اشتراک کیا جا رہا ہے"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{تصویر کا اشتراک کیا جا رہا ہے}other{# تصاویر کا اشتراک کیا جا رہا ہے}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{ویڈیو کا اشتراک کیا جا رہا ہے}other{# ویڈیوز کا اشتراک کیا جا رہا ہے}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# آئٹم کا اشتراک کیا جا رہا ہے}other{# آئٹمز کا اشتراک کیا جا رہا ہے}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"ٹیکسٹ کے ساتھ تصویر کا اشتراک کیا جا رہا ہے"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"لنک کے ساتھ تصویر کا اشتراک کیا جا رہا ہے"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# فائل کا اشتراک کیا جا رہا ہے}other{# فائلز کا اشتراک کیا جا رہا ہے}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{ٹیکسٹ کے ساتھ تصویر کا اشتراک کیا جا رہا ہے}other{ٹیکسٹ کے ساتھ # تصاویر کا اشتراک کیا جا رہا ہے}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{لنک کے ساتھ تصویر کا اشتراک کیا جا رہا ہے}other{لنک کے ساتھ # تصاویر کا اشتراک کیا جا رہا ہے}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{ٹیکسٹ کے ساتھ ویڈیو کا اشتراک کیا جا رہا ہے}other{ٹیکسٹ کے ساتھ # ویڈیوز کا اشتراک کیا جا رہا ہے}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{لنک کے ساتھ ویڈیو کا اشتراک کیا جا رہا ہے}other{لنک کے ساتھ # ویڈیوز کا اشتراک کیا جا رہا ہے}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{ٹیکسٹ کے ساتھ فائل کا اشتراک کیا جا رہا ہے}other{ٹیکسٹ کے ساتھ # فائلز کا اشتراک کیا جا رہا ہے}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{لنک کے ساتھ فائل کا اشتراک کیا جا رہا ہے}other{لنک کے ساتھ # فائلز کا اشتراک کیا جا رہا ہے}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{صرف تصویر}other{صرف تصاویر}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{صرف ویڈیو}other{صرف ویڈیوز}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{صرف فائل}other{صرف فائلز}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"تصویر کے پیش منظر کا تھمب نیل"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"ویڈیو کے پیش منظر کا تھمب نیل"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"فائل کے پیش منظر کا تھمب نیل"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"اشتراک کرنے کے لیے کوئی تجویز کردہ لوگ نہیں"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"ایپس کی فہرست"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"‏اس ایپ کو ریکارڈ کرنے کی اجازت عطا نہیں کی گئی ہے مگر اس USB آلہ کے ذریعے آڈیو کیپچر کر سکتی ہے۔"</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"ذاتی"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"دفتر"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"اس مواد کو ورک ایپس کے ساتھ نہیں کھولا جا سکتا"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"اس مواد کا اشتراک ذاتی ایپس کے ساتھ نہیں کیا جا سکتا"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"اس مواد کو ذاتی ایپس کے ساتھ نہیں کھولا جا سکتا"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"دفتری پروفائل روک دی گئی ہے"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"آن کرنے کیلئے تھپتھپائیں"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"ورک ایپس موقوف ہیں"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"غیر موقوف کریں"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"کوئی ورک ایپ نہیں"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"کوئی ذاتی ایپ نہیں"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"اپنی ذاتی پروفائل میں <xliff:g id="APP">%s</xliff:g> کھولیں؟"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"ٹیکسٹ شامل کریں"</string>
<string name="exclude_link" msgid="1332778255031992228">"لنک خارج کریں"</string>
<string name="include_link" msgid="827855767220339802">"لنک شامل کریں"</string>
+ <string name="pinned" msgid="7623664001331394139">"پن کردہ"</string>
</resources>
diff --git a/java/res/values-uz/strings.xml b/java/res/values-uz/strings.xml
index 2596c7c..24249f5 100644
--- a/java/res/values-uz/strings.xml
+++ b/java/res/values-uz/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Mahkamlash: <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Yechib olish: <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Tahrirlash"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # ta fayl}other{{file_name} + # ta fayl}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # ta fayl}other{+ # ta fayl}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ yana # ta fayl}other{+ yana # ta fayl}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Matn ulashilmoqda"</string>
<string name="sharing_link" msgid="2307694372813942916">"Havola ulashilmoqda"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Rasm ulashilmoqda}other{# ta rasm ulashilmoqda}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Video ulashilmoqda}other{# ta video ulashilmoqda}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{# ta fayl ulashilmoqda}other{# ta fayl ulashilmoqda}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Matnli havola ulashilmoqda"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Havolali rasm ulashilmoqda"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{# ta fayl ulashilmoqda}other{# ta fayl ulashilmoqda}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Matnli havolani yuborish}other{# ta matnli havolani yuborish}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Havolali rasmni yuborish}other{# ta havolali rasmni yuborish}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Matnli videoni yuborish}other{# ta matnli videoni yuborish}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Havolali videoni yuborish}other{# ta havolali videoni yuborish}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Matnli faylni yuborish}other{# ta matnli faylni yuborish}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Havolali faylni yuborish}other{# ta havolali faylni yuborish}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Faqat rasm}other{Faqat rasmlar}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Faqat video}other{Faqat videolar}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Faqat fayl}other{Faqat fayllar}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Rasmga razm solish eskizi"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Videoga razm solish eskizi"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Faylga razm solish eskizi"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Ulashish uchun hech kim tavsiya qilinmagan"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Ilovalar roʻyxati"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Bu ilovaga yozib olish ruxsati berilmagan, lekin shu USB orqali ovozlarni yozib olishi mumkin."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Shaxsiy"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Ish"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Bu kontent ishga oid ilovalar bilan ochilmaydi"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Bu kontent shaxsiy ilovalar bilan ulashilmaydi"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Bu kontent shaxsiy ilovalar bilan ochilmaydi"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Ish profili pauzada"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Yoqish uchun bosing"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Ishga oid ilovalar pauza qilingan"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Davom ettirish"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Ishga oid ilovalar topilmadi"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Shaxsiy ilovalar topilmadi"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> shaxsiy profilda ochilsinmi?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Matnni kiritish"</string>
<string name="exclude_link" msgid="1332778255031992228">"Havolani chiqarib tashlash"</string>
<string name="include_link" msgid="827855767220339802">"Havolani kiritish"</string>
+ <string name="pinned" msgid="7623664001331394139">"Mahkamlangan"</string>
</resources>
diff --git a/java/res/values-vi/strings.xml b/java/res/values-vi/strings.xml
index ed64998..b08d9a3 100644
--- a/java/res/values-vi/strings.xml
+++ b/java/res/values-vi/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Ghim <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Bỏ ghim <xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Chỉnh sửa"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + # tệp}other{{file_name} + # tệp}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # tệp}other{+ # tệp}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"Đang chia sẻ văn bản"</string>
- <string name="sharing_link" msgid="2307694372813942916">"Đang chia sẻ liên kết"</string>
- <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Đang chia sẻ hình ảnh}other{Đang chia sẻ # hình ảnh}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ # tệp khác}other{+ # tệp khác}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"Chia sẻ văn bản"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"Chia sẻ đường liên kết"</string>
+ <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Chia sẻ hình ảnh}other{Chia sẻ # hình ảnh}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Đang chia sẻ video}other{Đang chia sẻ # video}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Đang chia sẻ # mục}other{Đang chia sẻ # mục}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Đang chia sẻ hình ảnh có văn bản"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Đang chia sẻ hình ảnh có liên kết"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Đang chia sẻ # tệp}other{Đang chia sẻ # tệp}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Đang chia sẻ hình ảnh có văn bản}other{Đang chia sẻ # hình ảnh có văn bản}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Đang chia sẻ hình ảnh có đường liên kết}other{Đang chia sẻ # hình ảnh có đường liên kết}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Đang chia sẻ video có văn bản}other{Đang chia sẻ # video có văn bản}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Đang chia sẻ video có đường liên kết}other{Đang chia sẻ # video có đường liên kết}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Đang chia sẻ tệp có văn bản}other{Đang chia sẻ # tệp có văn bản}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Đang chia sẻ tệp có đường liên kết}other{Đang chia sẻ # tệp có đường liên kết}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Chỉ chia sẻ hình ảnh}other{Chỉ chia sẻ các hình ảnh}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{Chỉ chia sẻ video}other{Chỉ chia sẻ các video}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Chỉ chia sẻ tệp}other{Chỉ chia sẻ các tệp}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Hình thu nhỏ của ảnh xem trước"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Hình thu nhỏ của video xem trước"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Hình thu nhỏ xem trước tệp"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Không có gợi ý nào về người mà bạn có thể chia sẻ"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Danh sách ứng dụng"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Ứng dụng này chưa được cấp quyền ghi âm nhưng vẫn có thể ghi âm thông qua thiết bị USB này."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Cá nhân"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Công việc"</string>
@@ -74,16 +83,17 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Bạn không thể mở nội dung này bằng ứng dụng công việc"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Bạn không thể chia sẻ nội dung này bằng ứng dụng cá nhân"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Bạn không thể mở nội dung này bằng ứng dụng cá nhân"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Hồ sơ công việc đã bị tạm dừng"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Nhấn để bật"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Các ứng dụng công việc đã bị tạm dừng"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Tiếp tục"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Không có ứng dụng công việc"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Không có ứng dụng cá nhân"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Mở <xliff:g id="APP">%s</xliff:g> trong hồ sơ cá nhân của bạn?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Mở <xliff:g id="APP">%s</xliff:g> trong hồ sơ công việc của bạn?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Dùng trình duyệt cá nhân"</string>
<string name="miniresolver_use_work_browser" msgid="7892699758493230342">"Dùng trình duyệt công việc"</string>
- <string name="exclude_text" msgid="5508128757025928034">"Loại trừ văn bản"</string>
+ <string name="exclude_text" msgid="5508128757025928034">"Không kèm văn bản"</string>
<string name="include_text" msgid="642280283268536140">"Thêm văn bản"</string>
- <string name="exclude_link" msgid="1332778255031992228">"Loại trừ đường liên kết"</string>
+ <string name="exclude_link" msgid="1332778255031992228">"Không kèm đường liên kết"</string>
<string name="include_link" msgid="827855767220339802">"Thêm đường liên kết"</string>
+ <string name="pinned" msgid="7623664001331394139">"Đã ghim"</string>
</resources>
diff --git a/java/res/values-zh-rCN/strings.xml b/java/res/values-zh-rCN/strings.xml
index 4541bea..e208e10 100644
--- a/java/res/values-zh-rCN/strings.xml
+++ b/java/res/values-zh-rCN/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"固定<xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"取消置顶<xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"编辑"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} 以及另外 # 个文件}other{{file_name} 以及另外 # 个文件}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{另外 # 个文件}other{另外 # 个文件}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"正在分享文本"</string>
- <string name="sharing_link" msgid="2307694372813942916">"正在分享链接"</string>
- <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{正在分享图片}other{正在分享 # 张图片}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{还有 # 个文件}other{还有 # 个文件}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"分享文本"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"分享链接"</string>
+ <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{分享图片}other{分享 # 张图片}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{正在分享视频}other{正在分享 # 个视频}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{正在分享 # 个项目}other{正在分享 # 个项目}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"正在分享带有文本的图片"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"正在分享带有链接的图片"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{正在分享 # 个文件}other{正在分享 # 个文件}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{正在分享带有文本的图片}other{正在分享带有文本的 # 个图片}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{正在分享带有链接的图片}other{正在分享带有链接的 # 个图片}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{正在分享带有文本的视频}other{正在分享带有文本的 # 个视频}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{正在分享带有链接的视频}other{正在分享带有链接的 # 个视频}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{正在分享带有文本的文件}other{正在分享带有文本的 # 个文件}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{正在分享带有链接的文件}other{正在分享带有链接的 # 个文件}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{仅限图片}other{仅限图片}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{仅限视频}other{仅限视频}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{仅限文件}other{仅限文件}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"图片预览缩略图"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"视频预览缩略图"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"文件预览缩略图"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"没有任何推荐的分享对象"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"应用列表"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"此应用未获得录音权限,但能通过此 USB 设备录制音频。"</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"个人"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"工作"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"无法使用工作应用打开该内容"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"无法使用个人应用分享该内容"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"无法使用个人应用打开该内容"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"工作资料已被暂停"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"点按即可开启"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"工作应用已暂停"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"解除暂停"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"没有支持该内容的工作应用"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"没有支持该内容的个人应用"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"要使用个人资料打开 <xliff:g id="APP">%s</xliff:g> 吗?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"包括文本"</string>
<string name="exclude_link" msgid="1332778255031992228">"排除链接"</string>
<string name="include_link" msgid="827855767220339802">"包括链接"</string>
+ <string name="pinned" msgid="7623664001331394139">"已固定"</string>
</resources>
diff --git a/java/res/values-zh-rHK/strings.xml b/java/res/values-zh-rHK/strings.xml
index 1a5fcc3..837b158 100644
--- a/java/res/values-zh-rHK/strings.xml
+++ b/java/res/values-zh-rHK/strings.xml
@@ -45,37 +45,46 @@
<string name="use_a_different_app" msgid="2062380818535918975">"使用不同的應用程式"</string>
<string name="chooseActivity" msgid="6659724877523973446">"選擇一項操作"</string>
<string name="noApplications" msgid="1139487441772284671">"沒有應用程式可執行這項操作。"</string>
- <string name="forward_intent_to_owner" msgid="6454987608971162379">"您目前並未透過公司檔案使用這個應用程式"</string>
- <string name="forward_intent_to_work" msgid="2906094223089139419">"您目前透過公司檔案使用這個應用程式"</string>
+ <string name="forward_intent_to_owner" msgid="6454987608971162379">"你目前並未透過公司檔案使用這個應用程式"</string>
+ <string name="forward_intent_to_work" msgid="2906094223089139419">"你目前透過公司檔案使用這個應用程式"</string>
<string name="activity_resolver_use_always" msgid="8674194687637555245">"一律採用"</string>
<string name="activity_resolver_use_once" msgid="594173435998892989">"只此一次"</string>
<string name="activity_resolver_work_profiles_support" msgid="8228711455685203580">"<xliff:g id="APP">%1$s</xliff:g> 不支援工作設定檔"</string>
<string name="pin_specific_target" msgid="5057063421361441406">"固定<xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"取消將<xliff:g id="LABEL">%1$s</xliff:g>置頂"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"編輯"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{「{file_name}」和另外 # 個檔案}other{「{file_name}」和另外 # 個檔案}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{和 # 個檔案}other{和 # 個檔案}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"正在分享文字"</string>
- <string name="sharing_link" msgid="2307694372813942916">"正在分享連結"</string>
- <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{正在分享圖片}other{正在分享 # 張圖片}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{+ 還有 # 個檔案}other{+ 還有 # 個檔案}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"分享文字"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"分享連結"</string>
+ <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{分享圖片}other{分享 # 張圖片}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{正在分享影片}other{正在分享 # 部影片}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{正在分享 # 個項目}other{正在分享 # 個項目}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"正在分享圖片 (含有文字)"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"正在分享圖片 (含有連結)"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{正在分享 # 個檔案}other{正在分享 # 個檔案}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{正在分享圖片 (含有文字)}other{正在分享 # 張圖片 (含有文字)}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{正在分享圖片 (含有連結)}other{正在分享 # 張圖片 (含有連結)}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{正在分享影片 (含有文字)}other{正在分享 # 部影片 (含有文字)}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{正在分享影片 (含有連結)}other{正在分享 # 部影片 (含有連結)}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{正在分享檔案 (含有文字)}other{正在分享 # 個檔案 (含有文字)}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{正在分享檔案 (含有連結)}other{正在分享 # 個檔案 (含有連結)}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{僅含圖片}other{僅含圖片}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{僅含影片}other{僅含影片}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{僅含檔案}other{僅含檔案}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"圖片預覽縮圖"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"影片預覽縮圖"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"檔案預覽縮圖"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"沒有推薦的分享對象"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"應用程式清單"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"此應用程式尚未獲授予錄音權限,但可透過此 USB 裝置記錄音訊。"</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"個人"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"工作"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"個人檢視模式"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"工作檢視模式"</string>
- <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"已被您的 IT 管理員封鎖"</string>
+ <string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"已被你的 IT 管理員封鎖"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"無法使用工作應用程式分享此內容"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"無法使用工作應用程式開啟此內容"</string>
- <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"無法使用個人應用程式分享此內容"</string>
+ <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"無法與個人應用程式分享此內容"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"無法使用個人應用程式開啟此內容"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"工作設定檔已暫停使用"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"輕按即可啟用"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"已暫停工作應用程式"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"取消暫停"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"沒有適用的工作應用程式"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"沒有適用的個人應用程式"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"要在個人設定檔中開啟「<xliff:g id="APP">%s</xliff:g>」嗎?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"加入文字"</string>
<string name="exclude_link" msgid="1332778255031992228">"不包括連結"</string>
<string name="include_link" msgid="827855767220339802">"加入連結"</string>
+ <string name="pinned" msgid="7623664001331394139">"固定咗"</string>
</resources>
diff --git a/java/res/values-zh-rTW/strings.xml b/java/res/values-zh-rTW/strings.xml
index 2900347..0fddc70 100644
--- a/java/res/values-zh-rTW/strings.xml
+++ b/java/res/values-zh-rTW/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"將「<xliff:g id="LABEL">%1$s</xliff:g>」置頂"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"將「<xliff:g id="LABEL">%1$s</xliff:g>」取消固定"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"編輯"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{「{file_name}」和另外 # 個檔案}other{「{file_name}」和另外 # 個檔案}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{和 # 個檔案}other{和 # 個檔案}}"</string>
- <string name="sharing_text" msgid="8137537443603304062">"正在分享文字"</string>
- <string name="sharing_link" msgid="2307694372813942916">"正在分享連結"</string>
- <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{正在分享圖片}other{正在分享 # 張圖片}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{和另外 # 個檔案}other{和另外 # 個檔案}}"</string>
+ <string name="sharing_text" msgid="8137537443603304062">"分享文字"</string>
+ <string name="sharing_link" msgid="2307694372813942916">"分享連結"</string>
+ <string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{分享圖片}other{分享 # 張圖片}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{正在分享影片}other{正在分享 # 部影片}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{正在分享 # 個項目}other{正在分享 # 個項目}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"正在分享含有文字的圖片"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"正在分享含有連結的圖片"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{正在分享 # 個檔案}other{正在分享 # 個檔案}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{分享含有文字的圖片}other{分享 # 張含有文字的圖片}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{分享含有連結的圖片}other{分享 # 張含有連結的圖片}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{分享含有文字的影片}other{分享 # 部含有文字的影片}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{分享含有連結的影片}other{分享 # 部含有連結的影片}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{分享含有文字的檔案}other{分享含有文字的 # 個檔案}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{分享含有連結的檔案}other{分享含有連結的 # 個檔案}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{只有圖片}other{只有圖片}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{只有影片}other{只有影片}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{只有檔案}other{只有檔案}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"圖片預覽縮圖"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"影片預覽縮圖"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"檔案預覽縮圖"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"沒有建議的分享對象"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"應用程式清單"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"這個應用程式未取得錄製內容的權限,但可以透過這部 USB 裝置錄製音訊。"</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"個人"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"工作"</string>
@@ -72,10 +81,10 @@
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"IT 管理員已封鎖這項操作"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"無法透過工作應用程式分享這項內容"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"無法使用工作應用程式開啟這項內容"</string>
- <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"無法透過個人應用程式分享這項內容"</string>
+ <string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"無法與個人應用程式分享這項內容"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"無法使用個人應用程式開啟這項內容"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"工作資料夾已暫停使用"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"輕觸即可啟用"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"工作應用程式目前為暫停狀態"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"取消暫停"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"沒有適用的工作應用程式"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"沒有適用的個人應用程式"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"要在個人資料夾中開啟「<xliff:g id="APP">%s</xliff:g>」嗎?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"加回文字"</string>
<string name="exclude_link" msgid="1332778255031992228">"排除連結"</string>
<string name="include_link" msgid="827855767220339802">"加回連結"</string>
+ <string name="pinned" msgid="7623664001331394139">"已固定"</string>
</resources>
diff --git a/java/res/values-zu/strings.xml b/java/res/values-zu/strings.xml
index 9e1a207..b651eb0 100644
--- a/java/res/values-zu/strings.xml
+++ b/java/res/values-zu/strings.xml
@@ -53,17 +53,26 @@
<string name="pin_specific_target" msgid="5057063421361441406">"Phina i-<xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="unpin_specific_target" msgid="3115158908159857777">"Susa ukuphina ku-<xliff:g id="LABEL">%1$s</xliff:g>"</string>
<string name="screenshot_edit" msgid="3857183660047569146">"Hlela"</string>
- <string name="file_count" msgid="3991190034661965836">"{count,plural, =1{{file_name} + ifayela elingu-#}one{{file_name} + amafayela angu-#}other{{file_name} + amafayela angu-#}}"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{Ifayela eli-+ #}one{Amafayela angu-+ #}other{Amafayela angu-+ #}}"</string>
+ <string name="more_files" msgid="1043875756612339842">"{count,plural, =1{Ifayela elengeziwe eli-+ #}one{Amafayela engeziwe angu-+ #}other{Amafayela engeziwe angu-+ #}}"</string>
<string name="sharing_text" msgid="8137537443603304062">"Yabelana ngombhalo"</string>
<string name="sharing_link" msgid="2307694372813942916">"Yabelana ngelinki"</string>
<string name="sharing_images" msgid="5251443722186962006">"{count,plural, =1{Yabelana ngomfanekiso}one{Yabelana ngemifanekiso engu-#}other{Yabelana ngemifanekiso engu-#}}"</string>
<string name="sharing_videos" msgid="3583423190182877434">"{count,plural, =1{Yabelana ngevidiyo}one{Yabelana ngamavidiyo angu-#}other{Yabelana ngamavidiyo angu-#}}"</string>
- <string name="sharing_items" msgid="5266543892527310331">"{count,plural, =1{Yabelana ngento engu-#}one{Yabelana ngezinto ezingu-#}other{Yabelana ngezinto ezingu-#}}"</string>
- <string name="sharing_image_with_text" msgid="3844438616236662145">"Yabelana ngomfanekiso ngombhalo"</string>
- <string name="sharing_image_with_link" msgid="5318319026387721227">"Yabelana ngomfanekiso ngelinki"</string>
+ <string name="sharing_files" msgid="1275646542246028823">"{count,plural, =1{Yabelana ngefayela eli-#}one{Yabelana ngamafayela angu-#}other{Yabelana ngamafayela angu-#}}"</string>
+ <string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{Yabelana ngomfanekiso ngombhalo}one{Yabelana ngemifanekiso engu-# ngombhalo}other{Yabelana ngemifanekiso engu-# ngombhalo}}"</string>
+ <string name="sharing_images_with_link" msgid="8907893266387877733">"{count,plural, =1{Yabelana ngomfanekiso ngelinki}one{Yabelana ngemifanekiso engu-# ngelinki}other{Yabelana ngemifanekiso engu-# ngelinki}}"</string>
+ <string name="sharing_videos_with_text" msgid="4169898442482118146">"{count,plural, =1{Yabelana ngevidiyo ngombhalo}one{Yabelana ngamavidiyo angu-# ngombhalo}other{Yabelana ngamavidiyo angu-# ngombhalo}}"</string>
+ <string name="sharing_videos_with_link" msgid="6383290441403042321">"{count,plural, =1{Yabelana ngevidiyo ngelinki}one{Yabelana ngamavidiyo angu-# ngelinki}other{Yabelana ngamavidiyo angu-# ngelinki}}"</string>
+ <string name="sharing_files_with_text" msgid="7331187260405018080">"{count,plural, =1{Yabelana ngefayela ngombhalo}one{Yabelana ngamafayela angu-# ngombhalo}other{Yabelana ngamafayela angu-# ngombhalo}}"</string>
+ <string name="sharing_files_with_link" msgid="6052797122358827239">"{count,plural, =1{Yabelana ngefayela ngelinki}one{Yabelana ngamafayela angu-# ngelinki}other{Yabelana ngamafayela angu-# ngelinki}}"</string>
+ <string name="sharing_images_only" msgid="7762589767189955438">"{count,plural, =1{Isithombe kuphela}one{Izithombe kuphela}other{Izithombe kuphela}}"</string>
+ <string name="sharing_videos_only" msgid="5549729252364968606">"{count,plural, =1{ividiyo kuphela}one{Amavidiyo kuphela}other{Amavidiyo kuphela}}"</string>
+ <string name="sharing_files_only" msgid="6603666533766964768">"{count,plural, =1{Ifayela kuphela}one{Amafayela kuphela}other{Amafayela kuphela}}"</string>
+ <string name="image_preview_a11y_description" msgid="297102643932491797">"Isithonjana sokuhlola kuqala umfanekiso"</string>
+ <string name="video_preview_a11y_description" msgid="683440858811095990">"Isithonjana sokuhlola kuqala ividiyo"</string>
+ <string name="file_preview_a11y_description" msgid="7397224827802410602">"Isithonjana sokuhlola kuqala ifayela"</string>
<string name="chooser_no_direct_share_targets" msgid="4233416657754261844">"Ayinconyelwa ukuba abantu bayabelane"</string>
- <string name="chooser_all_apps_button_label" msgid="5655027129615750712">"Uhlu lwezinhlelo zokusebenza"</string>
<string name="usb_device_resolve_prompt_warn" msgid="4254493957548169620">"Lolu hlelo lokusebenza alunikeziwe imvume yokurekhoda kodwa lungathwebula umsindo ngale divayisi ye-USB."</string>
<string name="resolver_personal_tab" msgid="1381052735324320565">"Okomuntu siqu"</string>
<string name="resolver_work_tab" msgid="3588325717455216412">"Umsebenzi"</string>
@@ -74,8 +83,8 @@
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Lokhu okuqukethwe akukwazi ukukopishwa ngama-app womsebenzi"</string>
<string name="resolver_cant_share_with_personal_apps_explanation" msgid="6406971348929464569">"Lokhu okuqukethwe akukwazi ukwabiwa nama-app womuntu siqu"</string>
<string name="resolver_cant_access_personal_apps_explanation" msgid="6209543716289792706">"Lokhu okuqukethwe akukwazi ukukopishwa ngama-app womuntu siqu"</string>
- <string name="resolver_turn_on_work_apps" msgid="6464225110988983641">"Iphrofayela yomsebenzi iphunyuziwe"</string>
- <string name="resolver_switch_on_work" msgid="4615505942222617333">"Thepha ukuze uvule"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Ama-app omsebenzi amisiwe"</string>
+ <string name="resolver_switch_on_work" msgid="8678893259344318807">"Qhubekisa"</string>
<string name="resolver_no_work_apps_available" msgid="6139818641313189903">"Awekho ama-app womsebenzi"</string>
<string name="resolver_no_personal_apps_available" msgid="8479033344701050767">"Awekho ama-app womuntu siqu"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vula i-<xliff:g id="APP">%s</xliff:g> kwiphrofayela yakho siqu?"</string>
@@ -86,4 +95,5 @@
<string name="include_text" msgid="642280283268536140">"Faka umbhalo"</string>
<string name="exclude_link" msgid="1332778255031992228">"Ungafaki ilinki"</string>
<string name="include_link" msgid="827855767220339802">"Faka ilinki"</string>
+ <string name="pinned" msgid="7623664001331394139">"Kuphiniwe"</string>
</resources>
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index 67acb3a..c9f2c30 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -32,6 +32,11 @@
will push all ignoreOffset siblings below it when the drawer is moved i.e. setting the
top limit the ignoreOffset elements. -->
<attr name="ignoreOffsetTopLimit" format="reference" />
+ <!-- Specifies whether ResolverDrawerLayout should use an alternative nested fling logic
+ adjusted for the scrollable preview feature.
+ Controlled by the flag com.android.intentresolver.Flags#FLAG_SCROLLABLE_PREVIEW.
+ -->
+ <attr name="useScrollablePreviewNestedFlingLogic" format="boolean" />
</declare-styleable>
<declare-styleable name="ResolverDrawerLayout_LayoutParams">
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index ae80815..8843c81 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -33,7 +33,6 @@
<dimen name="chooser_preview_image_max_dimen">200dp</dimen>
<dimen name="chooser_header_scroll_elevation">4dp</dimen>
<dimen name="chooser_max_collapsed_height">288dp</dimen>
- <dimen name="chooser_direct_share_label_placeholder_max_width">72dp</dimen>
<dimen name="chooser_icon_size">56dp</dimen>
<dimen name="chooser_badge_size">22dp</dimen>
<dimen name="resolver_icon_size">32dp</dimen>
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index 4b5367c..0c77257 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -303,4 +303,8 @@
<string name="exclude_link">Exclude link</string>
<!-- Title for a button. Adds back a (previously excluded) web link into the shared content. -->
<string name="include_link">Include link</string>
+
+ <!-- Accesssibility content description for a sharesheet target that has been pinned to the
+ front of the list by the user. [CHAR LIMIT=NONE] -->
+ <string name="pinned">Pinned</string>
</resources>
diff --git a/java/src-debug/com/android/intentresolver/flags/DebugFeatureFlagRepository.kt b/java/src-debug/com/android/intentresolver/flags/DebugFeatureFlagRepository.kt
deleted file mode 100644
index 5067c0e..0000000
--- a/java/src-debug/com/android/intentresolver/flags/DebugFeatureFlagRepository.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver.flags
-
-import android.util.SparseBooleanArray
-import androidx.annotation.GuardedBy
-import com.android.systemui.flags.BooleanFlag
-import com.android.systemui.flags.FlagManager
-import com.android.systemui.flags.ReleasedFlag
-import com.android.systemui.flags.UnreleasedFlag
-import javax.annotation.concurrent.ThreadSafe
-
-@ThreadSafe
-internal class DebugFeatureFlagRepository(
- private val flagManager: FlagManager,
- private val deviceConfig: DeviceConfigProxy,
-) : FeatureFlagRepository {
- @GuardedBy("self")
- private val cache = hashMapOf<String, Boolean>()
-
- override fun isEnabled(flag: UnreleasedFlag): Boolean = isFlagEnabled(flag)
-
- override fun isEnabled(flag: ReleasedFlag): Boolean = isFlagEnabled(flag)
-
- private fun isFlagEnabled(flag: BooleanFlag): Boolean {
- synchronized(cache) {
- cache[flag.name]?.let { return it }
- }
- val flagValue = readFlagValue(flag)
- return synchronized(cache) {
- // the first read saved in the cache wins
- cache.getOrPut(flag.name) { flagValue }
- }
- }
-
- private fun readFlagValue(flag: BooleanFlag): Boolean {
- val localOverride = runCatching {
- flagManager.isEnabled(flag.name)
- }.getOrDefault(null)
- val remoteOverride = deviceConfig.isEnabled(flag)
-
- // Only check for teamfood if the default is false
- // and there is no server override.
- if (remoteOverride == null
- && !flag.default
- && localOverride == null
- && !flag.isTeamfoodFlag
- && flag.teamfood
- ) {
- return flagManager.isTeamfoodEnabled
- }
- return localOverride ?: remoteOverride ?: flag.default
- }
-
- companion object {
- /** keep in sync with [com.android.systemui.flags.Flags] */
- private const val TEAMFOOD_FLAG_NAME = "teamfood"
-
- private val BooleanFlag.isTeamfoodFlag: Boolean
- get() = name == TEAMFOOD_FLAG_NAME
-
- private val FlagManager.isTeamfoodEnabled: Boolean
- get() = runCatching {
- isEnabled(TEAMFOOD_FLAG_NAME) ?: false
- }.getOrDefault(false)
- }
-}
diff --git a/java/src-debug/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt b/java/src-debug/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt
deleted file mode 100644
index 4ddb044..0000000
--- a/java/src-debug/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver.flags
-
-import android.content.Context
-import android.os.Handler
-import android.os.Looper
-import com.android.systemui.flags.FlagManager
-
-class FeatureFlagRepositoryFactory {
- fun create(context: Context): FeatureFlagRepository =
- DebugFeatureFlagRepository(
- FlagManager(context, Handler(Looper.getMainLooper())),
- DeviceConfigProxy(),
- )
-}
diff --git a/java/src-release/com/android/intentresolver/flags/ReleaseFeatureFlagRepository.kt b/java/src-release/com/android/intentresolver/flags/ReleaseFeatureFlagRepository.kt
deleted file mode 100644
index f9fa2c6..0000000
--- a/java/src-release/com/android/intentresolver/flags/ReleaseFeatureFlagRepository.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver.flags
-
-import com.android.systemui.flags.ReleasedFlag
-import com.android.systemui.flags.UnreleasedFlag
-import javax.annotation.concurrent.ThreadSafe
-
-@ThreadSafe
-internal class ReleaseFeatureFlagRepository(
- private val deviceConfig: DeviceConfigProxy,
-) : FeatureFlagRepository {
- override fun isEnabled(flag: UnreleasedFlag): Boolean = flag.default
-
- override fun isEnabled(flag: ReleasedFlag): Boolean =
- deviceConfig.isEnabled(flag) ?: flag.default
-}
diff --git a/java/src/com/android/intentresolver/AnnotatedUserHandles.java b/java/src/com/android/intentresolver/AnnotatedUserHandles.java
index 168f36d..3565e75 100644
--- a/java/src/com/android/intentresolver/AnnotatedUserHandles.java
+++ b/java/src/com/android/intentresolver/AnnotatedUserHandles.java
@@ -16,12 +16,12 @@
package com.android.intentresolver;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.os.UserHandle;
import android.os.UserManager;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
/**
@@ -35,7 +35,7 @@ public final class AnnotatedUserHandles {
/**
* The {@link UserHandle} that launched Sharesheet.
* TODO: I believe this would always be the handle corresponding to {@code userIdOfCallingApp}
- * except possibly if the caller used {@link Activity#startActivityAsUser()} to launch
+ * except possibly if the caller used {@link Activity#startActivityAsUser} to launch
* Sharesheet as a different user than they themselves were running as. Verify and document.
*/
public final UserHandle userHandleSharesheetLaunchedAs;
@@ -57,21 +57,21 @@ public final class AnnotatedUserHandles {
/**
* The {@link UserHandle} that owns the "work tab" in a tabbed share UI. This is (an arbitrary)
- * one of the "managed" profiles associated with {@link personalProfileUserHandle}.
+ * one of the "managed" profiles associated with {@link #personalProfileUserHandle}.
*/
@Nullable
public final UserHandle workProfileUserHandle;
/**
- * The {@link UserHandle} of the clone profile belonging to {@link personalProfileUserHandle}.
+ * The {@link UserHandle} of the clone profile belonging to {@link #personalProfileUserHandle}.
*/
@Nullable
public final UserHandle cloneProfileUserHandle;
/**
- * The "tab owner" user handle (i.e., either {@link personalProfileUserHandle} or
- * {@link workProfileUserHandle}) that either matches or owns the profile of the
- * {@link userHandleSharesheetLaunchedAs}.
+ * The "tab owner" user handle (i.e., either {@link #personalProfileUserHandle} or
+ * {@link #workProfileUserHandle}) that either matches or owns the profile of the
+ * {@link #userHandleSharesheetLaunchedAs}.
*
* In the current implementation, we can assert that this is the same as
* `userHandleSharesheetLaunchedAs` except when the latter is the clone profile; then this is
@@ -105,7 +105,7 @@ public final class AnnotatedUserHandles {
.build();
}
- @VisibleForTesting static Builder newBuilder() {
+ @VisibleForTesting public static Builder newBuilder() {
return new Builder();
}
@@ -173,7 +173,7 @@ public final class AnnotatedUserHandles {
}
@VisibleForTesting
- static class Builder {
+ public static class Builder {
private int mUserIdOfCallingApp;
private UserHandle mUserHandleSharesheetLaunchedAs;
private UserHandle mPersonalProfileUserHandle;
diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java
index a54e8c6..310fcc2 100644
--- a/java/src/com/android/intentresolver/ChooserActionFactory.java
+++ b/java/src/com/android/intentresolver/ChooserActionFactory.java
@@ -16,7 +16,6 @@
package com.android.intentresolver;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityOptions;
import android.app.PendingIntent;
@@ -34,6 +33,8 @@ import android.text.TextUtils;
import android.util.Log;
import android.view.View;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
@@ -98,12 +99,11 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
private final @Nullable ChooserAction mModifyShareAction;
private final Consumer<Boolean> mExcludeSharedTextAction;
private final Consumer</* @Nullable */ Integer> mFinishCallback;
- private final EventLog mLogger;
+ private final EventLog mLog;
/**
* @param context
* @param chooserRequest data about the invocation of the current Sharesheet session.
- * @param integratedDeviceComponents info about other components that are available on this
* device to implement the supported action types.
* @param onUpdateSharedTextIsExcluded a delegate to be invoked when the "exclude shared text"
* setting is updated. The argument is whether the shared text is to be excluded.
@@ -117,7 +117,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
Context context,
ChooserRequestParameters chooserRequest,
ChooserIntegratedDeviceComponents integratedDeviceComponents,
- EventLog logger,
+ EventLog log,
Consumer<Boolean> onUpdateSharedTextIsExcluded,
Callable</* @Nullable */ View> firstVisibleImageQuery,
ActionActivityStarter activityStarter,
@@ -129,7 +129,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
chooserRequest.getTargetIntent(),
chooserRequest.getReferrerPackageName(),
finishCallback,
- logger),
+ log),
makeEditButtonRunnable(
getEditSharingTarget(
context,
@@ -137,11 +137,11 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
integratedDeviceComponents),
firstVisibleImageQuery,
activityStarter,
- logger),
+ log),
chooserRequest.getChooserActions(),
chooserRequest.getModifyShareAction(),
onUpdateSharedTextIsExcluded,
- logger,
+ log,
finishCallback);
}
@@ -153,7 +153,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
List<ChooserAction> customActions,
@Nullable ChooserAction modifyShareAction,
Consumer<Boolean> onUpdateSharedTextIsExcluded,
- EventLog logger,
+ EventLog log,
Consumer</* @Nullable */ Integer> finishCallback) {
mContext = context;
mCopyButtonRunnable = copyButtonRunnable;
@@ -161,7 +161,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
mCustomActions = ImmutableList.copyOf(customActions);
mModifyShareAction = modifyShareAction;
mExcludeSharedTextAction = onUpdateSharedTextIsExcluded;
- mLogger = logger;
+ mLog = log;
mFinishCallback = finishCallback;
}
@@ -188,7 +188,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
mCustomActions.get(i),
mFinishCallback,
() -> {
- mLogger.logCustomActionSelected(position);
+ mLog.logCustomActionSelected(position);
}
);
if (actionRow != null) {
@@ -209,7 +209,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
mModifyShareAction,
mFinishCallback,
() -> {
- mLogger.logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE);
+ mLog.logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE);
});
}
@@ -233,13 +233,13 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
Intent targetIntent,
String referrerPackageName,
Consumer<Integer> finishCallback,
- EventLog logger) {
+ EventLog log) {
final ClipData clipData;
try {
clipData = extractTextToCopy(targetIntent);
} catch (Throwable t) {
Log.e(TAG, "Failed to extract data to copy", t);
- return null;
+ return null;
}
if (clipData == null) {
return null;
@@ -249,7 +249,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
Context.CLIPBOARD_SERVICE);
clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName);
- logger.logActionSelected(EventLog.SELECTION_TYPE_COPY);
+ log.logActionSelected(EventLog.SELECTION_TYPE_COPY);
finishCallback.accept(Activity.RESULT_OK);
};
}
@@ -317,8 +317,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
ri,
context.getString(R.string.screenshot_edit),
"",
- resolveIntent,
- null);
+ resolveIntent);
dri.getDisplayIconHolder().setDisplayIcon(
context.getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));
return dri;
@@ -328,10 +327,10 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
TargetInfo editSharingTarget,
Callable</* @Nullable */ View> firstVisibleImageQuery,
ActionActivityStarter activityStarter,
- EventLog logger) {
+ EventLog log) {
return () -> {
// Log share completion via edit.
- logger.logActionSelected(EventLog.SELECTION_TYPE_EDIT);
+ log.logActionSelected(EventLog.SELECTION_TYPE_EDIT);
View firstImageView = null;
try {
@@ -373,10 +372,10 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
null,
null,
ActivityOptions.makeCustomAnimation(
- context,
- R.anim.slide_in_right,
- R.anim.slide_out_left)
- .toBundle());
+ context,
+ R.anim.slide_in_right,
+ R.anim.slide_out_left)
+ .toBundle());
} catch (PendingIntent.CanceledException e) {
Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled");
}
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index b27f054..9000ab3 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -24,10 +24,10 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROS
import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
+import static androidx.lifecycle.LifecycleKt.getCoroutineScope;
+
import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
-import android.annotation.IntDef;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -51,11 +51,9 @@ import android.database.Cursor;
import android.graphics.Insets;
import android.net.Uri;
import android.os.Bundle;
-import android.os.Environment;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
-import android.os.storage.StorageManager;
import android.service.chooser.ChooserTarget;
import android.util.Log;
import android.util.Slog;
@@ -67,15 +65,15 @@ import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.TextView;
+import androidx.annotation.IntDef;
import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.ViewPager;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
-import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
@@ -83,8 +81,10 @@ import com.android.intentresolver.contentpreview.BasePreviewViewModel;
import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl;
import com.android.intentresolver.contentpreview.PreviewViewModel;
-import com.android.intentresolver.flags.FeatureFlagRepository;
-import com.android.intentresolver.flags.FeatureFlagRepositoryFactory;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider;
+import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
import com.android.intentresolver.grid.ChooserGridAdapter;
import com.android.intentresolver.icons.DefaultTargetDataLoader;
import com.android.intentresolver.icons.TargetDataLoader;
@@ -100,7 +100,8 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import java.io.File;
+import dagger.hilt.android.AndroidEntryPoint;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.Collator;
@@ -115,12 +116,15 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
+import javax.inject.Inject;
+
/**
* The Chooser Activity handles intent resolution specifically for sharing intents -
* for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}.
*
*/
-public class ChooserActivity extends ResolverActivity implements
+@AndroidEntryPoint(ResolverActivity.class)
+public class ChooserActivity extends Hilt_ChooserActivity implements
ResolverListAdapter.ResolverListCommunicator {
private static final String TAG = "ChooserActivity";
@@ -161,7 +165,7 @@ public class ChooserActivity extends ResolverActivity implements
private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1;
private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2;
- @IntDef(flag = false, prefix = { "TARGET_TYPE_" }, value = {
+ @IntDef({
TARGET_TYPE_DEFAULT,
TARGET_TYPE_CHOOSER_TARGET,
TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER,
@@ -170,6 +174,9 @@ public class ChooserActivity extends ResolverActivity implements
@Retention(RetentionPolicy.SOURCE)
public @interface ShareTargetType {}
+ @Inject public FeatureFlags mFeatureFlags;
+ @Inject public EventLog mEventLog;
+
private ChooserIntegratedDeviceComponents mIntegratedDeviceComponents;
/* TODO: this is `nullable` because we have to defer the assignment til onCreate(). We make the
@@ -183,13 +190,9 @@ public class ChooserActivity extends ResolverActivity implements
private ChooserRefinementManager mRefinementManager;
- private FeatureFlagRepository mFeatureFlagRepository;
private ChooserContentPreviewUi mChooserContentPreviewUi;
private boolean mShouldDisplayLandscape;
- // statsd logger wrapper
- protected EventLog mEventLog;
-
private long mChooserShownTime;
protected boolean mIsSuccessfullySelected;
@@ -229,31 +232,52 @@ public class ChooserActivity extends ResolverActivity implements
*/
private boolean mFinishWhenStopped = false;
- public ChooserActivity() {}
-
@Override
protected void onCreate(Bundle savedInstanceState) {
Tracer.INSTANCE.markLaunched();
final long intentReceivedTime = System.currentTimeMillis();
mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
- getEventLog().logSharesheetTriggered();
-
- mFeatureFlagRepository = createFeatureFlagRepository();
- mIntegratedDeviceComponents = getIntegratedDeviceComponents();
-
try {
mChooserRequest = new ChooserRequestParameters(
getIntent(),
getReferrerPackageName(),
- getReferrer(),
- mFeatureFlagRepository);
+ getReferrer());
} catch (IllegalArgumentException e) {
Log.e(TAG, "Caller provided invalid Chooser request parameters", e);
finish();
super_onCreate(null);
return;
}
+ mPinnedSharedPrefs = getPinnedSharedPrefs(this);
+ mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
+ mShouldDisplayLandscape =
+ shouldDisplayLandscape(getResources().getConfiguration().orientation);
+ setRetainInOnStop(mChooserRequest.shouldRetainInOnStop());
+
+ createProfileRecords(
+ new AppPredictorFactory(
+ this,
+ mChooserRequest.getSharedText(),
+ mChooserRequest.getTargetIntentFilter()),
+ mChooserRequest.getTargetIntentFilter());
+
+
+ super.onCreate(
+ savedInstanceState,
+ mChooserRequest.getTargetIntent(),
+ mChooserRequest.getAdditionalTargets(),
+ mChooserRequest.getTitle(),
+ mChooserRequest.getDefaultTitleResource(),
+ mChooserRequest.getInitialIntents(),
+ /* resolutionList= */ null,
+ /* supportsAlwaysUseOption= */ false,
+ new DefaultTargetDataLoader(this, getLifecycle(), false),
+ /* safeForwardingMode= */ true);
+
+ getEventLog().logSharesheetTriggered();
+
+ mIntegratedDeviceComponents = getIntegratedDeviceComponents();
mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class);
@@ -279,39 +303,21 @@ public class ChooserActivity extends ResolverActivity implements
new ViewModelProvider(this, createPreviewViewModelFactory())
.get(BasePreviewViewModel.class);
mChooserContentPreviewUi = new ChooserContentPreviewUi(
- getLifecycle(),
- previewViewModel.createOrReuseProvider(mChooserRequest),
+ getCoroutineScope(getLifecycle()),
+ previewViewModel.createOrReuseProvider(mChooserRequest.getTargetIntent()),
mChooserRequest.getTargetIntent(),
previewViewModel.createOrReuseImageLoader(),
createChooserActionFactory(),
mEnterTransitionAnimationDelegate,
new HeadlineGeneratorImpl(this));
- mPinnedSharedPrefs = getPinnedSharedPrefs(this);
-
- mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
- mShouldDisplayLandscape =
- shouldDisplayLandscape(getResources().getConfiguration().orientation);
- setRetainInOnStop(mChooserRequest.shouldRetainInOnStop());
-
- createProfileRecords(
- new AppPredictorFactory(
- getApplicationContext(),
- mChooserRequest.getSharedText(),
- mChooserRequest.getTargetIntentFilter()),
- mChooserRequest.getTargetIntentFilter());
-
- super.onCreate(
- savedInstanceState,
- mChooserRequest.getTargetIntent(),
- mChooserRequest.getAdditionalTargets(),
- mChooserRequest.getTitle(),
- mChooserRequest.getDefaultTitleResource(),
- mChooserRequest.getInitialIntents(),
- /* resolutionList= */ null,
- /* supportsAlwaysUseOption= */ false,
- new DefaultTargetDataLoader(this, getLifecycle(), false),
- /* safeForwardingMode= */ true);
+ updateStickyContentPreview();
+ if (shouldShowStickyContentPreview()
+ || mChooserMultiProfilePagerAdapter
+ .getCurrentRootAdapter().getSystemRowCount() != 0) {
+ getEventLog().logActionShareWithPreview(
+ mChooserContentPreviewUi.getPreferredContentPreview());
+ }
mChooserShownTime = System.currentTimeMillis();
final long systemCost = mChooserShownTime - intentReceivedTime;
@@ -358,19 +364,15 @@ public class ChooserActivity extends ResolverActivity implements
return R.style.Theme_DeviceDefault_Chooser;
}
- protected FeatureFlagRepository createFeatureFlagRepository() {
- return new FeatureFlagRepositoryFactory().create(getApplicationContext());
- }
-
private void createProfileRecords(
AppPredictorFactory factory, IntentFilter targetIntentFilter) {
- UserHandle mainUserHandle = getPersonalProfileUserHandle();
+ UserHandle mainUserHandle = getAnnotatedUserHandles().personalProfileUserHandle;
ProfileRecord record = createProfileRecord(mainUserHandle, targetIntentFilter, factory);
if (record.shortcutLoader == null) {
Tracer.INSTANCE.endLaunchToShortcutTrace();
}
- UserHandle workUserHandle = getWorkProfileUserHandle();
+ UserHandle workUserHandle = getAnnotatedUserHandles().workProfileUserHandle;
if (workUserHandle != null) {
createProfileRecord(workUserHandle, targetIntentFilter, factory);
}
@@ -382,7 +384,7 @@ public class ChooserActivity extends ResolverActivity implements
ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic()
? null
: createShortcutLoader(
- getApplicationContext(),
+ this,
appPredictor,
userHandle,
targetIntentFilter,
@@ -406,7 +408,7 @@ public class ChooserActivity extends ResolverActivity implements
Consumer<ShortcutLoader.Result> callback) {
return new ShortcutLoader(
context,
- getLifecycle(),
+ getCoroutineScope(getLifecycle()),
appPredictor,
userHandle,
targetIntentFilter,
@@ -414,23 +416,11 @@ public class ChooserActivity extends ResolverActivity implements
}
static SharedPreferences getPinnedSharedPrefs(Context context) {
- // The code below is because in the android:ui process, no one can hear you scream.
- // The package info in the context isn't initialized in the way it is for normal apps,
- // so the standard, name-based context.getSharedPreferences doesn't work. Instead, we
- // build the path manually below using the same policy that appears in ContextImpl.
- // This fails silently under the hood if there's a problem, so if we find ourselves in
- // the case where we don't have access to credential encrypted storage we just won't
- // have our pinned target info.
- final File prefsFile = new File(new File(
- Environment.getDataUserCePackageDirectory(StorageManager.UUID_PRIVATE_INTERNAL,
- context.getUserId(), context.getPackageName()),
- "shared_prefs"),
- PINNED_SHARED_PREFS_NAME + ".xml");
- return context.getSharedPreferences(prefsFile, MODE_PRIVATE);
+ return context.getSharedPreferences(PINNED_SHARED_PREFS_NAME, MODE_PRIVATE);
}
@Override
- protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter(
+ protected ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter(
Intent[] initialIntents,
List<ResolveInfo> rList,
boolean filterLastUsed,
@@ -475,9 +465,12 @@ public class ChooserActivity extends ResolverActivity implements
/* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK,
/* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER);
- return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(),
- noWorkToPersonalEmptyState, noPersonalToWorkEmptyState,
- createCrossProfileIntentsChecker(), getTabOwnerUserHandleForLaunch());
+ return new NoCrossProfileEmptyStateProvider(
+ getAnnotatedUserHandles().personalProfileUserHandle,
+ noWorkToPersonalEmptyState,
+ noPersonalToWorkEmptyState,
+ createCrossProfileIntentsChecker(),
+ getAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
}
private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile(
@@ -491,7 +484,7 @@ public class ChooserActivity extends ResolverActivity implements
initialIntents,
rList,
filterLastUsed,
- /* userHandle */ getPersonalProfileUserHandle(),
+ /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,
targetDataLoader);
return new ChooserMultiProfilePagerAdapter(
/* context */ this,
@@ -499,8 +492,9 @@ public class ChooserActivity extends ResolverActivity implements
createEmptyStateProvider(/* workProfileUserHandle= */ null),
/* workProfileQuietModeChecker= */ () -> false,
/* workProfileUserHandle= */ null,
- getCloneProfileUserHandle(),
- mMaxTargetsPerRow);
+ getAnnotatedUserHandles().cloneProfileUserHandle,
+ mMaxTargetsPerRow,
+ mFeatureFlags);
}
private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles(
@@ -515,7 +509,7 @@ public class ChooserActivity extends ResolverActivity implements
selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
rList,
filterLastUsed,
- /* userHandle */ getPersonalProfileUserHandle(),
+ /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,
targetDataLoader);
ChooserGridAdapter workAdapter = createChooserGridAdapter(
/* context */ this,
@@ -523,40 +517,30 @@ public class ChooserActivity extends ResolverActivity implements
selectedProfile == PROFILE_WORK ? initialIntents : null,
rList,
filterLastUsed,
- /* userHandle */ getWorkProfileUserHandle(),
+ /* userHandle */ getAnnotatedUserHandles().workProfileUserHandle,
targetDataLoader);
return new ChooserMultiProfilePagerAdapter(
/* context */ this,
personalAdapter,
workAdapter,
- createEmptyStateProvider(/* workProfileUserHandle= */ getWorkProfileUserHandle()),
+ createEmptyStateProvider(getAnnotatedUserHandles().workProfileUserHandle),
() -> mWorkProfileAvailability.isQuietModeEnabled(),
selectedProfile,
- getWorkProfileUserHandle(),
- getCloneProfileUserHandle(),
- mMaxTargetsPerRow);
+ getAnnotatedUserHandles().workProfileUserHandle,
+ getAnnotatedUserHandles().cloneProfileUserHandle,
+ mMaxTargetsPerRow,
+ mFeatureFlags);
}
private int findSelectedProfile() {
int selectedProfile = getSelectedProfileExtra();
if (selectedProfile == -1) {
- selectedProfile = getProfileForUser(getTabOwnerUserHandleForLaunch());
+ selectedProfile = getProfileForUser(
+ getAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
}
return selectedProfile;
}
- @Override
- protected boolean postRebuildList(boolean rebuildCompleted) {
- updateStickyContentPreview();
- if (shouldShowStickyContentPreview()
- || mChooserMultiProfilePagerAdapter
- .getCurrentRootAdapter().getSystemRowCount() != 0) {
- getEventLog().logActionShareWithPreview(
- mChooserContentPreviewUi.getPreferredContentPreview());
- }
- return postRebuildListInternal(rebuildCompleted);
- }
-
/**
* Check if the profile currently used is a work profile.
* @return true if it is work profile, false if it is parent profile (or no work profile is
@@ -621,7 +605,7 @@ public class ChooserActivity extends ResolverActivity implements
}
@Override
- public void onConfigurationChanged(Configuration newConfig) {
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
if (viewPager.isLayoutRtl()) {
@@ -686,7 +670,10 @@ public class ChooserActivity extends ResolverActivity implements
ViewGroup layout = mChooserContentPreviewUi.displayContentPreview(
getResources(),
getLayoutInflater(),
- parent);
+ parent,
+ mFeatureFlags.scrollablePreview()
+ ? findViewById(R.id.chooser_headline_row_container)
+ : null);
if (layout != null) {
adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
@@ -807,7 +794,9 @@ public class ChooserActivity extends ResolverActivity implements
@Override
public int getLayoutResource() {
- return R.layout.chooser_grid;
+ return mFeatureFlags.scrollablePreview()
+ ? R.layout.chooser_grid_scrollable_preview
+ : R.layout.chooser_grid;
}
@Override // ResolverListCommunicator
@@ -1030,7 +1019,7 @@ public class ChooserActivity extends ResolverActivity implements
mIsSuccessfullySelected = true;
}
- private void maybeRemoveSharedText(@androidx.annotation.NonNull TargetInfo targetInfo) {
+ private void maybeRemoveSharedText(@NonNull TargetInfo targetInfo) {
Intent targetIntent = targetInfo.getTargetIntent();
if (targetIntent == null) {
return;
@@ -1105,7 +1094,8 @@ public class ChooserActivity extends ResolverActivity implements
ProfileRecord record = getProfileRecord(userHandle);
// We cannot use APS service when clone profile is present as APS service cannot sort
// cross profile targets as of now.
- return (record == null || getCloneProfileUserHandle() != null) ? null : record.appPredictor;
+ return ((record == null) || (getAnnotatedUserHandles().cloneProfileUserHandle != null))
+ ? null : record.appPredictor;
}
/**
@@ -1130,9 +1120,6 @@ public class ChooserActivity extends ResolverActivity implements
}
protected EventLog getEventLog() {
- if (mEventLog == null) {
- mEventLog = new EventLog();
- }
return mEventLog;
}
@@ -1156,7 +1143,7 @@ public class ChooserActivity extends ResolverActivity implements
}
@Override
- boolean isComponentFiltered(ComponentName name) {
+ public boolean isComponentFiltered(ComponentName name) {
return mChooserRequest.getFilteredComponentNames().contains(name);
}
@@ -1184,7 +1171,7 @@ public class ChooserActivity extends ResolverActivity implements
createListController(userHandle),
userHandle,
getTargetIntent(),
- mChooserRequest,
+ mChooserRequest.getReferrerFillInIntent(),
mMaxTargetsPerRow,
targetDataLoader);
@@ -1229,7 +1216,8 @@ public class ChooserActivity extends ResolverActivity implements
},
chooserListAdapter,
shouldShowContentPreview(),
- mMaxTargetsPerRow);
+ mMaxTargetsPerRow,
+ mFeatureFlags);
}
@VisibleForTesting
@@ -1242,12 +1230,12 @@ public class ChooserActivity extends ResolverActivity implements
ResolverListController resolverListController,
UserHandle userHandle,
Intent targetIntent,
- ChooserRequestParameters chooserRequest,
+ Intent referrerFillInIntent,
int maxTargetsPerRow,
TargetDataLoader targetDataLoader) {
UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()
- && userHandle.equals(getPersonalProfileUserHandle())
- ? getCloneProfileUserHandle() : userHandle;
+ && userHandle.equals(getAnnotatedUserHandles().personalProfileUserHandle)
+ ? getAnnotatedUserHandles().cloneProfileUserHandle : userHandle;
return new ChooserListAdapter(
context,
payloadIntents,
@@ -1257,18 +1245,19 @@ public class ChooserActivity extends ResolverActivity implements
createListController(userHandle),
userHandle,
targetIntent,
+ referrerFillInIntent,
this,
context.getPackageManager(),
getEventLog(),
- chooserRequest,
maxTargetsPerRow,
initialIntentsUserSpace,
- targetDataLoader);
+ targetDataLoader,
+ null);
}
@Override
protected void onWorkProfileStatusUpdated() {
- UserHandle workUser = getWorkProfileUserHandle();
+ UserHandle workUser = getAnnotatedUserHandles().workProfileUserHandle;
ProfileRecord record = workUser == null ? null : getProfileRecord(workUser);
if (record != null && record.shortcutLoader != null) {
record.shortcutLoader.reset();
@@ -1323,7 +1312,8 @@ public class ChooserActivity extends ResolverActivity implements
new ChooserActionFactory.ActionActivityStarter() {
@Override
public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) {
- safelyStartActivityAsUser(targetInfo, getPersonalProfileUserHandle());
+ safelyStartActivityAsUser(
+ targetInfo, getAnnotatedUserHandles().personalProfileUserHandle);
finish();
}
@@ -1333,11 +1323,12 @@ public class ChooserActivity extends ResolverActivity implements
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
ChooserActivity.this, sharedElement, sharedElementName);
safelyStartActivityAsUser(
- targetInfo, getPersonalProfileUserHandle(), options.toBundle());
+ targetInfo,
+ getAnnotatedUserHandles().personalProfileUserHandle,
+ options.toBundle());
// Can't finish right away because the shared element transition may not
// be ready to start.
mFinishWhenStopped = true;
-
}
},
(status) -> {
@@ -1490,7 +1481,7 @@ public class ChooserActivity extends ResolverActivity implements
* Returns {@link #PROFILE_PERSONAL}, otherwise.
**/
private int getProfileForUser(UserHandle currentUserHandle) {
- if (currentUserHandle.equals(getWorkProfileUserHandle())) {
+ if (currentUserHandle.equals(getAnnotatedUserHandles().workProfileUserHandle)) {
return PROFILE_WORK;
}
// We return personal profile, as it is the default when there is no work profile, personal
@@ -1510,19 +1501,21 @@ public class ChooserActivity extends ResolverActivity implements
}
@Override
- public void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) {
+ protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) {
setupScrollListener();
maybeSetupGlobalLayoutListener();
ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter;
- if (chooserListAdapter.getUserHandle()
- .equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) {
+ UserHandle listProfileUserHandle = chooserListAdapter.getUserHandle();
+ if (listProfileUserHandle.equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) {
mChooserMultiProfilePagerAdapter.getActiveAdapterView()
.setAdapter(mChooserMultiProfilePagerAdapter.getCurrentRootAdapter());
mChooserMultiProfilePagerAdapter
.setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage());
}
+ //TODO: move this block inside ChooserListAdapter (should be called when
+ // ResolverListAdapter#mPostListReadyRunnable is executed.
if (chooserListAdapter.getDisplayResolveInfoCount() == 0) {
chooserListAdapter.notifyDataSetChanged();
} else {
@@ -1530,25 +1523,28 @@ public class ChooserActivity extends ResolverActivity implements
}
if (rebuildComplete) {
- long duration = Tracer.INSTANCE.endAppTargetLoadingSection(listAdapter.getUserHandle());
+ long duration = Tracer.INSTANCE.endAppTargetLoadingSection(listProfileUserHandle);
if (duration >= 0) {
Log.d(TAG, "app target loading time " + duration + " ms");
}
addCallerChooserTargets();
getEventLog().logSharesheetAppLoadComplete();
- maybeQueryAdditionalPostProcessingTargets(chooserListAdapter);
+ maybeQueryAdditionalPostProcessingTargets(
+ listProfileUserHandle,
+ chooserListAdapter.getDisplayResolveInfos());
mLatencyTracker.onActionEnd(ACTION_LOAD_SHARE_SHEET);
}
}
- private void maybeQueryAdditionalPostProcessingTargets(ChooserListAdapter chooserListAdapter) {
- UserHandle userHandle = chooserListAdapter.getUserHandle();
+ private void maybeQueryAdditionalPostProcessingTargets(
+ UserHandle userHandle,
+ DisplayResolveInfo[] displayResolveInfos) {
ProfileRecord record = getProfileRecord(userHandle);
if (record == null || record.shortcutLoader == null) {
return;
}
record.loadingStartTime = SystemClock.elapsedRealtime();
- record.shortcutLoader.updateAppTargets(chooserListAdapter.getDisplayResolveInfos());
+ record.shortcutLoader.updateAppTargets(displayResolveInfos);
}
@MainThread
@@ -1596,7 +1592,8 @@ public class ChooserActivity extends ResolverActivity implements
getResources().getDimensionPixelSize(R.dimen.chooser_header_scroll_elevation);
mChooserMultiProfilePagerAdapter.getActiveAdapterView().addOnScrollListener(
new RecyclerView.OnScrollListener() {
- public void onScrollStateChanged(RecyclerView view, int scrollState) {
+ @Override
+ public void onScrollStateChanged(@NonNull RecyclerView view, int scrollState) {
if (scrollState == RecyclerView.SCROLL_STATE_IDLE) {
if (mScrollStatus == SCROLL_STATUS_SCROLLING_VERTICAL) {
mScrollStatus = SCROLL_STATUS_IDLE;
@@ -1610,7 +1607,8 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- public void onScrolled(RecyclerView view, int dx, int dy) {
+ @Override
+ public void onScrolled(@NonNull RecyclerView view, int dx, int dy) {
if (view.getChildCount() > 0) {
View child = view.getLayoutManager().findViewByPosition(0);
if (child == null || child.getTop() < 0) {
@@ -1656,11 +1654,13 @@ public class ChooserActivity extends ResolverActivity implements
}
private boolean shouldShowStickyContentPreviewNoOrientationCheck() {
- return shouldShowTabs()
- && (mMultiProfilePagerAdapter.getListAdapterForUserHandle(
- UserHandle.of(UserHandle.myUserId())).getCount() > 0
- || shouldShowContentPreviewWhenEmpty())
- && shouldShowContentPreview();
+ if (!shouldShowContentPreview()) {
+ return false;
+ }
+ boolean isEmpty = mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+ UserHandle.of(UserHandle.myUserId())).getCount() == 0;
+ return (mFeatureFlags.scrollablePreview() || shouldShowTabs())
+ && (!isEmpty || shouldShowContentPreviewWhenEmpty());
}
/**
diff --git a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
index 5f37352..aaa7554 100644
--- a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
+++ b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
@@ -70,7 +70,7 @@ public class ChooserGridLayoutManager extends GridLayoutManager {
return super.getRowCountForAccessibility(recycler, state) - 1;
}
- void setVerticalScrollEnabled(boolean verticalScrollEnabled) {
+ public void setVerticalScrollEnabled(boolean verticalScrollEnabled) {
mVerticalScrollEnabled = verticalScrollEnabled;
}
diff --git a/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
index 5fbf03a..7cd86bf 100644
--- a/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
+++ b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
@@ -16,12 +16,13 @@
package com.android.intentresolver;
-import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.provider.Settings;
import android.text.TextUtils;
+import androidx.annotation.Nullable;
+
import com.android.internal.annotations.VisibleForTesting;
/**
@@ -50,7 +51,8 @@ public class ChooserIntegratedDeviceComponents {
@VisibleForTesting
ChooserIntegratedDeviceComponents(
- ComponentName editSharingComponent, ComponentName nearbySharingComponent) {
+ @Nullable ComponentName editSharingComponent,
+ @Nullable ComponentName nearbySharingComponent) {
mEditSharingComponent = editSharingComponent;
mNearbySharingComponent = nearbySharingComponent;
}
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java
index e6d6dbf..876ad5c 100644
--- a/java/src/com/android/intentresolver/ChooserListAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -19,7 +19,6 @@ package com.android.intentresolver;
import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
-import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.prediction.AppTarget;
import android.content.ComponentName;
@@ -38,11 +37,16 @@ import android.os.UserManager;
import android.provider.DeviceConfig;
import android.service.chooser.ChooserTarget;
import android.text.Layout;
+import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.NotSelectableTargetInfo;
@@ -57,10 +61,23 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.Executor;
import java.util.stream.Collectors;
public class ChooserListAdapter extends ResolverListAdapter {
+
+ /**
+ * Delegate interface for injecting a chooser-specific operation to be performed before handling
+ * a package-change event. This allows the "driver" invoking the package-change to be generic,
+ * with no knowledge specific to the chooser implementation.
+ */
+ public interface PackageChangeCallback {
+ /** Perform any steps necessary before processing the package-change event. */
+ void beforeHandlingPackagesChanged();
+ }
+
private static final String TAG = "ChooserListAdapter";
private static final boolean DEBUG = false;
@@ -78,13 +95,17 @@ public class ChooserListAdapter extends ResolverListAdapter {
/** {@link #getBaseScore} */
public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f;
- private final ChooserRequestParameters mChooserRequest;
+ private final Intent mReferrerFillInIntent;
+
private final int mMaxRankedTargets;
private final EventLog mEventLog;
private final Set<TargetInfo> mRequestedIcons = new HashSet<>();
+ @Nullable
+ private final PackageChangeCallback mPackageChangeCallback;
+
// Reserve spots for incoming direct share targets by adding placeholders
private final TargetInfo mPlaceHolderTargetInfo;
private final TargetDataLoader mTargetDataLoader;
@@ -94,7 +115,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
private final ShortcutSelectionLogic mShortcutSelectionLogic;
// Sorted list of DisplayResolveInfos for the alphabetical app section.
- private List<DisplayResolveInfo> mSortedList = new ArrayList<>();
+ private final List<DisplayResolveInfo> mSortedList = new ArrayList<>();
private final ItemRevealAnimationTracker mAnimationTracker = new ItemRevealAnimationTracker();
@@ -138,13 +159,55 @@ public class ChooserListAdapter extends ResolverListAdapter {
ResolverListController resolverListController,
UserHandle userHandle,
Intent targetIntent,
+ Intent referrerFillInIntent,
+ ResolverListCommunicator resolverListCommunicator,
+ PackageManager packageManager,
+ EventLog eventLog,
+ int maxRankedTargets,
+ UserHandle initialIntentsUserSpace,
+ TargetDataLoader targetDataLoader,
+ @Nullable PackageChangeCallback packageChangeCallback) {
+ this(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ referrerFillInIntent,
+ resolverListCommunicator,
+ packageManager,
+ eventLog,
+ maxRankedTargets,
+ initialIntentsUserSpace,
+ targetDataLoader,
+ packageChangeCallback,
+ AsyncTask.SERIAL_EXECUTOR,
+ context.getMainExecutor());
+ }
+
+ @VisibleForTesting
+ public ChooserListAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ ResolverListController resolverListController,
+ UserHandle userHandle,
+ Intent targetIntent,
+ Intent referrerFillInIntent,
ResolverListCommunicator resolverListCommunicator,
PackageManager packageManager,
EventLog eventLog,
- ChooserRequestParameters chooserRequest,
int maxRankedTargets,
UserHandle initialIntentsUserSpace,
- TargetDataLoader targetDataLoader) {
+ TargetDataLoader targetDataLoader,
+ @Nullable PackageChangeCallback packageChangeCallback,
+ Executor bgExecutor,
+ Executor mainExecutor) {
// Don't send the initial intents through the shared ResolverActivity path,
// we want to separate them into a different section.
super(
@@ -158,13 +221,16 @@ public class ChooserListAdapter extends ResolverListAdapter {
targetIntent,
resolverListCommunicator,
initialIntentsUserSpace,
- targetDataLoader);
+ targetDataLoader,
+ bgExecutor,
+ mainExecutor);
- mChooserRequest = chooserRequest;
mMaxRankedTargets = maxRankedTargets;
+ mReferrerFillInIntent = referrerFillInIntent;
mPlaceHolderTargetInfo = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context);
mTargetDataLoader = targetDataLoader;
+ mPackageChangeCallback = packageChangeCallback;
createPlaceHolders();
mEventLog = eventLog;
mShortcutSelectionLogic = new ShortcutSelectionLogic(
@@ -227,9 +293,8 @@ public class ChooserListAdapter extends ResolverListAdapter {
ri.icon = 0;
}
ri.userHandle = initialIntentsUserSpace;
- // TODO: remove DisplayResolveInfo dependency on presentation getter
- DisplayResolveInfo displayResolveInfo = DisplayResolveInfo.newDisplayResolveInfo(
- ii, ri, ii, mTargetDataLoader.createPresentationGetter(ri));
+ DisplayResolveInfo displayResolveInfo =
+ DisplayResolveInfo.newDisplayResolveInfo(ii, ri, ii);
mCallerTargets.add(displayResolveInfo);
if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break;
}
@@ -238,6 +303,9 @@ public class ChooserListAdapter extends ResolverListAdapter {
@Override
public void handlePackagesChanged() {
+ if (mPackageChangeCallback != null) {
+ mPackageChangeCallback.beforeHandlingPackagesChanged();
+ }
if (DEBUG) {
Log.d(TAG, "clearing queryTargets on package change");
}
@@ -247,7 +315,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
@Override
- protected boolean rebuildList(boolean doPostProcessing) {
+ public boolean rebuildList(boolean doPostProcessing) {
mAnimationTracker.reset();
mSortedList.clear();
boolean result = super.rebuildList(doPostProcessing);
@@ -272,75 +340,77 @@ public class ChooserListAdapter extends ResolverListAdapter {
public void onBindView(View view, TargetInfo info, int position) {
final ViewHolder holder = (ViewHolder) view.getTag();
+ holder.reset();
+ // Always remove the spacing listener, attach as needed to direct share targets below.
+ holder.text.removeOnLayoutChangeListener(mPinTextSpacingListener);
+
if (info == null) {
holder.icon.setImageDrawable(loadIconPlaceholder());
return;
}
- holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo());
- mAnimationTracker.animateLabel(holder.text, info);
- if (holder.text2.getVisibility() == View.VISIBLE) {
+ final CharSequence displayLabel = Objects.requireNonNullElse(info.getDisplayLabel(), "");
+ final CharSequence extendedInfo = Objects.requireNonNullElse(info.getExtendedInfo(), "");
+ holder.bindLabel(displayLabel, extendedInfo);
+ if (!TextUtils.isEmpty(displayLabel)) {
+ mAnimationTracker.animateLabel(holder.text, info);
+ }
+ if (!TextUtils.isEmpty(extendedInfo) && holder.text2.getVisibility() == View.VISIBLE) {
mAnimationTracker.animateLabel(holder.text2, info);
}
+
holder.bindIcon(info);
- if (info.getDisplayIconHolder().getDisplayIcon() != null) {
+ if (info.hasDisplayIcon()) {
mAnimationTracker.animateIcon(holder.icon, info);
- } else {
- holder.icon.clearAnimation();
}
if (info.isSelectableTargetInfo()) {
// direct share targets should append the application name for a better readout
DisplayResolveInfo rInfo = info.getDisplayResolveInfo();
- CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
- CharSequence extendedInfo = info.getExtendedInfo();
- String contentDescription = String.join(" ", info.getDisplayLabel(),
- extendedInfo != null ? extendedInfo : "", appName);
+ CharSequence appName =
+ Objects.requireNonNullElse(rInfo == null ? null : rInfo.getDisplayLabel(), "");
+ String contentDescription =
+ String.join(" ", info.getDisplayLabel(), extendedInfo, appName);
+ if (info.isPinned()) {
+ contentDescription = String.join(
+ ". ",
+ contentDescription,
+ mContext.getResources().getString(R.string.pinned));
+ }
holder.updateContentDescription(contentDescription);
if (!info.hasDisplayIcon()) {
loadDirectShareIcon((SelectableTargetInfo) info);
}
} else if (info.isDisplayResolveInfo()) {
+ if (info.isPinned()) {
+ holder.updateContentDescription(String.join(
+ ". ",
+ info.getDisplayLabel(),
+ mContext.getResources().getString(R.string.pinned)));
+ }
DisplayResolveInfo dri = (DisplayResolveInfo) info;
if (!dri.hasDisplayIcon()) {
loadIcon(dri);
}
+ if (!dri.hasDisplayLabel()) {
+ loadLabel(dri);
+ }
}
- // If target is loading, show a special placeholder shape in the label, make unclickable
if (info.isPlaceHolderTargetInfo()) {
- final int maxWidth = mContext.getResources().getDimensionPixelSize(
- R.dimen.chooser_direct_share_label_placeholder_max_width);
- holder.text.setMaxWidth(maxWidth);
- holder.text.setBackground(mContext.getResources().getDrawable(
- R.drawable.chooser_direct_share_label_placeholder, mContext.getTheme()));
- // Prevent rippling by removing background containing ripple
- holder.itemView.setBackground(null);
- } else {
- holder.text.setMaxWidth(Integer.MAX_VALUE);
- holder.text.setBackground(null);
- holder.itemView.setBackground(holder.defaultItemViewBackground);
+ holder.bindPlaceholder();
}
- // Always remove the spacing listener, attach as needed to direct share targets below.
- holder.text.removeOnLayoutChangeListener(mPinTextSpacingListener);
-
if (info.isMultiDisplayResolveInfo()) {
// If the target is grouped show an indicator
- Drawable bkg = mContext.getDrawable(R.drawable.chooser_group_background);
- holder.text.setPaddingRelative(0, 0, bkg.getIntrinsicWidth() /* end */, 0);
- holder.text.setBackground(bkg);
+ holder.bindGroupIndicator(
+ mContext.getDrawable(R.drawable.chooser_group_background));
} else if (info.isPinned() && (getPositionTargetType(position) == TARGET_STANDARD
|| getPositionTargetType(position) == TARGET_SERVICE)) {
// If the appShare or directShare target is pinned and in the suggested row show a
// pinned indicator
- Drawable bkg = mContext.getDrawable(R.drawable.chooser_pinned_background);
- holder.text.setPaddingRelative(bkg.getIntrinsicWidth() /* start */, 0, 0, 0);
- holder.text.setBackground(bkg);
+ holder.bindPinnedIndicator(mContext.getDrawable(R.drawable.chooser_pinned_background));
holder.text.addOnLayoutChangeListener(mPinTextSpacingListener);
- } else {
- holder.text.setBackground(null);
- holder.text.setPaddingRelative(0, 0, 0, 0);
}
}
@@ -360,9 +430,13 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
}
- void updateAlphabeticalList() {
- // TODO: this procedure seems like it should be relatively lightweight. Why does it need to
- // run in an `AsyncTask`?
+ public void updateAlphabeticalList() {
+ final ChooserActivity.AzInfoComparator comparator =
+ new ChooserActivity.AzInfoComparator(mContext);
+ final List<DisplayResolveInfo> allTargets = new ArrayList<>();
+ allTargets.addAll(getTargetsInCurrentDisplayList());
+ allTargets.addAll(mCallerTargets);
+
new AsyncTask<Void, Void, List<DisplayResolveInfo>>() {
@Override
protected List<DisplayResolveInfo> doInBackground(Void... voids) {
@@ -375,32 +449,39 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
private List<DisplayResolveInfo> updateList() {
- List<DisplayResolveInfo> allTargets = new ArrayList<>();
- allTargets.addAll(getTargetsInCurrentDisplayList());
- allTargets.addAll(mCallerTargets);
+ loadMissingLabels(allTargets);
// Consolidate multiple targets from same app.
return allTargets
.stream()
.collect(Collectors.groupingBy(target ->
target.getResolvedComponentName().getPackageName()
- + "#" + target.getDisplayLabel()
- + '#' + target.getResolveInfo().userHandle.getIdentifier()
+ + "#" + target.getDisplayLabel()
+ + '#' + target.getResolveInfo().userHandle.getIdentifier()
))
.values()
.stream()
.map(appTargets ->
(appTargets.size() == 1)
- ? appTargets.get(0)
- : MultiDisplayResolveInfo.newMultiDisplayResolveInfo(appTargets))
- .sorted(new ChooserActivity.AzInfoComparator(mContext))
+ ? appTargets.get(0)
+ : MultiDisplayResolveInfo.newMultiDisplayResolveInfo(
+ appTargets))
+ .sorted(comparator)
.collect(Collectors.toList());
}
+
@Override
protected void onPostExecute(List<DisplayResolveInfo> newList) {
- mSortedList = newList;
+ mSortedList.clear();
+ mSortedList.addAll(newList);
notifyDataSetChanged();
}
+
+ private void loadMissingLabels(List<DisplayResolveInfo> targets) {
+ for (DisplayResolveInfo target: targets) {
+ mTargetDataLoader.getOrLoadLabel(target);
+ }
+ }
}.execute();
}
@@ -438,8 +519,14 @@ public class ChooserListAdapter extends ResolverListAdapter {
return count;
}
+ private static boolean hasSendAction(Intent intent) {
+ String action = intent.getAction();
+ return Intent.ACTION_SEND.equals(action)
+ || Intent.ACTION_SEND_MULTIPLE.equals(action);
+ }
+
public int getServiceTargetCount() {
- if (mChooserRequest.isSendActionTarget() && !ActivityManager.isLowRamDeviceStatic()) {
+ if (hasSendAction(getTargetIntent()) && !ActivityManager.isLowRamDeviceStatic()) {
return Math.min(mServiceTargets.size(), mMaxRankedTargets);
}
@@ -553,7 +640,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {
// Checks if this info is already listed in callerTargets.
for (TargetInfo existingInfo : mCallerTargets) {
- if (mResolverListCommunicator.resolveInfoMatch(
+ if (ResolveInfoHelpers.resolveInfoMatch(
dri.getResolveInfo(), existingInfo.getResolveInfo())) {
return false;
}
@@ -594,8 +681,8 @@ public class ChooserListAdapter extends ResolverListAdapter {
directShareToShortcutInfos,
directShareToAppTargets,
mContext.createContextAsUser(getUserHandle(), 0),
- mChooserRequest.getTargetIntent(),
- mChooserRequest.getReferrerFillInIntent(),
+ getTargetIntent(),
+ mReferrerFillInIntent,
mMaxRankedTargets,
mServiceTargets);
if (isUpdated) {
@@ -644,29 +731,23 @@ public class ChooserListAdapter extends ResolverListAdapter {
* in the head of input list and fill the tail with other elements in undetermined order.
*/
@Override
- AsyncTask<List<ResolvedComponentInfo>,
- Void,
- List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) {
- return new AsyncTask<List<ResolvedComponentInfo>,
- Void,
- List<ResolvedComponentInfo>>() {
- @Override
- protected List<ResolvedComponentInfo> doInBackground(
- List<ResolvedComponentInfo>... params) {
- Trace.beginSection("ChooserListAdapter#SortingTask");
- mResolverListController.topK(params[0], mMaxRankedTargets);
- Trace.endSection();
- return params[0];
- }
- @Override
- protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
- processSortedList(sortedComponents, doPostProcessing);
- if (doPostProcessing) {
- mResolverListCommunicator.updateProfileViewButton();
- notifyDataSetChanged();
- }
- }
- };
+ @WorkerThread
+ protected void sortComponents(List<ResolvedComponentInfo> components) {
+ Trace.beginSection("ChooserListAdapter#SortingTask");
+ mResolverListController.topK(components, mMaxRankedTargets);
+ Trace.endSection();
}
+ @Override
+ @MainThread
+ protected void onComponentsSorted(
+ @Nullable List<ResolvedComponentInfo> sortedComponents, boolean doPostProcessing) {
+ processSortedList(sortedComponents, doPostProcessing);
+ if (doPostProcessing) {
+ mResolverListCommunicator.updateProfileViewButton();
+ //TODO: this method is different from super's only in that `notifyDataSetChanged` is
+ // called conditionally here; is it really important?
+ notifyDataSetChanged();
+ }
+ }
}
diff --git a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
index c159243..080f9d2 100644
--- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
@@ -25,6 +25,7 @@ import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
import com.android.intentresolver.grid.ChooserGridAdapter;
import com.android.intentresolver.measurements.Tracer;
import com.android.internal.annotations.VisibleForTesting;
@@ -38,21 +39,22 @@ import java.util.function.Supplier;
* A {@link PagerAdapter} which describes the work and personal profile share sheet screens.
*/
@VisibleForTesting
-public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAdapter<
+public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<
RecyclerView, ChooserGridAdapter, ChooserListAdapter> {
private static final int SINGLE_CELL_SPAN_SIZE = 1;
private final ChooserProfileAdapterBinder mAdapterBinder;
private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier;
- ChooserMultiProfilePagerAdapter(
+ public ChooserMultiProfilePagerAdapter(
Context context,
ChooserGridAdapter adapter,
EmptyStateProvider emptyStateProvider,
Supplier<Boolean> workProfileQuietModeChecker,
UserHandle workProfileUserHandle,
UserHandle cloneProfileUserHandle,
- int maxTargetsPerRow) {
+ int maxTargetsPerRow,
+ FeatureFlags featureFlags) {
this(
context,
new ChooserProfileAdapterBinder(maxTargetsPerRow),
@@ -62,10 +64,11 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
/* defaultProfile= */ 0,
workProfileUserHandle,
cloneProfileUserHandle,
- new BottomPaddingOverrideSupplier(context));
+ new BottomPaddingOverrideSupplier(context),
+ featureFlags);
}
- ChooserMultiProfilePagerAdapter(
+ public ChooserMultiProfilePagerAdapter(
Context context,
ChooserGridAdapter personalAdapter,
ChooserGridAdapter workAdapter,
@@ -74,7 +77,8 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
@Profile int defaultProfile,
UserHandle workProfileUserHandle,
UserHandle cloneProfileUserHandle,
- int maxTargetsPerRow) {
+ int maxTargetsPerRow,
+ FeatureFlags featureFlags) {
this(
context,
new ChooserProfileAdapterBinder(maxTargetsPerRow),
@@ -84,7 +88,8 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
defaultProfile,
workProfileUserHandle,
cloneProfileUserHandle,
- new BottomPaddingOverrideSupplier(context));
+ new BottomPaddingOverrideSupplier(context),
+ featureFlags);
}
private ChooserMultiProfilePagerAdapter(
@@ -96,9 +101,9 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
@Profile int defaultProfile,
UserHandle workProfileUserHandle,
UserHandle cloneProfileUserHandle,
- BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {
+ BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier,
+ FeatureFlags featureFlags) {
super(
- context,
gridAdapter -> gridAdapter.getListAdapter(),
adapterBinder,
gridAdapters,
@@ -107,7 +112,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
defaultProfile,
workProfileUserHandle,
cloneProfileUserHandle,
- () -> makeProfileView(context),
+ () -> makeProfileView(context, featureFlags),
bottomPaddingOverrideSupplier);
mAdapterBinder = adapterBinder;
mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier;
@@ -131,10 +136,12 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
}
}
- private static ViewGroup makeProfileView(Context context) {
+ private static ViewGroup makeProfileView(
+ Context context, FeatureFlags featureFlags) {
LayoutInflater inflater = LayoutInflater.from(context);
- ViewGroup rootView = (ViewGroup) inflater.inflate(
- R.layout.chooser_list_per_profile, null, false);
+ ViewGroup rootView = featureFlags.scrollablePreview()
+ ? (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile_wrap, null, false)
+ : (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile, null, false);
RecyclerView recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list);
recyclerView.setAccessibilityDelegateCompat(
new ChooserRecyclerViewAccessibilityDelegate(recyclerView));
@@ -142,7 +149,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
}
@Override
- boolean rebuildActiveTab(boolean doPostProcessing) {
+ public boolean rebuildActiveTab(boolean doPostProcessing) {
if (doPostProcessing) {
Tracer.INSTANCE.beginAppTargetLoadingSection(getActiveListAdapter().getUserHandle());
}
@@ -150,7 +157,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
}
@Override
- boolean rebuildInactiveTab(boolean doPostProcessing) {
+ public boolean rebuildInactiveTab(boolean doPostProcessing) {
if (getItemCount() != 1 && doPostProcessing) {
Tracer.INSTANCE.beginAppTargetLoadingSection(getInactiveListAdapter().getUserHandle());
}
diff --git a/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
index 250b682..d6688d9 100644
--- a/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
+++ b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
@@ -16,20 +16,20 @@
package com.android.intentresolver;
-import android.annotation.NonNull;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
+import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
-class ChooserRecyclerViewAccessibilityDelegate extends RecyclerViewAccessibilityDelegate {
+public class ChooserRecyclerViewAccessibilityDelegate extends RecyclerViewAccessibilityDelegate {
private final Rect mTempRect = new Rect();
private final int[] mConsumed = new int[2];
- ChooserRecyclerViewAccessibilityDelegate(RecyclerView recyclerView) {
+ public ChooserRecyclerViewAccessibilityDelegate(RecyclerView recyclerView) {
super(recyclerView);
}
diff --git a/java/src/com/android/intentresolver/ChooserRefinementManager.java b/java/src/com/android/intentresolver/ChooserRefinementManager.java
index 2ebe48a..474b240 100644
--- a/java/src/com/android/intentresolver/ChooserRefinementManager.java
+++ b/java/src/com/android/intentresolver/ChooserRefinementManager.java
@@ -16,8 +16,6 @@
package com.android.intentresolver;
-import android.annotation.Nullable;
-import android.annotation.UiThread;
import android.app.Activity;
import android.app.Application;
import android.content.Intent;
@@ -28,22 +26,30 @@ import android.os.Parcel;
import android.os.ResultReceiver;
import android.util.Log;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.android.intentresolver.chooser.TargetInfo;
+import dagger.hilt.android.lifecycle.HiltViewModel;
+
import java.util.List;
import java.util.function.Consumer;
+import javax.inject.Inject;
+
+
/**
* Helper class to manage Sharesheet's "refinement" flow, where callers supply a "refinement
* activity" that will be invoked when a target is selected, allowing the calling app to add
- * additional extras and other refinements (subject to {@link Intent#filterEquals()}), e.g., to
+ * additional extras and other refinements (subject to {@link Intent#filterEquals}), e.g., to
* convert the format of the payload, or lazy-download some data that was deferred in the original
* call).
*/
+@HiltViewModel
@UiThread
public final class ChooserRefinementManager extends ViewModel {
private static final String TAG = "ChooserRefinement";
@@ -88,6 +94,9 @@ public final class ChooserRefinementManager extends ViewModel {
private MutableLiveData<RefinementCompletion> mRefinementCompletion = new MutableLiveData<>();
+ @Inject
+ public ChooserRefinementManager() {}
+
public LiveData<RefinementCompletion> getRefinementCompletion() {
return mRefinementCompletion;
}
diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java
index 5157986..7ad809e 100644
--- a/java/src/com/android/intentresolver/ChooserRequestParameters.java
+++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java
@@ -16,8 +16,6 @@
package com.android.intentresolver;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
@@ -32,7 +30,9 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
-import com.android.intentresolver.flags.FeatureFlagRepository;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.util.UriFilters;
import com.google.common.collect.ImmutableList;
@@ -104,8 +104,7 @@ public class ChooserRequestParameters {
public ChooserRequestParameters(
final Intent clientIntent,
String referrerPackageName,
- final Uri referrer,
- FeatureFlagRepository featureFlags) {
+ final Uri referrer) {
final Intent requestedTarget = parseTargetIntentExtra(
clientIntent.getParcelableExtra(Intent.EXTRA_INTENT));
mTarget = intentWithModifiedLaunchFlags(requestedTarget);
@@ -212,7 +211,7 @@ public class ChooserRequestParameters {
/**
* TODO: this returns a nullable array for convenience, but if the legacy APIs can be
- * refactored, returning {@link mAdditionalTargets} directly is simpler and safer.
+ * refactored, returning {@link #mAdditionalTargets} directly is simpler and safer.
*/
@Nullable
public Intent[] getAdditionalTargets() {
@@ -226,7 +225,7 @@ public class ChooserRequestParameters {
/**
* TODO: this returns a nullable array for convenience, but if the legacy APIs can be
- * refactored, returning {@link mInitialIntents} directly is simpler and safer.
+ * refactored, returning {@link #mInitialIntents} directly is simpler and safer.
*/
@Nullable
public Intent[] getInitialIntents() {
@@ -288,7 +287,7 @@ public class ChooserRequestParameters {
* requested target <em>wasn't</em> a send action; otherwise it is null. The second value is
* the resource ID of a default title string; this is nonzero only if the first value is null.
*
- * TODO: change the API for how these are passed up to {@link ResolverActivity#onCreate()}, or
+ * TODO: change the API for how these are passed up to {@link ResolverActivity#onCreate}, or
* create a real type (not {@link Pair}) to express the semantics described in this comment.
*/
private static Pair<CharSequence, Integer> makeTitleSpec(
@@ -371,7 +370,7 @@ public class ChooserRequestParameters {
* the required type. If false, throw an {@link IllegalArgumentException} if the extra is
* non-null but can't be assigned to variables of type {@code T}.
* @param streamEmptyIfNull Whether to return an empty stream if the optional extra isn't
- * present in the intent (or if it had the wrong type, but {@link warnOnTypeError} is true).
+ * present in the intent (or if it had the wrong type, but <em>warnOnTypeError</em> is true).
* If false, return null in these cases, and only return an empty stream if the intent
* explicitly provided an empty array for the specified extra.
*/
diff --git a/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
index 2cfceea..f0fcd14 100644
--- a/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
+++ b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
@@ -22,6 +22,7 @@ import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
+import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentManager;
import com.android.intentresolver.chooser.DisplayResolveInfo;
@@ -66,6 +67,7 @@ public class ChooserStackedAppDialogFragment extends ChooserTargetActionsDialogF
dismiss();
}
+ @NonNull
@Override
protected CharSequence getItemLabel(DisplayResolveInfo dri) {
final PackageManager pm = getContext().getPackageManager();
diff --git a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
index 4bfb21a..b6b7de9 100644
--- a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
+++ b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
@@ -21,8 +21,6 @@ import static android.content.Context.ACTIVITY_SERVICE;
import static java.util.stream.Collectors.toList;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.Dialog;
import android.content.ComponentName;
@@ -46,6 +44,8 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.RecyclerView;
diff --git a/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java
deleted file mode 100644
index a1c5340..0000000
--- a/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java
+++ /dev/null
@@ -1,235 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.os.UserHandle;
-import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import com.google.common.collect.ImmutableList;
-
-import java.util.Optional;
-import java.util.function.Function;
-import java.util.function.Supplier;
-
-/**
- * Implementation of {@link AbstractMultiProfilePagerAdapter} that consolidates the variation in
- * existing implementations; most overrides were only to vary type signatures (which are better
- * represented via generic types), and a few minor behavioral customizations are now implemented
- * through small injectable delegate classes.
- * TODO: now that the existing implementations are shown to be expressible in terms of this new
- * generic type, merge up into the base class and simplify the public APIs.
- * TODO: attempt to further restrict visibility in the methods we expose.
- * TODO: deprecate and audit/fix usages of any methods that refer to the "active" or "inactive"
- * adapters; these were marked {@link VisibleForTesting} and their usage seems like an accident
- * waiting to happen since clients seem to make assumptions about which adapter will be "active" in
- * a particular context, and more explicit APIs would make sure those were valid.
- * TODO: consider renaming legacy methods (e.g. why do we know it's a "list", not just a "page"?)
- *
- * @param <PageViewT> the type of the widget that represents the contents of a page in this adapter
- * @param <SinglePageAdapterT> the type of a "root" adapter class to be instantiated and included in
- * the per-profile records.
- * @param <ListAdapterT> the concrete type of a {@link ResolverListAdapter} implementation to
- * control the contents of a given per-profile list. This is provided for convenience, since it must
- * be possible to get the list adapter from the page adapter via our {@link mListAdapterExtractor}.
- *
- * TODO: this class doesn't make any explicit usage of the {@link ResolverListAdapter} API, so the
- * type constraint can probably be dropped once the API is merged upwards and cleaned.
- */
-class GenericMultiProfilePagerAdapter<
- PageViewT extends ViewGroup,
- SinglePageAdapterT,
- ListAdapterT extends ResolverListAdapter> extends AbstractMultiProfilePagerAdapter {
-
- /** Delegate to set up a given adapter and page view to be used together. */
- public interface AdapterBinder<PageViewT, SinglePageAdapterT> {
- /**
- * The given {@code view} will be associated with the given {@code adapter}. Do any work
- * necessary to configure them compatibly, introduce them to each other, etc.
- */
- void bind(PageViewT view, SinglePageAdapterT adapter);
- }
-
- private final Function<SinglePageAdapterT, ListAdapterT> mListAdapterExtractor;
- private final AdapterBinder<PageViewT, SinglePageAdapterT> mAdapterBinder;
- private final Supplier<ViewGroup> mPageViewInflater;
- private final Supplier<Optional<Integer>> mContainerBottomPaddingOverrideSupplier;
-
- private final ImmutableList<GenericProfileDescriptor<PageViewT, SinglePageAdapterT>> mItems;
-
- GenericMultiProfilePagerAdapter(
- Context context,
- Function<SinglePageAdapterT, ListAdapterT> listAdapterExtractor,
- AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder,
- ImmutableList<SinglePageAdapterT> adapters,
- EmptyStateProvider emptyStateProvider,
- Supplier<Boolean> workProfileQuietModeChecker,
- @Profile int defaultProfile,
- UserHandle workProfileUserHandle,
- UserHandle cloneProfileUserHandle,
- Supplier<ViewGroup> pageViewInflater,
- Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
- super(
- context,
- /* currentPage= */ defaultProfile,
- emptyStateProvider,
- workProfileQuietModeChecker,
- workProfileUserHandle,
- cloneProfileUserHandle);
-
- mListAdapterExtractor = listAdapterExtractor;
- mAdapterBinder = adapterBinder;
- mPageViewInflater = pageViewInflater;
- mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier;
-
- ImmutableList.Builder<GenericProfileDescriptor<PageViewT, SinglePageAdapterT>> items =
- new ImmutableList.Builder<>();
- for (SinglePageAdapterT adapter : adapters) {
- items.add(createProfileDescriptor(adapter));
- }
- mItems = items.build();
- }
-
- private GenericProfileDescriptor<PageViewT, SinglePageAdapterT>
- createProfileDescriptor(SinglePageAdapterT adapter) {
- return new GenericProfileDescriptor<>(mPageViewInflater.get(), adapter);
- }
-
- @Override
- protected GenericProfileDescriptor<PageViewT, SinglePageAdapterT> getItem(int pageIndex) {
- return mItems.get(pageIndex);
- }
-
- @Override
- public int getItemCount() {
- return mItems.size();
- }
-
- public PageViewT getListViewForIndex(int index) {
- return getItem(index).mView;
- }
-
- @Override
- @VisibleForTesting
- public SinglePageAdapterT getAdapterForIndex(int index) {
- return getItem(index).mAdapter;
- }
-
- @Override
- protected void setupListAdapter(int pageIndex) {
- mAdapterBinder.bind(getListViewForIndex(pageIndex), getAdapterForIndex(pageIndex));
- }
-
- @Override
- public ViewGroup instantiateItem(ViewGroup container, int position) {
- setupListAdapter(position);
- return super.instantiateItem(container, position);
- }
-
- @Override
- @Nullable
- protected ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) {
- if (getPersonalListAdapter().getUserHandle().equals(userHandle)
- || userHandle.equals(getCloneUserHandle())) {
- return getPersonalListAdapter();
- } else if (getWorkListAdapter() != null
- && getWorkListAdapter().getUserHandle().equals(userHandle)) {
- return getWorkListAdapter();
- }
- return null;
- }
-
- @Override
- @VisibleForTesting
- public ListAdapterT getActiveListAdapter() {
- return mListAdapterExtractor.apply(getAdapterForIndex(getCurrentPage()));
- }
-
- @Override
- @VisibleForTesting
- public ListAdapterT getInactiveListAdapter() {
- if (getCount() < 2) {
- return null;
- }
- return mListAdapterExtractor.apply(getAdapterForIndex(1 - getCurrentPage()));
- }
-
- @Override
- public ListAdapterT getPersonalListAdapter() {
- return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_PERSONAL));
- }
-
- @Override
- public ListAdapterT getWorkListAdapter() {
- if (!hasAdapterForIndex(PROFILE_WORK)) {
- return null;
- }
- return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_WORK));
- }
-
- @Override
- protected SinglePageAdapterT getCurrentRootAdapter() {
- return getAdapterForIndex(getCurrentPage());
- }
-
- @Override
- protected PageViewT getActiveAdapterView() {
- return getListViewForIndex(getCurrentPage());
- }
-
- @Override
- protected PageViewT getInactiveAdapterView() {
- if (getCount() < 2) {
- return null;
- }
- return getListViewForIndex(1 - getCurrentPage());
- }
-
- @Override
- protected void setupContainerPadding(View container) {
- Optional<Integer> bottomPaddingOverride = mContainerBottomPaddingOverrideSupplier.get();
- bottomPaddingOverride.ifPresent(paddingBottom ->
- container.setPadding(
- container.getPaddingLeft(),
- container.getPaddingTop(),
- container.getPaddingRight(),
- paddingBottom));
- }
-
- private boolean hasAdapterForIndex(int pageIndex) {
- return (pageIndex < getCount());
- }
-
- // TODO: `ChooserActivity` also has a per-profile record type. Maybe the "multi-profile pager"
- // should be the owner of all per-profile data (especially now that the API is generic)?
- private static class GenericProfileDescriptor<PageViewT, SinglePageAdapterT> extends
- ProfileDescriptor {
- private final SinglePageAdapterT mAdapter;
- private final PageViewT mView;
-
- GenericProfileDescriptor(ViewGroup rootView, SinglePageAdapterT adapter) {
- super(rootView);
- mAdapter = adapter;
- mView = (PageViewT) rootView.findViewById(com.android.internal.R.id.resolver_list);
- }
- }
-}
diff --git a/java/src/com/android/intentresolver/IntentForwarderActivity.java b/java/src/com/android/intentresolver/IntentForwarderActivity.java
index 5e8945f..15996d0 100644
--- a/java/src/com/android/intentresolver/IntentForwarderActivity.java
+++ b/java/src/com/android/intentresolver/IntentForwarderActivity.java
@@ -23,7 +23,6 @@ import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
import static com.android.intentresolver.ResolverActivity.EXTRA_CALLING_USER;
import static com.android.intentresolver.ResolverActivity.EXTRA_SELECTED_PROFILE;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityThread;
import android.app.AppGlobals;
@@ -45,6 +44,8 @@ import android.provider.Settings;
import android.util.Slog;
import android.widget.Toast;
+import androidx.annotation.Nullable;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -309,7 +310,7 @@ public class IntentForwarderActivity extends Activity {
* Check whether the intent can be forwarded to target user. Return the intent used for
* forwarding if it can be forwarded, {@code null} otherwise.
*/
- static Intent canForward(Intent incomingIntent, int sourceUserId, int targetUserId,
+ public static Intent canForward(Intent incomingIntent, int sourceUserId, int targetUserId,
IPackageManager packageManager, ContentResolver contentResolver) {
Intent forwardIntent = new Intent(incomingIntent);
forwardIntent.addFlags(
diff --git a/java/tests/src/com/android/intentresolver/RequireFeatureFlags.kt b/java/src/com/android/intentresolver/MainApplication.kt
index 1ddf746..0a82662 100644
--- a/java/tests/src/com/android/intentresolver/RequireFeatureFlags.kt
+++ b/java/src/com/android/intentresolver/MainApplication.kt
@@ -16,8 +16,7 @@
package com.android.intentresolver
-/**
- * Specifies expected feature flag values for a test.
- */
-@Target(AnnotationTarget.FUNCTION)
-annotation class RequireFeatureFlags(val flags: Array<String>, val values: BooleanArray)
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp(Application::class) open class MainApplication : Hilt_MainApplication()
diff --git a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java
index 4b06db3..42a29e5 100644
--- a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java
@@ -15,15 +15,6 @@
*/
package com.android.intentresolver;
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.UserIdInt;
-import android.app.AppGlobals;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.IPackageManager;
import android.os.Trace;
import android.os.UserHandle;
import android.view.View;
@@ -31,62 +22,124 @@ import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.emptystate.EmptyStateUiHelper;
import com.android.internal.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+
import java.util.HashSet;
-import java.util.List;
-import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
+import java.util.function.Function;
import java.util.function.Supplier;
/**
- * Skeletal {@link PagerAdapter} implementation of a work or personal profile page for
- * intent resolution (including share sheet).
+ * Skeletal {@link PagerAdapter} implementation for a UI with per-profile tabs (as in Sharesheet).
+ *
+ * TODO: attempt to further restrict visibility/improve encapsulation in the methods we expose.
+ * TODO: deprecate and audit/fix usages of any methods that refer to the "active" or "inactive"
+ * adapters; these were marked {@link VisibleForTesting} and their usage seems like an accident
+ * waiting to happen since clients seem to make assumptions about which adapter will be "active" in
+ * a particular context, and more explicit APIs would make sure those were valid.
+ * TODO: consider renaming legacy methods (e.g. why do we know it's a "list", not just a "page"?)
+ *
+ * @param <PageViewT> the type of the widget that represents the contents of a page in this adapter
+ * @param <SinglePageAdapterT> the type of a "root" adapter class to be instantiated and included in
+ * the per-profile records.
+ * @param <ListAdapterT> the concrete type of a {@link ResolverListAdapter} implementation to
+ * control the contents of a given per-profile list. This is provided for convenience, since it must
+ * be possible to get the list adapter from the page adapter via our {@link mListAdapterExtractor}.
+ *
+ * TODO: this is part of an in-progress refactor to merge with `GenericMultiProfilePagerAdapter`.
+ * As originally noted there, we've reduced explicit references to the `ResolverListAdapter` base
+ * type and may be able to drop the type constraint.
*/
-public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
+public class MultiProfilePagerAdapter<
+ PageViewT extends ViewGroup,
+ SinglePageAdapterT,
+ ListAdapterT extends ResolverListAdapter> extends PagerAdapter {
+
+ /**
+ * Delegate to set up a given adapter and page view to be used together.
+ * @param <PageViewT> (as in {@link MultiProfilePagerAdapter}).
+ * @param <SinglePageAdapterT> (as in {@link MultiProfilePagerAdapter}).
+ */
+ public interface AdapterBinder<PageViewT, SinglePageAdapterT> {
+ /**
+ * The given {@code view} will be associated with the given {@code adapter}. Do any work
+ * necessary to configure them compatibly, introduce them to each other, etc.
+ */
+ void bind(PageViewT view, SinglePageAdapterT adapter);
+ }
- private static final String TAG = "AbstractMultiProfilePagerAdapter";
- static final int PROFILE_PERSONAL = 0;
- static final int PROFILE_WORK = 1;
+ public static final int PROFILE_PERSONAL = 0;
+ public static final int PROFILE_WORK = 1;
@IntDef({PROFILE_PERSONAL, PROFILE_WORK})
- @interface Profile {}
+ public @interface Profile {}
- private final Context mContext;
- private int mCurrentPage;
- private OnProfileSelectedListener mOnProfileSelectedListener;
+ private final Function<SinglePageAdapterT, ListAdapterT> mListAdapterExtractor;
+ private final AdapterBinder<PageViewT, SinglePageAdapterT> mAdapterBinder;
+ private final Supplier<ViewGroup> mPageViewInflater;
+ private final Supplier<Optional<Integer>> mContainerBottomPaddingOverrideSupplier;
+
+ private final ImmutableList<ProfileDescriptor<PageViewT, SinglePageAdapterT>> mItems;
- private Set<Integer> mLoadedPages;
private final EmptyStateProvider mEmptyStateProvider;
private final UserHandle mWorkProfileUserHandle;
private final UserHandle mCloneProfileUserHandle;
private final Supplier<Boolean> mWorkProfileQuietModeChecker; // True when work is quiet.
- AbstractMultiProfilePagerAdapter(
- Context context,
- int currentPage,
+ private Set<Integer> mLoadedPages;
+ private int mCurrentPage;
+ private OnProfileSelectedListener mOnProfileSelectedListener;
+
+ protected MultiProfilePagerAdapter(
+ Function<SinglePageAdapterT, ListAdapterT> listAdapterExtractor,
+ AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder,
+ ImmutableList<SinglePageAdapterT> adapters,
EmptyStateProvider emptyStateProvider,
Supplier<Boolean> workProfileQuietModeChecker,
+ @Profile int defaultProfile,
UserHandle workProfileUserHandle,
- UserHandle cloneProfileUserHandle) {
- mContext = Objects.requireNonNull(context);
- mCurrentPage = currentPage;
+ UserHandle cloneProfileUserHandle,
+ Supplier<ViewGroup> pageViewInflater,
+ Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
+ mCurrentPage = defaultProfile;
mLoadedPages = new HashSet<>();
mWorkProfileUserHandle = workProfileUserHandle;
mCloneProfileUserHandle = cloneProfileUserHandle;
mEmptyStateProvider = emptyStateProvider;
mWorkProfileQuietModeChecker = workProfileQuietModeChecker;
+
+ mListAdapterExtractor = listAdapterExtractor;
+ mAdapterBinder = adapterBinder;
+ mPageViewInflater = pageViewInflater;
+ mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier;
+
+ ImmutableList.Builder<ProfileDescriptor<PageViewT, SinglePageAdapterT>> items =
+ new ImmutableList.Builder<>();
+ for (SinglePageAdapterT adapter : adapters) {
+ items.add(createProfileDescriptor(adapter));
+ }
+ mItems = items.build();
}
- void setOnProfileSelectedListener(OnProfileSelectedListener listener) {
- mOnProfileSelectedListener = listener;
+ private ProfileDescriptor<PageViewT, SinglePageAdapterT> createProfileDescriptor(
+ SinglePageAdapterT adapter) {
+ return new ProfileDescriptor<>(mPageViewInflater.get(), adapter);
}
- Context getContext() {
- return mContext;
+ public void setOnProfileSelectedListener(OnProfileSelectedListener listener) {
+ mOnProfileSelectedListener = listener;
}
/**
@@ -94,7 +147,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* an {@link ViewPager.OnPageChangeListener} where it keeps track of the currently displayed
* page and rebuilds the list.
*/
- void setupViewPager(ViewPager viewPager) {
+ public void setupViewPager(ViewPager viewPager) {
viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
@@ -120,22 +173,24 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
mLoadedPages.add(mCurrentPage);
}
- void clearInactiveProfileCache() {
+ public void clearInactiveProfileCache() {
if (mLoadedPages.size() == 1) {
return;
}
mLoadedPages.remove(1 - mCurrentPage);
}
+ @NonNull
@Override
- public ViewGroup instantiateItem(ViewGroup container, int position) {
- final ProfileDescriptor profileDescriptor = getItem(position);
- container.addView(profileDescriptor.rootView);
- return profileDescriptor.rootView;
+ public final ViewGroup instantiateItem(ViewGroup container, int position) {
+ setupListAdapter(position);
+ final ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(position);
+ container.addView(descriptor.mRootView);
+ return descriptor.mRootView;
}
@Override
- public void destroyItem(ViewGroup container, int position, Object view) {
+ public void destroyItem(ViewGroup container, int position, @NonNull Object view) {
container.removeView((View) view);
}
@@ -144,7 +199,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
return getItemCount();
}
- protected int getCurrentPage() {
+ public int getCurrentPage() {
return mCurrentPage;
}
@@ -154,7 +209,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
@Override
- public boolean isViewFromObject(View view, Object object) {
+ public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
@@ -177,9 +232,11 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* <code>1</code> would return the work profile {@link ProfileDescriptor}.</li>
* </ul>
*/
- abstract ProfileDescriptor getItem(int pageIndex);
+ private ProfileDescriptor<PageViewT, SinglePageAdapterT> getItem(int pageIndex) {
+ return mItems.get(pageIndex);
+ }
- protected ViewGroup getEmptyStateView(int pageIndex) {
+ public ViewGroup getEmptyStateView(int pageIndex) {
return getItem(pageIndex).getEmptyStateView();
}
@@ -188,13 +245,13 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* <p>For a normal consumer device with only one user returns <code>1</code>.
* <p>For a device with a work profile returns <code>2</code>.
*/
- abstract int getItemCount();
+ public final int getItemCount() {
+ return mItems.size();
+ }
- /**
- * Performs view-related initialization procedures for the adapter specified
- * by <code>pageIndex</code>.
- */
- abstract void setupListAdapter(int pageIndex);
+ public final PageViewT getListViewForIndex(int index) {
+ return getItem(index).mView;
+ }
/**
* Returns the adapter of the list view for the relevant page specified by
@@ -203,54 +260,99 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* depending on the adapter type.
*/
@VisibleForTesting
- public abstract Object getAdapterForIndex(int pageIndex);
+ public final SinglePageAdapterT getAdapterForIndex(int index) {
+ return getItem(index).mAdapter;
+ }
/**
- * Returns the {@link ResolverListAdapter} instance of the profile that represents
+ * Performs view-related initialization procedures for the adapter specified
+ * by <code>pageIndex</code>.
+ */
+ public final void setupListAdapter(int pageIndex) {
+ mAdapterBinder.bind(getListViewForIndex(pageIndex), getAdapterForIndex(pageIndex));
+ }
+
+ /**
+ * Returns the {@link ListAdapterT} instance of the profile that represents
* <code>userHandle</code>. If there is no such adapter for the specified
* <code>userHandle</code>, returns {@code null}.
* <p>For example, if there is a work profile on the device with user id 10, calling this method
- * with <code>UserHandle.of(10)</code> returns the work profile {@link ResolverListAdapter}.
+ * with <code>UserHandle.of(10)</code> returns the work profile {@link ListAdapterT}.
*/
@Nullable
- abstract ResolverListAdapter getListAdapterForUserHandle(UserHandle userHandle);
+ public final ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) {
+ if (getPersonalListAdapter().getUserHandle().equals(userHandle)
+ || userHandle.equals(getCloneUserHandle())) {
+ return getPersonalListAdapter();
+ } else if ((getWorkListAdapter() != null)
+ && getWorkListAdapter().getUserHandle().equals(userHandle)) {
+ return getWorkListAdapter();
+ }
+ return null;
+ }
/**
- * Returns the {@link ResolverListAdapter} instance of the profile that is currently visible
+ * Returns the {@link ListAdapterT} instance of the profile that is currently visible
* to the user.
* <p>For example, if the user is viewing the work tab in the share sheet, this method returns
- * the work profile {@link ResolverListAdapter}.
+ * the work profile {@link ListAdapterT}.
* @see #getInactiveListAdapter()
*/
@VisibleForTesting
- public abstract ResolverListAdapter getActiveListAdapter();
+ public final ListAdapterT getActiveListAdapter() {
+ return mListAdapterExtractor.apply(getAdapterForIndex(getCurrentPage()));
+ }
/**
- * If this is a device with a work profile, returns the {@link ResolverListAdapter} instance
+ * If this is a device with a work profile, returns the {@link ListAdapterT} instance
* of the profile that is <b><i>not</i></b> currently visible to the user. Otherwise returns
* {@code null}.
* <p>For example, if the user is viewing the work tab in the share sheet, this method returns
- * the personal profile {@link ResolverListAdapter}.
+ * the personal profile {@link ListAdapterT}.
* @see #getActiveListAdapter()
*/
@VisibleForTesting
- public abstract @Nullable ResolverListAdapter getInactiveListAdapter();
+ @Nullable
+ public final ListAdapterT getInactiveListAdapter() {
+ if (getCount() < 2) {
+ return null;
+ }
+ return mListAdapterExtractor.apply(getAdapterForIndex(1 - getCurrentPage()));
+ }
- public abstract ResolverListAdapter getPersonalListAdapter();
+ public final ListAdapterT getPersonalListAdapter() {
+ return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_PERSONAL));
+ }
- public abstract @Nullable ResolverListAdapter getWorkListAdapter();
+ @Nullable
+ public final ListAdapterT getWorkListAdapter() {
+ if (!hasAdapterForIndex(PROFILE_WORK)) {
+ return null;
+ }
+ return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_WORK));
+ }
- abstract Object getCurrentRootAdapter();
+ public final SinglePageAdapterT getCurrentRootAdapter() {
+ return getAdapterForIndex(getCurrentPage());
+ }
- abstract ViewGroup getActiveAdapterView();
+ public final PageViewT getActiveAdapterView() {
+ return getListViewForIndex(getCurrentPage());
+ }
- abstract @Nullable ViewGroup getInactiveAdapterView();
+ @Nullable
+ public final PageViewT getInactiveAdapterView() {
+ if (getCount() < 2) {
+ return null;
+ }
+ return getListViewForIndex(1 - getCurrentPage());
+ }
/**
* Rebuilds the tab that is currently visible to the user.
* <p>Returns {@code true} if rebuild has completed.
*/
- boolean rebuildActiveTab(boolean doPostProcessing) {
+ public boolean rebuildActiveTab(boolean doPostProcessing) {
Trace.beginSection("MultiProfilePagerAdapter#rebuildActiveTab");
boolean result = rebuildTab(getActiveListAdapter(), doPostProcessing);
Trace.endSection();
@@ -261,7 +363,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* Rebuilds the tab that is not currently visible to the user, if such one exists.
* <p>Returns {@code true} if rebuild has completed.
*/
- boolean rebuildInactiveTab(boolean doPostProcessing) {
+ public boolean rebuildInactiveTab(boolean doPostProcessing) {
Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab");
if (getItemCount() == 1) {
Trace.endSection();
@@ -280,7 +382,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
}
- private boolean rebuildTab(ResolverListAdapter activeListAdapter, boolean doPostProcessing) {
+ private boolean rebuildTab(ListAdapterT activeListAdapter, boolean doPostProcessing) {
if (shouldSkipRebuild(activeListAdapter)) {
activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true);
return false;
@@ -288,16 +390,20 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
return activeListAdapter.rebuildList(doPostProcessing);
}
- private boolean shouldSkipRebuild(ResolverListAdapter activeListAdapter) {
+ private boolean shouldSkipRebuild(ListAdapterT activeListAdapter) {
EmptyState emptyState = mEmptyStateProvider.getEmptyState(activeListAdapter);
return emptyState != null && emptyState.shouldSkipDataRebuild();
}
+ private boolean hasAdapterForIndex(int pageIndex) {
+ return (pageIndex < getCount());
+ }
+
/**
* The empty state screens are shown according to their priority:
* <ol>
* <li>(highest priority) cross-profile disabled by policy (handled in
- * {@link #rebuildTab(ResolverListAdapter, boolean)})</li>
+ * {@link #rebuildTab(ListAdapterT, boolean)})</li>
* <li>no apps available</li>
* <li>(least priority) work is off</li>
* </ol>
@@ -306,7 +412,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* the work profile on if there will not be any apps resolved
* anyway.
*/
- void showEmptyResolverListEmptyState(ResolverListAdapter listAdapter) {
+ public void showEmptyResolverListEmptyState(ListAdapterT listAdapter) {
final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter);
if (emptyState == null) {
@@ -319,9 +425,9 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
if (emptyState.getButtonClickListener() != null) {
clickListener = v -> emptyState.getButtonClickListener().onClick(() -> {
- ProfileDescriptor descriptor = getItem(
+ ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
userHandleToPageIndex(listAdapter.getUserHandle()));
- AbstractMultiProfilePagerAdapter.this.showSpinner(descriptor.getEmptyStateView());
+ descriptor.mEmptyStateUi.showSpinner();
});
}
@@ -340,45 +446,24 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
}
- /**
- * Utility class to check if there are cross profile intents, it is in a separate class so
- * it could be mocked in tests
- */
- public static class CrossProfileIntentsChecker {
-
- private final ContentResolver mContentResolver;
-
- public CrossProfileIntentsChecker(@NonNull ContentResolver contentResolver) {
- mContentResolver = contentResolver;
- }
-
- /**
- * Returns {@code true} if at least one of the provided {@code intents} can be forwarded
- * from {@code source} (user id) to {@code target} (user id).
- */
- public boolean hasCrossProfileIntents(List<Intent> intents, @UserIdInt int source,
- @UserIdInt int target) {
- IPackageManager packageManager = AppGlobals.getPackageManager();
-
- return intents.stream().anyMatch(intent ->
- null != IntentForwarderActivity.canForward(intent, source, target,
- packageManager, mContentResolver));
- }
- }
-
- protected void showEmptyState(ResolverListAdapter activeListAdapter, EmptyState emptyState,
+ protected void showEmptyState(
+ ListAdapterT activeListAdapter,
+ EmptyState emptyState,
View.OnClickListener buttonOnClick) {
- ProfileDescriptor descriptor = getItem(
+ ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
userHandleToPageIndex(activeListAdapter.getUserHandle()));
- descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.GONE);
+ descriptor.mRootView.findViewById(
+ com.android.internal.R.id.resolver_list).setVisibility(View.GONE);
+ descriptor.mEmptyStateUi.resetViewVisibilities();
+
ViewGroup emptyStateView = descriptor.getEmptyStateView();
- resetViewVisibilitiesForEmptyState(emptyStateView);
- emptyStateView.setVisibility(View.VISIBLE);
- View container = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_container);
+ View container = emptyStateView.findViewById(
+ com.android.internal.R.id.resolver_empty_state_container);
setupContainerPadding(container);
- TextView titleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title);
+ TextView titleView = emptyStateView.findViewById(
+ com.android.internal.R.id.resolver_empty_state_title);
String title = emptyState.getTitle();
if (title != null) {
titleView.setVisibility(View.VISIBLE);
@@ -387,7 +472,8 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
titleView.setVisibility(View.GONE);
}
- TextView subtitleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle);
+ TextView subtitleView = emptyStateView.findViewById(
+ com.android.internal.R.id.resolver_empty_state_subtitle);
String subtitle = emptyState.getSubtitle();
if (subtitle != null) {
subtitleView.setVisibility(View.VISIBLE);
@@ -399,7 +485,8 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
View defaultEmptyText = emptyStateView.findViewById(com.android.internal.R.id.empty);
defaultEmptyText.setVisibility(emptyState.useDefaultEmptyView() ? View.VISIBLE : View.GONE);
- Button button = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button);
+ Button button = emptyStateView.findViewById(
+ com.android.internal.R.id.resolver_empty_state_button);
button.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE);
button.setOnClickListener(buttonOnClick);
@@ -410,44 +497,50 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* Sets up the padding of the view containing the empty state screens.
* <p>This method is meant to be overridden so that subclasses can customize the padding.
*/
- protected void setupContainerPadding(View container) {}
-
- private void showSpinner(View emptyStateView) {
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.INVISIBLE);
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.INVISIBLE);
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).setVisibility(View.VISIBLE);
- emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE);
- }
-
- private void resetViewVisibilitiesForEmptyState(View emptyStateView) {
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.VISIBLE);
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle).setVisibility(View.VISIBLE);
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.INVISIBLE);
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).setVisibility(View.GONE);
- emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE);
- }
-
- protected void showListView(ResolverListAdapter activeListAdapter) {
- ProfileDescriptor descriptor = getItem(
+ public void setupContainerPadding(View container) {
+ Optional<Integer> bottomPaddingOverride = mContainerBottomPaddingOverrideSupplier.get();
+ bottomPaddingOverride.ifPresent(paddingBottom ->
+ container.setPadding(
+ container.getPaddingLeft(),
+ container.getPaddingTop(),
+ container.getPaddingRight(),
+ paddingBottom));
+ }
+
+ public void showListView(ListAdapterT activeListAdapter) {
+ ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
userHandleToPageIndex(activeListAdapter.getUserHandle()));
- descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.VISIBLE);
- View emptyStateView = descriptor.rootView.findViewById(com.android.internal.R.id.resolver_empty_state);
- emptyStateView.setVisibility(View.GONE);
+ descriptor.mRootView.findViewById(
+ com.android.internal.R.id.resolver_list).setVisibility(View.VISIBLE);
+ descriptor.mEmptyStateUi.hide();
}
- boolean shouldShowEmptyStateScreen(ResolverListAdapter listAdapter) {
+ public boolean shouldShowEmptyStateScreen(ListAdapterT listAdapter) {
int count = listAdapter.getUnfilteredCount();
return (count == 0 && listAdapter.getPlaceholderCount() == 0)
|| (listAdapter.getUserHandle().equals(mWorkProfileUserHandle)
&& mWorkProfileQuietModeChecker.get());
}
- protected static class ProfileDescriptor {
- final ViewGroup rootView;
+ // TODO: `ChooserActivity` also has a per-profile record type. Maybe the "multi-profile pager"
+ // should be the owner of all per-profile data (especially now that the API is generic)?
+ private static class ProfileDescriptor<PageViewT, SinglePageAdapterT> {
+ final ViewGroup mRootView;
+ final EmptyStateUiHelper mEmptyStateUi;
+
+ // TODO: post-refactoring, we may not need to retain these ivars directly (since they may
+ // be encapsulated within the `EmptyStateUiHelper`?).
private final ViewGroup mEmptyStateView;
- ProfileDescriptor(ViewGroup rootView) {
- this.rootView = rootView;
+
+ private final SinglePageAdapterT mAdapter;
+ private final PageViewT mView;
+
+ ProfileDescriptor(ViewGroup rootView, SinglePageAdapterT adapter) {
+ mRootView = rootView;
+ mAdapter = adapter;
mEmptyStateView = rootView.findViewById(com.android.internal.R.id.resolver_empty_state);
+ mView = (PageViewT) rootView.findViewById(com.android.internal.R.id.resolver_list);
+ mEmptyStateUi = new EmptyStateUiHelper(rootView);
}
protected ViewGroup getEmptyStateView() {
@@ -455,6 +548,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
}
+ /** Listener interface for changes between the per-profile UI tabs. */
public interface OnProfileSelectedListener {
/**
* Callback for when the user changes the active tab from personal to work or vice versa.
@@ -478,102 +572,9 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
/**
- * Returns an empty state to show for the current profile page (tab) if necessary.
- * This could be used e.g. to show a blocker on a tab if device management policy doesn't
- * allow to use it or there are no apps available.
- */
- public interface EmptyStateProvider {
- /**
- * When a non-null empty state is returned the corresponding profile page will show
- * this empty state
- * @param resolverListAdapter the current adapter
- */
- @Nullable
- default EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
- return null;
- }
- }
-
- /**
- * Empty state provider that combines multiple providers. Providers earlier in the list have
- * priority, that is if there is a provider that returns non-null empty state then all further
- * providers will be ignored.
- */
- public static class CompositeEmptyStateProvider implements EmptyStateProvider {
-
- private final EmptyStateProvider[] mProviders;
-
- public CompositeEmptyStateProvider(EmptyStateProvider... providers) {
- mProviders = providers;
- }
-
- @Nullable
- @Override
- public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
- for (EmptyStateProvider provider : mProviders) {
- EmptyState emptyState = provider.getEmptyState(resolverListAdapter);
- if (emptyState != null) {
- return emptyState;
- }
- }
- return null;
- }
- }
-
- /**
- * Describes how the blocked empty state should look like for a profile tab
- */
- public interface EmptyState {
- /**
- * Title that will be shown on the empty state
- */
- @Nullable
- default String getTitle() { return null; }
-
- /**
- * Subtitle that will be shown underneath the title on the empty state
- */
- @Nullable
- default String getSubtitle() { return null; }
-
- /**
- * If non-null then a button will be shown and this listener will be called
- * when the button is clicked
- */
- @Nullable
- default ClickListener getButtonClickListener() { return null; }
-
- /**
- * If true then default text ('No apps can perform this action') and style for the empty
- * state will be applied, title and subtitle will be ignored.
- */
- default boolean useDefaultEmptyView() { return false; }
-
- /**
- * Returns true if for this empty state we should skip rebuilding of the apps list
- * for this tab.
- */
- default boolean shouldSkipDataRebuild() { return false; }
-
- /**
- * Called when empty state is shown, could be used e.g. to track analytics events
- */
- default void onEmptyStateShown() {}
-
- interface ClickListener {
- void onClick(TabControl currentTab);
- }
-
- interface TabControl {
- void showSpinner();
- }
- }
-
-
- /**
* Listener for when the user switches on the work profile from the work tab.
*/
- interface OnSwitchOnWorkSelectedListener {
+ public interface OnSwitchOnWorkSelectedListener {
/**
* Callback for when the user switches on the work profile from the work tab.
*/
diff --git a/java/src/com/android/intentresolver/ResolvedComponentInfo.java b/java/src/com/android/intentresolver/ResolvedComponentInfo.java
index ecb72cb..aaa97c4 100644
--- a/java/src/com/android/intentresolver/ResolvedComponentInfo.java
+++ b/java/src/com/android/intentresolver/ResolvedComponentInfo.java
@@ -20,6 +20,8 @@ import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+
import java.util.ArrayList;
import java.util.List;
@@ -86,7 +88,7 @@ public final class ResolvedComponentInfo {
}
/**
- * @return whether this component was pinned by a call to {@link #setPinned()}.
+ * @return whether this component was pinned by a call to {@link #setPinned}.
* TODO: consolidate sources of pinning data and/or document how this differs from other places
* we make a "pinning" determination.
*/
diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java
index 35c7e89..0331c33 100644
--- a/java/src/com/android/intentresolver/ResolverActivity.java
+++ b/java/src/com/android/intentresolver/ResolverActivity.java
@@ -36,9 +36,6 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE
import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED;
-import android.annotation.Nullable;
-import android.annotation.StringRes;
-import android.annotation.UiThread;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityThread;
@@ -96,18 +93,26 @@ import android.widget.TabWidget;
import android.widget.TextView;
import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.annotation.UiThread;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager.widget.ViewPager;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CompositeEmptyStateProvider;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.Profile;
-import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
+import com.android.intentresolver.MultiProfilePagerAdapter.MyUserIdProvider;
+import com.android.intentresolver.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener;
+import com.android.intentresolver.MultiProfilePagerAdapter.Profile;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.emptystate.CompositeEmptyStateProvider;
+import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.emptystate.NoAppsAvailableEmptyStateProvider;
+import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider;
+import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
+import com.android.intentresolver.emptystate.WorkProfilePausedEmptyStateProvider;
import com.android.intentresolver.icons.DefaultTargetDataLoader;
import com.android.intentresolver.icons.TargetDataLoader;
import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
@@ -199,8 +204,10 @@ public class ResolverActivity extends FragmentActivity implements
private PackageMonitor mPersonalPackageMonitor;
private PackageMonitor mWorkPackageMonitor;
+ private TargetDataLoader mTargetDataLoader;
+
@VisibleForTesting
- protected AbstractMultiProfilePagerAdapter mMultiProfilePagerAdapter;
+ protected MultiProfilePagerAdapter mMultiProfilePagerAdapter;
protected WorkProfileAvailabilityManager mWorkProfileAvailability;
@@ -227,8 +234,8 @@ public class ResolverActivity extends FragmentActivity implements
static final String EXTRA_CALLING_USER =
"com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER";
- protected static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL;
- protected static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK;
+ protected static final int PROFILE_PERSONAL = MultiProfilePagerAdapter.PROFILE_PERSONAL;
+ protected static final int PROFILE_WORK = MultiProfilePagerAdapter.PROFILE_WORK;
private UserHandle mHeaderCreatorUser;
@@ -239,11 +246,20 @@ public class ResolverActivity extends FragmentActivity implements
// new component whose lifecycle is limited to the "created" Activity (so that we can just hold
// the annotations as a `final` ivar, which is a better way to show immutability).
private Supplier<AnnotatedUserHandles> mLazyAnnotatedUserHandles = () -> {
- final AnnotatedUserHandles result = AnnotatedUserHandles.forShareActivity(this);
+ final AnnotatedUserHandles result = computeAnnotatedUserHandles();
mLazyAnnotatedUserHandles = () -> result;
return result;
};
+ // This method is called exactly once during creation to compute the immutable annotations
+ // accessible through the lazy supplier {@link mLazyAnnotatedUserHandles}.
+ // TODO: this is only defined so that tests can provide an override that injects fake
+ // annotations. Dagger could provide a cleaner model for our testing/injection requirements.
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ protected AnnotatedUserHandles computeAnnotatedUserHandles() {
+ return AnnotatedUserHandles.forShareActivity(this);
+ }
+
@Nullable
private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
@@ -418,6 +434,7 @@ public class ResolverActivity extends FragmentActivity implements
mSupportsAlwaysUseOption = supportsAlwaysUseOption;
mSafeForwardingMode = safeForwardingMode;
+ mTargetDataLoader = targetDataLoader;
// The last argument of createResolverListAdapter is whether to do special handling
// of the last used choice to highlight it in the list. We need to always
@@ -438,11 +455,12 @@ public class ResolverActivity extends FragmentActivity implements
mPersonalPackageMonitor = createPackageMonitor(
mMultiProfilePagerAdapter.getPersonalListAdapter());
mPersonalPackageMonitor.register(
- this, getMainLooper(), getPersonalProfileUserHandle(), false);
+ this, getMainLooper(), getAnnotatedUserHandles().personalProfileUserHandle, false);
if (shouldShowTabs()) {
mWorkPackageMonitor = createPackageMonitor(
mMultiProfilePagerAdapter.getWorkListAdapter());
- mWorkPackageMonitor.register(this, getMainLooper(), getWorkProfileUserHandle(), false);
+ mWorkPackageMonitor.register(
+ this, getMainLooper(), getAnnotatedUserHandles().workProfileUserHandle, false);
}
mRegistered = true;
@@ -484,12 +502,12 @@ public class ResolverActivity extends FragmentActivity implements
+ (categories != null ? Arrays.toString(categories.toArray()) : ""));
}
- protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter(
+ protected MultiProfilePagerAdapter createMultiProfilePagerAdapter(
Intent[] initialIntents,
List<ResolveInfo> resolutionList,
boolean filterLastUsed,
TargetDataLoader targetDataLoader) {
- AbstractMultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null;
+ MultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null;
if (shouldShowTabs()) {
resolverMultiProfilePagerAdapter =
createResolverMultiProfilePagerAdapterForTwoProfiles(
@@ -509,9 +527,9 @@ public class ResolverActivity extends FragmentActivity implements
return new EmptyStateProvider() {};
}
- final AbstractMultiProfilePagerAdapter.EmptyState
- noWorkToPersonalEmptyState =
- new DevicePolicyBlockerEmptyState(/* context= */ this,
+ final EmptyState noWorkToPersonalEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
/* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
/* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
/* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_PERSONAL,
@@ -521,8 +539,9 @@ public class ResolverActivity extends FragmentActivity implements
/* devicePolicyEventCategory= */
ResolverActivity.METRICS_CATEGORY_RESOLVER);
- final AbstractMultiProfilePagerAdapter.EmptyState noPersonalToWorkEmptyState =
- new DevicePolicyBlockerEmptyState(/* context= */ this,
+ final EmptyState noPersonalToWorkEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
/* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
/* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
/* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_WORK,
@@ -532,9 +551,12 @@ public class ResolverActivity extends FragmentActivity implements
/* devicePolicyEventCategory= */
ResolverActivity.METRICS_CATEGORY_RESOLVER);
- return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(),
- noWorkToPersonalEmptyState, noPersonalToWorkEmptyState,
- createCrossProfileIntentsChecker(), getTabOwnerUserHandleForLaunch());
+ return new NoCrossProfileEmptyStateProvider(
+ getAnnotatedUserHandles().personalProfileUserHandle,
+ noWorkToPersonalEmptyState,
+ noPersonalToWorkEmptyState,
+ createCrossProfileIntentsChecker(),
+ getAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
}
protected int appliedThemeResId() {
@@ -591,7 +613,7 @@ public class ResolverActivity extends FragmentActivity implements
}
@Override
- public void onConfigurationChanged(Configuration newConfig) {
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
if (mIsIntentPicker && shouldShowTabs() && !useLayoutWithDefault()
@@ -1014,7 +1036,7 @@ public class ResolverActivity extends FragmentActivity implements
@Override // ResolverListCommunicator
public void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
if (listAdapter == mMultiProfilePagerAdapter.getActiveListAdapter()) {
- if (listAdapter.getUserHandle().equals(getWorkProfileUserHandle())
+ if (listAdapter.getUserHandle().equals(getAnnotatedUserHandles().workProfileUserHandle)
&& mWorkProfileAvailability.isWaitingToEnableWorkProfile()) {
// We have just turned on the work profile and entered the pass code to start it,
// now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no
@@ -1052,16 +1074,15 @@ public class ResolverActivity extends FragmentActivity implements
}
protected WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() {
- final UserHandle workUser = getWorkProfileUserHandle();
-
return new WorkProfileAvailabilityManager(
getSystemService(UserManager.class),
- workUser,
+ getAnnotatedUserHandles().workProfileUserHandle,
this::onWorkProfileStatusUpdated);
}
protected void onWorkProfileStatusUpdated() {
- if (mMultiProfilePagerAdapter.getCurrentUserHandle().equals(getWorkProfileUserHandle())) {
+ if (mMultiProfilePagerAdapter.getCurrentUserHandle().equals(
+ getAnnotatedUserHandles().workProfileUserHandle)) {
mMultiProfilePagerAdapter.rebuildActiveTab(true);
} else {
mMultiProfilePagerAdapter.clearInactiveProfileCache();
@@ -1079,8 +1100,8 @@ public class ResolverActivity extends FragmentActivity implements
UserHandle userHandle,
TargetDataLoader targetDataLoader) {
UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()
- && userHandle.equals(getPersonalProfileUserHandle())
- ? getCloneProfileUserHandle() : userHandle;
+ && userHandle.equals(getAnnotatedUserHandles().personalProfileUserHandle)
+ ? getAnnotatedUserHandles().cloneProfileUserHandle : userHandle;
return new ResolverListAdapter(
context,
payloadIntents,
@@ -1136,9 +1157,9 @@ public class ResolverActivity extends FragmentActivity implements
final EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider(
this,
workProfileUserHandle,
- getPersonalProfileUserHandle(),
+ getAnnotatedUserHandles().personalProfileUserHandle,
getMetricsCategory(),
- getTabOwnerUserHandleForLaunch()
+ getAnnotatedUserHandles().tabOwnerUserHandleForLaunch
);
// Return composite provider, the order matters (the higher, the more priority)
@@ -1188,7 +1209,7 @@ public class ResolverActivity extends FragmentActivity implements
initialIntents,
resolutionList,
filterLastUsed,
- /* userHandle */ getPersonalProfileUserHandle(),
+ /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,
targetDataLoader);
return new ResolverMultiProfilePagerAdapter(
/* context */ this,
@@ -1196,13 +1217,13 @@ public class ResolverActivity extends FragmentActivity implements
createEmptyStateProvider(/* workProfileUserHandle= */ null),
/* workProfileQuietModeChecker= */ () -> false,
/* workProfileUserHandle= */ null,
- getCloneProfileUserHandle());
+ getAnnotatedUserHandles().cloneProfileUserHandle);
}
private UserHandle getIntentUser() {
return getIntent().hasExtra(EXTRA_CALLING_USER)
? getIntent().getParcelableExtra(EXTRA_CALLING_USER)
- : getTabOwnerUserHandleForLaunch();
+ : getAnnotatedUserHandles().tabOwnerUserHandleForLaunch;
}
private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles(
@@ -1215,10 +1236,10 @@ public class ResolverActivity extends FragmentActivity implements
// this happens, we check for it here and set the current profile's tab.
int selectedProfile = getCurrentProfile();
UserHandle intentUser = getIntentUser();
- if (!getTabOwnerUserHandleForLaunch().equals(intentUser)) {
- if (getPersonalProfileUserHandle().equals(intentUser)) {
+ if (!getAnnotatedUserHandles().tabOwnerUserHandleForLaunch.equals(intentUser)) {
+ if (getAnnotatedUserHandles().personalProfileUserHandle.equals(intentUser)) {
selectedProfile = PROFILE_PERSONAL;
- } else if (getWorkProfileUserHandle().equals(intentUser)) {
+ } else if (getAnnotatedUserHandles().workProfileUserHandle.equals(intentUser)) {
selectedProfile = PROFILE_WORK;
}
} else {
@@ -1236,10 +1257,10 @@ public class ResolverActivity extends FragmentActivity implements
selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
resolutionList,
(filterLastUsed && UserHandle.myUserId()
- == getPersonalProfileUserHandle().getIdentifier()),
- /* userHandle */ getPersonalProfileUserHandle(),
+ == getAnnotatedUserHandles().personalProfileUserHandle.getIdentifier()),
+ /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,
targetDataLoader);
- UserHandle workProfileUserHandle = getWorkProfileUserHandle();
+ UserHandle workProfileUserHandle = getAnnotatedUserHandles().workProfileUserHandle;
ResolverListAdapter workAdapter = createResolverListAdapter(
/* context */ this,
/* payloadIntents */ mIntents,
@@ -1253,11 +1274,11 @@ public class ResolverActivity extends FragmentActivity implements
/* context */ this,
personalAdapter,
workAdapter,
- createEmptyStateProvider(getWorkProfileUserHandle()),
+ createEmptyStateProvider(workProfileUserHandle),
() -> mWorkProfileAvailability.isQuietModeEnabled(),
selectedProfile,
- getWorkProfileUserHandle(),
- getCloneProfileUserHandle());
+ workProfileUserHandle,
+ getAnnotatedUserHandles().cloneProfileUserHandle);
}
/**
@@ -1280,55 +1301,29 @@ public class ResolverActivity extends FragmentActivity implements
}
protected final @Profile int getCurrentProfile() {
- return (getTabOwnerUserHandleForLaunch().equals(getPersonalProfileUserHandle())
- ? PROFILE_PERSONAL : PROFILE_WORK);
+ UserHandle launchUser = getAnnotatedUserHandles().tabOwnerUserHandleForLaunch;
+ UserHandle personalUser = getAnnotatedUserHandles().personalProfileUserHandle;
+ return launchUser.equals(personalUser) ? PROFILE_PERSONAL : PROFILE_WORK;
}
protected final AnnotatedUserHandles getAnnotatedUserHandles() {
return mLazyAnnotatedUserHandles.get();
}
- protected final UserHandle getPersonalProfileUserHandle() {
- return getAnnotatedUserHandles().personalProfileUserHandle;
- }
-
- // TODO: have tests override `getAnnotatedUserHandles()`, and make this method `final`.
- // @NonFinalForTesting
- @Nullable
- protected UserHandle getWorkProfileUserHandle() {
- return getAnnotatedUserHandles().workProfileUserHandle;
- }
-
- // TODO: have tests override `getAnnotatedUserHandles()`, and make this method `final`.
- @Nullable
- protected UserHandle getCloneProfileUserHandle() {
- return getAnnotatedUserHandles().cloneProfileUserHandle;
- }
-
- // TODO: have tests override `getAnnotatedUserHandles()`, and make this method `final`.
- protected UserHandle getTabOwnerUserHandleForLaunch() {
- return getAnnotatedUserHandles().tabOwnerUserHandleForLaunch;
- }
-
- protected UserHandle getUserHandleSharesheetLaunchedAs() {
- return getAnnotatedUserHandles().userHandleSharesheetLaunchedAs;
- }
-
-
private boolean hasWorkProfile() {
- return getWorkProfileUserHandle() != null;
+ return getAnnotatedUserHandles().workProfileUserHandle != null;
}
private boolean hasCloneProfile() {
- return getCloneProfileUserHandle() != null;
+ return getAnnotatedUserHandles().cloneProfileUserHandle != null;
}
protected final boolean isLaunchedAsCloneProfile() {
- return hasCloneProfile()
- && getUserHandleSharesheetLaunchedAs().equals(getCloneProfileUserHandle());
+ UserHandle launchUser = getAnnotatedUserHandles().userHandleSharesheetLaunchedAs;
+ UserHandle cloneUser = getAnnotatedUserHandles().cloneProfileUserHandle;
+ return hasCloneProfile() && launchUser.equals(cloneUser);
}
-
protected final boolean shouldShowTabs() {
return hasWorkProfile();
}
@@ -1368,7 +1363,9 @@ public class ResolverActivity extends FragmentActivity implements
}
DevicePolicyEventLogger
.createEvent(DevicePolicyEnums.RESOLVER_CROSS_PROFILE_TARGET_OPENED)
- .setBoolean(currentUserHandle.equals(getPersonalProfileUserHandle()))
+ .setBoolean(
+ currentUserHandle.equals(
+ getAnnotatedUserHandles().personalProfileUserHandle))
.setStrings(getMetricsCategory(),
cti.isInDirectShareMetricsCategory() ? "direct_share" : "other_target")
.write();
@@ -1399,7 +1396,7 @@ public class ResolverActivity extends FragmentActivity implements
}
final Option optionForChooserTarget(TargetInfo target, int index) {
- return new Option(target.getDisplayLabel(), index);
+ return new Option(getOrLoadDisplayLabel(target), index);
}
public final Intent getTargetIntent() {
@@ -1475,8 +1472,11 @@ public class ResolverActivity extends FragmentActivity implements
return getString(defaultTitleRes);
} else {
return named
- ? getString(title.namedTitleRes, mMultiProfilePagerAdapter
- .getActiveListAdapter().getFilteredItem().getDisplayLabel())
+ ? getString(
+ title.namedTitleRes,
+ getOrLoadDisplayLabel(
+ mMultiProfilePagerAdapter
+ .getActiveListAdapter().getFilteredItem()))
: getString(title.titleRes);
}
}
@@ -1491,15 +1491,21 @@ public class ResolverActivity extends FragmentActivity implements
protected final void onRestart() {
super.onRestart();
if (!mRegistered) {
- mPersonalPackageMonitor.register(this, getMainLooper(),
- getPersonalProfileUserHandle(), false);
+ mPersonalPackageMonitor.register(
+ this,
+ getMainLooper(),
+ getAnnotatedUserHandles().personalProfileUserHandle,
+ false);
if (shouldShowTabs()) {
if (mWorkPackageMonitor == null) {
mWorkPackageMonitor = createPackageMonitor(
mMultiProfilePagerAdapter.getWorkListAdapter());
}
- mWorkPackageMonitor.register(this, getMainLooper(),
- getWorkProfileUserHandle(), false);
+ mWorkPackageMonitor.register(
+ this,
+ getMainLooper(),
+ getAnnotatedUserHandles().workProfileUserHandle,
+ false);
}
mRegistered = true;
}
@@ -1523,7 +1529,7 @@ public class ResolverActivity extends FragmentActivity implements
}
@Override
- protected final void onSaveInstanceState(Bundle outState) {
+ protected final void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
if (viewPager != null) {
@@ -1532,7 +1538,7 @@ public class ResolverActivity extends FragmentActivity implements
}
@Override
- protected final void onRestoreInstanceState(Bundle savedInstanceState) {
+ protected final void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
resetButtonBar();
ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
@@ -1807,9 +1813,10 @@ public class ResolverActivity extends FragmentActivity implements
((TextView) findViewById(com.android.internal.R.id.open_cross_profile)).setText(
getResources().getString(
- inWorkProfile ? R.string.miniresolver_open_in_personal
+ inWorkProfile
+ ? R.string.miniresolver_open_in_personal
: R.string.miniresolver_open_in_work,
- otherProfileResolveInfo.getDisplayLabel()));
+ getOrLoadDisplayLabel(otherProfileResolveInfo)));
((Button) findViewById(com.android.internal.R.id.use_same_profile_browser)).setText(
inWorkProfile ? R.string.miniresolver_use_work_browser
: R.string.miniresolver_use_personal_browser);
@@ -1973,7 +1980,7 @@ public class ResolverActivity extends FragmentActivity implements
DevicePolicyEventLogger
.createEvent(DevicePolicyEnums.RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET)
.setBoolean(activeListAdapter.getUserHandle()
- .equals(getPersonalProfileUserHandle()))
+ .equals(getAnnotatedUserHandles().personalProfileUserHandle))
.setStrings(getMetricsCategory())
.write();
safelyStartActivity(activeProfileTarget);
@@ -2080,7 +2087,7 @@ public class ResolverActivity extends FragmentActivity implements
viewPager.setVisibility(View.VISIBLE);
tabHost.setCurrentTab(mMultiProfilePagerAdapter.getCurrentPage());
mMultiProfilePagerAdapter.setOnProfileSelectedListener(
- new AbstractMultiProfilePagerAdapter.OnProfileSelectedListener() {
+ new MultiProfilePagerAdapter.OnProfileSelectedListener() {
@Override
public void onProfileSelected(int index) {
tabHost.setCurrentTab(index);
@@ -2256,7 +2263,7 @@ public class ResolverActivity extends FragmentActivity implements
// filtered item. We always show the same default app even in the inactive user profile.
boolean adapterForCurrentUserHasFilteredItem =
mMultiProfilePagerAdapter.getListAdapterForUserHandle(
- getTabOwnerUserHandleForLaunch()).hasFilteredItem();
+ getAnnotatedUserHandles().tabOwnerUserHandleForLaunch).hasFilteredItem();
return mSupportsAlwaysUseOption && adapterForCurrentUserHasFilteredItem;
}
@@ -2268,20 +2275,6 @@ public class ResolverActivity extends FragmentActivity implements
mRetainInOnStop = retainInOnStop;
}
- /**
- * Check a simple match for the component of two ResolveInfos.
- */
- @Override // ResolverListCommunicator
- public final boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs) {
- return lhs == null ? rhs == null
- : lhs.activityInfo == null ? rhs.activityInfo == null
- : Objects.equals(lhs.activityInfo.name, rhs.activityInfo.name)
- && Objects.equals(lhs.activityInfo.packageName, rhs.activityInfo.packageName)
- // Comparing against resolveInfo.userHandle in case cloned apps are present,
- // as they will have the same activityInfo.
- && Objects.equals(lhs.userHandle, rhs.userHandle);
- }
-
private boolean inactiveListAdapterHasItems() {
if (!shouldShowTabs()) {
return false;
@@ -2391,7 +2384,7 @@ public class ResolverActivity extends FragmentActivity implements
* {@link ResolverListController} configured for the provided {@code userHandle}.
*/
protected final UserHandle getQueryIntentsUser(UserHandle userHandle) {
- return mLazyAnnotatedUserHandles.get().getQueryIntentsUser(userHandle);
+ return getAnnotatedUserHandles().getQueryIntentsUser(userHandle);
}
/**
@@ -2411,10 +2404,18 @@ public class ResolverActivity extends FragmentActivity implements
// Add clonedProfileUserHandle to the list only if we are:
// a. Building the Personal Tab.
// b. CloneProfile exists on the device.
- if (userHandle.equals(getPersonalProfileUserHandle())
- && getCloneProfileUserHandle() != null) {
- userList.add(getCloneProfileUserHandle());
+ if (userHandle.equals(getAnnotatedUserHandles().personalProfileUserHandle)
+ && hasCloneProfile()) {
+ userList.add(getAnnotatedUserHandles().cloneProfileUserHandle);
}
return userList;
}
+
+ private CharSequence getOrLoadDisplayLabel(TargetInfo info) {
+ if (info.isDisplayResolveInfo()) {
+ mTargetDataLoader.getOrLoadLabel((DisplayResolveInfo) info);
+ }
+ CharSequence displayLabel = info.getDisplayLabel();
+ return displayLabel == null ? "" : displayLabel;
+ }
}
diff --git a/java/src/com/android/intentresolver/ResolverInfoHelpers.kt b/java/src/com/android/intentresolver/ResolverInfoHelpers.kt
new file mode 100644
index 0000000..8d1d865
--- /dev/null
+++ b/java/src/com/android/intentresolver/ResolverInfoHelpers.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 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.
+ */
+
+@file:JvmName("ResolveInfoHelpers")
+
+package com.android.intentresolver
+
+import android.content.pm.ActivityInfo
+import android.content.pm.ResolveInfo
+
+fun resolveInfoMatch(lhs: ResolveInfo?, rhs: ResolveInfo?): Boolean =
+ (lhs === rhs) ||
+ ((lhs != null && rhs != null) &&
+ activityInfoMatch(lhs.activityInfo, rhs.activityInfo) &&
+ // Comparing against resolveInfo.userHandle in case cloned apps are present,
+ // as they will have the same activityInfo.
+ lhs.userHandle == rhs.userHandle)
+
+private fun activityInfoMatch(lhs: ActivityInfo?, rhs: ActivityInfo?): Boolean =
+ (lhs === rhs) ||
+ (lhs != null && rhs != null && lhs.name == rhs.name && lhs.packageName == rhs.packageName)
diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java
index 282a672..564d8d1 100644
--- a/java/src/com/android/intentresolver/ResolverListAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverListAdapter.java
@@ -16,8 +16,6 @@
package com.android.intentresolver;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -27,6 +25,7 @@ import android.content.pm.ResolveInfo;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.drawable.Drawable;
+import android.net.Uri;
import android.os.AsyncTask;
import android.os.RemoteException;
import android.os.Trace;
@@ -42,8 +41,14 @@ import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.icons.LabelInfo;
import com.android.intentresolver.icons.TargetDataLoader;
import com.android.internal.annotations.VisibleForTesting;
@@ -53,6 +58,8 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
public class ResolverListAdapter extends BaseAdapter {
private static final String TAG = "ResolverListAdapter";
@@ -63,7 +70,7 @@ public class ResolverListAdapter extends BaseAdapter {
protected final Context mContext;
protected final LayoutInflater mInflater;
protected final ResolverListCommunicator mResolverListCommunicator;
- protected final ResolverListController mResolverListController;
+ public final ResolverListController mResolverListController;
private final List<Intent> mIntents;
private final Intent[] mInitialIntents;
@@ -75,6 +82,9 @@ public class ResolverListAdapter extends BaseAdapter {
private final Set<DisplayResolveInfo> mRequestedIcons = new HashSet<>();
private final Set<DisplayResolveInfo> mRequestedLabels = new HashSet<>();
+ private final Executor mBgExecutor;
+ private final Executor mCallbackExecutor;
+ private final AtomicBoolean mDestroyed = new AtomicBoolean();
private ResolveInfo mLastChosen;
private DisplayResolveInfo mOtherProfile;
@@ -86,7 +96,6 @@ public class ResolverListAdapter extends BaseAdapter {
private int mLastChosenPosition = -1;
private final boolean mFilterLastUsed;
- private Runnable mPostListReadyRunnable;
private boolean mIsTabLoaded;
// Represents the UserSpace in which the Initial Intents should be resolved.
private final UserHandle mInitialIntentsUserSpace;
@@ -103,6 +112,37 @@ public class ResolverListAdapter extends BaseAdapter {
ResolverListCommunicator resolverListCommunicator,
UserHandle initialIntentsUserSpace,
TargetDataLoader targetDataLoader) {
+ this(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ initialIntentsUserSpace,
+ targetDataLoader,
+ AsyncTask.SERIAL_EXECUTOR,
+ runnable -> context.getMainThreadHandler().post(runnable));
+ }
+
+ @VisibleForTesting
+ public ResolverListAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ ResolverListController resolverListController,
+ UserHandle userHandle,
+ Intent targetIntent,
+ ResolverListCommunicator resolverListCommunicator,
+ UserHandle initialIntentsUserSpace,
+ TargetDataLoader targetDataLoader,
+ Executor bgExecutor,
+ Executor callbackExecutor) {
mContext = context;
mIntents = payloadIntents;
mInitialIntents = initialIntents;
@@ -117,6 +157,12 @@ public class ResolverListAdapter extends BaseAdapter {
mTargetIntent = targetIntent;
mResolverListCommunicator = resolverListCommunicator;
mInitialIntentsUserSpace = initialIntentsUserSpace;
+ mBgExecutor = bgExecutor;
+ mCallbackExecutor = callbackExecutor;
+ }
+
+ protected Intent getTargetIntent() {
+ return mTargetIntent;
}
public final DisplayResolveInfo getFirstDisplayResolveInfo() {
@@ -189,18 +235,18 @@ public class ResolverListAdapter extends BaseAdapter {
packageName, userHandle, action);
}
- List<ResolvedComponentInfo> getUnfilteredResolveList() {
+ public List<ResolvedComponentInfo> getUnfilteredResolveList() {
return mUnfilteredResolveList;
}
/**
* Rebuild the list of resolvers. When rebuilding is complete, queue the {@code onPostListReady}
- * callback on the main handler with {@code rebuildCompleted} true.
+ * callback on the callback executor with {@code rebuildCompleted} true.
*
* In some cases some parts will need some asynchronous work to complete. Then this will first
- * immediately queue {@code onPostListReady} (on the main handler) with {@code rebuildCompleted}
- * false; only when the asynchronous work completes will this then go on to queue another
- * {@code onPostListReady} callback with {@code rebuildCompleted} true.
+ * immediately queue {@code onPostListReady} (on the callback executor) with
+ * {@code rebuildCompleted} false; only when the asynchronous work completes will this then go
+ * on to queue another {@code onPostListReady} callback with {@code rebuildCompleted} true.
*
* The {@code doPostProcessing} parameter is used to specify whether to update the UI and
* load additional targets (e.g. direct share) after the list has been rebuilt. We may choose
@@ -212,7 +258,7 @@ public class ResolverListAdapter extends BaseAdapter {
* with {@code rebuildCompleted} true at the end of some newly-launched asynchronous work.
* Otherwise the callback is only queued once, with {@code rebuildCompleted} true.
*/
- protected boolean rebuildList(boolean doPostProcessing) {
+ public boolean rebuildList(boolean doPostProcessing) {
Trace.beginSection("ResolverListAdapter#rebuildList");
mDisplayList.clear();
mIsTabLoaded = false;
@@ -357,8 +403,8 @@ public class ResolverListAdapter extends BaseAdapter {
otherProfileInfo,
mPm,
mTargetIntent,
- mResolverListCommunicator,
- mTargetDataLoader);
+ mResolverListCommunicator
+ );
} else {
mOtherProfile = null;
try {
@@ -402,35 +448,42 @@ public class ResolverListAdapter extends BaseAdapter {
// Send an "incomplete" list-ready while the async task is running.
postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ false);
- createSortingTask(doPostProcessing).execute(filteredResolveList);
+ mBgExecutor.execute(() -> {
+ List<ResolvedComponentInfo> sortedComponents = null;
+ //TODO: the try-catch logic here is to formally match the AsyncTask's behavior.
+ // Empirically, we don't need it as in the case on an exception, the app will crash and
+ // `onComponentsSorted` won't be invoked.
+ try {
+ sortComponents(filteredResolveList);
+ sortedComponents = filteredResolveList;
+ } catch (Throwable t) {
+ Log.e(TAG, "Failed to sort components", t);
+ throw t;
+ } finally {
+ final List<ResolvedComponentInfo> result = sortedComponents;
+ mCallbackExecutor.execute(() -> onComponentsSorted(result, doPostProcessing));
+ }
+ });
return false;
}
- AsyncTask<List<ResolvedComponentInfo>,
- Void,
- List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) {
- return new AsyncTask<List<ResolvedComponentInfo>,
- Void,
- List<ResolvedComponentInfo>>() {
- @Override
- protected List<ResolvedComponentInfo> doInBackground(
- List<ResolvedComponentInfo>... params) {
- mResolverListController.sort(params[0]);
- return params[0];
- }
- @Override
- protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
- processSortedList(sortedComponents, doPostProcessing);
- notifyDataSetChanged();
- if (doPostProcessing) {
- mResolverListCommunicator.updateProfileViewButton();
- }
- }
- };
+ @WorkerThread
+ protected void sortComponents(List<ResolvedComponentInfo> components) {
+ mResolverListController.sort(components);
}
- protected void processSortedList(List<ResolvedComponentInfo> sortedComponents,
- boolean doPostProcessing) {
+ @MainThread
+ protected void onComponentsSorted(
+ @Nullable List<ResolvedComponentInfo> sortedComponents, boolean doPostProcessing) {
+ processSortedList(sortedComponents, doPostProcessing);
+ notifyDataSetChanged();
+ if (doPostProcessing) {
+ mResolverListCommunicator.updateProfileViewButton();
+ }
+ }
+
+ protected void processSortedList(
+ @Nullable List<ResolvedComponentInfo> sortedComponents, boolean doPostProcessing) {
final int n = sortedComponents != null ? sortedComponents.size() : 0;
Trace.beginSection("ResolverListAdapter#processSortedList:" + n);
if (n != 0) {
@@ -471,8 +524,7 @@ public class ResolverListAdapter extends BaseAdapter {
ri,
ri.loadLabel(mPm),
null,
- ii,
- mTargetDataLoader.createPresentationGetter(ri)));
+ ii));
}
}
@@ -494,23 +546,23 @@ public class ResolverListAdapter extends BaseAdapter {
/**
* Some necessary methods for creating the list are initiated in onCreate and will also
* determine the layout known. We therefore can't update the UI inline and post to the
- * handler thread to update after the current task is finished.
+ * callback executor to update after the current task is finished.
* @param doPostProcessing Whether to update the UI and load additional direct share targets
* after the list has been rebuilt
* @param rebuildCompleted Whether the list has been completely rebuilt
*/
- void postListReadyRunnable(boolean doPostProcessing, boolean rebuildCompleted) {
- if (mPostListReadyRunnable == null) {
- mPostListReadyRunnable = new Runnable() {
- @Override
- public void run() {
- mResolverListCommunicator.onPostListReady(ResolverListAdapter.this,
- doPostProcessing, rebuildCompleted);
- mPostListReadyRunnable = null;
+ public void postListReadyRunnable(boolean doPostProcessing, boolean rebuildCompleted) {
+ Runnable listReadyRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mDestroyed.get()) {
+ return;
}
- };
- mContext.getMainThreadHandler().post(mPostListReadyRunnable);
- }
+ mResolverListCommunicator.onPostListReady(ResolverListAdapter.this,
+ doPostProcessing, rebuildCompleted);
+ }
+ };
+ mCallbackExecutor.execute(listReadyRunnable);
}
private void addResolveInfoWithAlternates(ResolvedComponentInfo rci) {
@@ -524,8 +576,7 @@ public class ResolverListAdapter extends BaseAdapter {
final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
intent,
add,
- (replaceIntent != null) ? replaceIntent : defaultIntent,
- mTargetDataLoader.createPresentationGetter(add));
+ (replaceIntent != null) ? replaceIntent : defaultIntent);
dri.setPinned(rci.isPinned());
if (rci.isPinned()) {
Log.i(TAG, "Pinned item: " + rci.name);
@@ -572,7 +623,7 @@ public class ResolverListAdapter extends BaseAdapter {
protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {
// Checks if this info is already listed in display.
for (DisplayResolveInfo existingInfo : mDisplayList) {
- if (mResolverListCommunicator
+ if (ResolveInfoHelpers
.resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) {
return false;
}
@@ -710,27 +761,25 @@ public class ResolverListAdapter extends BaseAdapter {
}
}
- private void loadLabel(DisplayResolveInfo info) {
+ protected final void loadLabel(DisplayResolveInfo info) {
if (mRequestedLabels.add(info)) {
mTargetDataLoader.loadLabel(info, (result) -> onLabelLoaded(info, result));
}
}
protected final void onLabelLoaded(
- DisplayResolveInfo displayResolveInfo, CharSequence[] result) {
+ DisplayResolveInfo displayResolveInfo, LabelInfo result) {
if (displayResolveInfo.hasDisplayLabel()) {
return;
}
- displayResolveInfo.setDisplayLabel(result[0]);
- displayResolveInfo.setExtendedInfo(result[1]);
+ displayResolveInfo.setDisplayLabel(result.getLabel());
+ displayResolveInfo.setExtendedInfo(result.getSubLabel());
notifyDataSetChanged();
}
public void onDestroy() {
- if (mPostListReadyRunnable != null) {
- mContext.getMainThreadHandler().removeCallbacks(mPostListReadyRunnable);
- mPostListReadyRunnable = null;
- }
+ mDestroyed.set(true);
+
if (mResolverListController != null) {
mResolverListController.destroy();
}
@@ -765,7 +814,7 @@ public class ResolverListAdapter extends BaseAdapter {
return mContext.getDrawable(R.drawable.resolver_icon_placeholder);
}
- void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) {
+ public void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) {
final DisplayResolveInfo iconInfo = getFilteredItem();
if (iconInfo != null) {
mTargetDataLoader.loadAppTargetIcon(
@@ -777,7 +826,7 @@ public class ResolverListAdapter extends BaseAdapter {
return mUserHandle;
}
- protected List<ResolvedComponentInfo> getResolversForUser(UserHandle userHandle) {
+ public final List<ResolvedComponentInfo> getResolversForUser(UserHandle userHandle) {
return mResolverListController.getResolversForIntentAsUser(
/* shouldGetResolvedFilter= */ true,
mResolverListCommunicator.shouldGetActivityMetadata(),
@@ -786,15 +835,16 @@ public class ResolverListAdapter extends BaseAdapter {
userHandle);
}
- protected List<Intent> getIntents() {
+ public final List<Intent> getIntents() {
+ // TODO: immutable copy?
return mIntents;
}
- protected boolean isTabLoaded() {
+ public boolean isTabLoaded() {
return mIsTabLoaded;
}
- protected void markTabLoaded() {
+ public void markTabLoaded() {
mIsTabLoaded = true;
}
@@ -828,8 +878,7 @@ public class ResolverListAdapter extends BaseAdapter {
ResolvedComponentInfo resolvedComponentInfo,
PackageManager pm,
Intent targetIntent,
- ResolverListCommunicator resolverListCommunicator,
- TargetDataLoader targetDataLoader) {
+ ResolverListCommunicator resolverListCommunicator) {
ResolveInfo resolveInfo = resolvedComponentInfo.getResolveInfoAt(0);
Intent pOrigIntent = resolverListCommunicator.getReplacementIntent(
@@ -838,25 +887,19 @@ public class ResolverListAdapter extends BaseAdapter {
Intent replacementIntent = resolverListCommunicator.getReplacementIntent(
resolveInfo.activityInfo, targetIntent);
- TargetPresentationGetter presentationGetter =
- targetDataLoader.createPresentationGetter(resolveInfo);
-
return DisplayResolveInfo.newDisplayResolveInfo(
resolvedComponentInfo.getIntentAt(0),
resolveInfo,
resolveInfo.loadLabel(pm),
resolveInfo.loadLabel(pm),
- pOrigIntent != null ? pOrigIntent : replacementIntent,
- presentationGetter);
+ pOrigIntent != null ? pOrigIntent : replacementIntent);
}
/**
* Necessary methods to communicate between {@link ResolverListAdapter}
* and {@link ResolverActivity}.
*/
- interface ResolverListCommunicator {
-
- boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs);
+ public interface ResolverListCommunicator {
Intent getReplacementIntent(ActivityInfo activityInfo, Intent defIntent);
@@ -893,6 +936,24 @@ public class ResolverListAdapter extends BaseAdapter {
public TextView text2;
public ImageView icon;
+ public final void reset() {
+ text.setText("");
+ text.setMaxLines(2);
+ text.setMaxWidth(Integer.MAX_VALUE);
+ text.setBackground(null);
+ text.setPaddingRelative(0, 0, 0, 0);
+
+ text2.setVisibility(View.GONE);
+ text2.setText("");
+
+ itemView.setContentDescription(null);
+ itemView.setBackground(defaultItemViewBackground);
+
+ icon.setImageDrawable(null);
+ icon.setColorFilter(null);
+ icon.clearAnimation();
+ }
+
@VisibleForTesting
public ViewHolder(View view) {
itemView = view;
@@ -937,5 +998,19 @@ public class ResolverListAdapter extends BaseAdapter {
icon.setColorFilter(null);
}
}
+
+ public void bindPlaceholder() {
+ itemView.setBackground(null);
+ }
+
+ public void bindGroupIndicator(Drawable indicator) {
+ text.setPaddingRelative(0, 0, /*end = */indicator.getIntrinsicWidth(), 0);
+ text.setBackground(indicator);
+ }
+
+ public void bindPinnedIndicator(Drawable indicator) {
+ text.setPaddingRelative(/*start = */indicator.getIntrinsicWidth(), 0, 0, 0);
+ text.setBackground(indicator);
+ }
}
}
diff --git a/java/src/com/android/intentresolver/ResolverListController.java b/java/src/com/android/intentresolver/ResolverListController.java
index d5a5fed..e88d766 100644
--- a/java/src/com/android/intentresolver/ResolverListController.java
+++ b/java/src/com/android/intentresolver/ResolverListController.java
@@ -17,7 +17,6 @@
package com.android.intentresolver;
-import android.annotation.WorkerThread;
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.content.ComponentName;
@@ -31,6 +30,8 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
+import androidx.annotation.WorkerThread;
+
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.model.AbstractResolverComparator;
@@ -254,7 +255,6 @@ public class ResolverListController {
isComputed = true;
}
- @VisibleForTesting
@WorkerThread
public void sort(List<ResolvedComponentInfo> inputList) {
try {
@@ -273,7 +273,6 @@ public class ResolverListController {
}
}
- @VisibleForTesting
@WorkerThread
public void topK(List<ResolvedComponentInfo> inputList, int k) {
if (inputList == null || inputList.isEmpty() || k <= 0) {
@@ -335,7 +334,7 @@ public class ResolverListController {
&& ai.name.equals(b.name.getClassName());
}
- boolean isComponentFiltered(ComponentName componentName) {
+ public boolean isComponentFiltered(ComponentName componentName) {
return false;
}
diff --git a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
index 85d97ad..591c23b 100644
--- a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
@@ -24,6 +24,7 @@ import android.widget.ListView;
import androidx.viewpager.widget.PagerAdapter;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
import com.android.internal.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
@@ -36,10 +37,10 @@ import java.util.function.Supplier;
*/
@VisibleForTesting
public class ResolverMultiProfilePagerAdapter extends
- GenericMultiProfilePagerAdapter<ListView, ResolverListAdapter, ResolverListAdapter> {
+ MultiProfilePagerAdapter<ListView, ResolverListAdapter, ResolverListAdapter> {
private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier;
- ResolverMultiProfilePagerAdapter(
+ public ResolverMultiProfilePagerAdapter(
Context context,
ResolverListAdapter adapter,
EmptyStateProvider emptyStateProvider,
@@ -57,14 +58,14 @@ public class ResolverMultiProfilePagerAdapter extends
new BottomPaddingOverrideSupplier());
}
- ResolverMultiProfilePagerAdapter(Context context,
- ResolverListAdapter personalAdapter,
- ResolverListAdapter workAdapter,
- EmptyStateProvider emptyStateProvider,
- Supplier<Boolean> workProfileQuietModeChecker,
- @Profile int defaultProfile,
- UserHandle workProfileUserHandle,
- UserHandle cloneProfileUserHandle) {
+ public ResolverMultiProfilePagerAdapter(Context context,
+ ResolverListAdapter personalAdapter,
+ ResolverListAdapter workAdapter,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ @Profile int defaultProfile,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle) {
this(
context,
ImmutableList.of(personalAdapter, workAdapter),
@@ -86,7 +87,6 @@ public class ResolverMultiProfilePagerAdapter extends
UserHandle cloneProfileUserHandle,
BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {
super(
- context,
listAdapter -> listAdapter,
(listView, bindAdapter) -> listView.setAdapter(bindAdapter),
listAdapters,
diff --git a/java/src/com/android/intentresolver/ResolverViewPager.java b/java/src/com/android/intentresolver/ResolverViewPager.java
index 0804a2b..0496579 100644
--- a/java/src/com/android/intentresolver/ResolverViewPager.java
+++ b/java/src/com/android/intentresolver/ResolverViewPager.java
@@ -69,7 +69,7 @@ public class ResolverViewPager extends ViewPager {
* Sets whether swiping sideways should happen.
* <p>Note that swiping is always disabled for RTL layouts (b/159110029 for context).
*/
- void setSwipingEnabled(boolean swipingEnabled) {
+ public void setSwipingEnabled(boolean swipingEnabled) {
mSwipingEnabled = swipingEnabled;
}
diff --git a/java/src/com/android/intentresolver/ShortcutSelectionLogic.java b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java
index 645b939..efaaf89 100644
--- a/java/src/com/android/intentresolver/ShortcutSelectionLogic.java
+++ b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java
@@ -16,7 +16,6 @@
package com.android.intentresolver;
-import android.annotation.Nullable;
import android.app.prediction.AppTarget;
import android.content.Context;
import android.content.Intent;
@@ -26,6 +25,8 @@ import android.content.pm.ShortcutInfo;
import android.service.chooser.ChooserTarget;
import android.util.Log;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.SelectableTargetInfo;
import com.android.intentresolver.chooser.TargetInfo;
diff --git a/java/src/com/android/intentresolver/SimpleIconFactory.java b/java/src/com/android/intentresolver/SimpleIconFactory.java
index ec5179a..750b24a 100644
--- a/java/src/com/android/intentresolver/SimpleIconFactory.java
+++ b/java/src/com/android/intentresolver/SimpleIconFactory.java
@@ -21,9 +21,6 @@ import static android.graphics.Paint.DITHER_FLAG;
import static android.graphics.Paint.FILTER_BITMAP_FLAG;
import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction;
-import android.annotation.AttrRes;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -50,6 +47,10 @@ import android.util.AttributeSet;
import android.util.Pools.SynchronizedPool;
import android.util.TypedValue;
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.android.internal.annotations.VisibleForTesting;
import org.xmlpull.v1.XmlPullParser;
@@ -719,10 +720,18 @@ public class SimpleIconFactory {
}
@Override
- public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) { }
+ public void inflate(
+ @NonNull Resources r,
+ @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs) {
+ }
@Override
- public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) { }
+ public void inflate(
+ @NonNull Resources r,
+ @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, Theme theme) {
+ }
/**
* Sets the scale associated with this drawable
diff --git a/java/src/com/android/intentresolver/TargetPresentationGetter.java b/java/src/com/android/intentresolver/TargetPresentationGetter.java
index f8b3656..910c65c 100644
--- a/java/src/com/android/intentresolver/TargetPresentationGetter.java
+++ b/java/src/com/android/intentresolver/TargetPresentationGetter.java
@@ -16,7 +16,6 @@
package com.android.intentresolver;
-import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
@@ -30,6 +29,8 @@ import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
+import androidx.annotation.Nullable;
+
/**
* Loads the icon and label for the provided ApplicationInfo. Defaults to using the application icon
* and label over any IntentFilter or Activity icon to increase user understanding, with an
@@ -37,7 +38,7 @@ import android.util.Log;
* resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses
* Strings to strip creative formatting.
*
- * Use one of the {@link TargetPresentationGetter#Factory} methods to create an instance of the
+ * Use one of the {@link TargetPresentationGetter.Factory} methods to create an instance of the
* appropriate concrete type.
*
* TODO: once this component (and its tests) are merged, it should be possible to refactor and
diff --git a/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java b/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
index 8b9bfb3..074537e 100644
--- a/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
@@ -16,6 +16,8 @@
package com.android.intentresolver.chooser;
+import android.service.chooser.ChooserTarget;
+
import java.util.ArrayList;
import java.util.Arrays;
diff --git a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
index 09cf319..536f11c 100644
--- a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
+++ b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
@@ -16,8 +16,6 @@
package com.android.intentresolver.chooser;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
@@ -27,10 +25,10 @@ import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.UserHandle;
-import com.android.intentresolver.TargetPresentationGetter;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
/**
@@ -39,12 +37,11 @@ import java.util.List;
*/
public class DisplayResolveInfo implements TargetInfo {
private final ResolveInfo mResolveInfo;
- private CharSequence mDisplayLabel;
- private CharSequence mExtendedInfo;
+ private volatile CharSequence mDisplayLabel;
+ private volatile CharSequence mExtendedInfo;
private final Intent mResolvedIntent;
private final List<Intent> mSourceIntents = new ArrayList<>();
private final boolean mIsSuspended;
- private TargetPresentationGetter mPresentationGetter;
private boolean mPinned = false;
private final IconHolder mDisplayIconHolder = new SettableIconHolder();
@@ -52,15 +49,13 @@ public class DisplayResolveInfo implements TargetInfo {
public static DisplayResolveInfo newDisplayResolveInfo(
Intent originalIntent,
ResolveInfo resolveInfo,
- @NonNull Intent resolvedIntent,
- @Nullable TargetPresentationGetter presentationGetter) {
+ @NonNull Intent resolvedIntent) {
return newDisplayResolveInfo(
originalIntent,
resolveInfo,
/* displayLabel=*/ null,
/* extendedInfo=*/ null,
- resolvedIntent,
- presentationGetter);
+ resolvedIntent);
}
/** Create a new {@code DisplayResolveInfo} instance. */
@@ -69,15 +64,13 @@ public class DisplayResolveInfo implements TargetInfo {
ResolveInfo resolveInfo,
CharSequence displayLabel,
CharSequence extendedInfo,
- @NonNull Intent resolvedIntent,
- @Nullable TargetPresentationGetter presentationGetter) {
+ @NonNull Intent resolvedIntent) {
return new DisplayResolveInfo(
originalIntent,
resolveInfo,
displayLabel,
extendedInfo,
- resolvedIntent,
- presentationGetter);
+ resolvedIntent);
}
private DisplayResolveInfo(
@@ -85,13 +78,11 @@ public class DisplayResolveInfo implements TargetInfo {
ResolveInfo resolveInfo,
CharSequence displayLabel,
CharSequence extendedInfo,
- @NonNull Intent resolvedIntent,
- @Nullable TargetPresentationGetter presentationGetter) {
+ @NonNull Intent resolvedIntent) {
mSourceIntents.add(originalIntent);
mResolveInfo = resolveInfo;
mDisplayLabel = displayLabel;
mExtendedInfo = extendedInfo;
- mPresentationGetter = presentationGetter;
final ActivityInfo ai = mResolveInfo.activityInfo;
mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
@@ -101,8 +92,7 @@ public class DisplayResolveInfo implements TargetInfo {
private DisplayResolveInfo(
DisplayResolveInfo other,
- @Nullable Intent baseIntentToSend,
- TargetPresentationGetter presentationGetter) {
+ @Nullable Intent baseIntentToSend) {
mSourceIntents.addAll(other.getAllSourceIntents());
mResolveInfo = other.mResolveInfo;
mIsSuspended = other.mIsSuspended;
@@ -112,7 +102,6 @@ public class DisplayResolveInfo implements TargetInfo {
mResolvedIntent = createResolvedIntent(
baseIntentToSend == null ? other.mResolvedIntent : baseIntentToSend,
mResolveInfo.activityInfo);
- mPresentationGetter = presentationGetter;
mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());
}
@@ -124,7 +113,6 @@ public class DisplayResolveInfo implements TargetInfo {
mDisplayLabel = other.mDisplayLabel;
mExtendedInfo = other.mExtendedInfo;
mResolvedIntent = other.mResolvedIntent;
- mPresentationGetter = other.mPresentationGetter;
mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());
}
@@ -147,10 +135,6 @@ public class DisplayResolveInfo implements TargetInfo {
}
public CharSequence getDisplayLabel() {
- if (mDisplayLabel == null && mPresentationGetter != null) {
- mDisplayLabel = mPresentationGetter.getLabel();
- mExtendedInfo = mPresentationGetter.getSubLabel();
- }
return mDisplayLabel;
}
@@ -186,8 +170,7 @@ public class DisplayResolveInfo implements TargetInfo {
return new DisplayResolveInfo(
this,
- TargetInfo.mergeRefinementIntoMatchingBaseIntent(matchingBase, proposedRefinement),
- mPresentationGetter);
+ TargetInfo.mergeRefinementIntoMatchingBaseIntent(matchingBase, proposedRefinement));
}
@Override
@@ -197,7 +180,7 @@ public class DisplayResolveInfo implements TargetInfo {
@Override
public ArrayList<DisplayResolveInfo> getAllDisplayTargets() {
- return new ArrayList<>(Arrays.asList(this));
+ return new ArrayList<>(List.of(this));
}
public void addAlternateSourceIntent(Intent alt) {
diff --git a/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java b/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java
index 10d4415..50aaec0 100644
--- a/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java
@@ -16,8 +16,6 @@
package com.android.intentresolver.chooser;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.prediction.AppTarget;
import android.content.ComponentName;
@@ -27,8 +25,11 @@ import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.os.Bundle;
import android.os.UserHandle;
+import android.service.chooser.ChooserTarget;
import android.util.HashedStringCache;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.common.collect.ImmutableList;
@@ -43,7 +44,7 @@ import java.util.List;
public final class ImmutableTargetInfo implements TargetInfo {
private static final String TAG = "TargetInfo";
- /** Delegate interface to implement {@link TargetInfo#getHashedTargetIdForMetrics()}. */
+ /** Delegate interface to implement {@link TargetInfo#getHashedTargetIdForMetrics}. */
public interface TargetHashProvider {
/** Request a hash for the specified {@code target}. */
HashedStringCache.HashResult getHashedTargetIdForMetrics(
@@ -53,15 +54,15 @@ public final class ImmutableTargetInfo implements TargetInfo {
/** Delegate interface to request that the target be launched by a particular API. */
public interface TargetActivityStarter {
/**
- * Request that the delegate use the {@link Activity#startAsCaller()} API to launch the
- * specified {@code target}.
+ * Request that the delegate use the {@link Activity#startActivityAsCaller} API to launch
+ * the specified {@code target}.
*
* @return true if the target was launched successfully.
*/
boolean startAsCaller(TargetInfo target, Activity activity, Bundle options, int userId);
/**
- * Request that the delegate use the {@link Activity#startAsUser()} API to launch the
+ * Request that the delegate use the {@link Activity#startActivityAsUser} API to launch the
* specified {@code target}.
*
* @return true if the target was launched successfully.
@@ -145,7 +146,7 @@ public final class ImmutableTargetInfo implements TargetInfo {
/**
* Configure an {@link Intent} to be built in to the output target as the "base intent to
* send," which may be a refinement of any of our source targets. This is private because
- * it's only used internally by {@link #tryToCloneWithAppliedRefinement()}; if it's ever
+ * it's only used internally by {@link #tryToCloneWithAppliedRefinement}; if it's ever
* expanded, the builder should probably be responsible for enforcing the refinement check.
*/
private Builder setBaseIntentToSend(Intent baseIntent) {
@@ -229,8 +230,8 @@ public final class ImmutableTargetInfo implements TargetInfo {
/**
* Configure the full list of source intents we could resolve for this target. This is
- * effectively the same as calling {@link #setResolvedIntent()} with the first element of
- * the list, and {@link #setAlternateSourceIntents()} with the remainder (or clearing those
+ * effectively the same as calling {@link #setResolvedIntent} with the first element of
+ * the list, and {@link #setAlternateSourceIntents} with the remainder (or clearing those
* fields on the builder if there are no corresponding elements in the list).
*/
public Builder setAllSourceIntents(List<Intent> sourceIntents) {
diff --git a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
index 6444e13..46803a0 100644
--- a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
@@ -16,7 +16,6 @@
package com.android.intentresolver.chooser;
-import android.annotation.Nullable;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.AnimatedVectorDrawable;
@@ -24,6 +23,8 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.R;
import java.util.function.Supplier;
diff --git a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
index 5766db0..c4aa902 100644
--- a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
@@ -16,7 +16,6 @@
package com.android.intentresolver.chooser;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.prediction.AppTarget;
import android.content.ComponentName;
@@ -33,6 +32,8 @@ import android.text.SpannableStringBuilder;
import android.util.HashedStringCache;
import android.util.Log;
+import androidx.annotation.Nullable;
+
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import java.util.ArrayList;
diff --git a/java/src/com/android/intentresolver/chooser/TargetInfo.java b/java/src/com/android/intentresolver/chooser/TargetInfo.java
index 9d79399..ba6c3c0 100644
--- a/java/src/com/android/intentresolver/chooser/TargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/TargetInfo.java
@@ -17,14 +17,15 @@
package com.android.intentresolver.chooser;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.prediction.AppTarget;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
@@ -32,6 +33,12 @@ import android.service.chooser.ChooserTarget;
import android.text.TextUtils;
import android.util.HashedStringCache;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.ChooserRefinementManager;
+import com.android.intentresolver.ResolverActivity;
+
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -187,9 +194,9 @@ public interface TargetInfo {
* Attempt to apply a {@code proposedRefinement} that the {@link ChooserRefinementManager}
* received from the caller's refinement flow. This may succeed only if the target has a source
* intent that matches the filtering parameters of the proposed refinement (according to
- * {@link Intent#filterEquals()}). Then the first such match is the "base intent," and the
- * proposed refinement is merged into that base (via {@link Intent#fillIn()}; this can never
- * result in a change to the {@link Intent#filterEquals()} status of the base, but may e.g. add
+ * {@link Intent#filterEquals}). Then the first such match is the "base intent," and the
+ * proposed refinement is merged into that base (via {@link Intent#fillIn}; this can never
+ * result in a change to the {@link Intent#filterEquals} status of the base, but may e.g. add
* new "extras" that weren't previously given in the base intent).
*
* @return a copy of this {@link TargetInfo} where the "base intent to send" is the result of
@@ -280,7 +287,7 @@ public interface TargetInfo {
}
/**
- * @return the {@link ShortcutManager} data for any shortcut associated with this target.
+ * @return the {@link ShortcutInfo} for any shortcut associated with this target.
*/
@Nullable
default ShortcutInfo getDirectShareShortcutInfo() {
@@ -422,7 +429,7 @@ public interface TargetInfo {
/**
* @return true if this target should be logged with the "direct_share" metrics category in
- * {@link ResolverActivity#maybeLogCrossProfileTargetLaunch()}. This is defined for legacy
+ * {@link ResolverActivity#maybeLogCrossProfileTargetLaunch}. This is defined for legacy
* compatibility and is <em>not</em> likely to be a good indicator of whether this is actually a
* "direct share" target (e.g. because it historically also applies to "empty" and "placeholder"
* targets).
diff --git a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt
index 103e8bf..10ee5af 100644
--- a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt
+++ b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt
@@ -16,6 +16,7 @@
package com.android.intentresolver.contentpreview
+import android.content.Intent
import androidx.annotation.MainThread
import androidx.lifecycle.ViewModel
import com.android.intentresolver.ChooserRequestParameters
@@ -24,7 +25,7 @@ import com.android.intentresolver.ChooserRequestParameters
abstract class BasePreviewViewModel : ViewModel() {
@MainThread
abstract fun createOrReuseProvider(
- chooserRequest: ChooserRequestParameters
+ targetIntent: Intent
): PreviewDataProvider
@MainThread abstract fun createOrReuseImageLoader(): ImageLoader
diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
index d279f11..a015147 100644
--- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
@@ -16,8 +16,6 @@
package com.android.intentresolver.contentpreview;
-import static androidx.lifecycle.LifecycleKt.getCoroutineScope;
-
import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE;
import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE;
import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT;
@@ -28,11 +26,11 @@ import android.content.res.Resources;
import android.net.Uri;
import android.text.TextUtils;
import android.view.LayoutInflater;
+import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import androidx.lifecycle.Lifecycle;
import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback;
@@ -40,6 +38,8 @@ import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatu
import java.util.List;
import java.util.function.Consumer;
+import kotlinx.coroutines.CoroutineScope;
+
/**
* Collection of helpers for building the content preview UI displayed in
* {@link com.android.intentresolver.ChooserActivity}.
@@ -47,7 +47,7 @@ import java.util.function.Consumer;
*/
public final class ChooserContentPreviewUi {
- private final Lifecycle mLifecycle;
+ private final CoroutineScope mScope;
/**
* Delegate to build the default system action buttons to display in the preview layout, if/when
@@ -92,14 +92,14 @@ public final class ChooserContentPreviewUi {
final ContentPreviewUi mContentPreviewUi;
public ChooserContentPreviewUi(
- Lifecycle lifecycle,
+ CoroutineScope scope,
PreviewDataProvider previewData,
Intent targetIntent,
ImageLoader imageLoader,
ActionFactory actionFactory,
TransitionElementStatusCallback transitionElementStatusCallback,
HeadlineGenerator headlineGenerator) {
- mLifecycle = lifecycle;
+ mScope = scope;
mContentPreviewUi = createContentPreview(
previewData,
targetIntent,
@@ -125,7 +125,7 @@ public final class ChooserContentPreviewUi {
int previewType = previewData.getPreviewType();
if (previewType == CONTENT_PREVIEW_TEXT) {
return createTextPreview(
- mLifecycle,
+ mScope,
targetIntent,
actionFactory,
imageLoader,
@@ -137,8 +137,7 @@ public final class ChooserContentPreviewUi {
actionFactory,
headlineGenerator);
if (previewData.getUriCount() > 0) {
- previewData.getFirstFileName(
- mLifecycle, fileContentPreviewUi::setFirstFileName);
+ previewData.getFirstFileName(mScope, fileContentPreviewUi::setFirstFileName);
}
return fileContentPreviewUi;
}
@@ -148,7 +147,7 @@ public final class ChooserContentPreviewUi {
if (!TextUtils.isEmpty(text)) {
FilesPlusTextContentPreviewUi previewUi =
new FilesPlusTextContentPreviewUi(
- mLifecycle,
+ mScope,
isSingleImageShare,
previewData.getUriCount(),
targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT),
@@ -159,7 +158,7 @@ public final class ChooserContentPreviewUi {
headlineGenerator);
if (previewData.getUriCount() > 0) {
JavaFlowHelper.collectToList(
- getCoroutineScope(mLifecycle),
+ mScope,
previewData.getImagePreviewFileInfoFlow(),
previewUi::updatePreviewMetadata);
}
@@ -167,7 +166,7 @@ public final class ChooserContentPreviewUi {
}
return new UnifiedContentPreviewUi(
- getCoroutineScope(mLifecycle),
+ mScope,
isSingleImageShare,
targetIntent.getType(),
actionFactory,
@@ -188,19 +187,22 @@ public final class ChooserContentPreviewUi {
* specified {@code intent}.
*/
public ViewGroup displayContentPreview(
- Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ViewGroup parent,
+ @Nullable View headlineViewParent) {
- return mContentPreviewUi.display(resources, layoutInflater, parent);
+ return mContentPreviewUi.display(resources, layoutInflater, parent, headlineViewParent);
}
private static TextContentPreviewUi createTextPreview(
- Lifecycle lifecycle,
+ CoroutineScope scope,
Intent targetIntent,
ChooserContentPreviewUi.ActionFactory actionFactory,
ImageLoader imageLoader,
HeadlineGenerator headlineGenerator) {
CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
- String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE);
+ CharSequence previewTitle = targetIntent.getCharSequenceExtra(Intent.EXTRA_TITLE);
ClipData previewData = targetIntent.getClipData();
Uri previewThumbnail = null;
if (previewData != null) {
@@ -210,7 +212,7 @@ public final class ChooserContentPreviewUi {
}
}
return new TextContentPreviewUi(
- lifecycle,
+ scope,
sharingText,
previewTitle,
previewThumbnail,
diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java
index ebab147..ad1c6c0 100644
--- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java
+++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java
@@ -18,7 +18,7 @@ package com.android.intentresolver.contentpreview;
import static java.lang.annotation.RetentionPolicy.SOURCE;
-import android.annotation.IntDef;
+import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
index 2d81794..dce146b 100644
--- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
@@ -24,10 +24,13 @@ import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewStub;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.R;
import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.ScrollableImagePreviewView;
@@ -40,7 +43,10 @@ abstract class ContentPreviewUi {
public abstract int getType();
public abstract ViewGroup display(
- Resources resources, LayoutInflater layoutInflater, ViewGroup parent);
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ViewGroup parent,
+ @Nullable View headlineViewParent);
protected static void updateViewWithImage(ImageView imageView, Bitmap image) {
if (image == null) {
@@ -57,23 +63,28 @@ abstract class ContentPreviewUi {
fadeAnim.start();
}
- protected static void displayHeadline(ViewGroup layout, String headline) {
- if (layout != null) {
- TextView titleView = layout.findViewById(R.id.headline);
- if (titleView != null) {
- if (!TextUtils.isEmpty(headline)) {
- titleView.setText(headline);
- titleView.setVisibility(View.VISIBLE);
- } else {
- titleView.setVisibility(View.GONE);
- }
- }
+ protected static void inflateHeadline(View layout) {
+ ViewStub stub = layout.findViewById(R.id.chooser_headline_row_stub);
+ if (stub != null) {
+ stub.inflate();
+ }
+ }
+
+ protected static void displayHeadline(View layout, String headline) {
+ TextView titleView = layout == null ? null : layout.findViewById(R.id.headline);
+ if (titleView == null) {
+ return;
+ }
+ if (!TextUtils.isEmpty(headline)) {
+ titleView.setText(headline);
+ titleView.setVisibility(View.VISIBLE);
+ } else {
+ titleView.setVisibility(View.GONE);
}
}
protected static void displayModifyShareAction(
- ViewGroup layout,
- ChooserContentPreviewUi.ActionFactory actionFactory) {
+ View layout, ChooserContentPreviewUi.ActionFactory actionFactory) {
ActionRow.Action modifyShareAction = actionFactory.getModifyShareAction();
if (modifyShareAction != null && layout != null) {
TextView modifyShareView = layout.findViewById(R.id.reselection_action);
diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java
index 2075818..89e7e52 100644
--- a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java
@@ -67,18 +67,30 @@ class FileContentPreviewUi extends ContentPreviewUi {
}
@Override
- public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
- ViewGroup layout = displayInternal(resources, layoutInflater, parent);
- displayModifyShareAction(layout, mActionFactory);
+ public ViewGroup display(
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ViewGroup parent,
+ @Nullable View headlineViewParent) {
+ ViewGroup layout = displayInternal(resources, layoutInflater, parent, headlineViewParent);
+ displayModifyShareAction(
+ headlineViewParent == null ? layout : headlineViewParent, mActionFactory);
return layout;
}
private ViewGroup displayInternal(
- Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ViewGroup parent,
+ @Nullable View headlineViewParent) {
mContentPreview = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_file, parent, false);
+ if (headlineViewParent == null) {
+ headlineViewParent = mContentPreview;
+ }
+ inflateHeadline(headlineViewParent);
- displayHeadline(mContentPreview, mHeadlineGenerator.getFilesHeadline(mFileCount));
+ displayHeadline(headlineViewParent, mHeadlineGenerator.getFilesHeadline(mFileCount));
if (mFileCount == 0) {
mContentPreview.setVisibility(View.GONE);
diff --git a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java
index 6e1212e..78fc658 100644
--- a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java
@@ -31,7 +31,6 @@ import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
-import androidx.lifecycle.Lifecycle;
import com.android.intentresolver.R;
import com.android.intentresolver.widget.ActionRow;
@@ -41,6 +40,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.function.Consumer;
+import kotlinx.coroutines.CoroutineScope;
+
/**
* FilesPlusTextContentPreviewUi is shown when the user is sending 1 or more files along with
* non-empty EXTRA_TEXT. The text can be toggled with a checkbox. If a single image file is being
@@ -48,7 +49,7 @@ import java.util.function.Consumer;
* file content).
*/
class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
- private final Lifecycle mLifecycle;
+ private final CoroutineScope mScope;
@Nullable
private final String mIntentMimeType;
private final CharSequence mText;
@@ -59,6 +60,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
private final boolean mIsSingleImage;
private final int mFileCount;
private ViewGroup mContentPreviewView;
+ private View mHeadliveView;
private boolean mIsMetadataUpdated = false;
@Nullable
private Uri mFirstFilePreviewUri;
@@ -68,7 +70,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
private static final boolean SHOW_TOGGLE_CHECKMARK = false;
FilesPlusTextContentPreviewUi(
- Lifecycle lifecycle,
+ CoroutineScope scope,
boolean isSingleImage,
int fileCount,
CharSequence text,
@@ -81,7 +83,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
throw new IllegalArgumentException(
"fileCount = " + fileCount + " and isSingleImage = true");
}
- mLifecycle = lifecycle;
+ mScope = scope;
mIntentMimeType = intentMimeType;
mFileCount = fileCount;
mIsSingleImage = isSingleImage;
@@ -98,9 +100,14 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
}
@Override
- public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
- ViewGroup layout = displayInternal(layoutInflater, parent);
- displayModifyShareAction(layout, mActionFactory);
+ public ViewGroup display(
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ViewGroup parent,
+ @Nullable View headlineViewParent) {
+ ViewGroup layout = displayInternal(layoutInflater, parent, headlineViewParent);
+ displayModifyShareAction(
+ headlineViewParent == null ? layout : headlineViewParent, mActionFactory);
return layout;
}
@@ -118,13 +125,18 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
mFirstFilePreviewUri = files.isEmpty() ? null : files.get(0).getPreviewUri();
mIsMetadataUpdated = true;
if (mContentPreviewView != null) {
- updateUiWithMetadata(mContentPreviewView);
+ updateUiWithMetadata(mContentPreviewView, mHeadliveView);
}
}
- private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) {
+ private ViewGroup displayInternal(
+ LayoutInflater layoutInflater,
+ ViewGroup parent,
+ @Nullable View headlineViewParent) {
mContentPreviewView = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_files_text, parent, false);
+ mHeadliveView = headlineViewParent == null ? mContentPreviewView : headlineViewParent;
+ inflateHeadline(mHeadliveView);
final ActionRow actionRow =
mContentPreviewView.findViewById(com.android.internal.R.id.chooser_action_row);
@@ -134,12 +146,12 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
if (!mIsSingleImage) {
mContentPreviewView.requireViewById(R.id.image_view).setVisibility(View.GONE);
}
- prepareTextPreview(mContentPreviewView, mActionFactory);
+ prepareTextPreview(mContentPreviewView, mHeadliveView, mActionFactory);
if (mIsMetadataUpdated) {
- updateUiWithMetadata(mContentPreviewView);
+ updateUiWithMetadata(mContentPreviewView, mHeadliveView);
} else {
updateHeadline(
- mContentPreviewView,
+ mHeadliveView,
mFileCount,
mTypeClassifier.isImageType(mIntentMimeType),
mTypeClassifier.isVideoType(mIntentMimeType));
@@ -148,13 +160,14 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
return mContentPreviewView;
}
- private void updateUiWithMetadata(ViewGroup contentPreviewView) {
- updateHeadline(contentPreviewView, mFileCount, mAllImages, mAllVideos);
+ private void updateUiWithMetadata(ViewGroup contentPreviewView, View headlineView) {
+ prepareTextPreview(contentPreviewView, headlineView, mActionFactory);
+ updateHeadline(headlineView, mFileCount, mAllImages, mAllVideos);
ImageView imagePreview = mContentPreviewView.requireViewById(R.id.image_view);
if (mIsSingleImage && mFirstFilePreviewUri != null) {
mImageLoader.loadImage(
- mLifecycle,
+ mScope,
mFirstFilePreviewUri,
bitmap -> {
if (bitmap == null) {
@@ -169,8 +182,8 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
}
private void updateHeadline(
- ViewGroup contentPreview, int fileCount, boolean allImages, boolean allVideos) {
- CheckBox includeText = contentPreview.requireViewById(R.id.include_text_action);
+ View headlineView, int fileCount, boolean allImages, boolean allVideos) {
+ CheckBox includeText = headlineView.requireViewById(R.id.include_text_action);
String headline;
if (includeText.getVisibility() == View.VISIBLE && includeText.isChecked()) {
if (allImages) {
@@ -190,14 +203,15 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
}
}
- displayHeadline(contentPreview, headline);
+ displayHeadline(headlineView, headline);
}
private void prepareTextPreview(
ViewGroup contentPreview,
+ View headlineView,
ChooserContentPreviewUi.ActionFactory actionFactory) {
final TextView textView = contentPreview.requireViewById(R.id.content_preview_text);
- CheckBox includeText = contentPreview.requireViewById(R.id.include_text_action);
+ CheckBox includeText = headlineView.requireViewById(R.id.include_text_action);
boolean isLink = HttpUriMatcher.isHttpUri(mText.toString());
textView.setAutoLinkMask(isLink ? Linkify.WEB_URLS : 0);
textView.setText(mText);
@@ -213,7 +227,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
textView.setText(getNoTextString(contentPreview.getResources()));
}
shareTextAction.accept(!isChecked);
- updateHeadline(contentPreview, mFileCount, mAllImages, mAllVideos);
+ updateHeadline(headlineView, mFileCount, mAllImages, mAllVideos);
});
if (SHOW_TOGGLE_CHECKMARK) {
includeText.setVisibility(View.VISIBLE);
diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt
index 1aace8c..ef1e55d 100644
--- a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt
+++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt
@@ -16,36 +16,55 @@
package com.android.intentresolver.contentpreview
-import android.annotation.StringRes
import android.content.Context
-import com.android.intentresolver.R
import android.util.PluralsMessageFormatter
+import androidx.annotation.StringRes
+import com.android.intentresolver.R
private const val PLURALS_COUNT = "count"
/**
- * HeadlineGenerator generates the text to show at the top of the sharesheet as a brief
- * description of the content being shared.
+ * HeadlineGenerator generates the text to show at the top of the sharesheet as a brief description
+ * of the content being shared.
*/
class HeadlineGeneratorImpl(private val context: Context) : HeadlineGenerator {
override fun getTextHeadline(text: CharSequence): String {
return context.getString(
- getTemplateResource(text, R.string.sharing_link, R.string.sharing_text))
+ getTemplateResource(text, R.string.sharing_link, R.string.sharing_text)
+ )
}
override fun getImagesWithTextHeadline(text: CharSequence, count: Int): String {
- return getPluralString(getTemplateResource(
- text, R.string.sharing_images_with_link, R.string.sharing_images_with_text), count)
+ return getPluralString(
+ getTemplateResource(
+ text,
+ R.string.sharing_images_with_link,
+ R.string.sharing_images_with_text
+ ),
+ count
+ )
}
override fun getVideosWithTextHeadline(text: CharSequence, count: Int): String {
- return getPluralString(getTemplateResource(
- text, R.string.sharing_videos_with_link, R.string.sharing_videos_with_text), count)
+ return getPluralString(
+ getTemplateResource(
+ text,
+ R.string.sharing_videos_with_link,
+ R.string.sharing_videos_with_text
+ ),
+ count
+ )
}
override fun getFilesWithTextHeadline(text: CharSequence, count: Int): String {
- return getPluralString(getTemplateResource(
- text, R.string.sharing_files_with_link, R.string.sharing_files_with_text), count)
+ return getPluralString(
+ getTemplateResource(
+ text,
+ R.string.sharing_files_with_link,
+ R.string.sharing_files_with_text
+ ),
+ count
+ )
}
override fun getImagesHeadline(count: Int): String {
@@ -70,7 +89,9 @@ class HeadlineGeneratorImpl(private val context: Context) : HeadlineGenerator {
@StringRes
private fun getTemplateResource(
- text: CharSequence, @StringRes linkResource: Int, @StringRes nonLinkResource: Int
+ text: CharSequence,
+ @StringRes linkResource: Int,
+ @StringRes nonLinkResource: Int
): Int {
return if (text.toString().isHttpUri()) linkResource else nonLinkResource
}
diff --git a/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt
index 8d0fb84..629651a 100644
--- a/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt
+++ b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt
@@ -18,8 +18,8 @@ package com.android.intentresolver.contentpreview
import android.graphics.Bitmap
import android.net.Uri
-import androidx.lifecycle.Lifecycle
import java.util.function.Consumer
+import kotlinx.coroutines.CoroutineScope
/** A content preview image loader. */
interface ImageLoader : suspend (Uri) -> Bitmap?, suspend (Uri, Boolean) -> Bitmap? {
@@ -30,7 +30,7 @@ interface ImageLoader : suspend (Uri) -> Bitmap?, suspend (Uri, Boolean) -> Bitm
* @param callback a callback that will be invoked with the loaded image or null if loading has
* failed.
*/
- fun loadImage(callerLifecycle: Lifecycle, uri: Uri, callback: Consumer<Bitmap?>)
+ fun loadImage(callerScope: CoroutineScope, uri: Uri, callback: Consumer<Bitmap?>)
/** Prepopulate the image loader cache. */
fun prePopulate(uris: List<Uri>)
diff --git a/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt
index 22dd112..572ccf0 100644
--- a/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt
+++ b/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt
@@ -24,8 +24,6 @@ import android.util.Size
import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting
import androidx.collection.LruCache
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.coroutineScope
import java.util.function.Consumer
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
@@ -70,8 +68,8 @@ constructor(
override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = loadImageAsync(uri, caching)
- override fun loadImage(callerLifecycle: Lifecycle, uri: Uri, callback: Consumer<Bitmap?>) {
- callerLifecycle.coroutineScope.launch {
+ override fun loadImage(callerScope: CoroutineScope, uri: Uri, callback: Consumer<Bitmap?>) {
+ callerScope.launch {
val image = loadImageAsync(uri, caching = true)
if (isActive) {
callback.accept(image)
diff --git a/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt b/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt
index 9001693..31a7006 100644
--- a/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt
+++ b/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt
@@ -19,13 +19,17 @@ package com.android.intentresolver.contentpreview
import android.content.res.Resources
import android.util.Log
import android.view.LayoutInflater
+import android.view.View
import android.view.ViewGroup
internal class NoContextPreviewUi(private val type: Int) : ContentPreviewUi() {
override fun getType(): Int = type
override fun display(
- resources: Resources?, layoutInflater: LayoutInflater?, parent: ViewGroup?
+ resources: Resources?,
+ layoutInflater: LayoutInflater?,
+ parent: ViewGroup?,
+ headlineViewParent: View?,
): ViewGroup? {
Log.e(TAG, "Unexpected content preview type: $type")
return null
diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt
index 9f1cc6c..38918d7 100644
--- a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt
+++ b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt
@@ -29,8 +29,6 @@ import android.text.TextUtils
import android.util.Log
import androidx.annotation.OpenForTesting
import androidx.annotation.VisibleForTesting
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.coroutineScope
import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE
import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE
import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT
@@ -185,11 +183,11 @@ constructor(
* is not provided, derived from the URI.
*/
@Throws(IndexOutOfBoundsException::class)
- fun getFirstFileName(callerLifecycle: Lifecycle, callback: Consumer<String>) {
+ fun getFirstFileName(callerScope: CoroutineScope, callback: Consumer<String>) {
if (records.isEmpty()) {
throw IndexOutOfBoundsException("There are no shared URIs")
}
- callerLifecycle.coroutineScope.launch {
+ callerScope.launch {
val result = scope.async { getFirstFileName() }.await()
callback.accept(result)
}
@@ -264,44 +262,46 @@ constructor(
private val query by lazy { readQueryResult() }
- private fun readQueryResult(): QueryResult {
- val cursor =
- contentResolver.querySafe(uri)?.takeIf { it.moveToFirst() } ?: return QueryResult()
-
- var flagColIdx = -1
- var displayIconUriColIdx = -1
- var nameColIndex = -1
- var titleColIndex = -1
- // TODO: double-check why Cursor#getColumnInded didn't work
- cursor.columnNames.forEachIndexed { i, columnName ->
- when (columnName) {
- DocumentsContract.Document.COLUMN_FLAGS -> flagColIdx = i
- MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI -> displayIconUriColIdx = i
- OpenableColumns.DISPLAY_NAME -> nameColIndex = i
- Downloads.Impl.COLUMN_TITLE -> titleColIndex = i
+ private fun readQueryResult(): QueryResult =
+ contentResolver.querySafe(uri)?.use { cursor ->
+ if (!cursor.moveToFirst()) return@use null
+
+ var flagColIdx = -1
+ var displayIconUriColIdx = -1
+ var nameColIndex = -1
+ var titleColIndex = -1
+ // TODO: double-check why Cursor#getColumnInded didn't work
+ cursor.columnNames.forEachIndexed { i, columnName ->
+ when (columnName) {
+ DocumentsContract.Document.COLUMN_FLAGS -> flagColIdx = i
+ MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI -> displayIconUriColIdx = i
+ OpenableColumns.DISPLAY_NAME -> nameColIndex = i
+ Downloads.Impl.COLUMN_TITLE -> titleColIndex = i
+ }
}
- }
-
- val supportsThumbnail =
- flagColIdx >= 0 && ((cursor.getInt(flagColIdx) and FLAG_SUPPORTS_THUMBNAIL) != 0)
- var title = ""
- if (nameColIndex >= 0) {
- title = cursor.getString(nameColIndex) ?: ""
- }
- if (TextUtils.isEmpty(title) && titleColIndex >= 0) {
- title = cursor.getString(titleColIndex) ?: ""
- }
+ val supportsThumbnail =
+ flagColIdx >= 0 &&
+ ((cursor.getInt(flagColIdx) and FLAG_SUPPORTS_THUMBNAIL) != 0)
- val iconUri =
- if (displayIconUriColIdx >= 0) {
- cursor.getString(displayIconUriColIdx)?.let(Uri::parse)
- } else {
- null
+ var title = ""
+ if (nameColIndex >= 0) {
+ title = cursor.getString(nameColIndex) ?: ""
+ }
+ if (TextUtils.isEmpty(title) && titleColIndex >= 0) {
+ title = cursor.getString(titleColIndex) ?: ""
}
- return QueryResult(supportsThumbnail, title, iconUri)
- }
+ val iconUri =
+ if (displayIconUriColIdx >= 0) {
+ cursor.getString(displayIconUriColIdx)?.let(Uri::parse)
+ } else {
+ null
+ }
+
+ QueryResult(supportsThumbnail, title, iconUri)
+ }
+ ?: QueryResult()
}
private class QueryResult(
diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
index 6013f5a..6350756 100644
--- a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
+++ b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
@@ -17,6 +17,7 @@
package com.android.intentresolver.contentpreview
import android.app.Application
+import android.content.Intent
import androidx.annotation.MainThread
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
@@ -25,26 +26,32 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.android.intentresolver.ChooserRequestParameters
import com.android.intentresolver.R
+import com.android.intentresolver.inject.Background
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.plus
/** A trivial view model to keep a [PreviewDataProvider] instance over a configuration change */
-class PreviewViewModel(
+@HiltViewModel
+class PreviewViewModel
+@Inject
+constructor(
private val application: Application,
- private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ @Background private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : BasePreviewViewModel() {
private var previewDataProvider: PreviewDataProvider? = null
private var imageLoader: ImagePreviewImageLoader? = null
@MainThread
override fun createOrReuseProvider(
- chooserRequest: ChooserRequestParameters
+ targetIntent: Intent
): PreviewDataProvider =
previewDataProvider
?: PreviewDataProvider(
viewModelScope + dispatcher,
- chooserRequest.targetIntent,
+ targetIntent,
application.contentResolver
)
.also { previewDataProvider = it }
diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java
index c38ed03..b0dc3c5 100644
--- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java
@@ -20,6 +20,7 @@ import static com.android.intentresolver.util.UriFilters.isOwnedByCurrentUser;
import android.content.res.Resources;
import android.net.Uri;
+import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
@@ -28,13 +29,14 @@ import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
-import androidx.lifecycle.Lifecycle;
import com.android.intentresolver.R;
import com.android.intentresolver.widget.ActionRow;
+import kotlinx.coroutines.CoroutineScope;
+
class TextContentPreviewUi extends ContentPreviewUi {
- private final Lifecycle mLifecycle;
+ private final CoroutineScope mScope;
@Nullable
private final CharSequence mSharingText;
@Nullable
@@ -46,14 +48,14 @@ class TextContentPreviewUi extends ContentPreviewUi {
private final HeadlineGenerator mHeadlineGenerator;
TextContentPreviewUi(
- Lifecycle lifecycle,
+ CoroutineScope scope,
@Nullable CharSequence sharingText,
@Nullable CharSequence previewTitle,
@Nullable Uri previewThumbnail,
ChooserContentPreviewUi.ActionFactory actionFactory,
ImageLoader imageLoader,
HeadlineGenerator headlineGenerator) {
- mLifecycle = lifecycle;
+ mScope = scope;
mSharingText = sharingText;
mPreviewTitle = previewTitle;
mPreviewThumbnail = previewThumbnail;
@@ -68,17 +70,27 @@ class TextContentPreviewUi extends ContentPreviewUi {
}
@Override
- public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
- ViewGroup layout = displayInternal(layoutInflater, parent);
- displayModifyShareAction(layout, mActionFactory);
+ public ViewGroup display(
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ViewGroup parent,
+ @Nullable View headlineViewParent) {
+ ViewGroup layout = displayInternal(layoutInflater, parent, headlineViewParent);
+ displayModifyShareAction(
+ headlineViewParent == null ? layout : headlineViewParent, mActionFactory);
return layout;
}
private ViewGroup displayInternal(
LayoutInflater layoutInflater,
- ViewGroup parent) {
+ ViewGroup parent,
+ @Nullable View headlineViewParent) {
ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_text, parent, false);
+ if (headlineViewParent == null) {
+ headlineViewParent = contentPreviewLayout;
+ }
+ inflateHeadline(headlineViewParent);
final ActionRow actionRow =
contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
@@ -93,13 +105,9 @@ class TextContentPreviewUi extends ContentPreviewUi {
TextView textView = contentPreviewLayout.findViewById(
com.android.internal.R.id.content_preview_text);
- String text = mSharingText.toString();
- // If we're only previewing one line, then strip out newlines.
- if (textView.getMaxLines() == 1) {
- text = text.replace("\n", " ");
- }
- textView.setText(text);
+ textView.setText(
+ textView.getMaxLines() == 1 ? replaceLineBreaks(mSharingText) : mSharingText);
TextView previewTitleView = contentPreviewLayout.findViewById(
com.android.internal.R.id.content_preview_title);
@@ -115,7 +123,7 @@ class TextContentPreviewUi extends ContentPreviewUi {
previewThumbnailView.setVisibility(View.GONE);
} else {
mImageLoader.loadImage(
- mLifecycle,
+ mScope,
mPreviewThumbnail,
(bitmap) -> updateViewWithImage(
contentPreviewLayout.findViewById(
@@ -131,8 +139,22 @@ class TextContentPreviewUi extends ContentPreviewUi {
copyButton.setVisibility(View.GONE);
}
- displayHeadline(contentPreviewLayout, mHeadlineGenerator.getTextHeadline(mSharingText));
+ displayHeadline(headlineViewParent, mHeadlineGenerator.getTextHeadline(mSharingText));
return contentPreviewLayout;
}
+
+ @Nullable
+ private static CharSequence replaceLineBreaks(@Nullable CharSequence text) {
+ if (text == null) {
+ return null;
+ }
+ SpannableStringBuilder string = new SpannableStringBuilder(text);
+ for (int i = 0, size = string.length(); i < size; i++) {
+ if (string.charAt(i) == '\n') {
+ string.replace(i, i + 1, " ");
+ }
+ }
+ return string;
+ }
}
diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
index 8e635ab..8ddd527 100644
--- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
@@ -52,6 +52,8 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
private List<FileInfo> mFiles;
@Nullable
private ViewGroup mContentPreviewView;
+ @Nullable
+ private View mHeadlineView;
UnifiedContentPreviewUi(
CoroutineScope scope,
@@ -83,9 +85,14 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
}
@Override
- public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
- ViewGroup layout = displayInternal(layoutInflater, parent);
- displayModifyShareAction(layout, mActionFactory);
+ public ViewGroup display(
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ViewGroup parent,
+ @Nullable View headlineViewParent) {
+ ViewGroup layout = displayInternal(layoutInflater, parent, headlineViewParent);
+ displayModifyShareAction(
+ headlineViewParent == null ? layout : headlineViewParent, mActionFactory);
return layout;
}
@@ -96,13 +103,16 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
.toList());
mFiles = files;
if (mContentPreviewView != null) {
- updatePreviewWithFiles(mContentPreviewView, files);
+ updatePreviewWithFiles(mContentPreviewView, mHeadlineView, files);
}
}
- private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) {
+ private ViewGroup displayInternal(
+ LayoutInflater layoutInflater, ViewGroup parent, @Nullable View headlineViewParent) {
mContentPreviewView = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_image, parent, false);
+ mHeadlineView = headlineViewParent == null ? mContentPreviewView : headlineViewParent;
+ inflateHeadline(mHeadlineView);
final ActionRow actionRow =
mContentPreviewView.findViewById(com.android.internal.R.id.chooser_action_row);
@@ -122,10 +132,10 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
mItemCount);
if (mFiles != null) {
- updatePreviewWithFiles(mContentPreviewView, mFiles);
+ updatePreviewWithFiles(mContentPreviewView, mHeadlineView, mFiles);
} else {
displayHeadline(
- mContentPreviewView,
+ mHeadlineView,
mItemCount,
mTypeClassifier.isImageType(mIntentMimeType),
mTypeClassifier.isVideoType(mIntentMimeType));
@@ -135,7 +145,8 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
return mContentPreviewView;
}
- private void updatePreviewWithFiles(ViewGroup contentPreviewView, List<FileInfo> files) {
+ private void updatePreviewWithFiles(
+ ViewGroup contentPreviewView, View headlineView, List<FileInfo> files) {
final int count = files.size();
ScrollableImagePreviewView imagePreview =
contentPreviewView.requireViewById(R.id.scrollable_image_preview);
@@ -158,11 +169,11 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
allVideos = allVideos && previewType == ScrollableImagePreviewView.PreviewType.Video;
}
- displayHeadline(contentPreviewView, count, allImages, allVideos);
+ displayHeadline(headlineView, count, allImages, allVideos);
}
private void displayHeadline(
- ViewGroup layout, int count, boolean allImages, boolean allVideos) {
+ View layout, int count, boolean allImages, boolean allVideos) {
if (allImages) {
displayHeadline(layout, mHeadlineGenerator.getImagesHeadline(count));
} else if (allVideos) {
diff --git a/java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.java
new file mode 100644
index 0000000..41422b6
--- /dev/null
+++ b/java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.emptystate;
+
+import android.annotation.Nullable;
+
+import com.android.intentresolver.ResolverListAdapter;
+
+/**
+ * Empty state provider that combines multiple providers. Providers earlier in the list have
+ * priority, that is if there is a provider that returns non-null empty state then all further
+ * providers will be ignored.
+ */
+public class CompositeEmptyStateProvider implements EmptyStateProvider {
+
+ private final EmptyStateProvider[] mProviders;
+
+ public CompositeEmptyStateProvider(EmptyStateProvider... providers) {
+ mProviders = providers;
+ }
+
+ @Nullable
+ @Override
+ public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ for (EmptyStateProvider provider : mProviders) {
+ EmptyState emptyState = provider.getEmptyState(resolverListAdapter);
+ if (emptyState != null) {
+ return emptyState;
+ }
+ }
+ return null;
+ }
+}
diff --git a/java/src/com/android/intentresolver/emptystate/CrossProfileIntentsChecker.java b/java/src/com/android/intentresolver/emptystate/CrossProfileIntentsChecker.java
new file mode 100644
index 0000000..2164e53
--- /dev/null
+++ b/java/src/com/android/intentresolver/emptystate/CrossProfileIntentsChecker.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.emptystate;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.AppGlobals;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.pm.IPackageManager;
+
+import com.android.intentresolver.IntentForwarderActivity;
+
+import java.util.List;
+
+/**
+ * Utility class to check if there are cross profile intents, it is in a separate class so
+ * it could be mocked in tests
+ */
+public class CrossProfileIntentsChecker {
+
+ private final ContentResolver mContentResolver;
+ private final IPackageManager mPackageManager;
+
+ public CrossProfileIntentsChecker(@NonNull ContentResolver contentResolver) {
+ this(contentResolver, AppGlobals.getPackageManager());
+ }
+
+ CrossProfileIntentsChecker(
+ @NonNull ContentResolver contentResolver, IPackageManager packageManager) {
+ mContentResolver = contentResolver;
+ mPackageManager = packageManager;
+ }
+
+ /**
+ * Returns {@code true} if at least one of the provided {@code intents} can be forwarded
+ * from {@code source} (user id) to {@code target} (user id).
+ */
+ public boolean hasCrossProfileIntents(
+ List<Intent> intents, @UserIdInt int source, @UserIdInt int target) {
+ return intents.stream().anyMatch(intent ->
+ null != IntentForwarderActivity.canForward(intent, source, target,
+ mPackageManager, mContentResolver));
+ }
+}
+
diff --git a/java/src/com/android/intentresolver/emptystate/EmptyState.java b/java/src/com/android/intentresolver/emptystate/EmptyState.java
new file mode 100644
index 0000000..cde99fe
--- /dev/null
+++ b/java/src/com/android/intentresolver/emptystate/EmptyState.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.emptystate;
+
+import android.annotation.Nullable;
+
+/**
+ * Model for the "empty state"/"blocker" UI to display instead of a profile tab's normal contents.
+ */
+public interface EmptyState {
+ /**
+ * Get the title to show on the empty state.
+ */
+ @Nullable
+ default String getTitle() {
+ return null;
+ }
+
+ /**
+ * Get the subtitle string to show underneath the title on the empty state.
+ */
+ @Nullable
+ default String getSubtitle() {
+ return null;
+ }
+
+ /**
+ * Get the handler for an optional button associated with this empty state. If the result is
+ * non-null, the empty-state UI will be built with a button that dispatches this handler.
+ */
+ @Nullable
+ default ClickListener getButtonClickListener() {
+ return null;
+ }
+
+ /**
+ * Get whether to show the default UI for the empty state. If true, the UI will show the default
+ * blocker text ('No apps can perform this action') and style; title and subtitle are ignored.
+ */
+ default boolean useDefaultEmptyView() {
+ return false;
+ }
+
+ /**
+ * Returns true if for this empty state we should skip rebuilding of the apps list
+ * for this tab.
+ */
+ default boolean shouldSkipDataRebuild() {
+ return false;
+ }
+
+ /**
+ * Called when empty state is shown, could be used e.g. to track analytics events.
+ */
+ default void onEmptyStateShown() {}
+
+ interface ClickListener {
+ void onClick(TabControl currentTab);
+ }
+
+ interface TabControl {
+ void showSpinner();
+ }
+}
diff --git a/java/src/com/android/intentresolver/emptystate/EmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/EmptyStateProvider.java
new file mode 100644
index 0000000..c326128
--- /dev/null
+++ b/java/src/com/android/intentresolver/emptystate/EmptyStateProvider.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.emptystate;
+
+import android.annotation.Nullable;
+
+import com.android.intentresolver.ResolverListAdapter;
+
+/**
+ * Returns an empty state to show for the current profile page (tab) if necessary.
+ * This could be used e.g. to show a blocker on a tab if device management policy doesn't
+ * allow to use it or there are no apps available.
+ */
+public interface EmptyStateProvider {
+ /**
+ * When a non-null empty state is returned the corresponding profile page will show
+ * this empty state
+ * @param resolverListAdapter the current adapter
+ */
+ @Nullable
+ default EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ return null;
+ }
+}
diff --git a/java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java b/java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java
new file mode 100644
index 0000000..d7ef8c7
--- /dev/null
+++ b/java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.emptystate;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Helper for building `MultiProfilePagerAdapter` tab UIs for profile tabs that are "blocked" by
+ * some empty-state status.
+ */
+public class EmptyStateUiHelper {
+ private final View mEmptyStateView;
+
+ public EmptyStateUiHelper(ViewGroup rootView) {
+ mEmptyStateView =
+ rootView.requireViewById(com.android.internal.R.id.resolver_empty_state);
+ }
+
+ public void resetViewVisibilities() {
+ mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_title)
+ .setVisibility(View.VISIBLE);
+ mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_subtitle)
+ .setVisibility(View.VISIBLE);
+ mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_button)
+ .setVisibility(View.INVISIBLE);
+ mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_progress)
+ .setVisibility(View.GONE);
+ mEmptyStateView.requireViewById(com.android.internal.R.id.empty)
+ .setVisibility(View.GONE);
+ mEmptyStateView.setVisibility(View.VISIBLE);
+ }
+
+ public void showSpinner() {
+ mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_title)
+ .setVisibility(View.INVISIBLE);
+ // TODO: subtitle?
+ mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_button)
+ .setVisibility(View.INVISIBLE);
+ mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_progress)
+ .setVisibility(View.VISIBLE);
+ mEmptyStateView.requireViewById(com.android.internal.R.id.empty)
+ .setVisibility(View.GONE);
+ }
+
+ public void hide() {
+ mEmptyStateView.setVisibility(View.GONE);
+ }
+}
+
diff --git a/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java
index a7b50f3..2653c56 100644
--- a/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java
+++ b/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java
@@ -14,13 +14,11 @@
* limitations under the License.
*/
-package com.android.intentresolver;
+package com.android.intentresolver.emptystate;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_PERSONAL_APPS;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.admin.DevicePolicyEventLogger;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
@@ -28,8 +26,11 @@ import android.content.pm.ResolveInfo;
import android.os.UserHandle;
import android.stats.devicepolicy.nano.DevicePolicyEnums;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.ResolvedComponentInfo;
+import com.android.intentresolver.ResolverListAdapter;
import com.android.internal.R;
import java.util.List;
@@ -51,9 +52,12 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider {
@NonNull
private final UserHandle mTabOwnerUserHandleForLaunch;
- public NoAppsAvailableEmptyStateProvider(Context context, UserHandle workProfileUserHandle,
- UserHandle personalProfileUserHandle, String metricsCategory,
- UserHandle tabOwnerUserHandleForLaunch) {
+ public NoAppsAvailableEmptyStateProvider(
+ @NonNull Context context,
+ @Nullable UserHandle workProfileUserHandle,
+ @Nullable UserHandle personalProfileUserHandle,
+ @NonNull String metricsCategory,
+ @NonNull UserHandle tabOwnerUserHandleForLaunch) {
mContext = context;
mWorkProfileUserHandle = workProfileUserHandle;
mPersonalProfileUserHandle = personalProfileUserHandle;
@@ -76,12 +80,12 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider {
title = mContext.getSystemService(
DevicePolicyManager.class).getResources().getString(
RESOLVER_NO_PERSONAL_APPS,
- () -> mContext.getString(R.string.resolver_no_personal_apps_available));
+ () -> mContext.getString(R.string.resolver_no_personal_apps_available));
} else {
title = mContext.getSystemService(
DevicePolicyManager.class).getResources().getString(
RESOLVER_NO_WORK_APPS,
- () -> mContext.getString(R.string.resolver_no_work_apps_available));
+ () -> mContext.getString(R.string.resolver_no_work_apps_available));
}
return new NoAppsAvailableEmptyState(
@@ -128,8 +132,9 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider {
private boolean mIsPersonalProfile;
- public NoAppsAvailableEmptyState(String title, String metricsCategory,
- boolean isPersonalProfile) {
+ public NoAppsAvailableEmptyState(@NonNull String title,
+ @NonNull String metricsCategory,
+ boolean isPersonalProfile) {
mTitle = title;
mMetricsCategory = metricsCategory;
mIsPersonalProfile = isPersonalProfile;
diff --git a/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java
index 6f72bb0..ce7bd8d 100644
--- a/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java
+++ b/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java
@@ -14,19 +14,18 @@
* limitations under the License.
*/
-package com.android.intentresolver;
+package com.android.intentresolver.emptystate;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.StringRes;
import android.app.admin.DevicePolicyEventLogger;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.os.UserHandle;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+
+import com.android.intentresolver.ResolverListAdapter;
/**
* Empty state provider that does not allow cross profile sharing, it will return a blocker
@@ -92,10 +91,14 @@ public class NoCrossProfileEmptyStateProvider implements EmptyStateProvider {
@NonNull
private final String mEventCategory;
- public DevicePolicyBlockerEmptyState(Context context, String devicePolicyStringTitleId,
- @StringRes int defaultTitleResource, String devicePolicyStringSubtitleId,
+ public DevicePolicyBlockerEmptyState(
+ @NonNull Context context,
+ String devicePolicyStringTitleId,
+ @StringRes int defaultTitleResource,
+ String devicePolicyStringSubtitleId,
@StringRes int defaultSubtitleResource,
- int devicePolicyEventId, String devicePolicyEventCategory) {
+ int devicePolicyEventId,
+ @NonNull String devicePolicyEventCategory) {
mContext = context;
mDevicePolicyStringTitleId = devicePolicyStringTitleId;
mDefaultTitleResource = defaultTitleResource;
diff --git a/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java
index 2f3dfbd..612828e 100644
--- a/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java
+++ b/java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java
@@ -14,21 +14,23 @@
* limitations under the License.
*/
-package com.android.intentresolver;
+package com.android.intentresolver.emptystate;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.admin.DevicePolicyEventLogger;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.os.UserHandle;
import android.stats.devicepolicy.nano.DevicePolicyEnums;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener;
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.WorkProfileAvailabilityManager;
/**
* Chooser/ResolverActivity empty state provider that returns empty state which is shown when
@@ -65,7 +67,7 @@ public class WorkProfilePausedEmptyStateProvider implements EmptyStateProvider {
final String title = mContext.getSystemService(DevicePolicyManager.class)
.getResources().getString(RESOLVER_WORK_PAUSED_TITLE,
- () -> mContext.getString(R.string.resolver_turn_on_work_apps));
+ () -> mContext.getString(R.string.resolver_turn_on_work_apps));
return new WorkProfileOffEmptyState(title, (tab) -> {
tab.showSpinner();
diff --git a/java/src/com/android/intentresolver/flags/DeviceConfigProxy.kt b/java/src/com/android/intentresolver/flags/DeviceConfigProxy.kt
deleted file mode 100644
index d1494fe..0000000
--- a/java/src/com/android/intentresolver/flags/DeviceConfigProxy.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver.flags
-
-import android.provider.DeviceConfig
-import com.android.systemui.flags.ParcelableFlag
-
-internal class DeviceConfigProxy {
- fun isEnabled(flag: ParcelableFlag<Boolean>): Boolean? {
- return runCatching {
- val hasProperty = DeviceConfig.getProperty(flag.namespace, flag.name) != null
- if (hasProperty) {
- DeviceConfig.getBoolean(flag.namespace, flag.name, flag.default)
- } else {
- null
- }
- }.getOrDefault(null)
- }
-}
diff --git a/java/src/com/android/intentresolver/flags/Flags.kt b/java/src/com/android/intentresolver/flags/Flags.kt
deleted file mode 100644
index 2c20d34..0000000
--- a/java/src/com/android/intentresolver/flags/Flags.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver.flags
-
-import com.android.systemui.flags.ReleasedFlag
-import com.android.systemui.flags.UnreleasedFlag
-
-// Flag id, name and namespace should be kept in sync with [com.android.systemui.flags.Flags] to
-// make the flags available in the flag flipper app (see go/sysui-flags).
-// All flags added should be included in UnbundledChooserActivityTest.ALL_FLAGS.
-object Flags {
- private fun releasedFlag(name: String) = ReleasedFlag(name, "systemui")
-
- private fun unreleasedFlag(name: String, teamfood: Boolean = false) =
- UnreleasedFlag(name, "systemui", teamfood)
-}
diff --git a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
index fadea93..51d4e67 100644
--- a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
+++ b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
@@ -32,9 +32,12 @@ import android.view.animation.DecelerateInterpolator;
import android.widget.Space;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.FeatureFlags;
import com.android.intentresolver.R;
import com.android.intentresolver.ResolverListAdapter.ViewHolder;
import com.android.internal.annotations.VisibleForTesting;
@@ -107,6 +110,9 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
private final boolean mShouldShowContentPreview;
private final int mChooserWidthPixels;
private final int mChooserRowTextOptionTranslatePixelSize;
+ private final FeatureFlags mFeatureFlags;
+ @Nullable
+ private RecyclerView mRecyclerView;
private int mChooserTargetWidth = 0;
@@ -119,7 +125,8 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
ChooserActivityDelegate chooserActivityDelegate,
ChooserListAdapter wrappedAdapter,
boolean shouldShowContentPreview,
- int maxTargetsPerRow) {
+ int maxTargetsPerRow,
+ FeatureFlags featureFlags) {
super();
mChooserActivityDelegate = chooserActivityDelegate;
@@ -133,6 +140,7 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
mChooserWidthPixels = context.getResources().getDimensionPixelSize(R.dimen.chooser_width);
mChooserRowTextOptionTranslatePixelSize = context.getResources().getDimensionPixelSize(
R.dimen.chooser_row_text_option_translate);
+ mFeatureFlags = featureFlags;
wrappedAdapter.registerDataSetObserver(new DataSetObserver() {
@Override
@@ -149,6 +157,18 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
});
}
+ @Override
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+ if (mFeatureFlags.scrollablePreview()) {
+ mRecyclerView = recyclerView;
+ }
+ }
+
+ @Override
+ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+ mRecyclerView = null;
+ }
+
public void setFooterHeight(int height) {
mFooterHeight = height;
}
@@ -198,7 +218,8 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
public int getSystemRowCount() {
// For the tabbed case we show the sticky content preview above the tabs,
// please refer to shouldShowStickyContentPreview
- if (mChooserActivityDelegate.shouldShowTabs()) {
+ if (mChooserActivityDelegate.shouldShowTabs()
+ || mFeatureFlags.scrollablePreview()) {
return 0;
}
@@ -267,8 +288,9 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
+ getFooterRowCount();
}
+ @NonNull
@Override
- public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
switch (viewType) {
case VIEW_TYPE_CONTENT_PREVIEW:
return new ItemViewHolder(
@@ -304,7 +326,7 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
return new FooterViewHolder(sp, viewType);
default:
// Since we catch all possible viewTypes above, no chance this is being called.
- return null;
+ throw new IllegalStateException("unmatched view type");
}
}
@@ -318,6 +340,15 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
mAzLabelVisibility = isVisible;
int azRowPos = getAzLabelRowPosition();
if (azRowPos >= 0) {
+ if (mRecyclerView != null) {
+ for (int i = 0, size = mRecyclerView.getChildCount(); i < size; i++) {
+ View child = mRecyclerView.getChildAt(i);
+ if (mRecyclerView.getChildAdapterPosition(child) == azRowPos) {
+ child.setVisibility(isVisible ? View.VISIBLE : View.GONE);
+ }
+ }
+ return;
+ }
notifyItemChanged(azRowPos);
}
}
diff --git a/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt b/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt
index 0e4d020..054fbe7 100644
--- a/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt
+++ b/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt
@@ -18,7 +18,6 @@ package com.android.intentresolver.icons
import android.app.ActivityManager
import android.content.Context
-import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable
import android.os.AsyncTask
import android.os.UserHandle
@@ -95,7 +94,7 @@ class DefaultTargetDataLoader(
.executeOnExecutor(executor)
}
- override fun loadLabel(info: DisplayResolveInfo, callback: Consumer<Array<CharSequence?>>) {
+ override fun loadLabel(info: DisplayResolveInfo, callback: Consumer<LabelInfo>) {
val taskId = nextTaskId.getAndIncrement()
LoadLabelTask(context, info, isAudioCaptureDevice, presentationFactory) { result ->
removeTask(taskId)
@@ -105,8 +104,14 @@ class DefaultTargetDataLoader(
.executeOnExecutor(executor)
}
- override fun createPresentationGetter(info: ResolveInfo): TargetPresentationGetter =
- presentationFactory.makePresentationGetter(info)
+ override fun getOrLoadLabel(info: DisplayResolveInfo) {
+ if (!info.hasDisplayLabel()) {
+ val result =
+ LoadLabelTask.loadLabel(context, info, isAudioCaptureDevice, presentationFactory)
+ info.displayLabel = result.label
+ info.extendedInfo = result.subLabel
+ }
+ }
private fun addTask(id: Int, task: AsyncTask<*, *, *>) {
synchronized(activeTasks) { activeTasks.put(id, task) }
diff --git a/java/src/com/android/intentresolver/flags/FeatureFlagRepository.kt b/java/src/com/android/intentresolver/icons/LabelInfo.kt
index 5b5d769..a9c4cd7 100644
--- a/java/src/com/android/intentresolver/flags/FeatureFlagRepository.kt
+++ b/java/src/com/android/intentresolver/icons/LabelInfo.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 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.
@@ -14,12 +14,6 @@
* limitations under the License.
*/
-package com.android.intentresolver.flags
+package com.android.intentresolver.icons
-import com.android.systemui.flags.ReleasedFlag
-import com.android.systemui.flags.UnreleasedFlag
-
-interface FeatureFlagRepository {
- fun isEnabled(flag: UnreleasedFlag): Boolean
- fun isEnabled(flag: ReleasedFlag): Boolean
-}
+class LabelInfo(val label: CharSequence?, val subLabel: CharSequence?)
diff --git a/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java b/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java
index 6aee69b..0f135d6 100644
--- a/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java
+++ b/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java
@@ -16,7 +16,6 @@
package com.android.intentresolver.icons;
-import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ActivityInfo;
@@ -30,6 +29,7 @@ import android.graphics.drawable.Icon;
import android.os.Trace;
import android.util.Log;
+import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.android.intentresolver.SimpleIconFactory;
diff --git a/java/src/com/android/intentresolver/icons/LoadLabelTask.java b/java/src/com/android/intentresolver/icons/LoadLabelTask.java
index a0867b8..6d443f7 100644
--- a/java/src/com/android/intentresolver/icons/LoadLabelTask.java
+++ b/java/src/com/android/intentresolver/icons/LoadLabelTask.java
@@ -28,16 +28,16 @@ import com.android.intentresolver.chooser.DisplayResolveInfo;
import java.util.function.Consumer;
-class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> {
+class LoadLabelTask extends AsyncTask<Void, Void, LabelInfo> {
private final Context mContext;
private final DisplayResolveInfo mDisplayResolveInfo;
private final boolean mIsAudioCaptureDevice;
protected final TargetPresentationGetter.Factory mPresentationFactory;
- private final Consumer<CharSequence[]> mCallback;
+ private final Consumer<LabelInfo> mCallback;
LoadLabelTask(Context context, DisplayResolveInfo dri,
boolean isAudioCaptureDevice, TargetPresentationGetter.Factory presentationFactory,
- Consumer<CharSequence[]> callback) {
+ Consumer<LabelInfo> callback) {
mContext = context;
mDisplayResolveInfo = dri;
mIsAudioCaptureDevice = isAudioCaptureDevice;
@@ -46,49 +46,52 @@ class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> {
}
@Override
- protected CharSequence[] doInBackground(Void... voids) {
+ protected LabelInfo doInBackground(Void... voids) {
try {
Trace.beginSection("app-label");
- return loadLabel();
+ return loadLabel(
+ mContext, mDisplayResolveInfo, mIsAudioCaptureDevice, mPresentationFactory);
} finally {
Trace.endSection();
}
}
- private CharSequence[] loadLabel() {
- TargetPresentationGetter pg = mPresentationFactory.makePresentationGetter(
- mDisplayResolveInfo.getResolveInfo());
+ static LabelInfo loadLabel(
+ Context context,
+ DisplayResolveInfo displayResolveInfo,
+ boolean isAudioCaptureDevice,
+ TargetPresentationGetter.Factory presentationFactory) {
+ TargetPresentationGetter pg = presentationFactory.makePresentationGetter(
+ displayResolveInfo.getResolveInfo());
- if (mIsAudioCaptureDevice) {
+ if (isAudioCaptureDevice) {
// This is an audio capture device, so check record permissions
- ActivityInfo activityInfo = mDisplayResolveInfo.getResolveInfo().activityInfo;
+ ActivityInfo activityInfo = displayResolveInfo.getResolveInfo().activityInfo;
String packageName = activityInfo.packageName;
int uid = activityInfo.applicationInfo.uid;
boolean hasRecordPermission =
PermissionChecker.checkPermissionForPreflight(
- mContext,
+ context,
android.Manifest.permission.RECORD_AUDIO, -1, uid,
packageName)
== android.content.pm.PackageManager.PERMISSION_GRANTED;
if (!hasRecordPermission) {
// Doesn't have record permission, so warn the user
- return new CharSequence[]{
+ return new LabelInfo(
pg.getLabel(),
- mContext.getString(R.string.usb_device_resolve_prompt_warn)
- };
+ context.getString(R.string.usb_device_resolve_prompt_warn));
}
}
- return new CharSequence[]{
+ return new LabelInfo(
pg.getLabel(),
- pg.getSubLabel()
- };
+ pg.getSubLabel());
}
@Override
- protected void onPostExecute(CharSequence[] result) {
+ protected void onPostExecute(LabelInfo result) {
mCallback.accept(result);
}
}
diff --git a/java/src/com/android/intentresolver/icons/TargetDataLoader.kt b/java/src/com/android/intentresolver/icons/TargetDataLoader.kt
index 50f731f..07c6217 100644
--- a/java/src/com/android/intentresolver/icons/TargetDataLoader.kt
+++ b/java/src/com/android/intentresolver/icons/TargetDataLoader.kt
@@ -16,10 +16,8 @@
package com.android.intentresolver.icons
-import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable
import android.os.UserHandle
-import com.android.intentresolver.TargetPresentationGetter
import com.android.intentresolver.chooser.DisplayResolveInfo
import com.android.intentresolver.chooser.SelectableTargetInfo
import java.util.function.Consumer
@@ -41,10 +39,8 @@ abstract class TargetDataLoader {
)
/** Load target label */
- abstract fun loadLabel(info: DisplayResolveInfo, callback: Consumer<Array<CharSequence?>>)
+ abstract fun loadLabel(info: DisplayResolveInfo, callback: Consumer<LabelInfo>)
- /** Create a presentation getter to be used with a [DisplayResolveInfo] */
- // TODO: get rid of DisplayResolveInfo's dependency on the presentation getter and remove this
- // method.
- abstract fun createPresentationGetter(info: ResolveInfo): TargetPresentationGetter
+ /** Loads DisplayResolveInfo's display label synchronously, if needed */
+ abstract fun getOrLoadLabel(info: DisplayResolveInfo)
}
diff --git a/java/src/com/android/intentresolver/inject/ActivityModule.kt b/java/src/com/android/intentresolver/inject/ActivityModule.kt
new file mode 100644
index 0000000..21bfe4c
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/ActivityModule.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.inject
+
+import android.app.Activity
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityComponent
+import kotlinx.coroutines.CoroutineScope
+
+@Module
+@InstallIn(ActivityComponent::class)
+object ActivityModule {
+
+ @Provides
+ @ActivityOwned
+ fun lifecycle(activity: Activity): Lifecycle {
+ check(activity is LifecycleOwner) { "activity must implement LifecycleOwner" }
+ return activity.lifecycle
+ }
+
+ @Provides
+ @ActivityOwned
+ fun activityScope(activity: Activity): CoroutineScope {
+ check(activity is LifecycleOwner) { "activity must implement LifecycleOwner" }
+ return activity.lifecycleScope
+ }
+}
diff --git a/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt b/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt
new file mode 100644
index 0000000..e0f8e88
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.inject
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+
+@Module
+@InstallIn(SingletonComponent::class)
+object ConcurrencyModule {
+
+ @Provides @Main fun mainDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
+
+ /** Injectable alternative to [MainScope()][kotlinx.coroutines.MainScope] */
+ @Provides
+ @Singleton
+ @Main
+ fun mainCoroutineScope(@Main mainDispatcher: CoroutineDispatcher) =
+ CoroutineScope(SupervisorJob() + mainDispatcher)
+
+ @Provides @Background fun backgroundDispatcher(): CoroutineDispatcher = Dispatchers.IO
+}
diff --git a/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt b/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt
new file mode 100644
index 0000000..05cf210
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt
@@ -0,0 +1,15 @@
+package com.android.intentresolver.inject
+
+import com.android.intentresolver.FeatureFlags
+import com.android.intentresolver.FeatureFlagsImpl
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+object FeatureFlagsModule {
+
+ @Provides fun featureFlags(): FeatureFlags = FeatureFlagsImpl()
+}
diff --git a/java/src/com/android/intentresolver/inject/FrameworkModule.kt b/java/src/com/android/intentresolver/inject/FrameworkModule.kt
new file mode 100644
index 0000000..2f6cc6a
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/FrameworkModule.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.inject
+
+import android.app.ActivityManager
+import android.app.admin.DevicePolicyManager
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.pm.LauncherApps
+import android.content.pm.ShortcutManager
+import android.os.UserManager
+import android.view.WindowManager
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+
+private fun <T> Context.requireSystemService(serviceClass: Class<T>): T {
+ return checkNotNull(getSystemService(serviceClass))
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+object FrameworkModule {
+
+ @Provides
+ fun contentResolver(@ApplicationContext ctx: Context) =
+ requireNotNull(ctx.contentResolver) { "ContentResolver is expected but missing" }
+
+ @Provides
+ fun activityManager(@ApplicationContext ctx: Context) =
+ ctx.requireSystemService(ActivityManager::class.java)
+
+ @Provides
+ fun clipboardManager(@ApplicationContext ctx: Context) =
+ ctx.requireSystemService(ClipboardManager::class.java)
+
+ @Provides
+ fun devicePolicyManager(@ApplicationContext ctx: Context) =
+ ctx.requireSystemService(DevicePolicyManager::class.java)
+
+ @Provides
+ fun launcherApps(@ApplicationContext ctx: Context) =
+ ctx.requireSystemService(LauncherApps::class.java)
+
+ @Provides
+ fun packageManager(@ApplicationContext ctx: Context) =
+ requireNotNull(ctx.packageManager) { "PackageManager is expected but missing" }
+
+ @Provides
+ fun shortcutManager(@ApplicationContext ctx: Context) =
+ ctx.requireSystemService(ShortcutManager::class.java)
+
+ @Provides
+ fun userManager(@ApplicationContext ctx: Context) =
+ ctx.requireSystemService(UserManager::class.java)
+
+ @Provides
+ fun windowManager(@ApplicationContext ctx: Context) =
+ ctx.requireSystemService(WindowManager::class.java)
+}
diff --git a/java/src/com/android/intentresolver/inject/Qualifiers.kt b/java/src/com/android/intentresolver/inject/Qualifiers.kt
new file mode 100644
index 0000000..157e8f7
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/Qualifiers.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.inject
+
+import javax.inject.Qualifier
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ActivityOwned
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ApplicationOwned
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ApplicationUser
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ProfileParent
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Background
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Default
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Main
diff --git a/java/src/com/android/intentresolver/inject/SingletonModule.kt b/java/src/com/android/intentresolver/inject/SingletonModule.kt
new file mode 100644
index 0000000..e517800
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/SingletonModule.kt
@@ -0,0 +1,22 @@
+package com.android.intentresolver.inject
+
+import android.content.Context
+import com.android.intentresolver.logging.EventLogImpl
+import dagger.Module
+import dagger.Provides
+import dagger.Reusable
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@InstallIn(SingletonComponent::class)
+@Module
+object SingletonModule {
+ @Provides @Singleton fun instanceIdSequence() = EventLogImpl.newIdSequence()
+
+ @Provides
+ @Reusable
+ @ApplicationOwned
+ fun resources(@ApplicationContext context: Context) = context.resources
+}
diff --git a/java/src/com/android/intentresolver/logging/EventLog.kt b/java/src/com/android/intentresolver/logging/EventLog.kt
new file mode 100644
index 0000000..476bd4b
--- /dev/null
+++ b/java/src/com/android/intentresolver/logging/EventLog.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.logging
+
+import android.net.Uri
+import android.util.HashedStringCache
+
+/** Logs notable events during ShareSheet usage. */
+interface EventLog {
+
+ companion object {
+ const val SELECTION_TYPE_SERVICE = 1
+ const val SELECTION_TYPE_APP = 2
+ const val SELECTION_TYPE_STANDARD = 3
+ const val SELECTION_TYPE_COPY = 4
+ const val SELECTION_TYPE_NEARBY = 5
+ const val SELECTION_TYPE_EDIT = 6
+ const val SELECTION_TYPE_MODIFY_SHARE = 7
+ const val SELECTION_TYPE_CUSTOM_ACTION = 8
+ }
+
+ fun logChooserActivityShown(isWorkProfile: Boolean, targetMimeType: String?, systemCost: Long)
+
+ fun logShareStarted(
+ packageName: String?,
+ mimeType: String?,
+ appProvidedDirect: Int,
+ appProvidedApp: Int,
+ isWorkprofile: Boolean,
+ previewType: Int,
+ intent: String?,
+ customActionCount: Int,
+ modifyShareActionProvided: Boolean
+ )
+
+ fun logCustomActionSelected(positionPicked: Int)
+ fun logShareTargetSelected(
+ targetType: Int,
+ packageName: String?,
+ positionPicked: Int,
+ directTargetAlsoRanked: Int,
+ numCallerProvided: Int,
+ directTargetHashed: HashedStringCache.HashResult?,
+ isPinned: Boolean,
+ successfullySelected: Boolean,
+ selectionCost: Long
+ )
+
+ fun logDirectShareTargetReceived(category: Int, latency: Int)
+ fun logActionShareWithPreview(previewType: Int)
+ fun logActionSelected(targetType: Int)
+ fun logContentPreviewWarning(uri: Uri?)
+ fun logSharesheetTriggered()
+ fun logSharesheetAppLoadComplete()
+ fun logSharesheetDirectLoadComplete()
+ fun logSharesheetDirectLoadTimeout()
+ fun logSharesheetProfileChanged()
+ fun logSharesheetExpansionChanged(isCollapsed: Boolean)
+ fun logSharesheetAppShareRankingTimeout()
+ fun logSharesheetEmptyDirectShareRow()
+}
diff --git a/java/src/com/android/intentresolver/logging/EventLog.java b/java/src/com/android/intentresolver/logging/EventLogImpl.java
index b30e825..84029e7 100644
--- a/java/src/com/android/intentresolver/logging/EventLog.java
+++ b/java/src/com/android/intentresolver/logging/EventLogImpl.java
@@ -16,7 +16,6 @@
package com.android.intentresolver.logging;
-import android.annotation.Nullable;
import android.content.Intent;
import android.metrics.LogMaker;
import android.net.Uri;
@@ -24,6 +23,8 @@ import android.provider.MediaStore;
import android.util.HashedStringCache;
import android.util.Log;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.ChooserActivity;
import com.android.intentresolver.contentpreview.ContentPreviewType;
import com.android.internal.annotations.VisibleForTesting;
@@ -32,84 +33,42 @@ import com.android.internal.logging.InstanceIdSequence;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
-import com.android.internal.logging.UiEventLoggerImpl;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.FrameworkStatsLog;
+import javax.inject.Inject;
+
/**
* Helper for writing Sharesheet atoms to statsd log.
- * @hide
*/
-public class EventLog {
+public class EventLogImpl implements EventLog {
private static final String TAG = "ChooserActivity";
private static final boolean DEBUG = true;
- public static final int SELECTION_TYPE_SERVICE = 1;
- public static final int SELECTION_TYPE_APP = 2;
- public static final int SELECTION_TYPE_STANDARD = 3;
- public static final int SELECTION_TYPE_COPY = 4;
- public static final int SELECTION_TYPE_NEARBY = 5;
- public static final int SELECTION_TYPE_EDIT = 6;
- public static final int SELECTION_TYPE_MODIFY_SHARE = 7;
- public static final int SELECTION_TYPE_CUSTOM_ACTION = 8;
-
- /**
- * This shim is provided only for testing. In production, clients will only ever use a
- * {@link DefaultFrameworkStatsLogger}.
- */
- @VisibleForTesting
- interface FrameworkStatsLogger {
- /** Overload to use for logging {@code FrameworkStatsLog.SHARESHEET_STARTED}. */
- void write(
- int frameworkEventId,
- int appEventId,
- String packageName,
- int instanceId,
- String mimeType,
- int numAppProvidedDirectTargets,
- int numAppProvidedAppTargets,
- boolean isWorkProfile,
- int previewType,
- int intentType,
- int numCustomActions,
- boolean modifyShareActionProvided);
-
- /** Overload to use for logging {@code FrameworkStatsLog.RANKING_SELECTED}. */
- void write(
- int frameworkEventId,
- int appEventId,
- String packageName,
- int instanceId,
- int positionPicked,
- boolean isPinned);
- }
-
private static final int SHARESHEET_INSTANCE_ID_MAX = (1 << 13);
- // A small per-notification ID, used for statsd logging.
- // TODO: consider precomputing and storing as final.
- private static InstanceIdSequence sInstanceIdSequence;
- private InstanceId mInstanceId;
+ private final InstanceId mInstanceId;
private final UiEventLogger mUiEventLogger;
private final FrameworkStatsLogger mFrameworkStatsLogger;
private final MetricsLogger mMetricsLogger;
- public EventLog() {
- this(new UiEventLoggerImpl(), new DefaultFrameworkStatsLogger(), new MetricsLogger());
+ public static InstanceIdSequence newIdSequence() {
+ return new InstanceIdSequence(SHARESHEET_INSTANCE_ID_MAX);
}
- @VisibleForTesting
- EventLog(
- UiEventLogger uiEventLogger,
- FrameworkStatsLogger frameworkLogger,
- MetricsLogger metricsLogger) {
+ @Inject
+ public EventLogImpl(UiEventLogger uiEventLogger, FrameworkStatsLogger frameworkLogger,
+ MetricsLogger metricsLogger, InstanceId instanceId) {
mUiEventLogger = uiEventLogger;
mFrameworkStatsLogger = frameworkLogger;
mMetricsLogger = metricsLogger;
+ mInstanceId = instanceId;
}
+
/** Records metrics for the start time of the {@link ChooserActivity}. */
+ @Override
public void logChooserActivityShown(
boolean isWorkProfile, String targetMimeType, long systemCost) {
mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN)
@@ -120,6 +79,7 @@ public class EventLog {
}
/** Logs a UiEventReported event for the system sharesheet completing initial start-up. */
+ @Override
public void logShareStarted(
String packageName,
String mimeType,
@@ -133,7 +93,7 @@ public class EventLog {
mFrameworkStatsLogger.write(FrameworkStatsLog.SHARESHEET_STARTED,
/* event_id = 1 */ SharesheetStartedEvent.SHARE_STARTED.getId(),
/* package_name = 2 */ packageName,
- /* instance_id = 3 */ getInstanceId().getId(),
+ /* instance_id = 3 */ mInstanceId.getId(),
/* mime_type = 4 */ mimeType,
/* num_app_provided_direct_targets = 5 */ appProvidedDirect,
/* num_app_provided_app_targets = 6 */ appProvidedApp,
@@ -149,12 +109,13 @@ public class EventLog {
*
* @param positionPicked index of the custom action within the list of custom actions.
*/
+ @Override
public void logCustomActionSelected(int positionPicked) {
mFrameworkStatsLogger.write(FrameworkStatsLog.RANKING_SELECTED,
/* event_id = 1 */
SharesheetTargetSelectedEvent.SHARESHEET_CUSTOM_ACTION_SELECTED.getId(),
/* package_name = 2 */ null,
- /* instance_id = 3 */ getInstanceId().getId(),
+ /* instance_id = 3 */ mInstanceId.getId(),
/* position_picked = 4 */ positionPicked,
/* is_pinned = 5 */ false);
}
@@ -164,6 +125,7 @@ public class EventLog {
* TODO: document parameters and/or consider breaking up by targetType so we don't have to
* support an overly-generic signature.
*/
+ @Override
public void logShareTargetSelected(
int targetType,
String packageName,
@@ -177,7 +139,7 @@ public class EventLog {
mFrameworkStatsLogger.write(FrameworkStatsLog.RANKING_SELECTED,
/* event_id = 1 */ SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(),
/* package_name = 2 */ packageName,
- /* instance_id = 3 */ getInstanceId().getId(),
+ /* instance_id = 3 */ mInstanceId.getId(),
/* position_picked = 4 */ positionPicked,
/* is_pinned = 5 */ isPinned);
@@ -209,6 +171,7 @@ public class EventLog {
}
/** Log when direct share targets were received. */
+ @Override
public void logDirectShareTargetReceived(int category, int latency) {
mMetricsLogger.write(new LogMaker(category).setSubtype(latency));
}
@@ -217,12 +180,14 @@ public class EventLog {
* Log when we display a preview UI of the specified {@code previewType} as part of our
* Sharesheet session.
*/
+ @Override
public void logActionShareWithPreview(int previewType) {
mMetricsLogger.write(
new LogMaker(MetricsEvent.ACTION_SHARE_WITH_PREVIEW).setSubtype(previewType));
}
/** Log when the user selects an action button with the specified {@code targetType}. */
+ @Override
public void logActionSelected(int targetType) {
if (targetType == SELECTION_TYPE_COPY) {
LogMaker targetLogMaker = new LogMaker(
@@ -232,12 +197,13 @@ public class EventLog {
mFrameworkStatsLogger.write(FrameworkStatsLog.RANKING_SELECTED,
/* event_id = 1 */ SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(),
/* package_name = 2 */ "",
- /* instance_id = 3 */ getInstanceId().getId(),
+ /* instance_id = 3 */ mInstanceId.getId(),
/* position_picked = 4 */ -1,
/* is_pinned = 5 */ false);
}
/** Log a warning that we couldn't display the content preview from the supplied {@code uri}. */
+ @Override
public void logContentPreviewWarning(Uri uri) {
// The ContentResolver already logs the exception. Log something more informative.
Log.w(TAG, "Could not load (" + uri.toString() + ") thumbnail/name for preview. If "
@@ -248,55 +214,63 @@ public class EventLog {
}
/** Logs a UiEventReported event for the system sharesheet being triggered by the user. */
+ @Override
public void logSharesheetTriggered() {
- log(SharesheetStandardEvent.SHARESHEET_TRIGGERED, getInstanceId());
+ log(SharesheetStandardEvent.SHARESHEET_TRIGGERED, mInstanceId);
}
/** Logs a UiEventReported event for the system sharesheet completing loading app targets. */
+ @Override
public void logSharesheetAppLoadComplete() {
- log(SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE, getInstanceId());
+ log(SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE, mInstanceId);
}
/**
* Logs a UiEventReported event for the system sharesheet completing loading service targets.
*/
+ @Override
public void logSharesheetDirectLoadComplete() {
- log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_COMPLETE, getInstanceId());
+ log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_COMPLETE, mInstanceId);
}
/**
* Logs a UiEventReported event for the system sharesheet timing out loading service targets.
*/
+ @Override
public void logSharesheetDirectLoadTimeout() {
- log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_TIMEOUT, getInstanceId());
+ log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_TIMEOUT, mInstanceId);
}
/**
* Logs a UiEventReported event for the system sharesheet switching
* between work and main profile.
*/
+ @Override
public void logSharesheetProfileChanged() {
- log(SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED, getInstanceId());
+ log(SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED, mInstanceId);
}
/** Logs a UiEventReported event for the system sharesheet getting expanded or collapsed. */
+ @Override
public void logSharesheetExpansionChanged(boolean isCollapsed) {
log(isCollapsed ? SharesheetStandardEvent.SHARESHEET_COLLAPSED :
- SharesheetStandardEvent.SHARESHEET_EXPANDED, getInstanceId());
+ SharesheetStandardEvent.SHARESHEET_EXPANDED, mInstanceId);
}
/**
* Logs a UiEventReported event for the system sharesheet app share ranking timing out.
*/
+ @Override
public void logSharesheetAppShareRankingTimeout() {
- log(SharesheetStandardEvent.SHARESHEET_APP_SHARE_RANKING_TIMEOUT, getInstanceId());
+ log(SharesheetStandardEvent.SHARESHEET_APP_SHARE_RANKING_TIMEOUT, mInstanceId);
}
/**
* Logs a UiEventReported event for the system sharesheet when direct share row is empty.
*/
+ @Override
public void logSharesheetEmptyDirectShareRow() {
- log(SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW, getInstanceId());
+ log(SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW, mInstanceId);
}
/**
@@ -313,19 +287,6 @@ public class EventLog {
}
/**
- * @return A unique {@link InstanceId} to join across events recorded by this logger instance.
- */
- private InstanceId getInstanceId() {
- if (mInstanceId == null) {
- if (sInstanceIdSequence == null) {
- sInstanceIdSequence = new InstanceIdSequence(SHARESHEET_INSTANCE_ID_MAX);
- }
- mInstanceId = sInstanceIdSequence.newInstanceId();
- }
- return mInstanceId;
- }
-
- /**
* The UiEvent enums that this class can log.
*/
enum SharesheetStartedEvent implements UiEventLogger.UiEventEnum {
@@ -488,52 +449,4 @@ public class EventLog {
return 0;
}
}
-
- private static class DefaultFrameworkStatsLogger implements FrameworkStatsLogger {
- @Override
- public void write(
- int frameworkEventId,
- int appEventId,
- String packageName,
- int instanceId,
- String mimeType,
- int numAppProvidedDirectTargets,
- int numAppProvidedAppTargets,
- boolean isWorkProfile,
- int previewType,
- int intentType,
- int numCustomActions,
- boolean modifyShareActionProvided) {
- FrameworkStatsLog.write(
- frameworkEventId,
- /* event_id = 1 */ appEventId,
- /* package_name = 2 */ packageName,
- /* instance_id = 3 */ instanceId,
- /* mime_type = 4 */ mimeType,
- /* num_app_provided_direct_targets */ numAppProvidedDirectTargets,
- /* num_app_provided_app_targets */ numAppProvidedAppTargets,
- /* is_workprofile */ isWorkProfile,
- /* previewType = 8 */ previewType,
- /* intentType = 9 */ intentType,
- /* num_provided_custom_actions = 10 */ numCustomActions,
- /* modify_share_action_provided = 11 */ modifyShareActionProvided);
- }
-
- @Override
- public void write(
- int frameworkEventId,
- int appEventId,
- String packageName,
- int instanceId,
- int positionPicked,
- boolean isPinned) {
- FrameworkStatsLog.write(
- frameworkEventId,
- /* event_id = 1 */ appEventId,
- /* package_name = 2 */ packageName,
- /* instance_id = 3 */ instanceId,
- /* position_picked = 4 */ positionPicked,
- /* is_pinned = 5 */ isPinned);
- }
- }
}
diff --git a/java/src/com/android/intentresolver/logging/EventLogModule.kt b/java/src/com/android/intentresolver/logging/EventLogModule.kt
new file mode 100644
index 0000000..eba8ecc
--- /dev/null
+++ b/java/src/com/android/intentresolver/logging/EventLogModule.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.logging
+
+import com.android.internal.logging.InstanceId
+import com.android.internal.logging.InstanceIdSequence
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.UiEventLoggerImpl
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityComponent
+import dagger.hilt.android.scopes.ActivityScoped
+
+@Module
+@InstallIn(ActivityComponent::class)
+interface EventLogModule {
+
+ @Binds @ActivityScoped fun eventLog(value: EventLogImpl): EventLog
+
+ companion object {
+ @Provides
+ fun instanceId(sequence: InstanceIdSequence): InstanceId = sequence.newInstanceId()
+
+ @Provides fun uiEventLogger(): UiEventLogger = UiEventLoggerImpl()
+
+ @Provides fun frameworkLogger(): FrameworkStatsLogger = object : FrameworkStatsLogger {}
+
+ @Provides fun metricsLogger(): MetricsLogger = MetricsLogger()
+ }
+}
diff --git a/java/src/com/android/intentresolver/logging/FrameworkStatsLogger.kt b/java/src/com/android/intentresolver/logging/FrameworkStatsLogger.kt
new file mode 100644
index 0000000..6508d30
--- /dev/null
+++ b/java/src/com/android/intentresolver/logging/FrameworkStatsLogger.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.logging
+
+import com.android.internal.util.FrameworkStatsLog
+
+/** A documenting annotation for FrameworkStatsLog methods and their associated UiEvents. */
+internal annotation class ForUiEvent(vararg val uiEventId: Int)
+
+/** Isolates the specific method signatures to use for each of the logged UiEvents. */
+interface FrameworkStatsLogger {
+
+ @ForUiEvent(FrameworkStatsLog.SHARESHEET_STARTED)
+ fun write(
+ frameworkEventId: Int,
+ appEventId: Int,
+ packageName: String?,
+ instanceId: Int,
+ mimeType: String?,
+ numAppProvidedDirectTargets: Int,
+ numAppProvidedAppTargets: Int,
+ isWorkProfile: Boolean,
+ previewType: Int,
+ intentType: Int,
+ numCustomActions: Int,
+ modifyShareActionProvided: Boolean,
+ ) {
+ FrameworkStatsLog.write(
+ frameworkEventId, /* event_id = 1 */
+ appEventId, /* package_name = 2 */
+ packageName, /* instance_id = 3 */
+ instanceId, /* mime_type = 4 */
+ mimeType, /* num_app_provided_direct_targets */
+ numAppProvidedDirectTargets, /* num_app_provided_app_targets */
+ numAppProvidedAppTargets, /* is_workprofile */
+ isWorkProfile, /* previewType = 8 */
+ previewType, /* intentType = 9 */
+ intentType, /* num_provided_custom_actions = 10 */
+ numCustomActions, /* modify_share_action_provided = 11 */
+ modifyShareActionProvided
+ )
+ }
+
+ @ForUiEvent(FrameworkStatsLog.RANKING_SELECTED)
+ fun write(
+ frameworkEventId: Int,
+ appEventId: Int,
+ packageName: String?,
+ instanceId: Int,
+ positionPicked: Int,
+ isPinned: Boolean,
+ ) {
+ FrameworkStatsLog.write(
+ frameworkEventId, /* event_id = 1 */
+ appEventId, /* package_name = 2 */
+ packageName, /* instance_id = 3 */
+ instanceId, /* position_picked = 4 */
+ positionPicked, /* is_pinned = 5 */
+ isPinned
+ )
+ }
+}
diff --git a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
index ff2d6a0..724fa84 100644
--- a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
@@ -16,7 +16,6 @@
package com.android.intentresolver.model;
-import android.annotation.Nullable;
import android.app.usage.UsageStatsManager;
import android.content.ComponentName;
import android.content.Context;
@@ -30,10 +29,13 @@ import android.os.Message;
import android.os.UserHandle;
import android.util.Log;
-import com.android.intentresolver.logging.EventLog;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.ResolvedComponentInfo;
import com.android.intentresolver.ResolverActivity;
+import com.android.intentresolver.ResolverListController;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.logging.EventLog;
import java.text.Collator;
import java.util.ArrayList;
@@ -75,6 +77,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
private EventLog mEventLog;
protected final Handler mHandler = new Handler(Looper.getMainLooper()) {
+ @Override
public void handleMessage(Message msg) {
switch (msg.what) {
case RANKER_SERVICE_RESULT:
@@ -229,7 +232,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
* {@link ResolvedComponentInfo#getResolveInfoAt(int)} from the parameters of {@link
* #compare(ResolvedComponentInfo, ResolvedComponentInfo)}
*/
- abstract int compare(ResolveInfo lhs, ResolveInfo rhs);
+ public abstract int compare(ResolveInfo lhs, ResolveInfo rhs);
/**
* Computes features for each target. This will be called before calls to {@link
@@ -245,7 +248,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
}
/** Implementation of compute called after {@link #beforeCompute()}. */
- abstract void doCompute(List<ResolvedComponentInfo> targets);
+ public abstract void doCompute(List<ResolvedComponentInfo> targets);
/**
* Returns the score that was calculated for the corresponding {@link ResolvedComponentInfo}
@@ -254,12 +257,12 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
public abstract float getScore(TargetInfo targetInfo);
/** Handles result message sent to mHandler. */
- abstract void handleResultMessage(Message message);
+ public abstract void handleResultMessage(Message message);
/**
* Reports to UsageStats what was chosen.
*/
- public final void updateChooserCounts(String packageName, UserHandle user, String action) {
+ public void updateChooserCounts(String packageName, UserHandle user, String action) {
if (mUsmMap.containsKey(user)) {
mUsmMap.get(user).reportChooserSelection(
packageName,
diff --git a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
index 621ae30..0651d26 100644
--- a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
@@ -18,7 +18,6 @@ package com.android.intentresolver.model;
import static android.app.prediction.AppTargetEvent.ACTION_LAUNCH;
-import android.annotation.Nullable;
import android.app.prediction.AppPredictor;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
@@ -31,9 +30,12 @@ import android.os.Message;
import android.os.UserHandle;
import android.util.Log;
-import com.android.intentresolver.logging.EventLog;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.ResolvedComponentInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.logging.EventLog;
+import com.android.intentresolver.shortcuts.ScopedAppTargetListCallback;
import com.google.android.collect.Lists;
@@ -85,12 +87,12 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp
}
@Override
- int compare(ResolveInfo lhs, ResolveInfo rhs) {
+ public int compare(ResolveInfo lhs, ResolveInfo rhs) {
return mComparatorModel.getComparator().compare(lhs, rhs);
}
@Override
- void doCompute(List<ResolvedComponentInfo> targets) {
+ public void doCompute(List<ResolvedComponentInfo> targets) {
if (targets.isEmpty()) {
mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT);
return;
@@ -105,33 +107,44 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp
.setClassName(target.name.getClassName())
.build());
}
- mAppPredictor.sortTargets(appTargets, Executors.newSingleThreadExecutor(),
- sortedAppTargets -> {
- if (sortedAppTargets.isEmpty()) {
- Log.i(TAG, "AppPredictionService disabled. Using resolver.");
- // APS for chooser is disabled. Fallback to resolver.
- mResolverRankerService =
- new ResolverRankerServiceResolverComparator(
- mContext,
- mIntent,
- mReferrerPackage,
- () -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT),
- getEventLog(),
- mUser,
- mPromoteToFirst);
- mComparatorModel = buildUpdatedModel();
- mResolverRankerService.compute(targets);
- } else {
- Log.i(TAG, "AppPredictionService response received");
- // Skip sending to Handler which takes extra time to dispatch messages.
- handleResult(sortedAppTargets);
- }
- }
+ mAppPredictor.sortTargets(
+ appTargets,
+ Executors.newSingleThreadExecutor(),
+ new ScopedAppTargetListCallback(
+ mContext,
+ sortedAppTargets -> {
+ onAppTargetsSorted(targets, sortedAppTargets);
+ return kotlin.Unit.INSTANCE;
+ }).toConsumer()
);
}
+ private void onAppTargetsSorted(
+ List<ResolvedComponentInfo> targets, List<AppTarget> sortedAppTargets) {
+ if (sortedAppTargets.isEmpty()) {
+ Log.i(TAG, "AppPredictionService disabled. Using resolver.");
+ // APS for chooser is disabled. Fallback to resolver.
+ mResolverRankerService =
+ new ResolverRankerServiceResolverComparator(
+ mContext,
+ mIntent,
+ mReferrerPackage,
+ () -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT),
+ getEventLog(),
+ mUser,
+ mPromoteToFirst);
+ mComparatorModel = buildUpdatedModel();
+ mResolverRankerService.compute(targets);
+ } else {
+ Log.i(TAG, "AppPredictionService response received");
+ // Skip sending to Handler which takes extra time to dispatch
+ // messages.
+ handleResult(sortedAppTargets);
+ }
+ }
+
@Override
- void handleResultMessage(Message msg) {
+ public void handleResultMessage(Message msg) {
// Null value is okay if we have defaulted to the ResolverRankerService.
if (msg.what == RANKER_SERVICE_RESULT && msg.obj != null) {
final List<AppTarget> sortedAppTargets = (List<AppTarget>) msg.obj;
diff --git a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
index 7d47366..f380415 100644
--- a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
@@ -17,7 +17,6 @@
package com.android.intentresolver.model;
-import android.annotation.Nullable;
import android.app.usage.UsageStats;
import android.content.ComponentName;
import android.content.Context;
@@ -39,9 +38,11 @@ import android.service.resolver.ResolverRankerService;
import android.service.resolver.ResolverTarget;
import android.util.Log;
-import com.android.intentresolver.logging.EventLog;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.ResolvedComponentInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.logging.EventLog;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -101,9 +102,9 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom
* the userSpace provided by context.
*/
public ResolverRankerServiceResolverComparator(Context launchedFromContext, Intent intent,
- String referrerPackage, Runnable afterCompute,
- EventLog eventLog, UserHandle targetUserSpace,
- ComponentName promoteToFirst) {
+ String referrerPackage, Runnable afterCompute,
+ EventLog eventLog, UserHandle targetUserSpace,
+ ComponentName promoteToFirst) {
this(launchedFromContext, intent, referrerPackage, afterCompute, eventLog,
Lists.newArrayList(targetUserSpace), promoteToFirst);
}
@@ -117,9 +118,8 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom
* different from the userSpace provided by context.
*/
public ResolverRankerServiceResolverComparator(Context launchedFromContext, Intent intent,
- String referrerPackage, Runnable afterCompute,
- EventLog eventLog, List<UserHandle> targetUserSpaceList,
- @Nullable ComponentName promoteToFirst) {
+ String referrerPackage, Runnable afterCompute, EventLog eventLog,
+ List<UserHandle> targetUserSpaceList, @Nullable ComponentName promoteToFirst) {
super(launchedFromContext, intent, targetUserSpaceList, promoteToFirst);
mCollator = Collator.getInstance(
launchedFromContext.getResources().getConfiguration().locale);
diff --git a/java/src/com/android/intentresolver/shortcuts/ScopedAppTargetListCallback.kt b/java/src/com/android/intentresolver/shortcuts/ScopedAppTargetListCallback.kt
new file mode 100644
index 0000000..9606a6a
--- /dev/null
+++ b/java/src/com/android/intentresolver/shortcuts/ScopedAppTargetListCallback.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.shortcuts
+
+import android.app.prediction.AppPredictor
+import android.app.prediction.AppTarget
+import android.content.Context
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.coroutineScope
+import java.util.function.Consumer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.launch
+
+/**
+ * A memory leak workaround for b/290971946. Drops the references to the actual [callback] when the
+ * [scope] is cancelled allowing it to be garbage-collected (and only leaking this instance).
+ */
+class ScopedAppTargetListCallback(
+ scope: CoroutineScope?,
+ callback: (List<AppTarget>) -> Unit,
+) {
+
+ @Volatile private var callbackRef: ((List<AppTarget>) -> Unit)? = callback
+
+ constructor(
+ context: Context,
+ callback: (List<AppTarget>) -> Unit,
+ ) : this((context as? LifecycleOwner)?.lifecycle?.coroutineScope, callback)
+
+ init {
+ scope?.launch { awaitCancellation() }?.invokeOnCompletion { callbackRef = null }
+ }
+
+ private fun notifyCallback(result: List<AppTarget>) {
+ callbackRef?.invoke(result)
+ }
+
+ fun toConsumer(): Consumer<MutableList<AppTarget>?> =
+ Consumer<MutableList<AppTarget>?> { notifyCallback(it ?: emptyList()) }
+
+ fun toAppPredictorCallback(): AppPredictor.Callback =
+ AppPredictor.Callback { notifyCallback(it) }
+}
diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
index f05542e..a8b59fb 100644
--- a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
+++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
@@ -35,14 +35,13 @@ import androidx.annotation.MainThread
import androidx.annotation.OpenForTesting
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.coroutineScope
import com.android.intentresolver.chooser.DisplayResolveInfo
import com.android.intentresolver.measurements.Tracer
import com.android.intentresolver.measurements.runTracing
import java.util.concurrent.Executor
import java.util.function.Consumer
import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.BufferOverflow
@@ -50,6 +49,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
/**
@@ -58,14 +58,14 @@ import kotlinx.coroutines.launch
* A ShortcutLoader instance can be viewed as a per-profile singleton hot stream of shortcut
* updates. The shortcut loading is triggered in the constructor or by the [reset] method, the
* processing happens on the [dispatcher] and the result is delivered through the [callback] on the
- * default [lifecycle]'s dispatcher, the main thread.
+ * default [scope]'s dispatcher, the main thread.
*/
@OpenForTesting
open class ShortcutLoader
@VisibleForTesting
constructor(
private val context: Context,
- private val lifecycle: Lifecycle,
+ private val scope: CoroutineScope,
private val appPredictor: AppPredictorProxy?,
private val userHandle: UserHandle,
private val isPersonalProfile: Boolean,
@@ -75,7 +75,9 @@ constructor(
) {
private val shortcutToChooserTargetConverter = ShortcutToChooserTargetConverter()
private val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
- private val appPredictorCallback = AppPredictor.Callback { onAppPredictorCallback(it) }
+ private val appPredictorCallback =
+ ScopedAppTargetListCallback(scope) { onAppPredictorCallback(it) }.toAppPredictorCallback()
+
private val appTargetSource =
MutableSharedFlow<Array<DisplayResolveInfo>?>(
replay = 1,
@@ -84,19 +86,19 @@ constructor(
private val shortcutSource =
MutableSharedFlow<ShortcutData?>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val isDestroyed
- get() = !lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)
+ get() = !scope.isActive
@MainThread
constructor(
context: Context,
- lifecycle: Lifecycle,
+ scope: CoroutineScope,
appPredictor: AppPredictor?,
userHandle: UserHandle,
targetIntentFilter: IntentFilter?,
callback: Consumer<Result>
) : this(
context,
- lifecycle,
+ scope,
appPredictor?.let { AppPredictorProxy(it) },
userHandle,
userHandle == UserHandle.of(ActivityManager.getCurrentUser()),
@@ -107,7 +109,7 @@ constructor(
init {
appPredictor?.registerPredictionUpdates(dispatcher.asExecutor(), appPredictorCallback)
- lifecycle.coroutineScope
+ scope
.launch {
appTargetSource
.combine(shortcutSource) { appTargets, shortcutData ->
@@ -135,13 +137,13 @@ constructor(
reset()
}
- /** Clear application targets (see [updateAppTargets] and initiate shrtcuts loading. */
+ /** Clear application targets (see [updateAppTargets] and initiate shortcuts loading. */
@OpenForTesting
open fun reset() {
Log.d(TAG, "reset shortcut loader for user $userHandle")
appTargetSource.tryEmit(null)
shortcutSource.tryEmit(null)
- lifecycle.coroutineScope.launch(dispatcher) { loadShortcuts() }
+ scope.launch(dispatcher) { loadShortcuts() }
}
/**
diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java b/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java
index a37d655..3192994 100644
--- a/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java
+++ b/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java
@@ -16,8 +16,6 @@
package com.android.intentresolver.shortcuts;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.prediction.AppTarget;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
@@ -25,6 +23,9 @@ import android.content.pm.ShortcutManager;
import android.os.Bundle;
import android.service.chooser.ChooserTarget;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
diff --git a/java/src/com/android/intentresolver/v2/ActivityLogic.kt b/java/src/com/android/intentresolver/v2/ActivityLogic.kt
new file mode 100644
index 0000000..c81bed0
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ActivityLogic.kt
@@ -0,0 +1,156 @@
+package com.android.intentresolver.v2
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL
+import android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK
+import android.content.Intent
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.core.content.getSystemService
+import com.android.intentresolver.AnnotatedUserHandles
+import com.android.intentresolver.R
+import com.android.intentresolver.WorkProfileAvailabilityManager
+import com.android.intentresolver.icons.TargetDataLoader
+
+/**
+ * Logic for IntentResolver Activities. Anything that is not the same across activities (including
+ * test activities) should be in this interface. Expect there to be one implementation for each
+ * activity, including test activities, but all implementations should delegate to a
+ * CommonActivityLogic implementation.
+ */
+interface ActivityLogic : CommonActivityLogic {
+ /** The intent for the target. This will always come before additional targets, if any. */
+ val targetIntent: Intent
+ /** Whether the intent is for home. */
+ val resolvingHome: Boolean
+ /** Custom title to display. */
+ val title: CharSequence?
+ /** Resource ID for the title to display when there is no custom title. */
+ val defaultTitleResId: Int
+ /** Intents received to be processed. */
+ val initialIntents: List<Intent>?
+ /** Whether or not this activity supports choosing a default handler for the intent. */
+ val supportsAlwaysUseOption: Boolean
+ /** Fetches display info for processed candidates. */
+ val targetDataLoader: TargetDataLoader
+ /** The theme to use. */
+ val themeResId: Int
+ /**
+ * Message showing that intent is forwarded from managed profile to owner or other way around.
+ */
+ val profileSwitchMessage: String?
+ /** The intents for potential actual targets. [targetIntent] must be first. */
+ val payloadIntents: List<Intent>
+
+ /**
+ * Called after Activity superclass creation, but before any other onCreate logic is performed.
+ */
+ fun preInitialization()
+
+ /** Sets [profileSwitchMessage] to null */
+ fun clearProfileSwitchMessage()
+}
+
+/**
+ * Logic that is common to all IntentResolver activities. Anything that is the same across
+ * activities (including test activities), should live here.
+ */
+interface CommonActivityLogic {
+ /** The tag to use when logging. */
+ val tag: String
+ /** A reference to the activity owning, and used by, this logic. */
+ val activity: ComponentActivity
+ /** The name of the referring package. */
+ val referrerPackageName: String?
+ /** User manager system service. */
+ val userManager: UserManager
+ /** Device policy manager system service. */
+ val devicePolicyManager: DevicePolicyManager
+ /** Current [UserHandle]s retrievable by type. */
+ val annotatedUserHandles: AnnotatedUserHandles?
+ /** Monitors for changes to work profile availability. */
+ val workProfileAvailabilityManager: WorkProfileAvailabilityManager
+
+ /** Returns display message indicating intent forwarding or null if not intent forwarding. */
+ fun forwardMessageFor(intent: Intent): String?
+}
+
+/**
+ * Concrete implementation of the [CommonActivityLogic] interface meant to be delegated to by
+ * [ActivityLogic] implementations. Test implementations of [ActivityLogic] may need to create their
+ * own [CommonActivityLogic] implementation.
+ */
+class CommonActivityLogicImpl(
+ override val tag: String,
+ activityProvider: () -> ComponentActivity,
+ onWorkProfileStatusUpdated: () -> Unit,
+) : CommonActivityLogic {
+
+ override val activity: ComponentActivity by lazy { activityProvider() }
+
+ override val referrerPackageName: String? by lazy {
+ activity.referrer.let {
+ if (ANDROID_APP_URI_SCHEME == it?.scheme) {
+ it.host
+ } else {
+ null
+ }
+ }
+ }
+
+ override val userManager: UserManager by lazy { activity.getSystemService()!! }
+
+ override val devicePolicyManager: DevicePolicyManager by lazy { activity.getSystemService()!! }
+
+ override val annotatedUserHandles: AnnotatedUserHandles? by lazy {
+ try {
+ AnnotatedUserHandles.forShareActivity(activity)
+ } catch (e: SecurityException) {
+ Log.e(tag, "Request from UID without necessary permissions", e)
+ null
+ }
+ }
+
+ override val workProfileAvailabilityManager: WorkProfileAvailabilityManager by lazy {
+ WorkProfileAvailabilityManager(
+ userManager,
+ annotatedUserHandles?.workProfileUserHandle,
+ onWorkProfileStatusUpdated,
+ )
+ }
+
+ private val forwardToPersonalMessage: String? by lazy {
+ devicePolicyManager.resources.getString(FORWARD_INTENT_TO_PERSONAL) {
+ activity.getString(R.string.forward_intent_to_owner)
+ }
+ }
+
+ private val forwardToWorkMessage: String? by lazy {
+ devicePolicyManager.resources.getString(FORWARD_INTENT_TO_WORK) {
+ activity.getString(R.string.forward_intent_to_work)
+ }
+ }
+
+ override fun forwardMessageFor(intent: Intent): String? {
+ val contentUserHint = intent.contentUserHint
+ if (
+ contentUserHint != UserHandle.USER_CURRENT && contentUserHint != UserHandle.myUserId()
+ ) {
+ val originUserInfo = userManager.getUserInfo(contentUserHint)
+ val originIsManaged = originUserInfo?.isManagedProfile ?: false
+ val targetIsManaged = userManager.isManagedProfile
+ return when {
+ originIsManaged && !targetIsManaged -> forwardToPersonalMessage
+ !originIsManaged && targetIsManaged -> forwardToWorkMessage
+ else -> null
+ }
+ }
+ return null
+ }
+
+ companion object {
+ private const val ANDROID_APP_URI_SCHEME = "android-app"
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ChooserActionFactory.java b/java/src/com/android/intentresolver/v2/ChooserActionFactory.java
new file mode 100644
index 0000000..db84038
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ChooserActionFactory.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2;
+
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.service.chooser.ChooserAction;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.R;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
+import com.android.intentresolver.logging.EventLog;
+import com.android.intentresolver.widget.ActionRow;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.function.Consumer;
+
+/**
+ * Implementation of {@link ChooserContentPreviewUi.ActionFactory} specialized to the application
+ * requirements of Sharesheet / {@link ChooserActivity}.
+ */
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public final class ChooserActionFactory implements ChooserContentPreviewUi.ActionFactory {
+ /**
+ * Delegate interface to launch activities when the actions are selected.
+ */
+ public interface ActionActivityStarter {
+ /**
+ * Request an activity launch for the provided target. Implementations may choose to exit
+ * the current activity when the target is launched.
+ */
+ void safelyStartActivityAsPersonalProfileUser(TargetInfo info);
+
+ /**
+ * Request an activity launch for the provided target, optionally employing the specified
+ * shared element transition. Implementations may choose to exit the current activity when
+ * the target is launched.
+ */
+ default void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+ TargetInfo info, View sharedElement, String sharedElementName) {
+ safelyStartActivityAsPersonalProfileUser(info);
+ }
+ }
+
+ private static final String TAG = "ChooserActions";
+
+ private static final int URI_PERMISSION_INTENT_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
+
+ // Boolean extra used to inform the editor that it may want to customize the editing experience
+ // for the sharesheet editing flow.
+ private static final String EDIT_SOURCE = "edit_source";
+ private static final String EDIT_SOURCE_SHARESHEET = "sharesheet";
+
+ private static final String CHIP_LABEL_METADATA_KEY = "android.service.chooser.chip_label";
+ private static final String CHIP_ICON_METADATA_KEY = "android.service.chooser.chip_icon";
+
+ private static final String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image";
+
+ private final Context mContext;
+
+ @Nullable
+ private final Runnable mCopyButtonRunnable;
+ private final Runnable mEditButtonRunnable;
+ private final ImmutableList<ChooserAction> mCustomActions;
+ private final @Nullable ChooserAction mModifyShareAction;
+ private final Consumer<Boolean> mExcludeSharedTextAction;
+ private final Consumer</* @Nullable */ Integer> mFinishCallback;
+ private final EventLog mLog;
+
+ /**
+ * @param context
+ * @param imageEditor an explicit Activity to launch for editing images
+ * @param onUpdateSharedTextIsExcluded a delegate to be invoked when the "exclude shared text"
+ * setting is updated. The argument is whether the shared text is to be excluded.
+ * @param firstVisibleImageQuery a delegate that provides a reference to the first visible image
+ * View in the Sharesheet UI, if any, or null.
+ * @param activityStarter a delegate to launch activities when actions are selected.
+ * @param finishCallback a delegate to close the Sharesheet UI (e.g. because some action was
+ * completed).
+ */
+ public ChooserActionFactory(
+ Context context,
+ Intent targetIntent,
+ String referrerPackageName,
+ List<ChooserAction> chooserActions,
+ ChooserAction modifyShareAction,
+ Optional<ComponentName> imageEditor,
+ EventLog log,
+ Consumer<Boolean> onUpdateSharedTextIsExcluded,
+ Callable</* @Nullable */ View> firstVisibleImageQuery,
+ ActionActivityStarter activityStarter,
+ Consumer</* @Nullable */ Integer> finishCallback) {
+ this(
+ context,
+ makeCopyButtonRunnable(
+ context,
+ targetIntent,
+ referrerPackageName,
+ finishCallback,
+ log),
+ makeEditButtonRunnable(
+ getEditSharingTarget(
+ context,
+ targetIntent,
+ imageEditor),
+ firstVisibleImageQuery,
+ activityStarter,
+ log),
+ chooserActions,
+ modifyShareAction,
+ onUpdateSharedTextIsExcluded,
+ log,
+ finishCallback);
+ }
+
+ @VisibleForTesting
+ ChooserActionFactory(
+ Context context,
+ @Nullable Runnable copyButtonRunnable,
+ Runnable editButtonRunnable,
+ List<ChooserAction> customActions,
+ @Nullable ChooserAction modifyShareAction,
+ Consumer<Boolean> onUpdateSharedTextIsExcluded,
+ EventLog log,
+ Consumer</* @Nullable */ Integer> finishCallback) {
+ mContext = context;
+ mCopyButtonRunnable = copyButtonRunnable;
+ mEditButtonRunnable = editButtonRunnable;
+ mCustomActions = ImmutableList.copyOf(customActions);
+ mModifyShareAction = modifyShareAction;
+ mExcludeSharedTextAction = onUpdateSharedTextIsExcluded;
+ mLog = log;
+ mFinishCallback = finishCallback;
+ }
+
+ @Override
+ @Nullable
+ public Runnable getEditButtonRunnable() {
+ return mEditButtonRunnable;
+ }
+
+ @Override
+ @Nullable
+ public Runnable getCopyButtonRunnable() {
+ return mCopyButtonRunnable;
+ }
+
+ /** Create custom actions */
+ @Override
+ public List<ActionRow.Action> createCustomActions() {
+ List<ActionRow.Action> actions = new ArrayList<>();
+ for (int i = 0; i < mCustomActions.size(); i++) {
+ final int position = i;
+ ActionRow.Action actionRow = createCustomAction(
+ mContext,
+ mCustomActions.get(i),
+ mFinishCallback,
+ () -> {
+ mLog.logCustomActionSelected(position);
+ }
+ );
+ if (actionRow != null) {
+ actions.add(actionRow);
+ }
+ }
+ return actions;
+ }
+
+ /**
+ * Provides a share modification action, if any.
+ */
+ @Override
+ @Nullable
+ public ActionRow.Action getModifyShareAction() {
+ return createCustomAction(
+ mContext,
+ mModifyShareAction,
+ mFinishCallback,
+ () -> {
+ mLog.logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE);
+ });
+ }
+
+ /**
+ * <p>
+ * Creates an exclude-text action that can be called when the user changes shared text
+ * status in the Media + Text preview.
+ * </p>
+ * <p>
+ * <code>true</code> argument value indicates that the text should be excluded.
+ * </p>
+ */
+ @Override
+ public Consumer<Boolean> getExcludeSharedTextAction() {
+ return mExcludeSharedTextAction;
+ }
+
+ @Nullable
+ private static Runnable makeCopyButtonRunnable(
+ Context context,
+ Intent targetIntent,
+ String referrerPackageName,
+ Consumer<Integer> finishCallback,
+ EventLog log) {
+ final ClipData clipData;
+ try {
+ clipData = extractTextToCopy(targetIntent);
+ } catch (Throwable t) {
+ Log.e(TAG, "Failed to extract data to copy", t);
+ return null;
+ }
+ if (clipData == null) {
+ return null;
+ }
+ return () -> {
+ ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(
+ Context.CLIPBOARD_SERVICE);
+ clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName);
+
+ log.logActionSelected(EventLog.SELECTION_TYPE_COPY);
+ finishCallback.accept(Activity.RESULT_OK);
+ };
+ }
+
+ @Nullable
+ private static ClipData extractTextToCopy(Intent targetIntent) {
+ if (targetIntent == null) {
+ return null;
+ }
+
+ final String action = targetIntent.getAction();
+
+ ClipData clipData = null;
+ if (Intent.ACTION_SEND.equals(action)) {
+ String extraText = targetIntent.getStringExtra(Intent.EXTRA_TEXT);
+
+ if (extraText != null) {
+ clipData = ClipData.newPlainText(null, extraText);
+ } else {
+ Log.w(TAG, "No data available to copy to clipboard");
+ }
+ } else {
+ // expected to only be visible with ACTION_SEND (when a text is shared)
+ Log.d(TAG, "Action (" + action + ") not supported for copying to clipboard");
+ }
+ return clipData;
+ }
+
+ private static TargetInfo getEditSharingTarget(
+ Context context,
+ Intent originalIntent,
+ Optional<ComponentName> imageEditor) {
+
+ final Intent resolveIntent = new Intent(originalIntent);
+ // Retain only URI permission grant flags if present. Other flags may prevent the scene
+ // transition animation from running (i.e FLAG_ACTIVITY_NO_ANIMATION,
+ // FLAG_ACTIVITY_NEW_TASK, FLAG_ACTIVITY_NEW_DOCUMENT) but also not needed.
+ resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS);
+ imageEditor.ifPresent(resolveIntent::setComponent);
+ resolveIntent.setAction(Intent.ACTION_EDIT);
+ resolveIntent.putExtra(EDIT_SOURCE, EDIT_SOURCE_SHARESHEET);
+ String originalAction = originalIntent.getAction();
+ if (Intent.ACTION_SEND.equals(originalAction)) {
+ if (resolveIntent.getData() == null) {
+ Uri uri = resolveIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+ if (uri != null) {
+ String mimeType = context.getContentResolver().getType(uri);
+ resolveIntent.setDataAndType(uri, mimeType);
+ }
+ }
+ } else {
+ Log.e(TAG, originalAction + " is not supported.");
+ return null;
+ }
+ final ResolveInfo ri = context.getPackageManager().resolveActivity(
+ resolveIntent, PackageManager.GET_META_DATA);
+ if (ri == null || ri.activityInfo == null) {
+ Log.e(TAG, "Device-specified editor (" + imageEditor + ") not available");
+ return null;
+ }
+
+ final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent,
+ ri,
+ context.getString(R.string.screenshot_edit),
+ "",
+ resolveIntent);
+ dri.getDisplayIconHolder().setDisplayIcon(
+ context.getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));
+ return dri;
+ }
+
+ private static Runnable makeEditButtonRunnable(
+ TargetInfo editSharingTarget,
+ Callable</* @Nullable */ View> firstVisibleImageQuery,
+ ActionActivityStarter activityStarter,
+ EventLog log) {
+ return () -> {
+ // Log share completion via edit.
+ log.logActionSelected(EventLog.SELECTION_TYPE_EDIT);
+
+ View firstImageView = null;
+ try {
+ firstImageView = firstVisibleImageQuery.call();
+ } catch (Exception e) { /* ignore */ }
+ // Action bar is user-independent; always start as primary.
+ if (firstImageView == null) {
+ activityStarter.safelyStartActivityAsPersonalProfileUser(editSharingTarget);
+ } else {
+ activityStarter.safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+ editSharingTarget, firstImageView, IMAGE_EDITOR_SHARED_ELEMENT);
+ }
+ };
+ }
+
+ @Nullable
+ private static ActionRow.Action createCustomAction(
+ Context context,
+ ChooserAction action,
+ Consumer<Integer> finishCallback,
+ Runnable loggingRunnable) {
+ if (action == null || action.getAction() == null) {
+ return null;
+ }
+ Drawable icon = action.getIcon().loadDrawable(context);
+ if (icon == null && TextUtils.isEmpty(action.getLabel())) {
+ return null;
+ }
+ return new ActionRow.Action(
+ action.getLabel(),
+ icon,
+ () -> {
+ try {
+ action.getAction().send(
+ null,
+ 0,
+ null,
+ null,
+ null,
+ null,
+ ActivityOptions.makeCustomAnimation(
+ context,
+ R.anim.slide_in_right,
+ R.anim.slide_out_left)
+ .toBundle());
+ } catch (PendingIntent.CanceledException e) {
+ Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled");
+ }
+ if (loggingRunnable != null) {
+ loggingRunnable.run();
+ }
+ finishCallback.accept(Activity.RESULT_OK);
+ }
+ );
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java
new file mode 100644
index 0000000..7081264
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java
@@ -0,0 +1,1845 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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.intentresolver.v2;
+
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
+import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
+import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
+
+import static androidx.lifecycle.LifecycleKt.getCoroutineScope;
+
+import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
+
+import static java.util.Objects.requireNonNull;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.app.prediction.AppTargetId;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.content.res.Configuration;
+import android.database.Cursor;
+import android.graphics.Insets;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.service.chooser.ChooserTarget;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.widget.TextView;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.viewpager.widget.ViewPager;
+
+import com.android.intentresolver.AnnotatedUserHandles;
+import com.android.intentresolver.ChooserGridLayoutManager;
+import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.ChooserRefinementManager;
+import com.android.intentresolver.ChooserRequestParameters;
+import com.android.intentresolver.ChooserStackedAppDialogFragment;
+import com.android.intentresolver.ChooserTargetActionsDialogFragment;
+import com.android.intentresolver.EnterTransitionAnimationDelegate;
+import com.android.intentresolver.FeatureFlags;
+import com.android.intentresolver.IntentForwarderActivity;
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.ResolverListController;
+import com.android.intentresolver.ResolverViewPager;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.contentpreview.BasePreviewViewModel;
+import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
+import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl;
+import com.android.intentresolver.contentpreview.PreviewViewModel;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.grid.ChooserGridAdapter;
+import com.android.intentresolver.icons.TargetDataLoader;
+import com.android.intentresolver.logging.EventLog;
+import com.android.intentresolver.measurements.Tracer;
+import com.android.intentresolver.model.AbstractResolverComparator;
+import com.android.intentresolver.model.AppPredictionServiceResolverComparator;
+import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
+import com.android.intentresolver.shortcuts.AppPredictorFactory;
+import com.android.intentresolver.shortcuts.ShortcutLoader;
+import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider;
+import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
+import com.android.intentresolver.v2.platform.ImageEditor;
+import com.android.intentresolver.v2.platform.NearbyShare;
+import com.android.intentresolver.widget.ImagePreviewView;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+import kotlin.Unit;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+
+/**
+ * The Chooser Activity handles intent resolution specifically for sharing intents -
+ * for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}.
+ *
+ */
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+@AndroidEntryPoint(ResolverActivity.class)
+public class ChooserActivity extends Hilt_ChooserActivity implements
+ ResolverListAdapter.ResolverListCommunicator {
+ private static final String TAG = "ChooserActivity";
+
+ /**
+ * Boolean extra to change the following behavior: Normally, ChooserActivity finishes itself
+ * in onStop when launched in a new task. If this extra is set to true, we do not finish
+ * ourselves when onStop gets called.
+ */
+ public static final String EXTRA_PRIVATE_RETAIN_IN_ON_STOP
+ = "com.android.internal.app.ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP";
+
+ /**
+ * Transition name for the first image preview.
+ * To be used for shared element transition into this activity.
+ * @hide
+ */
+ public static final String FIRST_IMAGE_PREVIEW_TRANSITION_NAME = "screenshot_preview_image";
+
+ private static final boolean DEBUG = true;
+
+ public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share";
+ private static final String SHORTCUT_TARGET = "shortcut_target";
+
+ // TODO: these data structures are for one-time use in shuttling data from where they're
+ // populated in `ShortcutToChooserTargetConverter` to where they're consumed in
+ // `ShortcutSelectionLogic` which packs the appropriate elements into the final `TargetInfo`.
+ // That flow should be refactored so that `ChooserActivity` isn't responsible for holding their
+ // intermediate data, and then these members can be removed.
+ private final Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache = new HashMap<>();
+ private final Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache = new HashMap<>();
+
+ private static final int TARGET_TYPE_DEFAULT = 0;
+ private static final int TARGET_TYPE_CHOOSER_TARGET = 1;
+ private static final int TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER = 2;
+ private static final int TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE = 3;
+
+ private static final int SCROLL_STATUS_IDLE = 0;
+ private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1;
+ private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2;
+
+ @Inject public FeatureFlags mFeatureFlags;
+ @Inject public EventLog mEventLog;
+ @Inject @ImageEditor public Optional<ComponentName> mImageEditor;
+ @Inject @NearbyShare public Optional<ComponentName> mNearbyShare;
+ @Inject public TargetDataLoader mTargetDataLoader;
+
+ private ChooserRefinementManager mRefinementManager;
+
+ private ChooserContentPreviewUi mChooserContentPreviewUi;
+
+ private boolean mShouldDisplayLandscape;
+ private long mChooserShownTime;
+ protected boolean mIsSuccessfullySelected;
+
+ private int mCurrAvailableWidth = 0;
+ private Insets mLastAppliedInsets = null;
+ private int mLastNumberOfChildren = -1;
+ private int mMaxTargetsPerRow = 1;
+
+ private static final int MAX_LOG_RANK_POSITION = 12;
+
+ // TODO: are these used anywhere? They should probably be migrated to ChooserRequestParameters.
+ private static final int MAX_EXTRA_INITIAL_INTENTS = 2;
+ private static final int MAX_EXTRA_CHOOSER_TARGETS = 2;
+
+ private SharedPreferences mPinnedSharedPrefs;
+ private static final String PINNED_SHARED_PREFS_NAME = "chooser_pin_settings";
+
+ private final ExecutorService mBackgroundThreadPoolExecutor = Executors.newFixedThreadPool(5);
+
+ private int mScrollStatus = SCROLL_STATUS_IDLE;
+
+ @VisibleForTesting
+ protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter;
+ private final EnterTransitionAnimationDelegate mEnterTransitionAnimationDelegate =
+ new EnterTransitionAnimationDelegate(this, () -> mResolverDrawerLayout);
+
+ private View mContentView = null;
+
+ private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>();
+
+ private boolean mExcludeSharedText = false;
+ /**
+ * When we intend to finish the activity with a shared element transition, we can't immediately
+ * finish() when the transition is invoked, as the receiving end may not be able to start the
+ * animation and the UI breaks if this takes too long. Instead we defer finishing until onStop
+ * in order to wait for the transition to begin.
+ */
+ private boolean mFinishWhenStopped = false;
+
+ private final AtomicLong mIntentReceivedTime = new AtomicLong(-1);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Tracer.INSTANCE.markLaunched();
+ super.onCreate(savedInstanceState);
+ setLogic(new ChooserActivityLogic(
+ TAG,
+ () -> this,
+ this::onWorkProfileStatusUpdated,
+ () -> mTargetDataLoader,
+ this::onPreinitialization));
+ addInitializer(this::init);
+ }
+
+ private void init() {
+ if (getChooserRequest() == null) {
+ finish();
+ return;
+ }
+ if (isFinishing()) {
+ // Performing a clean exit:
+ // Skip initializing any additional resources.
+ return;
+ }
+ setTheme(mLogic.getThemeResId());
+
+ getEventLog().logSharesheetTriggered();
+
+ mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class);
+
+ mRefinementManager.getRefinementCompletion().observe(this, completion -> {
+ if (completion.consume()) {
+ TargetInfo targetInfo = completion.getTargetInfo();
+ // targetInfo is non-null if the refinement process was successful.
+ if (targetInfo != null) {
+ maybeRemoveSharedText(targetInfo);
+
+ // We already block suspended targets from going to refinement, and we probably
+ // can't recover a Chooser session if that's the reason the refined target fails
+ // to launch now. Fire-and-forget the refined launch; ignore the return value
+ // and just make sure the Sharesheet session gets cleaned up regardless.
+ ChooserActivity.super.onTargetSelected(targetInfo, false);
+ }
+
+ finish();
+ }
+ });
+
+ BasePreviewViewModel previewViewModel =
+ new ViewModelProvider(this, createPreviewViewModelFactory())
+ .get(BasePreviewViewModel.class);
+ ChooserRequestParameters chooserRequest = requireChooserRequest();
+ mChooserContentPreviewUi = new ChooserContentPreviewUi(
+ getCoroutineScope(getLifecycle()),
+ previewViewModel.createOrReuseProvider(chooserRequest.getTargetIntent()),
+ chooserRequest.getTargetIntent(),
+ previewViewModel.createOrReuseImageLoader(),
+ createChooserActionFactory(),
+ mEnterTransitionAnimationDelegate,
+ new HeadlineGeneratorImpl(this));
+
+ updateStickyContentPreview();
+ if (shouldShowStickyContentPreview()
+ || mChooserMultiProfilePagerAdapter
+ .getCurrentRootAdapter().getSystemRowCount() != 0) {
+ getEventLog().logActionShareWithPreview(
+ mChooserContentPreviewUi.getPreferredContentPreview());
+ }
+
+ mChooserShownTime = System.currentTimeMillis();
+ final long systemCost = mChooserShownTime - mIntentReceivedTime.get();
+ getEventLog().logChooserActivityShown(
+ isWorkProfile(), chooserRequest.getTargetType(), systemCost);
+
+ if (mResolverDrawerLayout != null) {
+ mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange);
+
+ mResolverDrawerLayout.setOnCollapsedChangedListener(
+ isCollapsed -> {
+ mChooserMultiProfilePagerAdapter.setIsCollapsed(isCollapsed);
+ getEventLog().logSharesheetExpansionChanged(isCollapsed);
+ });
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "System Time Cost is " + systemCost);
+ }
+
+ getEventLog().logShareStarted(
+ mLogic.getReferrerPackageName(),
+ chooserRequest.getTargetType(),
+ chooserRequest.getCallerChooserTargets().size(),
+ (chooserRequest.getInitialIntents() == null)
+ ? 0 : chooserRequest.getInitialIntents().length,
+ isWorkProfile(),
+ mChooserContentPreviewUi.getPreferredContentPreview(),
+ chooserRequest.getTargetAction(),
+ chooserRequest.getChooserActions().size(),
+ chooserRequest.getModifyShareAction() != null
+ );
+
+ mEnterTransitionAnimationDelegate.postponeTransition();
+ }
+
+ protected final Unit onPreinitialization() {
+ mIntentReceivedTime.set(System.currentTimeMillis());
+ mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
+
+ mPinnedSharedPrefs = getPinnedSharedPrefs(this);
+ mMaxTargetsPerRow =
+ getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
+ mShouldDisplayLandscape =
+ shouldDisplayLandscape(getResources().getConfiguration().orientation);
+
+
+ ChooserRequestParameters chooserRequest = getChooserRequest();
+ if (chooserRequest == null) {
+ return Unit.INSTANCE;
+ }
+ setRetainInOnStop(chooserRequest.shouldRetainInOnStop());
+
+ createProfileRecords(
+ new AppPredictorFactory(
+ this,
+ chooserRequest.getSharedText(),
+ chooserRequest.getTargetIntentFilter()
+ ),
+ chooserRequest.getTargetIntentFilter()
+ );
+ return Unit.INSTANCE;
+ }
+
+ @Nullable
+ private ChooserRequestParameters getChooserRequest() {
+ return ((ChooserActivityLogic) mLogic).getChooserRequestParameters();
+ }
+
+ private ChooserRequestParameters requireChooserRequest() {
+ return requireNonNull(getChooserRequest());
+ }
+
+ private AnnotatedUserHandles requireAnnotatedUserHandles() {
+ return requireNonNull(mLogic.getAnnotatedUserHandles());
+ }
+
+ private void createProfileRecords(
+ AppPredictorFactory factory, IntentFilter targetIntentFilter) {
+ UserHandle mainUserHandle = requireAnnotatedUserHandles().personalProfileUserHandle;
+ ProfileRecord record = createProfileRecord(mainUserHandle, targetIntentFilter, factory);
+ if (record.shortcutLoader == null) {
+ Tracer.INSTANCE.endLaunchToShortcutTrace();
+ }
+
+ UserHandle workUserHandle = requireAnnotatedUserHandles().workProfileUserHandle;
+ if (workUserHandle != null) {
+ createProfileRecord(workUserHandle, targetIntentFilter, factory);
+ }
+ }
+
+ private ProfileRecord createProfileRecord(
+ UserHandle userHandle, IntentFilter targetIntentFilter, AppPredictorFactory factory) {
+ AppPredictor appPredictor = factory.create(userHandle);
+ ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic()
+ ? null
+ : createShortcutLoader(
+ this,
+ appPredictor,
+ userHandle,
+ targetIntentFilter,
+ shortcutsResult -> onShortcutsLoaded(userHandle, shortcutsResult));
+ ProfileRecord record = new ProfileRecord(appPredictor, shortcutLoader);
+ mProfileRecords.put(userHandle.getIdentifier(), record);
+ return record;
+ }
+
+ @Nullable
+ private ProfileRecord getProfileRecord(UserHandle userHandle) {
+ return mProfileRecords.get(userHandle.getIdentifier(), null);
+ }
+
+ @VisibleForTesting
+ protected ShortcutLoader createShortcutLoader(
+ Context context,
+ AppPredictor appPredictor,
+ UserHandle userHandle,
+ IntentFilter targetIntentFilter,
+ Consumer<ShortcutLoader.Result> callback) {
+ return new ShortcutLoader(
+ context,
+ getCoroutineScope(getLifecycle()),
+ appPredictor,
+ userHandle,
+ targetIntentFilter,
+ callback);
+ }
+
+ static SharedPreferences getPinnedSharedPrefs(Context context) {
+ return context.getSharedPreferences(PINNED_SHARED_PREFS_NAME, MODE_PRIVATE);
+ }
+
+ @Override
+ protected ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter(
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ if (shouldShowTabs()) {
+ mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForTwoProfiles(
+ initialIntents, rList, filterLastUsed, targetDataLoader);
+ } else {
+ mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForOneProfile(
+ initialIntents, rList, filterLastUsed, targetDataLoader);
+ }
+ return mChooserMultiProfilePagerAdapter;
+ }
+
+ @Override
+ protected EmptyStateProvider createBlockerEmptyStateProvider() {
+ final boolean isSendAction = requireChooserRequest().isSendActionTarget();
+
+ final EmptyState noWorkToPersonalEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
+ /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
+ /* devicePolicyStringSubtitleId= */
+ isSendAction ? RESOLVER_CANT_SHARE_WITH_PERSONAL : RESOLVER_CANT_ACCESS_PERSONAL,
+ /* defaultSubtitleResource= */
+ isSendAction ? R.string.resolver_cant_share_with_personal_apps_explanation
+ : R.string.resolver_cant_access_personal_apps_explanation,
+ /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL,
+ /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER);
+
+ final EmptyState noPersonalToWorkEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
+ /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
+ /* devicePolicyStringSubtitleId= */
+ isSendAction ? RESOLVER_CANT_SHARE_WITH_WORK : RESOLVER_CANT_ACCESS_WORK,
+ /* defaultSubtitleResource= */
+ isSendAction ? R.string.resolver_cant_share_with_work_apps_explanation
+ : R.string.resolver_cant_access_work_apps_explanation,
+ /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK,
+ /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER);
+
+ return new NoCrossProfileEmptyStateProvider(
+ requireAnnotatedUserHandles().personalProfileUserHandle,
+ noWorkToPersonalEmptyState,
+ noPersonalToWorkEmptyState,
+ createCrossProfileIntentsChecker(),
+ requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
+ }
+
+ private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile(
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ ChooserGridAdapter adapter = createChooserGridAdapter(
+ /* context */ this,
+ mLogic.getPayloadIntents(),
+ initialIntents,
+ rList,
+ filterLastUsed,
+ /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle,
+ targetDataLoader);
+ return new ChooserMultiProfilePagerAdapter(
+ /* context */ this,
+ adapter,
+ createEmptyStateProvider(/* workProfileUserHandle= */ null),
+ /* workProfileQuietModeChecker= */ () -> false,
+ /* workProfileUserHandle= */ null,
+ requireAnnotatedUserHandles().cloneProfileUserHandle,
+ mMaxTargetsPerRow,
+ mFeatureFlags);
+ }
+
+ private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles(
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ int selectedProfile = findSelectedProfile();
+ ChooserGridAdapter personalAdapter = createChooserGridAdapter(
+ /* context */ this,
+ mLogic.getPayloadIntents(),
+ selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
+ rList,
+ filterLastUsed,
+ /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle,
+ targetDataLoader);
+ ChooserGridAdapter workAdapter = createChooserGridAdapter(
+ /* context */ this,
+ mLogic.getPayloadIntents(),
+ selectedProfile == PROFILE_WORK ? initialIntents : null,
+ rList,
+ filterLastUsed,
+ /* userHandle */ requireAnnotatedUserHandles().workProfileUserHandle,
+ targetDataLoader);
+ return new ChooserMultiProfilePagerAdapter(
+ /* context */ this,
+ personalAdapter,
+ workAdapter,
+ createEmptyStateProvider(requireAnnotatedUserHandles().workProfileUserHandle),
+ () -> mLogic.getWorkProfileAvailabilityManager().isQuietModeEnabled(),
+ selectedProfile,
+ requireAnnotatedUserHandles().workProfileUserHandle,
+ requireAnnotatedUserHandles().cloneProfileUserHandle,
+ mMaxTargetsPerRow,
+ mFeatureFlags);
+ }
+
+ private int findSelectedProfile() {
+ int selectedProfile = getSelectedProfileExtra();
+ if (selectedProfile == -1) {
+ selectedProfile = getProfileForUser(
+ requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
+ }
+ return selectedProfile;
+ }
+
+ /**
+ * Check if the profile currently used is a work profile.
+ * @return true if it is work profile, false if it is parent profile (or no work profile is
+ * set up)
+ */
+ protected boolean isWorkProfile() {
+ return getSystemService(UserManager.class)
+ .getUserInfo(UserHandle.myUserId()).isManagedProfile();
+ }
+
+ @Override
+ protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) {
+ return new PackageMonitor() {
+ @Override
+ public void onSomePackagesChanged() {
+ handlePackagesChanged(listAdapter);
+ }
+ };
+ }
+
+ /**
+ * Update UI to reflect changes in data.
+ */
+ public void handlePackagesChanged() {
+ handlePackagesChanged(/* listAdapter */ null);
+ }
+
+ /**
+ * Update UI to reflect changes in data.
+ * <p>If {@code listAdapter} is {@code null}, both profile list adapters are updated if
+ * available.
+ */
+ private void handlePackagesChanged(@Nullable ResolverListAdapter listAdapter) {
+ // Refresh pinned items
+ mPinnedSharedPrefs = getPinnedSharedPrefs(this);
+ if (listAdapter == null) {
+ mChooserMultiProfilePagerAdapter.refreshPackagesInAllTabs();
+ } else {
+ listAdapter.handlePackagesChanged();
+ }
+ updateProfileViewButton();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.d(TAG, "onResume: " + getComponentName().flattenToShortString());
+ mFinishWhenStopped = false;
+ mRefinementManager.onActivityResume();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ if (viewPager.isLayoutRtl()) {
+ mMultiProfilePagerAdapter.setupViewPager(viewPager);
+ }
+
+ mShouldDisplayLandscape = shouldDisplayLandscape(newConfig.orientation);
+ mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
+ mChooserMultiProfilePagerAdapter.setMaxTargetsPerRow(mMaxTargetsPerRow);
+ adjustPreviewWidth(newConfig.orientation, null);
+ updateStickyContentPreview();
+ updateTabPadding();
+ }
+
+ private boolean shouldDisplayLandscape(int orientation) {
+ // Sharesheet fixes the # of items per row and therefore can not correctly lay out
+ // when in the restricted size of multi-window mode. In the future, would be nice
+ // to use minimum dp size requirements instead
+ return orientation == Configuration.ORIENTATION_LANDSCAPE && !isInMultiWindowMode();
+ }
+
+ private void adjustPreviewWidth(int orientation, View parent) {
+ int width = -1;
+ if (mShouldDisplayLandscape) {
+ width = getResources().getDimensionPixelSize(R.dimen.chooser_preview_width);
+ }
+
+ parent = parent == null ? getWindow().getDecorView() : parent;
+
+ updateLayoutWidth(com.android.internal.R.id.content_preview_file_layout, width, parent);
+ }
+
+ private void updateTabPadding() {
+ if (shouldShowTabs()) {
+ View tabs = findViewById(com.android.internal.R.id.tabs);
+ float iconSize = getResources().getDimension(R.dimen.chooser_icon_size);
+ // The entire width consists of icons or padding. Divide the item padding in half to get
+ // paddingHorizontal.
+ float padding = (tabs.getWidth() - mMaxTargetsPerRow * iconSize)
+ / mMaxTargetsPerRow / 2;
+ // Subtract the margin the buttons already have.
+ padding -= getResources().getDimension(R.dimen.resolver_profile_tab_margin);
+ tabs.setPadding((int) padding, 0, (int) padding, 0);
+ }
+ }
+
+ private void updateLayoutWidth(int layoutResourceId, int width, View parent) {
+ View view = parent.findViewById(layoutResourceId);
+ if (view != null && view.getLayoutParams() != null) {
+ LayoutParams params = view.getLayoutParams();
+ params.width = width;
+ view.setLayoutParams(params);
+ }
+ }
+
+ /**
+ * Create a view that will be shown in the content preview area
+ * @param parent reference to the parent container where the view should be attached to
+ * @return content preview view
+ */
+ protected ViewGroup createContentPreviewView(ViewGroup parent) {
+ ViewGroup layout = mChooserContentPreviewUi.displayContentPreview(
+ getResources(),
+ getLayoutInflater(),
+ parent,
+ mFeatureFlags.scrollablePreview()
+ ? findViewById(R.id.chooser_headline_row_container)
+ : null);
+
+ if (layout != null) {
+ adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
+ }
+
+ return layout;
+ }
+
+ @Nullable
+ private View getFirstVisibleImgPreviewView() {
+ View imagePreview = findViewById(R.id.scrollable_image_preview);
+ return imagePreview instanceof ImagePreviewView
+ ? ((ImagePreviewView) imagePreview).getTransitionView()
+ : null;
+ }
+
+ /**
+ * Wrapping the ContentResolver call to expose for easier mocking,
+ * and to avoid mocking Android core classes.
+ */
+ @VisibleForTesting
+ public Cursor queryResolver(ContentResolver resolver, Uri uri) {
+ return resolver.query(uri, null, null, null, null);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mRefinementManager != null) {
+ mRefinementManager.onActivityStop(isChangingConfigurations());
+ }
+
+ if (mFinishWhenStopped) {
+ mFinishWhenStopped = false;
+ finish();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ if (isFinishing()) {
+ mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET);
+ }
+
+ mBackgroundThreadPoolExecutor.shutdownNow();
+
+ destroyProfileRecords();
+ }
+
+ private void destroyProfileRecords() {
+ for (int i = 0; i < mProfileRecords.size(); ++i) {
+ mProfileRecords.valueAt(i).destroy();
+ }
+ mProfileRecords.clear();
+ }
+
+ @Override // ResolverListCommunicator
+ public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
+ ChooserRequestParameters chooserRequest = getChooserRequest();
+ if (chooserRequest == null) {
+ return defIntent;
+ }
+
+ Intent result = defIntent;
+ if (chooserRequest.getReplacementExtras() != null) {
+ final Bundle replExtras =
+ chooserRequest.getReplacementExtras().getBundle(aInfo.packageName);
+ if (replExtras != null) {
+ result = new Intent(defIntent);
+ result.putExtras(replExtras);
+ }
+ }
+ if (aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_PARENT)
+ || aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE)) {
+ result = Intent.createChooser(result,
+ getIntent().getCharSequenceExtra(Intent.EXTRA_TITLE));
+
+ // Don't auto-launch single intents if the intent is being forwarded. This is done
+ // because automatically launching a resolving application as a response to the user
+ // action of switching accounts is pretty unexpected.
+ result.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false);
+ }
+ return result;
+ }
+
+ @Override
+ public void onActivityStarted(TargetInfo cti) {
+ ChooserRequestParameters chooserRequest = requireChooserRequest();
+ if (chooserRequest.getChosenComponentSender() != null) {
+ final ComponentName target = cti.getResolvedComponentName();
+ if (target != null) {
+ final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target);
+ try {
+ chooserRequest.getChosenComponentSender().sendIntent(
+ this, Activity.RESULT_OK, fillIn, null, null);
+ } catch (IntentSender.SendIntentException e) {
+ Slog.e(TAG, "Unable to launch supplied IntentSender to report "
+ + "the chosen component: " + e);
+ }
+ }
+ }
+ }
+
+ private void addCallerChooserTargets() {
+ ChooserRequestParameters chooserRequest = requireChooserRequest();
+ if (!chooserRequest.getCallerChooserTargets().isEmpty()) {
+ // Send the caller's chooser targets only to the default profile.
+ UserHandle defaultUser = (findSelectedProfile() == PROFILE_WORK)
+ ? requireAnnotatedUserHandles().workProfileUserHandle
+ : requireAnnotatedUserHandles().personalProfileUserHandle;
+ if (mChooserMultiProfilePagerAdapter.getCurrentUserHandle() == defaultUser) {
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults(
+ /* origTarget */ null,
+ new ArrayList<>(chooserRequest.getCallerChooserTargets()),
+ TARGET_TYPE_DEFAULT,
+ /* directShareShortcutInfoCache */ Collections.emptyMap(),
+ /* directShareAppTargetCache */ Collections.emptyMap());
+ }
+ }
+ }
+
+ @Override
+ public int getLayoutResource() {
+ return mFeatureFlags.scrollablePreview()
+ ? R.layout.chooser_grid_scrollable_preview
+ : R.layout.chooser_grid;
+ }
+
+ @Override // ResolverListCommunicator
+ public boolean shouldGetActivityMetadata() {
+ return true;
+ }
+
+ @Override
+ public boolean shouldAutoLaunchSingleChoice(TargetInfo target) {
+ // Note that this is only safe because the Intent handled by the ChooserActivity is
+ // guaranteed to contain no extras unknown to the local ClassLoader. That is why this
+ // method can not be replaced in the ResolverActivity whole hog.
+ if (!super.shouldAutoLaunchSingleChoice(target)) {
+ return false;
+ }
+
+ return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true);
+ }
+
+ private void showTargetDetails(TargetInfo targetInfo) {
+ if (targetInfo == null) return;
+
+ List<DisplayResolveInfo> targetList = targetInfo.getAllDisplayTargets();
+ if (targetList.isEmpty()) {
+ Log.e(TAG, "No displayable data to show target details");
+ return;
+ }
+
+ // TODO: implement these type-conditioned behaviors polymorphically, and consider moving
+ // the logic into `ChooserTargetActionsDialogFragment.show()`.
+ boolean isShortcutPinned = targetInfo.isSelectableTargetInfo() && targetInfo.isPinned();
+ IntentFilter intentFilter = targetInfo.isSelectableTargetInfo()
+ ? requireChooserRequest().getTargetIntentFilter() : null;
+ String shortcutTitle = targetInfo.isSelectableTargetInfo()
+ ? targetInfo.getDisplayLabel().toString() : null;
+ String shortcutIdKey = targetInfo.getDirectShareShortcutId();
+
+ ChooserTargetActionsDialogFragment.show(
+ getSupportFragmentManager(),
+ targetList,
+ // Adding userHandle from ResolveInfo allows the app icon in Dialog Box to be
+ // resolved correctly within the same tab.
+ targetInfo.getResolveInfo().userHandle,
+ shortcutIdKey,
+ shortcutTitle,
+ isShortcutPinned,
+ intentFilter);
+ }
+
+ @Override
+ protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) {
+ if (mRefinementManager.maybeHandleSelection(
+ target,
+ requireChooserRequest().getRefinementIntentSender(),
+ getApplication(),
+ getMainThreadHandler())) {
+ return false;
+ }
+ updateModelAndChooserCounts(target);
+ maybeRemoveSharedText(target);
+ return super.onTargetSelected(target, alwaysCheck);
+ }
+
+ @Override
+ public void startSelected(int which, boolean always, boolean filtered) {
+ ChooserListAdapter currentListAdapter =
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter();
+ TargetInfo targetInfo = currentListAdapter
+ .targetInfoForPosition(which, filtered);
+ if (targetInfo != null && targetInfo.isNotSelectableTargetInfo()) {
+ return;
+ }
+
+ final long selectionCost = System.currentTimeMillis() - mChooserShownTime;
+
+ if ((targetInfo != null) && targetInfo.isMultiDisplayResolveInfo()) {
+ MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo;
+ if (!mti.hasSelected()) {
+ // Add userHandle based badge to the stackedAppDialogBox.
+ ChooserStackedAppDialogFragment.show(
+ getSupportFragmentManager(),
+ mti,
+ which,
+ targetInfo.getResolveInfo().userHandle);
+ return;
+ }
+ }
+
+ super.startSelected(which, always, filtered);
+
+ // TODO: both of the conditions around this switch logic *should* be redundant, and
+ // can be removed if certain invariants can be guaranteed. In particular, it seems
+ // like targetInfo (from `ChooserListAdapter.targetInfoForPosition()`) is *probably*
+ // expected to be null only at out-of-bounds indexes where `getPositionTargetType()`
+ // returns TARGET_BAD; then the switch falls through to a default no-op, and we don't
+ // need to null-check targetInfo. We only need the null check if it's possible that
+ // the ChooserListAdapter contains null elements "in the middle" of its list data,
+ // such that they're classified as belonging to one of the real target types. That
+ // should probably never happen. But why would this method ever be invoked with a
+ // null target at all? Even an out-of-bounds index should never be "selected"...
+ if ((currentListAdapter.getCount() > 0) && (targetInfo != null)) {
+ switch (currentListAdapter.getPositionTargetType(which)) {
+ case ChooserListAdapter.TARGET_SERVICE:
+ getEventLog().logShareTargetSelected(
+ EventLog.SELECTION_TYPE_SERVICE,
+ targetInfo.getResolveInfo().activityInfo.processName,
+ which,
+ /* directTargetAlsoRanked= */ getRankedPosition(targetInfo),
+ requireChooserRequest().getCallerChooserTargets().size(),
+ targetInfo.getHashedTargetIdForMetrics(this),
+ targetInfo.isPinned(),
+ mIsSuccessfullySelected,
+ selectionCost
+ );
+ return;
+ case ChooserListAdapter.TARGET_CALLER:
+ case ChooserListAdapter.TARGET_STANDARD:
+ getEventLog().logShareTargetSelected(
+ EventLog.SELECTION_TYPE_APP,
+ targetInfo.getResolveInfo().activityInfo.processName,
+ (which - currentListAdapter.getSurfacedTargetInfo().size()),
+ /* directTargetAlsoRanked= */ -1,
+ currentListAdapter.getCallerTargetCount(),
+ /* directTargetHashed= */ null,
+ targetInfo.isPinned(),
+ mIsSuccessfullySelected,
+ selectionCost
+ );
+ return;
+ case ChooserListAdapter.TARGET_STANDARD_AZ:
+ // A-Z targets are unranked standard targets; we use a value of -1 to mark that
+ // they are from the alphabetical pool.
+ // TODO: why do we log a different selection type if the -1 value already
+ // designates the same condition?
+ getEventLog().logShareTargetSelected(
+ EventLog.SELECTION_TYPE_STANDARD,
+ targetInfo.getResolveInfo().activityInfo.processName,
+ /* value= */ -1,
+ /* directTargetAlsoRanked= */ -1,
+ /* numCallerProvided= */ 0,
+ /* directTargetHashed= */ null,
+ /* isPinned= */ false,
+ mIsSuccessfullySelected,
+ selectionCost
+ );
+ return;
+ }
+ }
+ }
+
+ private int getRankedPosition(TargetInfo targetInfo) {
+ String targetPackageName =
+ targetInfo.getChooserTargetComponentName().getPackageName();
+ ChooserListAdapter currentListAdapter =
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter();
+ int maxRankedResults = Math.min(
+ currentListAdapter.getDisplayResolveInfoCount(), MAX_LOG_RANK_POSITION);
+
+ for (int i = 0; i < maxRankedResults; i++) {
+ if (currentListAdapter.getDisplayResolveInfo(i)
+ .getResolveInfo().activityInfo.packageName.equals(targetPackageName)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ protected boolean shouldAddFooterView() {
+ // To accommodate for window insets
+ return true;
+ }
+
+ @Override
+ protected void applyFooterView(int height) {
+ mChooserMultiProfilePagerAdapter.setFooterHeightInEveryAdapter(height);
+ }
+
+ private void logDirectShareTargetReceived(UserHandle forUser) {
+ ProfileRecord profileRecord = getProfileRecord(forUser);
+ if (profileRecord == null) {
+ return;
+ }
+ getEventLog().logDirectShareTargetReceived(
+ MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER,
+ (int) (SystemClock.elapsedRealtime() - profileRecord.loadingStartTime));
+ }
+
+ void updateModelAndChooserCounts(TargetInfo info) {
+ if (info != null && info.isMultiDisplayResolveInfo()) {
+ info = ((MultiDisplayResolveInfo) info).getSelectedTarget();
+ }
+ if (info != null) {
+ sendClickToAppPredictor(info);
+ final ResolveInfo ri = info.getResolveInfo();
+ Intent targetIntent = mLogic.getTargetIntent();
+ if (ri != null && ri.activityInfo != null && targetIntent != null) {
+ ChooserListAdapter currentListAdapter =
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter();
+ if (currentListAdapter != null) {
+ sendImpressionToAppPredictor(info, currentListAdapter);
+ currentListAdapter.updateModel(info);
+ currentListAdapter.updateChooserCounts(
+ ri.activityInfo.packageName,
+ targetIntent.getAction(),
+ ri.userHandle);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "ResolveInfo Package is " + ri.activityInfo.packageName);
+ Log.d(TAG, "Action to be updated is " + targetIntent.getAction());
+ }
+ } else if (DEBUG) {
+ Log.d(TAG, "Can not log Chooser Counts of null ResolveInfo");
+ }
+ }
+ mIsSuccessfullySelected = true;
+ }
+
+ private void maybeRemoveSharedText(@NonNull TargetInfo targetInfo) {
+ Intent targetIntent = targetInfo.getTargetIntent();
+ if (targetIntent == null) {
+ return;
+ }
+ Intent originalTargetIntent = new Intent(requireChooserRequest().getTargetIntent());
+ // Our TargetInfo implementations add associated component to the intent, let's do the same
+ // for the sake of the comparison below.
+ if (targetIntent.getComponent() != null) {
+ originalTargetIntent.setComponent(targetIntent.getComponent());
+ }
+ // Use filterEquals as a way to check that the primary intent is in use (and not an
+ // alternative one). For example, an app is sharing an image and a link with mime type
+ // "image/png" and provides an alternative intent to share only the link with mime type
+ // "text/uri". Should there be a target that accepts only the latter, the alternative intent
+ // will be used and we don't want to exclude the link from it.
+ if (mExcludeSharedText && originalTargetIntent.filterEquals(targetIntent)) {
+ targetIntent.removeExtra(Intent.EXTRA_TEXT);
+ }
+ }
+
+ private void sendImpressionToAppPredictor(TargetInfo targetInfo, ChooserListAdapter adapter) {
+ // Send DS target impression info to AppPredictor, only when user chooses app share.
+ if (targetInfo.isChooserTargetInfo()) {
+ return;
+ }
+
+ AppPredictor directShareAppPredictor = getAppPredictor(
+ mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
+ if (directShareAppPredictor == null) {
+ return;
+ }
+ List<TargetInfo> surfacedTargetInfo = adapter.getSurfacedTargetInfo();
+ List<AppTargetId> targetIds = new ArrayList<>();
+ for (TargetInfo chooserTargetInfo : surfacedTargetInfo) {
+ ShortcutInfo shortcutInfo = chooserTargetInfo.getDirectShareShortcutInfo();
+ if (shortcutInfo != null) {
+ ComponentName componentName =
+ chooserTargetInfo.getChooserTargetComponentName();
+ targetIds.add(new AppTargetId(
+ String.format(
+ "%s/%s/%s",
+ shortcutInfo.getId(),
+ componentName.flattenToString(),
+ SHORTCUT_TARGET)));
+ }
+ }
+ directShareAppPredictor.notifyLaunchLocationShown(LAUNCH_LOCATION_DIRECT_SHARE, targetIds);
+ }
+
+ private void sendClickToAppPredictor(TargetInfo targetInfo) {
+ if (!targetInfo.isChooserTargetInfo()) {
+ return;
+ }
+
+ AppPredictor directShareAppPredictor = getAppPredictor(
+ mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
+ if (directShareAppPredictor == null) {
+ return;
+ }
+ AppTarget appTarget = targetInfo.getDirectShareAppTarget();
+ if (appTarget != null) {
+ // This is a direct share click that was provided by the APS
+ directShareAppPredictor.notifyAppTargetEvent(
+ new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH)
+ .setLaunchLocation(LAUNCH_LOCATION_DIRECT_SHARE)
+ .build());
+ }
+ }
+
+ @Nullable
+ private AppPredictor getAppPredictor(UserHandle userHandle) {
+ ProfileRecord record = getProfileRecord(userHandle);
+ // We cannot use APS service when clone profile is present as APS service cannot sort
+ // cross profile targets as of now.
+ return ((record == null) || (requireAnnotatedUserHandles().cloneProfileUserHandle != null))
+ ? null : record.appPredictor;
+ }
+
+ /**
+ * Sort intents alphabetically based on display label.
+ */
+ static class AzInfoComparator implements Comparator<DisplayResolveInfo> {
+ Comparator<DisplayResolveInfo> mComparator;
+ AzInfoComparator(Context context) {
+ Collator collator = Collator
+ .getInstance(context.getResources().getConfiguration().locale);
+ // Adding two stage comparator, first stage compares using displayLabel, next stage
+ // compares using resolveInfo.userHandle
+ mComparator = Comparator.comparing(DisplayResolveInfo::getDisplayLabel, collator)
+ .thenComparingInt(target -> target.getResolveInfo().userHandle.getIdentifier());
+ }
+
+ @Override
+ public int compare(
+ DisplayResolveInfo lhsp, DisplayResolveInfo rhsp) {
+ return mComparator.compare(lhsp, rhsp);
+ }
+ }
+
+ protected EventLog getEventLog() {
+ return mEventLog;
+ }
+
+ public class ChooserListController extends ResolverListController {
+ public ChooserListController(
+ Context context,
+ PackageManager pm,
+ Intent targetIntent,
+ String referrerPackageName,
+ int launchedFromUid,
+ AbstractResolverComparator resolverComparator,
+ UserHandle queryIntentsAsUser) {
+ super(
+ context,
+ pm,
+ targetIntent,
+ referrerPackageName,
+ launchedFromUid,
+ resolverComparator,
+ queryIntentsAsUser);
+ }
+
+ @Override
+ public boolean isComponentFiltered(ComponentName name) {
+ return requireChooserRequest().getFilteredComponentNames().contains(name);
+ }
+
+ @Override
+ public boolean isComponentPinned(ComponentName name) {
+ return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
+ }
+ }
+
+ @VisibleForTesting
+ public ChooserGridAdapter createChooserGridAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ UserHandle userHandle,
+ TargetDataLoader targetDataLoader) {
+ ChooserRequestParameters parameters = requireChooserRequest();
+ ChooserListAdapter chooserListAdapter = createChooserListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ createListController(userHandle),
+ userHandle,
+ mLogic.getTargetIntent(),
+ parameters.getReferrerFillInIntent(),
+ mMaxTargetsPerRow,
+ targetDataLoader);
+
+ return new ChooserGridAdapter(
+ context,
+ new ChooserGridAdapter.ChooserActivityDelegate() {
+ @Override
+ public boolean shouldShowTabs() {
+ return ChooserActivity.this.shouldShowTabs();
+ }
+
+ @Override
+ public View buildContentPreview(ViewGroup parent) {
+ return createContentPreviewView(parent);
+ }
+
+ @Override
+ public void onTargetSelected(int itemIndex) {
+ startSelected(itemIndex, false, true);
+ }
+
+ @Override
+ public void onTargetLongPressed(int selectedPosition) {
+ final TargetInfo longPressedTargetInfo =
+ mChooserMultiProfilePagerAdapter
+ .getActiveListAdapter()
+ .targetInfoForPosition(
+ selectedPosition, /* filtered= */ true);
+ // Only a direct share target or an app target is expected
+ if (longPressedTargetInfo.isDisplayResolveInfo()
+ || longPressedTargetInfo.isSelectableTargetInfo()) {
+ showTargetDetails(longPressedTargetInfo);
+ }
+ }
+
+ @Override
+ public void updateProfileViewButton(View newButtonFromProfileRow) {
+ mProfileView = newButtonFromProfileRow;
+ mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick);
+ ChooserActivity.this.updateProfileViewButton();
+ }
+ },
+ chooserListAdapter,
+ shouldShowContentPreview(),
+ mMaxTargetsPerRow,
+ mFeatureFlags);
+ }
+
+ @VisibleForTesting
+ public ChooserListAdapter createChooserListAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ ResolverListController resolverListController,
+ UserHandle userHandle,
+ Intent targetIntent,
+ Intent referrerFillInIntent,
+ int maxTargetsPerRow,
+ TargetDataLoader targetDataLoader) {
+ UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()
+ && userHandle.equals(requireAnnotatedUserHandles().personalProfileUserHandle)
+ ? requireAnnotatedUserHandles().cloneProfileUserHandle : userHandle;
+ return new ChooserListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ createListController(userHandle),
+ userHandle,
+ targetIntent,
+ referrerFillInIntent,
+ this,
+ context.getPackageManager(),
+ getEventLog(),
+ maxTargetsPerRow,
+ initialIntentsUserSpace,
+ targetDataLoader,
+ () -> {
+ ProfileRecord record = getProfileRecord(userHandle);
+ if (record != null && record.shortcutLoader != null) {
+ record.shortcutLoader.reset();
+ }
+ });
+ }
+
+ @Override
+ protected Unit onWorkProfileStatusUpdated() {
+ UserHandle workUser = requireAnnotatedUserHandles().workProfileUserHandle;
+ ProfileRecord record = workUser == null ? null : getProfileRecord(workUser);
+ if (record != null && record.shortcutLoader != null) {
+ record.shortcutLoader.reset();
+ }
+ return super.onWorkProfileStatusUpdated();
+ }
+
+ @Override
+ @VisibleForTesting
+ protected ChooserListController createListController(UserHandle userHandle) {
+ AppPredictor appPredictor = getAppPredictor(userHandle);
+ AbstractResolverComparator resolverComparator;
+ if (appPredictor != null) {
+ resolverComparator = new AppPredictionServiceResolverComparator(
+ this,
+ mLogic.getTargetIntent(),
+ mLogic.getReferrerPackageName(),
+ appPredictor,
+ userHandle,
+ getEventLog(),
+ mNearbyShare.orElse(null)
+ );
+ } else {
+ resolverComparator =
+ new ResolverRankerServiceResolverComparator(
+ this,
+ mLogic.getTargetIntent(),
+ mLogic.getReferrerPackageName(),
+ null,
+ getEventLog(),
+ getResolverRankerServiceUserHandleList(userHandle),
+ mNearbyShare.orElse(null));
+ }
+
+ return new ChooserListController(
+ this,
+ mPm,
+ mLogic.getTargetIntent(),
+ mLogic.getReferrerPackageName(),
+ requireAnnotatedUserHandles().userIdOfCallingApp,
+ resolverComparator,
+ getQueryIntentsUser(userHandle));
+ }
+
+ @VisibleForTesting
+ protected ViewModelProvider.Factory createPreviewViewModelFactory() {
+ return PreviewViewModel.Companion.getFactory();
+ }
+
+ private ChooserActionFactory createChooserActionFactory() {
+ ChooserRequestParameters request = requireChooserRequest();
+ return new ChooserActionFactory(
+ this,
+ request.getTargetIntent(),
+ request.getReferrerPackageName(),
+ request.getChooserActions(),
+ request.getModifyShareAction(),
+ mImageEditor,
+ getEventLog(),
+ (isExcluded) -> mExcludeSharedText = isExcluded,
+ this::getFirstVisibleImgPreviewView,
+ new ChooserActionFactory.ActionActivityStarter() {
+ @Override
+ public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) {
+ safelyStartActivityAsUser(
+ targetInfo,
+ requireAnnotatedUserHandles().personalProfileUserHandle
+ );
+ finish();
+ }
+
+ @Override
+ public void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+ TargetInfo targetInfo, View sharedElement, String sharedElementName) {
+ ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
+ ChooserActivity.this, sharedElement, sharedElementName);
+ safelyStartActivityAsUser(
+ targetInfo,
+ requireAnnotatedUserHandles().personalProfileUserHandle,
+ options.toBundle());
+ // Can't finish right away because the shared element transition may not
+ // be ready to start.
+ mFinishWhenStopped = true;
+ }
+ },
+ (status) -> {
+ if (status != null) {
+ setResult(status);
+ }
+ finish();
+ });
+ }
+
+ /*
+ * Need to dynamically adjust how many icons can fit per row before we add them,
+ * which also means setting the correct offset to initially show the content
+ * preview area + 2 rows of targets
+ */
+ private void handleLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+ int oldTop, int oldRight, int oldBottom) {
+ if (mChooserMultiProfilePagerAdapter == null) {
+ return;
+ }
+ RecyclerView recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
+ ChooserGridAdapter gridAdapter = mChooserMultiProfilePagerAdapter.getCurrentRootAdapter();
+ // Skip height calculation if recycler view was scrolled to prevent it inaccurately
+ // calculating the height, as the logic below does not account for the scrolled offset.
+ if (gridAdapter == null || recyclerView == null
+ || recyclerView.computeVerticalScrollOffset() != 0) {
+ return;
+ }
+
+ final int availableWidth = right - left - v.getPaddingLeft() - v.getPaddingRight();
+ boolean isLayoutUpdated =
+ gridAdapter.calculateChooserTargetWidth(availableWidth)
+ || recyclerView.getAdapter() == null
+ || availableWidth != mCurrAvailableWidth;
+
+ boolean insetsChanged = !Objects.equals(mLastAppliedInsets, mSystemWindowInsets);
+
+ if (isLayoutUpdated
+ || insetsChanged
+ || mLastNumberOfChildren != recyclerView.getChildCount()) {
+ mCurrAvailableWidth = availableWidth;
+ if (isLayoutUpdated) {
+ // It is very important we call setAdapter from here. Otherwise in some cases
+ // the resolver list doesn't get populated, such as b/150922090, b/150918223
+ // and b/150936654
+ recyclerView.setAdapter(gridAdapter);
+ ((GridLayoutManager) recyclerView.getLayoutManager()).setSpanCount(
+ mMaxTargetsPerRow);
+
+ updateTabPadding();
+ }
+
+ UserHandle currentUserHandle = mChooserMultiProfilePagerAdapter.getCurrentUserHandle();
+ int currentProfile = getProfileForUser(currentUserHandle);
+ int initialProfile = findSelectedProfile();
+ if (currentProfile != initialProfile) {
+ return;
+ }
+
+ if (mLastNumberOfChildren == recyclerView.getChildCount() && !insetsChanged) {
+ return;
+ }
+
+ getMainThreadHandler().post(() -> {
+ if (mResolverDrawerLayout == null || gridAdapter == null) {
+ return;
+ }
+ int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter);
+ mResolverDrawerLayout.setCollapsibleHeightReserved(offset);
+ mEnterTransitionAnimationDelegate.markOffsetCalculated();
+ mLastAppliedInsets = mSystemWindowInsets;
+ });
+ }
+ }
+
+ private int calculateDrawerOffset(
+ int top, int bottom, RecyclerView recyclerView, ChooserGridAdapter gridAdapter) {
+
+ int offset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0;
+ int rowsToShow = gridAdapter.getSystemRowCount()
+ + gridAdapter.getProfileRowCount()
+ + gridAdapter.getServiceTargetRowCount()
+ + gridAdapter.getCallerAndRankedTargetRowCount();
+
+ // then this is most likely not a SEND_* action, so check
+ // the app target count
+ if (rowsToShow == 0) {
+ rowsToShow = gridAdapter.getRowCount();
+ }
+
+ // still zero? then use a default height and leave, which
+ // can happen when there are no targets to show
+ if (rowsToShow == 0 && !shouldShowStickyContentPreview()) {
+ offset += getResources().getDimensionPixelSize(
+ R.dimen.chooser_max_collapsed_height);
+ return offset;
+ }
+
+ View stickyContentPreview = findViewById(com.android.internal.R.id.content_preview_container);
+ if (shouldShowStickyContentPreview() && isStickyContentPreviewShowing()) {
+ offset += stickyContentPreview.getHeight();
+ }
+
+ if (shouldShowTabs()) {
+ offset += findViewById(com.android.internal.R.id.tabs).getHeight();
+ }
+
+ if (recyclerView.getVisibility() == View.VISIBLE) {
+ rowsToShow = Math.min(4, rowsToShow);
+ boolean shouldShowExtraRow = shouldShowExtraRow(rowsToShow);
+ mLastNumberOfChildren = recyclerView.getChildCount();
+ for (int i = 0, childCount = recyclerView.getChildCount();
+ i < childCount && rowsToShow > 0; i++) {
+ View child = recyclerView.getChildAt(i);
+ if (((GridLayoutManager.LayoutParams)
+ child.getLayoutParams()).getSpanIndex() != 0) {
+ continue;
+ }
+ int height = child.getHeight();
+ offset += height;
+ if (shouldShowExtraRow) {
+ offset += height;
+ }
+ rowsToShow--;
+ }
+ } else {
+ ViewGroup currentEmptyStateView =
+ mChooserMultiProfilePagerAdapter.getActiveEmptyStateView();
+ if (currentEmptyStateView.getVisibility() == View.VISIBLE) {
+ offset += currentEmptyStateView.getHeight();
+ }
+ }
+
+ return Math.min(offset, bottom - top);
+ }
+
+ /**
+ * If we have a tabbed view and are showing 1 row in the current profile and an empty
+ * state screen in another profile, to prevent cropping of the empty state screen we show
+ * a second row in the current profile.
+ */
+ private boolean shouldShowExtraRow(int rowsToShow) {
+ return rowsToShow == 1
+ && mChooserMultiProfilePagerAdapter
+ .shouldShowEmptyStateScreenInAnyInactiveAdapter();
+ }
+
+ /**
+ * Returns {@link #PROFILE_WORK}, if the given user handle matches work user handle.
+ * Returns {@link #PROFILE_PERSONAL}, otherwise.
+ **/
+ private int getProfileForUser(UserHandle currentUserHandle) {
+ if (currentUserHandle.equals(requireAnnotatedUserHandles().workProfileUserHandle)) {
+ return PROFILE_WORK;
+ }
+ // We return personal profile, as it is the default when there is no work profile, personal
+ // profile represents rootUser, clonedUser & secondaryUser, covering all use cases.
+ return PROFILE_PERSONAL;
+ }
+
+ @Override
+ protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) {
+ setupScrollListener();
+ maybeSetupGlobalLayoutListener();
+
+ ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter;
+ UserHandle listProfileUserHandle = chooserListAdapter.getUserHandle();
+ if (listProfileUserHandle.equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) {
+ mChooserMultiProfilePagerAdapter.getActiveAdapterView()
+ .setAdapter(mChooserMultiProfilePagerAdapter.getCurrentRootAdapter());
+ mChooserMultiProfilePagerAdapter
+ .setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage());
+ }
+
+ //TODO: move this block inside ChooserListAdapter (should be called when
+ // ResolverListAdapter#mPostListReadyRunnable is executed.
+ if (chooserListAdapter.getDisplayResolveInfoCount() == 0) {
+ chooserListAdapter.notifyDataSetChanged();
+ } else {
+ chooserListAdapter.updateAlphabeticalList();
+ }
+
+ if (rebuildComplete) {
+ long duration = Tracer.INSTANCE.endAppTargetLoadingSection(listProfileUserHandle);
+ if (duration >= 0) {
+ Log.d(TAG, "app target loading time " + duration + " ms");
+ }
+ addCallerChooserTargets();
+ getEventLog().logSharesheetAppLoadComplete();
+ maybeQueryAdditionalPostProcessingTargets(
+ listProfileUserHandle,
+ chooserListAdapter.getDisplayResolveInfos());
+ mLatencyTracker.onActionEnd(ACTION_LOAD_SHARE_SHEET);
+ }
+ }
+
+ private void maybeQueryAdditionalPostProcessingTargets(
+ UserHandle userHandle,
+ DisplayResolveInfo[] displayResolveInfos) {
+ ProfileRecord record = getProfileRecord(userHandle);
+ if (record == null || record.shortcutLoader == null) {
+ return;
+ }
+ record.loadingStartTime = SystemClock.elapsedRealtime();
+ record.shortcutLoader.updateAppTargets(displayResolveInfos);
+ }
+
+ @MainThread
+ private void onShortcutsLoaded(UserHandle userHandle, ShortcutLoader.Result result) {
+ if (DEBUG) {
+ Log.d(TAG, "onShortcutsLoaded for user: " + userHandle);
+ }
+ mDirectShareShortcutInfoCache.putAll(result.getDirectShareShortcutInfoCache());
+ mDirectShareAppTargetCache.putAll(result.getDirectShareAppTargetCache());
+ ChooserListAdapter adapter =
+ mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle);
+ if (adapter != null) {
+ for (ShortcutLoader.ShortcutResultInfo resultInfo : result.getShortcutsByApp()) {
+ adapter.addServiceResults(
+ resultInfo.getAppTarget(),
+ resultInfo.getShortcuts(),
+ result.isFromAppPredictor()
+ ? TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
+ : TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER,
+ mDirectShareShortcutInfoCache,
+ mDirectShareAppTargetCache);
+ }
+ adapter.completeServiceTargetLoading();
+ }
+
+ if (mMultiProfilePagerAdapter.getActiveListAdapter() == adapter) {
+ long duration = Tracer.INSTANCE.endLaunchToShortcutTrace();
+ if (duration >= 0) {
+ Log.d(TAG, "stat to first shortcut time: " + duration + " ms");
+ }
+ }
+ logDirectShareTargetReceived(userHandle);
+ sendVoiceChoicesIfNeeded();
+ getEventLog().logSharesheetDirectLoadComplete();
+ }
+
+ private void setupScrollListener() {
+ if (mResolverDrawerLayout == null) {
+ return;
+ }
+ int elevatedViewResId = shouldShowTabs() ? com.android.internal.R.id.tabs : com.android.internal.R.id.chooser_header;
+ final View elevatedView = mResolverDrawerLayout.findViewById(elevatedViewResId);
+ final float defaultElevation = elevatedView.getElevation();
+ final float chooserHeaderScrollElevation =
+ getResources().getDimensionPixelSize(R.dimen.chooser_header_scroll_elevation);
+ mChooserMultiProfilePagerAdapter.getActiveAdapterView().addOnScrollListener(
+ new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(RecyclerView view, int scrollState) {
+ if (scrollState == RecyclerView.SCROLL_STATE_IDLE) {
+ if (mScrollStatus == SCROLL_STATUS_SCROLLING_VERTICAL) {
+ mScrollStatus = SCROLL_STATUS_IDLE;
+ setHorizontalScrollingEnabled(true);
+ }
+ } else if (scrollState == RecyclerView.SCROLL_STATE_DRAGGING) {
+ if (mScrollStatus == SCROLL_STATUS_IDLE) {
+ mScrollStatus = SCROLL_STATUS_SCROLLING_VERTICAL;
+ setHorizontalScrollingEnabled(false);
+ }
+ }
+ }
+
+ @Override
+ public void onScrolled(RecyclerView view, int dx, int dy) {
+ if (view.getChildCount() > 0) {
+ View child = view.getLayoutManager().findViewByPosition(0);
+ if (child == null || child.getTop() < 0) {
+ elevatedView.setElevation(chooserHeaderScrollElevation);
+ return;
+ }
+ }
+
+ elevatedView.setElevation(defaultElevation);
+ }
+ });
+ }
+
+ private void maybeSetupGlobalLayoutListener() {
+ if (shouldShowTabs()) {
+ return;
+ }
+ final View recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
+ recyclerView.getViewTreeObserver()
+ .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ // Fixes an issue were the accessibility border disappears on list creation.
+ recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ final TextView titleView = findViewById(com.android.internal.R.id.title);
+ if (titleView != null) {
+ titleView.setFocusable(true);
+ titleView.setFocusableInTouchMode(true);
+ titleView.requestFocus();
+ titleView.requestAccessibilityFocus();
+ }
+ }
+ });
+ }
+
+ /**
+ * The sticky content preview is shown only when we have a tabbed view. It's shown above
+ * the tabs so it is not part of the scrollable list. If we are not in tabbed view,
+ * we instead show the content preview as a regular list item.
+ */
+ private boolean shouldShowStickyContentPreview() {
+ return shouldShowStickyContentPreviewNoOrientationCheck();
+ }
+
+ private boolean shouldShowStickyContentPreviewNoOrientationCheck() {
+ if (!shouldShowContentPreview()) {
+ return false;
+ }
+ boolean isEmpty = mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+ UserHandle.of(UserHandle.myUserId())).getCount() == 0;
+ return (mFeatureFlags.scrollablePreview() || shouldShowTabs())
+ && (!isEmpty || shouldShowContentPreviewWhenEmpty());
+ }
+
+ /**
+ * This method could be used to override the default behavior when we hide the preview area
+ * when the current tab doesn't have any items.
+ *
+ * @return true if we want to show the content preview area even if the tab for the current
+ * user is empty
+ */
+ protected boolean shouldShowContentPreviewWhenEmpty() {
+ return false;
+ }
+
+ /**
+ * @return true if we want to show the content preview area
+ */
+ protected boolean shouldShowContentPreview() {
+ ChooserRequestParameters chooserRequest = getChooserRequest();
+ return (chooserRequest != null) && chooserRequest.isSendActionTarget();
+ }
+
+ private void updateStickyContentPreview() {
+ if (shouldShowStickyContentPreviewNoOrientationCheck()) {
+ // The sticky content preview is only shown when we show the work and personal tabs.
+ // We don't show it in landscape as otherwise there is no room for scrolling.
+ // If the sticky content preview will be shown at some point with orientation change,
+ // then always preload it to avoid subsequent resizing of the share sheet.
+ ViewGroup contentPreviewContainer =
+ findViewById(com.android.internal.R.id.content_preview_container);
+ if (contentPreviewContainer.getChildCount() == 0) {
+ ViewGroup contentPreviewView = createContentPreviewView(contentPreviewContainer);
+ contentPreviewContainer.addView(contentPreviewView);
+ }
+ }
+ if (shouldShowStickyContentPreview()) {
+ showStickyContentPreview();
+ } else {
+ hideStickyContentPreview();
+ }
+ }
+
+ private void showStickyContentPreview() {
+ if (isStickyContentPreviewShowing()) {
+ return;
+ }
+ ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
+ contentPreviewContainer.setVisibility(View.VISIBLE);
+ }
+
+ private boolean isStickyContentPreviewShowing() {
+ ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
+ return contentPreviewContainer.getVisibility() == View.VISIBLE;
+ }
+
+ private void hideStickyContentPreview() {
+ if (!isStickyContentPreviewShowing()) {
+ return;
+ }
+ ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
+ contentPreviewContainer.setVisibility(View.GONE);
+ }
+
+ private View findRootView() {
+ if (mContentView == null) {
+ mContentView = findViewById(android.R.id.content);
+ }
+ return mContentView;
+ }
+
+ /**
+ * Intentionally override the {@link ResolverActivity} implementation as we only need that
+ * implementation for the intent resolver case.
+ */
+ @Override
+ public void onButtonClick(View v) {}
+
+ /**
+ * Intentionally override the {@link ResolverActivity} implementation as we only need that
+ * implementation for the intent resolver case.
+ */
+ @Override
+ protected void resetButtonBar() {}
+
+ @Override
+ protected String getMetricsCategory() {
+ return METRICS_CATEGORY_CHOOSER;
+ }
+
+ @Override
+ protected void onProfileTabSelected() {
+ // This fixes an edge case where after performing a variety of gestures, vertical scrolling
+ // ends up disabled. That's because at some point the old tab's vertical scrolling is
+ // disabled and the new tab's is enabled. For context, see b/159997845
+ setVerticalScrollEnabled(true);
+ if (mResolverDrawerLayout != null) {
+ mResolverDrawerLayout.scrollNestedScrollableChildBackToTop();
+ }
+ }
+
+ @Override
+ protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
+ if (shouldShowTabs()) {
+ mChooserMultiProfilePagerAdapter
+ .setEmptyStateBottomOffset(insets.getSystemWindowInsetBottom());
+ }
+
+ WindowInsets result = super.onApplyWindowInsets(v, insets);
+ if (mResolverDrawerLayout != null) {
+ mResolverDrawerLayout.requestLayout();
+ }
+ return result;
+ }
+
+ private void setHorizontalScrollingEnabled(boolean enabled) {
+ ResolverViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ viewPager.setSwipingEnabled(enabled);
+ }
+
+ private void setVerticalScrollEnabled(boolean enabled) {
+ ChooserGridLayoutManager layoutManager =
+ (ChooserGridLayoutManager) mChooserMultiProfilePagerAdapter.getActiveAdapterView()
+ .getLayoutManager();
+ layoutManager.setVerticalScrollEnabled(enabled);
+ }
+
+ @Override
+ void onHorizontalSwipeStateChanged(int state) {
+ if (state == ViewPager.SCROLL_STATE_DRAGGING) {
+ if (mScrollStatus == SCROLL_STATUS_IDLE) {
+ mScrollStatus = SCROLL_STATUS_SCROLLING_HORIZONTAL;
+ setVerticalScrollEnabled(false);
+ }
+ } else if (state == ViewPager.SCROLL_STATE_IDLE) {
+ if (mScrollStatus == SCROLL_STATUS_SCROLLING_HORIZONTAL) {
+ mScrollStatus = SCROLL_STATUS_IDLE;
+ setVerticalScrollEnabled(true);
+ }
+ }
+ }
+
+ @Override
+ protected void maybeLogProfileChange() {
+ getEventLog().logSharesheetProfileChanged();
+ }
+
+ private static class ProfileRecord {
+ /** The {@link AppPredictor} for this profile, if any. */
+ @Nullable
+ public final AppPredictor appPredictor;
+ /**
+ * null if we should not load shortcuts.
+ */
+ @Nullable
+ public final ShortcutLoader shortcutLoader;
+ public long loadingStartTime;
+
+ private ProfileRecord(
+ @Nullable AppPredictor appPredictor,
+ @Nullable ShortcutLoader shortcutLoader) {
+ this.appPredictor = appPredictor;
+ this.shortcutLoader = shortcutLoader;
+ }
+
+ public void destroy() {
+ if (appPredictor != null) {
+ appPredictor.destroy();
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt b/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt
new file mode 100644
index 0000000..7bc39a2
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt
@@ -0,0 +1,87 @@
+package com.android.intentresolver.v2
+
+import android.app.Activity
+import android.content.Intent
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.annotation.OpenForTesting
+import com.android.intentresolver.ChooserRequestParameters
+import com.android.intentresolver.R
+import com.android.intentresolver.icons.TargetDataLoader
+import com.android.intentresolver.v2.util.mutableLazy
+
+private const val TAG = "ChooserActivityLogic"
+
+/**
+ * Activity logic for [ChooserActivity].
+ *
+ * TODO: Make this class no longer open once [ChooserActivity] no longer needs to cast to access
+ * [chooserRequestParameters]. For now, this class being open is better than using reflection
+ * there.
+ */
+@OpenForTesting
+open class ChooserActivityLogic(
+ tag: String,
+ activityProvider: () -> ComponentActivity,
+ onWorkProfileStatusUpdated: () -> Unit,
+ targetDataLoaderProvider: () -> TargetDataLoader,
+ private val onPreInitialization: () -> Unit,
+) :
+ ActivityLogic,
+ CommonActivityLogic by CommonActivityLogicImpl(
+ tag,
+ activityProvider,
+ onWorkProfileStatusUpdated,
+ ) {
+
+ override val targetIntent: Intent by lazy { chooserRequestParameters?.targetIntent ?: Intent() }
+
+ override val resolvingHome: Boolean = false
+
+ override val title: CharSequence? by lazy { chooserRequestParameters?.title }
+
+ override val defaultTitleResId: Int by lazy {
+ chooserRequestParameters?.defaultTitleResource ?: 0
+ }
+
+ override val initialIntents: List<Intent>? by lazy {
+ chooserRequestParameters?.initialIntents?.toList()
+ }
+
+ override val supportsAlwaysUseOption: Boolean = false
+
+ override val targetDataLoader: TargetDataLoader by lazy { targetDataLoaderProvider() }
+
+ override val themeResId: Int = R.style.Theme_DeviceDefault_Chooser
+
+ private val _profileSwitchMessage = mutableLazy { forwardMessageFor(targetIntent) }
+ override val profileSwitchMessage: String? by _profileSwitchMessage
+
+ override val payloadIntents: List<Intent> by lazy {
+ buildList {
+ add(targetIntent)
+ chooserRequestParameters?.additionalTargets?.let { addAll(it) }
+ }
+ }
+
+ val chooserRequestParameters: ChooserRequestParameters? by lazy {
+ try {
+ ChooserRequestParameters(
+ (activity as Activity).intent,
+ referrerPackageName,
+ (activity as Activity).referrer,
+ )
+ } catch (e: IllegalArgumentException) {
+ Log.e(tag, "Caller provided invalid Chooser request parameters", e)
+ null
+ }
+ }
+
+ override fun preInitialization() {
+ onPreInitialization()
+ }
+
+ override fun clearProfileSwitchMessage() {
+ _profileSwitchMessage.setLazy(null)
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java
new file mode 100644
index 0000000..de0a942
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2019 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.intentresolver.v2;
+
+import android.content.Context;
+import android.os.UserHandle;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.viewpager.widget.PagerAdapter;
+
+import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.ChooserRecyclerViewAccessibilityDelegate;
+import com.android.intentresolver.FeatureFlags;
+import com.android.intentresolver.R;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.grid.ChooserGridAdapter;
+import com.android.intentresolver.measurements.Tracer;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+
+/**
+ * A {@link PagerAdapter} which describes the work and personal profile share sheet screens.
+ */
+@VisibleForTesting
+public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<
+ RecyclerView, ChooserGridAdapter, ChooserListAdapter> {
+ private static final int SINGLE_CELL_SPAN_SIZE = 1;
+
+ private final ChooserProfileAdapterBinder mAdapterBinder;
+ private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier;
+
+ public ChooserMultiProfilePagerAdapter(
+ Context context,
+ ChooserGridAdapter adapter,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle,
+ int maxTargetsPerRow,
+ FeatureFlags featureFlags) {
+ this(
+ context,
+ new ChooserProfileAdapterBinder(maxTargetsPerRow),
+ ImmutableList.of(adapter),
+ emptyStateProvider,
+ workProfileQuietModeChecker,
+ /* defaultProfile= */ 0,
+ workProfileUserHandle,
+ cloneProfileUserHandle,
+ new BottomPaddingOverrideSupplier(context),
+ featureFlags);
+ }
+
+ public ChooserMultiProfilePagerAdapter(
+ Context context,
+ ChooserGridAdapter personalAdapter,
+ ChooserGridAdapter workAdapter,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ @Profile int defaultProfile,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle,
+ int maxTargetsPerRow,
+ FeatureFlags featureFlags) {
+ this(
+ context,
+ new ChooserProfileAdapterBinder(maxTargetsPerRow),
+ ImmutableList.of(personalAdapter, workAdapter),
+ emptyStateProvider,
+ workProfileQuietModeChecker,
+ defaultProfile,
+ workProfileUserHandle,
+ cloneProfileUserHandle,
+ new BottomPaddingOverrideSupplier(context),
+ featureFlags);
+ }
+
+ private ChooserMultiProfilePagerAdapter(
+ Context context,
+ ChooserProfileAdapterBinder adapterBinder,
+ ImmutableList<ChooserGridAdapter> gridAdapters,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ @Profile int defaultProfile,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle,
+ BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier,
+ FeatureFlags featureFlags) {
+ super(
+ gridAdapter -> gridAdapter.getListAdapter(),
+ adapterBinder,
+ gridAdapters,
+ emptyStateProvider,
+ workProfileQuietModeChecker,
+ defaultProfile,
+ workProfileUserHandle,
+ cloneProfileUserHandle,
+ () -> makeProfileView(context, featureFlags),
+ bottomPaddingOverrideSupplier);
+ mAdapterBinder = adapterBinder;
+ mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier;
+ }
+
+ public void setMaxTargetsPerRow(int maxTargetsPerRow) {
+ mAdapterBinder.setMaxTargetsPerRow(maxTargetsPerRow);
+ }
+
+ public void setEmptyStateBottomOffset(int bottomOffset) {
+ mBottomPaddingOverrideSupplier.setEmptyStateBottomOffset(bottomOffset);
+ setupContainerPadding();
+ }
+
+ /**
+ * Notify adapter about the drawer's collapse state. This will affect the app divider's
+ * visibility.
+ */
+ public void setIsCollapsed(boolean isCollapsed) {
+ for (int i = 0, size = getItemCount(); i < size; i++) {
+ getAdapterForIndex(i).setAzLabelVisibility(!isCollapsed);
+ }
+ }
+
+ private static ViewGroup makeProfileView(
+ Context context, FeatureFlags featureFlags) {
+ LayoutInflater inflater = LayoutInflater.from(context);
+ ViewGroup rootView = featureFlags.scrollablePreview()
+ ? (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile_wrap, null, false)
+ : (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile, null, false);
+ RecyclerView recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list);
+ recyclerView.setAccessibilityDelegateCompat(
+ new ChooserRecyclerViewAccessibilityDelegate(recyclerView));
+ return rootView;
+ }
+
+ @Override
+ public boolean onHandlePackagesChanged(
+ ChooserListAdapter listAdapter, boolean waitingToEnableWorkProfile) {
+ // TODO: why do we need to do the extra `notifyDataSetChanged()` in (only) the Chooser case?
+ getActiveListAdapter().notifyDataSetChanged();
+ return super.onHandlePackagesChanged(listAdapter, waitingToEnableWorkProfile);
+ }
+
+ @Override
+ protected final boolean rebuildTab(ChooserListAdapter listAdapter, boolean doPostProcessing) {
+ if (doPostProcessing) {
+ Tracer.INSTANCE.beginAppTargetLoadingSection(listAdapter.getUserHandle());
+ }
+ return super.rebuildTab(listAdapter, doPostProcessing);
+ }
+
+ /** Apply the specified {@code height} as the footer in each tab's adapter. */
+ public void setFooterHeightInEveryAdapter(int height) {
+ for (int i = 0; i < getItemCount(); ++i) {
+ getAdapterForIndex(i).setFooterHeight(height);
+ }
+ }
+
+ private static class BottomPaddingOverrideSupplier implements Supplier<Optional<Integer>> {
+ private final Context mContext;
+ private int mBottomOffset;
+
+ BottomPaddingOverrideSupplier(Context context) {
+ mContext = context;
+ }
+
+ public void setEmptyStateBottomOffset(int bottomOffset) {
+ mBottomOffset = bottomOffset;
+ }
+
+ @Override
+ public Optional<Integer> get() {
+ int initialBottomPadding = mContext.getResources().getDimensionPixelSize(
+ R.dimen.resolver_empty_state_container_padding_bottom);
+ return Optional.of(initialBottomPadding + mBottomOffset);
+ }
+ }
+
+ private static class ChooserProfileAdapterBinder implements
+ AdapterBinder<RecyclerView, ChooserGridAdapter> {
+ private int mMaxTargetsPerRow;
+
+ ChooserProfileAdapterBinder(int maxTargetsPerRow) {
+ mMaxTargetsPerRow = maxTargetsPerRow;
+ }
+
+ public void setMaxTargetsPerRow(int maxTargetsPerRow) {
+ mMaxTargetsPerRow = maxTargetsPerRow;
+ }
+
+ @Override
+ public void bind(
+ RecyclerView recyclerView, ChooserGridAdapter chooserGridAdapter) {
+ GridLayoutManager glm = (GridLayoutManager) recyclerView.getLayoutManager();
+ glm.setSpanCount(mMaxTargetsPerRow);
+ glm.setSpanSizeLookup(
+ new GridLayoutManager.SpanSizeLookup() {
+ @Override
+ public int getSpanSize(int position) {
+ return chooserGridAdapter.shouldCellSpan(position)
+ ? SINGLE_CELL_SPAN_SIZE
+ : glm.getSpanCount();
+ }
+ });
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ChooserSelector.kt b/java/src/com/android/intentresolver/v2/ChooserSelector.kt
new file mode 100644
index 0000000..378bc06
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ChooserSelector.kt
@@ -0,0 +1,36 @@
+package com.android.intentresolver.v2
+
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import com.android.intentresolver.FeatureFlags
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+@AndroidEntryPoint(BroadcastReceiver::class)
+class ChooserSelector : Hilt_ChooserSelector() {
+
+ @Inject lateinit var featureFlags: FeatureFlags
+
+ override fun onReceive(context: Context, intent: Intent) {
+ super.onReceive(context, intent)
+ if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
+ context.packageManager.setComponentEnabledSetting(
+ ComponentName(CHOOSER_PACKAGE, CHOOSER_PACKAGE + CHOOSER_CLASS),
+ if (featureFlags.modularFramework()) {
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ } else {
+ PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
+ },
+ /* flags = */ 0,
+ )
+ }
+ }
+
+ companion object {
+ private const val CHOOSER_PACKAGE = "com.android.intentresolver"
+ private const val CHOOSER_CLASS = ".v2.ChooserActivity"
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java
new file mode 100644
index 0000000..2d9be81
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java
@@ -0,0 +1,666 @@
+/*
+ * Copyright (C) 2019 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.intentresolver.v2;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.viewpager.widget.PagerAdapter;
+import androidx.viewpager.widget.ViewPager;
+
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.v2.emptystate.EmptyStateUiHelper;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Skeletal {@link PagerAdapter} implementation for a UI with per-profile tabs (as in Sharesheet).
+ * <p>
+ * TODO: attempt to further restrict visibility/improve encapsulation in the methods we expose.
+ * <p>
+ * TODO: deprecate and audit/fix usages of any methods that refer to the "active" or "inactive"
+ * <p>
+ * adapters; these were marked {@link VisibleForTesting} and their usage seems like an accident
+ * waiting to happen since clients seem to make assumptions about which adapter will be "active" in
+ * a particular context, and more explicit APIs would make sure those were valid.
+ * <p>
+ * TODO: consider renaming legacy methods (e.g. why do we know it's a "list", not just a "page"?)
+ * <p>
+ * TODO: this is part of an in-progress refactor to merge with `GenericMultiProfilePagerAdapter`.
+ * As originally noted there, we've reduced explicit references to the `ResolverListAdapter` base
+ * type and may be able to drop the type constraint.
+ *
+ * @param <PageViewT> the type of the widget that represents the contents of a page in this adapter
+ * @param <SinglePageAdapterT> the type of a "root" adapter class to be instantiated and included in
+ * the per-profile records.
+ * @param <ListAdapterT> the concrete type of a {@link ResolverListAdapter} implementation to
+ * control the contents of a given per-profile list. This is provided for convenience, since it must
+ * be possible to get the list adapter from the page adapter via our
+ * <code>mListAdapterExtractor</code>.
+ */
+public class MultiProfilePagerAdapter<
+ PageViewT extends ViewGroup,
+ SinglePageAdapterT,
+ ListAdapterT extends ResolverListAdapter> extends PagerAdapter {
+
+ /**
+ * Delegate to set up a given adapter and page view to be used together.
+ * @param <PageViewT> (as in {@link MultiProfilePagerAdapter}).
+ * @param <SinglePageAdapterT> (as in {@link MultiProfilePagerAdapter}).
+ */
+ public interface AdapterBinder<PageViewT, SinglePageAdapterT> {
+ /**
+ * The given {@code view} will be associated with the given {@code adapter}. Do any work
+ * necessary to configure them compatibly, introduce them to each other, etc.
+ */
+ void bind(PageViewT view, SinglePageAdapterT adapter);
+ }
+
+ public static final int PROFILE_PERSONAL = 0;
+ public static final int PROFILE_WORK = 1;
+
+ @IntDef({PROFILE_PERSONAL, PROFILE_WORK})
+ public @interface Profile {}
+
+ private final Function<SinglePageAdapterT, ListAdapterT> mListAdapterExtractor;
+ private final AdapterBinder<PageViewT, SinglePageAdapterT> mAdapterBinder;
+ private final Supplier<ViewGroup> mPageViewInflater;
+
+ private final ImmutableList<ProfileDescriptor<PageViewT, SinglePageAdapterT>> mItems;
+
+ private final EmptyStateProvider mEmptyStateProvider;
+ private final UserHandle mWorkProfileUserHandle;
+ private final UserHandle mCloneProfileUserHandle;
+ private final Supplier<Boolean> mWorkProfileQuietModeChecker; // True when work is quiet.
+
+ private Set<Integer> mLoadedPages;
+ private int mCurrentPage;
+ private OnProfileSelectedListener mOnProfileSelectedListener;
+
+ protected MultiProfilePagerAdapter(
+ Function<SinglePageAdapterT, ListAdapterT> listAdapterExtractor,
+ AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder,
+ ImmutableList<SinglePageAdapterT> adapters,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ @Profile int defaultProfile,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle,
+ Supplier<ViewGroup> pageViewInflater,
+ Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
+ mCurrentPage = defaultProfile;
+ mLoadedPages = new HashSet<>();
+ mWorkProfileUserHandle = workProfileUserHandle;
+ mCloneProfileUserHandle = cloneProfileUserHandle;
+ mEmptyStateProvider = emptyStateProvider;
+ mWorkProfileQuietModeChecker = workProfileQuietModeChecker;
+
+ mListAdapterExtractor = listAdapterExtractor;
+ mAdapterBinder = adapterBinder;
+ mPageViewInflater = pageViewInflater;
+
+ ImmutableList.Builder<ProfileDescriptor<PageViewT, SinglePageAdapterT>> items =
+ new ImmutableList.Builder<>();
+ for (SinglePageAdapterT adapter : adapters) {
+ items.add(createProfileDescriptor(adapter, containerBottomPaddingOverrideSupplier));
+ }
+ mItems = items.build();
+ }
+
+ private ProfileDescriptor<PageViewT, SinglePageAdapterT> createProfileDescriptor(
+ SinglePageAdapterT adapter,
+ Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
+ return new ProfileDescriptor<>(
+ mPageViewInflater.get(), adapter, containerBottomPaddingOverrideSupplier);
+ }
+
+ public void setOnProfileSelectedListener(OnProfileSelectedListener listener) {
+ mOnProfileSelectedListener = listener;
+ }
+
+ /**
+ * Sets this instance of this class as {@link ViewPager}'s {@link PagerAdapter} and sets
+ * an {@link ViewPager.OnPageChangeListener} where it keeps track of the currently displayed
+ * page and rebuilds the list.
+ */
+ public void setupViewPager(ViewPager viewPager) {
+ viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
+ @Override
+ public void onPageSelected(int position) {
+ mCurrentPage = position;
+ if (!mLoadedPages.contains(position)) {
+ rebuildActiveTab(true);
+ mLoadedPages.add(position);
+ }
+ if (mOnProfileSelectedListener != null) {
+ mOnProfileSelectedListener.onProfileSelected(position);
+ }
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ if (mOnProfileSelectedListener != null) {
+ mOnProfileSelectedListener.onProfilePageStateChanged(state);
+ }
+ }
+ });
+ viewPager.setAdapter(this);
+ viewPager.setCurrentItem(mCurrentPage);
+ mLoadedPages.add(mCurrentPage);
+ }
+
+ public void clearInactiveProfileCache() {
+ if (mLoadedPages.size() == 1) {
+ return;
+ }
+ mLoadedPages.remove(1 - mCurrentPage);
+ }
+
+ @Override
+ public final ViewGroup instantiateItem(ViewGroup container, int position) {
+ setupListAdapter(position);
+ final ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(position);
+ container.addView(descriptor.mRootView);
+ return descriptor.mRootView;
+ }
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object view) {
+ container.removeView((View) view);
+ }
+
+ @Override
+ public int getCount() {
+ return getItemCount();
+ }
+
+ public int getCurrentPage() {
+ return mCurrentPage;
+ }
+
+ public final @Profile int getActiveProfile() {
+ // TODO: here and elsewhere in this class, distinguish between a "profile ID" integer and
+ // its mapped "page index." When we support more than two profiles, this won't be a "stable
+ // mapping" -- some particular profile may not be represented by a "page," but the ones that
+ // are will be assigned contiguous page numbers that skip over the holes.
+ return getCurrentPage();
+ }
+
+ @VisibleForTesting
+ public UserHandle getCurrentUserHandle() {
+ return getActiveListAdapter().getUserHandle();
+ }
+
+ @Override
+ public boolean isViewFromObject(View view, Object object) {
+ return view == object;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ return null;
+ }
+
+ public UserHandle getCloneUserHandle() {
+ return mCloneProfileUserHandle;
+ }
+
+ /**
+ * Returns the {@link ProfileDescriptor} relevant to the given <code>pageIndex</code>.
+ * <ul>
+ * <li>For a device with only one user, <code>pageIndex</code> value of
+ * <code>0</code> would return the personal profile {@link ProfileDescriptor}.</li>
+ * <li>For a device with a work profile, <code>pageIndex</code> value of <code>0</code> would
+ * return the personal profile {@link ProfileDescriptor}, and <code>pageIndex</code> value of
+ * <code>1</code> would return the work profile {@link ProfileDescriptor}.</li>
+ * </ul>
+ */
+ private ProfileDescriptor<PageViewT, SinglePageAdapterT> getItem(int pageIndex) {
+ return mItems.get(pageIndex);
+ }
+
+ private ViewGroup getEmptyStateView(int pageIndex) {
+ return getItem(pageIndex).getEmptyStateView();
+ }
+
+ public ViewGroup getActiveEmptyStateView() {
+ return getEmptyStateView(getCurrentPage());
+ }
+
+ /**
+ * Returns the number of {@link ProfileDescriptor} objects.
+ * <p>For a normal consumer device with only one user returns <code>1</code>.
+ * <p>For a device with a work profile returns <code>2</code>.
+ */
+ public final int getItemCount() {
+ return mItems.size();
+ }
+
+ public final PageViewT getListViewForIndex(int index) {
+ return getItem(index).mView;
+ }
+
+ /**
+ * Returns the adapter of the list view for the relevant page specified by
+ * <code>pageIndex</code>.
+ * <p>This method is meant to be implemented with an implementation-specific return type
+ * depending on the adapter type.
+ */
+ @VisibleForTesting
+ public final SinglePageAdapterT getAdapterForIndex(int index) {
+ return getItem(index).mAdapter;
+ }
+
+ /**
+ * Performs view-related initialization procedures for the adapter specified
+ * by <code>pageIndex</code>.
+ */
+ public final void setupListAdapter(int pageIndex) {
+ mAdapterBinder.bind(getListViewForIndex(pageIndex), getAdapterForIndex(pageIndex));
+ }
+
+ /**
+ * Returns the {@link ListAdapterT} instance of the profile that represents
+ * <code>userHandle</code>. If there is no such adapter for the specified
+ * <code>userHandle</code>, returns {@code null}.
+ * <p>For example, if there is a work profile on the device with user id 10, calling this method
+ * with <code>UserHandle.of(10)</code> returns the work profile {@link ListAdapterT}.
+ */
+ @Nullable
+ public final ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) {
+ if (getPersonalListAdapter().getUserHandle().equals(userHandle)
+ || userHandle.equals(getCloneUserHandle())) {
+ return getPersonalListAdapter();
+ } else if ((getWorkListAdapter() != null)
+ && getWorkListAdapter().getUserHandle().equals(userHandle)) {
+ return getWorkListAdapter();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link ListAdapterT} instance of the profile that is currently visible
+ * to the user.
+ * <p>For example, if the user is viewing the work tab in the share sheet, this method returns
+ * the work profile {@link ListAdapterT}.
+ * @see #getInactiveListAdapter()
+ */
+ @VisibleForTesting
+ public final ListAdapterT getActiveListAdapter() {
+ return mListAdapterExtractor.apply(getAdapterForIndex(getCurrentPage()));
+ }
+
+ /**
+ * If this is a device with a work profile, returns the {@link ListAdapterT} instance
+ * of the profile that is <b><i>not</i></b> currently visible to the user. Otherwise returns
+ * {@code null}.
+ * <p>For example, if the user is viewing the work tab in the share sheet, this method returns
+ * the personal profile {@link ListAdapterT}.
+ * @see #getActiveListAdapter()
+ */
+ @VisibleForTesting
+ @Nullable
+ public final ListAdapterT getInactiveListAdapter() {
+ if (getCount() < 2) {
+ return null;
+ }
+ return mListAdapterExtractor.apply(getAdapterForIndex(1 - getCurrentPage()));
+ }
+
+ public final ListAdapterT getPersonalListAdapter() {
+ return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_PERSONAL));
+ }
+
+ /** @return whether our tab data contains a page for the specified {@code profile} ID. */
+ public final boolean hasPageForProfile(@Profile int profile) {
+ // TODO: here and elsewhere in this class, distinguish between a "profile ID" integer and
+ // its mapped "page index." When we support more than two profiles, this won't be a "stable
+ // mapping" -- some particular profile may not be represented by a "page," but the ones that
+ // are will be assigned contiguous page numbers that skip over the holes.
+ return hasAdapterForIndex(profile);
+ }
+
+ @Nullable
+ public final ListAdapterT getWorkListAdapter() {
+ if (!hasAdapterForIndex(PROFILE_WORK)) {
+ return null;
+ }
+ return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_WORK));
+ }
+
+ public final SinglePageAdapterT getCurrentRootAdapter() {
+ return getAdapterForIndex(getCurrentPage());
+ }
+
+ public final PageViewT getActiveAdapterView() {
+ return getListViewForIndex(getCurrentPage());
+ }
+
+ @Nullable
+ public final PageViewT getInactiveAdapterView() {
+ if (getCount() < 2) {
+ return null;
+ }
+ return getListViewForIndex(1 - getCurrentPage());
+ }
+
+ private boolean anyAdapterHasItems() {
+ for (int i = 0; i < mItems.size(); ++i) {
+ ListAdapterT listAdapter = mListAdapterExtractor.apply(getAdapterForIndex(i));
+ if (listAdapter.getCount() > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void refreshPackagesInAllTabs() {
+ // TODO: handle all inactive profiles; for now we can only have at most one. It's unclear if
+ // this legacy logic really requires the active tab to be rebuilt first, or if we could just
+ // iterate over the tabs in arbitrary order.
+ getActiveListAdapter().handlePackagesChanged();
+ if (getCount() > 1) {
+ getInactiveListAdapter().handlePackagesChanged();
+ }
+ }
+
+ /**
+ * Notify that there has been a package change which could potentially modify the set of targets
+ * that should be shown in the specified {@code listAdapter}. This <em>may</em> result in
+ * "rebuilding" the target list for that adapter.
+ *
+ * @param listAdapter an adapter that may need to be updated after the package-change event.
+ * @param waitingToEnableWorkProfile whether we've turned on the work profile, but haven't yet
+ * seen an {@code ACTION_USER_UNLOCKED} broadcast. In this case we skip the rebuild of any
+ * work-profile adapter because we wouldn't expect meaningful results -- but another rebuild
+ * will be prompted when we eventually get the broadcast.
+ *
+ * @return whether we're able to proceed with a Sharesheet session after processing this
+ * package-change event. If false, we were able to rebuild the targets but determined that there
+ * aren't any we could present in the UI without the app looking broken, so we should just quit.
+ */
+ public boolean onHandlePackagesChanged(
+ ListAdapterT listAdapter, boolean waitingToEnableWorkProfile) {
+ if (listAdapter == getActiveListAdapter()) {
+ if (listAdapter.getUserHandle().equals(mWorkProfileUserHandle)
+ && waitingToEnableWorkProfile) {
+ // We have just turned on the work profile and entered the passcode to start it,
+ // now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no
+ // point in reloading the list now, since the work profile user is still turning on.
+ return true;
+ }
+
+ boolean listRebuilt = rebuildActiveTab(true);
+ if (listRebuilt) {
+ listAdapter.notifyDataSetChanged();
+ }
+
+ // TODO: shouldn't we check that the inactive tabs are built before declaring that we
+ // have to quit for lack of items?
+ return anyAdapterHasItems();
+ } else {
+ clearInactiveProfileCache();
+ return true;
+ }
+ }
+
+ /**
+ * Fully-rebuild the active tab and, if specified, partially-rebuild any other inactive tabs.
+ */
+ public boolean rebuildTabs(boolean includePartialRebuildOfInactiveTabs) {
+ // TODO: we may be able to determine `includePartialRebuildOfInactiveTabs` ourselves as
+ // a function of our own instance state. OTOH the purpose of this "partial rebuild" is to
+ // be able to evaluate the intermediate state of one particular profile tab (i.e. work
+ // profile) that may not generalize well when we have other "inactive tabs." I.e., either we
+ // rebuild *all* the inactive tabs just to evaluate some auto-launch conditions that only
+ // depend on personal and/or work tabs, or we have to explicitly specify the ones we care
+ // about. It's not the pager-adapter's business to know "which ones we care about," so maybe
+ // they should be rebuilt lazily when-and-if it comes up (e.g. during the evaluation of
+ // autolaunch conditions).
+ boolean rebuildCompleted = rebuildActiveTab(true) || getActiveListAdapter().isTabLoaded();
+ if (includePartialRebuildOfInactiveTabs) {
+ boolean rebuildInactiveCompleted =
+ rebuildInactiveTab(false) || getInactiveListAdapter().isTabLoaded();
+ rebuildCompleted = rebuildCompleted && rebuildInactiveCompleted;
+ }
+ return rebuildCompleted;
+ }
+
+ /**
+ * Rebuilds the tab that is currently visible to the user.
+ * <p>Returns {@code true} if rebuild has completed.
+ */
+ public final boolean rebuildActiveTab(boolean doPostProcessing) {
+ Trace.beginSection("MultiProfilePagerAdapter#rebuildActiveTab");
+ boolean result = rebuildTab(getActiveListAdapter(), doPostProcessing);
+ Trace.endSection();
+ return result;
+ }
+
+ /**
+ * Rebuilds the tab that is not currently visible to the user, if such one exists.
+ * <p>Returns {@code true} if rebuild has completed.
+ */
+ private boolean rebuildInactiveTab(boolean doPostProcessing) {
+ Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab");
+ if (getItemCount() == 1) {
+ Trace.endSection();
+ return false;
+ }
+ boolean result = rebuildTab(getInactiveListAdapter(), doPostProcessing);
+ Trace.endSection();
+ return result;
+ }
+
+ private int userHandleToPageIndex(UserHandle userHandle) {
+ if (userHandle.equals(getPersonalListAdapter().getUserHandle())) {
+ return PROFILE_PERSONAL;
+ } else {
+ return PROFILE_WORK;
+ }
+ }
+
+ protected boolean rebuildTab(ListAdapterT activeListAdapter, boolean doPostProcessing) {
+ if (shouldSkipRebuild(activeListAdapter)) {
+ activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true);
+ return false;
+ }
+ return activeListAdapter.rebuildList(doPostProcessing);
+ }
+
+ private boolean shouldSkipRebuild(ListAdapterT activeListAdapter) {
+ EmptyState emptyState = mEmptyStateProvider.getEmptyState(activeListAdapter);
+ return emptyState != null && emptyState.shouldSkipDataRebuild();
+ }
+
+ private boolean hasAdapterForIndex(int pageIndex) {
+ return (pageIndex < getCount());
+ }
+
+ /**
+ * The empty state screens are shown according to their priority:
+ * <ol>
+ * <li>(highest priority) cross-profile disabled by policy (handled in
+ * {@link #rebuildTab(ListAdapterT, boolean)})</li>
+ * <li>no apps available</li>
+ * <li>(least priority) work is off</li>
+ * </ol>
+ *
+ * The intention is to prevent the user from having to turn
+ * the work profile on if there will not be any apps resolved
+ * anyway.
+ *
+ * TODO: move this comment to the place where we configure our composite provider.
+ */
+ public void showEmptyResolverListEmptyState(ListAdapterT listAdapter) {
+ final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter);
+
+ if (emptyState == null) {
+ return;
+ }
+
+ emptyState.onEmptyStateShown();
+
+ View.OnClickListener clickListener = null;
+
+ if (emptyState.getButtonClickListener() != null) {
+ clickListener = v -> emptyState.getButtonClickListener().onClick(() -> {
+ ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
+ userHandleToPageIndex(listAdapter.getUserHandle()));
+ descriptor.mEmptyStateUi.showSpinner();
+ });
+ }
+
+ showEmptyState(listAdapter, emptyState, clickListener);
+ }
+
+ /**
+ * Class to get user id of the current process
+ */
+ public static class MyUserIdProvider {
+ /**
+ * @return user id of the current process
+ */
+ public int getMyUserId() {
+ return UserHandle.myUserId();
+ }
+ }
+
+ private void showEmptyState(
+ ListAdapterT activeListAdapter,
+ EmptyState emptyState,
+ View.OnClickListener buttonOnClick) {
+ ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
+ userHandleToPageIndex(activeListAdapter.getUserHandle()));
+ descriptor.mEmptyStateUi.showEmptyState(emptyState, buttonOnClick);
+ activeListAdapter.markTabLoaded();
+ }
+
+ /**
+ * Sets up the padding of the view containing the empty state screens for the current adapter
+ * view.
+ */
+ protected final void setupContainerPadding() {
+ getItem(getCurrentPage()).setupContainerPadding();
+ }
+
+ public void showListView(ListAdapterT activeListAdapter) {
+ ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
+ userHandleToPageIndex(activeListAdapter.getUserHandle()));
+ descriptor.mEmptyStateUi.hide();
+ }
+
+ /**
+ * @return whether any "inactive" tab's adapter would show an empty-state screen in our current
+ * application state.
+ */
+ public final boolean shouldShowEmptyStateScreenInAnyInactiveAdapter() {
+ if (getCount() < 2) {
+ return false;
+ }
+ // TODO: check against *any* inactive adapter; for now we only have one.
+ return shouldShowEmptyStateScreen(getInactiveListAdapter());
+ }
+
+ public boolean shouldShowEmptyStateScreen(ListAdapterT listAdapter) {
+ int count = listAdapter.getUnfilteredCount();
+ return (count == 0 && listAdapter.getPlaceholderCount() == 0)
+ || (listAdapter.getUserHandle().equals(mWorkProfileUserHandle)
+ && mWorkProfileQuietModeChecker.get());
+ }
+
+ // TODO: `ChooserActivity` also has a per-profile record type. Maybe the "multi-profile pager"
+ // should be the owner of all per-profile data (especially now that the API is generic)?
+ private static class ProfileDescriptor<PageViewT, SinglePageAdapterT> {
+ final ViewGroup mRootView;
+ final EmptyStateUiHelper mEmptyStateUi;
+
+ // TODO: post-refactoring, we may not need to retain these ivars directly (since they may
+ // be encapsulated within the `EmptyStateUiHelper`?).
+ private final ViewGroup mEmptyStateView;
+
+ private final SinglePageAdapterT mAdapter;
+ private final PageViewT mView;
+
+ ProfileDescriptor(
+ ViewGroup rootView,
+ SinglePageAdapterT adapter,
+ Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
+ mRootView = rootView;
+ mAdapter = adapter;
+ mEmptyStateView = rootView.findViewById(com.android.internal.R.id.resolver_empty_state);
+ mView = (PageViewT) rootView.findViewById(com.android.internal.R.id.resolver_list);
+ mEmptyStateUi = new EmptyStateUiHelper(
+ rootView,
+ com.android.internal.R.id.resolver_list,
+ containerBottomPaddingOverrideSupplier);
+ }
+
+ protected ViewGroup getEmptyStateView() {
+ return mEmptyStateView;
+ }
+
+ private void setupContainerPadding() {
+ mEmptyStateUi.setupContainerPadding();
+ }
+ }
+
+ /** Listener interface for changes between the per-profile UI tabs. */
+ public interface OnProfileSelectedListener {
+ /**
+ * Callback for when the user changes the active tab from personal to work or vice versa.
+ * <p>This callback is only called when the intent resolver or share sheet shows
+ * the work and personal profiles.
+ * @param profileIndex {@link #PROFILE_PERSONAL} if the personal profile was selected or
+ * {@link #PROFILE_WORK} if the work profile was selected.
+ */
+ void onProfileSelected(int profileIndex);
+
+
+ /**
+ * Callback for when the scroll state changes. Useful for discovering when the user begins
+ * dragging, when the pager is automatically settling to the current page, or when it is
+ * fully stopped/idle.
+ * @param state {@link ViewPager#SCROLL_STATE_IDLE}, {@link ViewPager#SCROLL_STATE_DRAGGING}
+ * or {@link ViewPager#SCROLL_STATE_SETTLING}
+ * @see ViewPager.OnPageChangeListener#onPageScrollStateChanged
+ */
+ void onProfilePageStateChanged(int state);
+ }
+
+ /**
+ * Listener for when the user switches on the work profile from the work tab.
+ */
+ public interface OnSwitchOnWorkSelectedListener {
+ /**
+ * Callback for when the user switches on the work profile from the work tab.
+ */
+ void onSwitchOnWorkSelected();
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ResolverActivity.java b/java/src/com/android/intentresolver/v2/ResolverActivity.java
new file mode 100644
index 0000000..2ba50ec
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ResolverActivity.java
@@ -0,0 +1,2181 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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.intentresolver.v2;
+
+import static android.Manifest.permission.INTERACT_ACROSS_PROFILES;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.PermissionChecker.PID_UNKNOWN;
+import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
+import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED;
+
+import static java.util.Collections.emptyList;
+import static java.util.Objects.requireNonNull;
+import static java.util.Objects.requireNonNullElse;
+
+import android.app.ActivityManager;
+import android.app.ActivityThread;
+import android.app.VoiceInteractor.PickOptionRequest;
+import android.app.VoiceInteractor.PickOptionRequest.Option;
+import android.app.VoiceInteractor.Prompt;
+import android.app.admin.DevicePolicyEventLogger;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.PermissionChecker;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Insets;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.PatternMatcher;
+import android.os.RemoteException;
+import android.os.StrictMode;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.stats.devicepolicy.DevicePolicyEnums;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.Space;
+import android.widget.TabHost;
+import android.widget.TabWidget;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.annotation.UiThread;
+import androidx.fragment.app.FragmentActivity;
+import androidx.viewpager.widget.ViewPager;
+
+import com.android.intentresolver.AnnotatedUserHandles;
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.ResolverListController;
+import com.android.intentresolver.WorkProfileAvailabilityManager;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.emptystate.CompositeEmptyStateProvider;
+import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.icons.TargetDataLoader;
+import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
+import com.android.intentresolver.v2.MultiProfilePagerAdapter.MyUserIdProvider;
+import com.android.intentresolver.v2.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener;
+import com.android.intentresolver.v2.MultiProfilePagerAdapter.Profile;
+import com.android.intentresolver.v2.data.repository.DevicePolicyResources;
+import com.android.intentresolver.v2.emptystate.NoAppsAvailableEmptyStateProvider;
+import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider;
+import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
+import com.android.intentresolver.v2.emptystate.WorkProfilePausedEmptyStateProvider;
+import com.android.intentresolver.v2.ui.ActionTitle;
+import com.android.intentresolver.widget.ResolverDrawerLayout;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.internal.util.LatencyTracker;
+
+import kotlin.Unit;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This is a copy of ResolverActivity to support IntentResolver's ChooserActivity. This code is
+ * *not* the resolver that is actually triggered by the system right now (you want
+ * frameworks/base/core/java/com/android/internal/app/ResolverActivity.java for that), the full
+ * migration is not complete.
+ */
+@UiThread
+public class ResolverActivity extends FragmentActivity implements
+ ResolverListAdapter.ResolverListCommunicator {
+
+ private final List<Runnable> mInit = new ArrayList<>();
+
+ protected ActivityLogic mLogic;
+
+ private DevicePolicyResources mDevicePolicyResources;
+
+ public ResolverActivity() {
+ mIsIntentPicker = getClass().equals(ResolverActivity.class);
+ }
+
+ protected ResolverActivity(boolean isIntentPicker) {
+ mIsIntentPicker = isIntentPicker;
+ }
+
+ private Button mAlwaysButton;
+ private Button mOnceButton;
+ protected View mProfileView;
+ private int mLastSelected = AbsListView.INVALID_POSITION;
+ private int mLayoutId;
+ private PickTargetOptionRequest mPickOptionRequest;
+ // Expected to be true if this object is ResolverActivity or is ResolverWrapperActivity.
+ private final boolean mIsIntentPicker;
+ protected ResolverDrawerLayout mResolverDrawerLayout;
+ protected PackageManager mPm;
+
+ private static final String TAG = "ResolverActivity";
+ private static final boolean DEBUG = false;
+ private static final String LAST_SHOWN_TAB_KEY = "last_shown_tab_key";
+
+ private boolean mRegistered;
+
+ protected Insets mSystemWindowInsets = null;
+ private Space mFooterSpacer = null;
+
+ /** See {@link #setRetainInOnStop}. */
+ private boolean mRetainInOnStop;
+
+ protected static final String METRICS_CATEGORY_RESOLVER = "intent_resolver";
+ protected static final String METRICS_CATEGORY_CHOOSER = "intent_chooser";
+
+ /** Tracks if we should ignore future broadcasts telling us the work profile is enabled */
+ private boolean mWorkProfileHasBeenEnabled = false;
+
+ private static final String TAB_TAG_PERSONAL = "personal";
+ private static final String TAB_TAG_WORK = "work";
+
+ private PackageMonitor mPersonalPackageMonitor;
+ private PackageMonitor mWorkPackageMonitor;
+
+ @VisibleForTesting
+ protected MultiProfilePagerAdapter mMultiProfilePagerAdapter;
+
+
+ // Intent extra for connected audio devices
+ public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device";
+
+ /**
+ * Integer extra to indicate which profile should be automatically selected.
+ * <p>Can only be used if there is a work profile.
+ * <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}.
+ */
+ protected static final String EXTRA_SELECTED_PROFILE =
+ "com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE";
+
+ /**
+ * {@link UserHandle} extra to indicate the user of the user that the starting intent
+ * originated from.
+ * <p>This is not necessarily the same as {@link #getUserId()} or {@link UserHandle#myUserId()},
+ * as there are edge cases when the intent resolver is launched in the other profile.
+ * For example, when we have 0 resolved apps in current profile and multiple resolved
+ * apps in the other profile, opening a link from the current profile launches the intent
+ * resolver in the other one. b/148536209 for more info.
+ */
+ static final String EXTRA_CALLING_USER =
+ "com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER";
+
+ protected static final int PROFILE_PERSONAL = MultiProfilePagerAdapter.PROFILE_PERSONAL;
+ protected static final int PROFILE_WORK = MultiProfilePagerAdapter.PROFILE_WORK;
+
+ private UserHandle mHeaderCreatorUser;
+
+ @Nullable
+ private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
+
+ protected final LatencyTracker mLatencyTracker = getLatencyTracker();
+
+ protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) {
+ return new PackageMonitor() {
+ @Override
+ public void onSomePackagesChanged() {
+ listAdapter.handlePackagesChanged();
+ updateProfileViewButton();
+ }
+
+ @Override
+ public boolean onPackageChanged(String packageName, int uid, String[] components) {
+ // We care about all package changes, not just the whole package itself which is
+ // default behavior.
+ return true;
+ }
+ };
+ }
+ protected interface Initializer {
+ void initialize(ActivityLogic value);
+ }
+
+ protected void setLogic(ActivityLogic logic) {
+ mLogic = logic;
+ }
+
+ protected void addInitializer(Runnable initializer) {
+ mInit.add(initializer);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (isFinishing()) {
+ // Performing a clean exit:
+ // Skip initializing anything.
+ return;
+ }
+ mDevicePolicyResources = new DevicePolicyResources(getApplication().getResources(),
+ requireNonNull(getSystemService(DevicePolicyManager.class)));
+ setLogic(new ResolverActivityLogic(
+ TAG,
+ () -> this,
+ this::onWorkProfileStatusUpdated));
+ addInitializer(this::init);
+ }
+
+ @Override
+ protected final void onPostCreate(@Nullable Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ mInit.forEach(Runnable::run);
+
+ if (savedInstanceState != null) {
+ resetButtonBar();
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ if (viewPager != null) {
+ viewPager.setCurrentItem(savedInstanceState.getInt(LAST_SHOWN_TAB_KEY));
+ }
+ mMultiProfilePagerAdapter.clearInactiveProfileCache();
+ }
+ }
+
+ private void init() {
+ setTheme(mLogic.getThemeResId());
+ mLogic.preInitialization();
+
+ Intent intent = mLogic.getTargetIntent();
+ List<Intent> initialIntents = mLogic.getInitialIntents();
+ TargetDataLoader targetDataLoader = mLogic.getTargetDataLoader();
+
+ // Calling UID did not have valid permissions
+ if (mLogic.getAnnotatedUserHandles() == null) {
+ finish();
+ return;
+ }
+
+ mPm = getPackageManager();
+
+ // The last argument of createResolverListAdapter is whether to do special handling
+ // of the last used choice to highlight it in the list. We need to always
+ // turn this off when running under voice interaction, since it results in
+ // a more complicated UI that the current voice interaction flow is not able
+ // to handle. We also turn it off when multiple tabs are shown to simplify the UX.
+ // We also turn it off when clonedProfile is present on the device, because we might have
+ // different "last chosen" activities in the different profiles, and PackageManager doesn't
+ // provide any more information to help us select between them.
+ boolean filterLastUsed = mLogic.getSupportsAlwaysUseOption() && !isVoiceInteraction()
+ && !shouldShowTabs() && !hasCloneProfile();
+ mMultiProfilePagerAdapter = createMultiProfilePagerAdapter(
+ requireNonNullElse(initialIntents, emptyList()).toArray(new Intent[0]),
+ /* resolutionList = */ null,
+ filterLastUsed,
+ targetDataLoader
+ );
+ if (configureContentView(targetDataLoader)) {
+ return;
+ }
+
+ mPersonalPackageMonitor = createPackageMonitor(
+ mMultiProfilePagerAdapter.getPersonalListAdapter());
+ mPersonalPackageMonitor.register(
+ this,
+ getMainLooper(),
+ requireAnnotatedUserHandles().personalProfileUserHandle,
+ false
+ );
+ if (hasWorkProfile()) {
+ mWorkPackageMonitor = createPackageMonitor(
+ mMultiProfilePagerAdapter.getWorkListAdapter());
+ mWorkPackageMonitor.register(
+ this,
+ getMainLooper(),
+ requireAnnotatedUserHandles().workProfileUserHandle,
+ false
+ );
+ }
+
+ mRegistered = true;
+
+ final ResolverDrawerLayout rdl = findViewById(com.android.internal.R.id.contentPanel);
+ if (rdl != null) {
+ rdl.setOnDismissedListener(new ResolverDrawerLayout.OnDismissedListener() {
+ @Override
+ public void onDismissed() {
+ finish();
+ }
+ });
+
+ boolean hasTouchScreen = getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN);
+
+ if (isVoiceInteraction() || !hasTouchScreen) {
+ rdl.setCollapsed(false);
+ }
+
+ rdl.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ rdl.setOnApplyWindowInsetsListener(this::onApplyWindowInsets);
+
+ mResolverDrawerLayout = rdl;
+ }
+
+ mProfileView = findViewById(com.android.internal.R.id.profile_button);
+ if (mProfileView != null) {
+ mProfileView.setOnClickListener(this::onProfileClick);
+ updateProfileViewButton();
+ }
+
+ final Set<String> categories = intent.getCategories();
+ MetricsLogger.action(this, mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()
+ ? MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_APP_FEATURED
+ : MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_NONE_FEATURED,
+ intent.getAction() + ":" + intent.getType() + ":"
+ + (categories != null ? Arrays.toString(categories.toArray()) : ""));
+ }
+
+ protected MultiProfilePagerAdapter createMultiProfilePagerAdapter(
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ MultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null;
+ if (shouldShowTabs()) {
+ resolverMultiProfilePagerAdapter =
+ createResolverMultiProfilePagerAdapterForTwoProfiles(
+ initialIntents, resolutionList, filterLastUsed, targetDataLoader);
+ } else {
+ resolverMultiProfilePagerAdapter = createResolverMultiProfilePagerAdapterForOneProfile(
+ initialIntents, resolutionList, filterLastUsed, targetDataLoader);
+ }
+ return resolverMultiProfilePagerAdapter;
+ }
+
+ protected EmptyStateProvider createBlockerEmptyStateProvider() {
+ final boolean shouldShowNoCrossProfileIntentsEmptyState = getUser().equals(getIntentUser());
+
+ if (!shouldShowNoCrossProfileIntentsEmptyState) {
+ // Implementation that doesn't show any blockers
+ return new EmptyStateProvider() {};
+ }
+
+ final EmptyState noWorkToPersonalEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
+ /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
+ /* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_PERSONAL,
+ /* defaultSubtitleResource= */
+ R.string.resolver_cant_access_personal_apps_explanation,
+ /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL,
+ /* devicePolicyEventCategory= */
+ ResolverActivity.METRICS_CATEGORY_RESOLVER);
+
+ final EmptyState noPersonalToWorkEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
+ /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
+ /* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_WORK,
+ /* defaultSubtitleResource= */
+ R.string.resolver_cant_access_work_apps_explanation,
+ /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK,
+ /* devicePolicyEventCategory= */
+ ResolverActivity.METRICS_CATEGORY_RESOLVER);
+
+ return new NoCrossProfileEmptyStateProvider(
+ requireAnnotatedUserHandles().personalProfileUserHandle,
+ noWorkToPersonalEmptyState,
+ noPersonalToWorkEmptyState,
+ createCrossProfileIntentsChecker(),
+ requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
+ }
+
+ /**
+ * Numerous layouts are supported, each with optional ViewGroups.
+ * Make sure the inset gets added to the correct View, using
+ * a footer for Lists so it can properly scroll under the navbar.
+ */
+ protected boolean shouldAddFooterView() {
+ if (useLayoutWithDefault()) return true;
+
+ View buttonBar = findViewById(com.android.internal.R.id.button_bar);
+ if (buttonBar == null || buttonBar.getVisibility() == View.GONE) return true;
+
+ return false;
+ }
+
+ protected void applyFooterView(int height) {
+ if (mFooterSpacer == null) {
+ mFooterSpacer = new Space(getApplicationContext());
+ } else {
+ ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
+ .getActiveAdapterView().removeFooterView(mFooterSpacer);
+ }
+ mFooterSpacer.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT,
+ mSystemWindowInsets.bottom));
+ ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
+ .getActiveAdapterView().addFooterView(mFooterSpacer);
+ }
+
+ protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
+ mSystemWindowInsets = insets.getSystemWindowInsets();
+
+ mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top,
+ mSystemWindowInsets.right, 0);
+
+ resetButtonBar();
+
+ if (shouldUseMiniResolver()) {
+ View buttonContainer = findViewById(com.android.internal.R.id.button_bar_container);
+ buttonContainer.setPadding(0, 0, 0, mSystemWindowInsets.bottom
+ + getResources().getDimensionPixelOffset(R.dimen.resolver_button_bar_spacing));
+ }
+
+ // Need extra padding so the list can fully scroll up
+ if (shouldAddFooterView()) {
+ applyFooterView(mSystemWindowInsets.bottom);
+ }
+
+ return insets.consumeSystemWindowInsets();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
+ if (mIsIntentPicker && shouldShowTabs() && !useLayoutWithDefault()
+ && !shouldUseMiniResolver()) {
+ updateIntentPickerPaddings();
+ }
+
+ if (mSystemWindowInsets != null) {
+ mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top,
+ mSystemWindowInsets.right, 0);
+ }
+ }
+
+ public int getLayoutResource() {
+ return R.layout.resolver_list;
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ final Window window = this.getWindow();
+ final WindowManager.LayoutParams attrs = window.getAttributes();
+ attrs.privateFlags &= ~SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+ window.setAttributes(attrs);
+
+ if (mRegistered) {
+ mPersonalPackageMonitor.unregister();
+ if (mWorkPackageMonitor != null) {
+ mWorkPackageMonitor.unregister();
+ }
+ mRegistered = false;
+ }
+ final Intent intent = getIntent();
+ if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction()
+ && !mLogic.getResolvingHome() && !mRetainInOnStop) {
+ // This resolver is in the unusual situation where it has been
+ // launched at the top of a new task. We don't let it be added
+ // to the recent tasks shown to the user, and we need to make sure
+ // that each time we are launched we get the correct launching
+ // uid (not re-using the same resolver from an old launching uid),
+ // so we will now finish ourself since being no longer visible,
+ // the user probably can't get back to us.
+ if (!isChangingConfigurations()) {
+ finish();
+ }
+ }
+ // TODO: should we clean up the work-profile manager before we potentially finish() above?
+ mLogic.getWorkProfileAvailabilityManager().unregisterWorkProfileStateReceiver(this);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (!isChangingConfigurations() && mPickOptionRequest != null) {
+ mPickOptionRequest.cancel();
+ }
+ if (mMultiProfilePagerAdapter != null
+ && mMultiProfilePagerAdapter.getActiveListAdapter() != null) {
+ mMultiProfilePagerAdapter.getActiveListAdapter().onDestroy();
+ }
+ }
+
+ public void onButtonClick(View v) {
+ final int id = v.getId();
+ ListView listView = (ListView) mMultiProfilePagerAdapter.getActiveAdapterView();
+ ResolverListAdapter currentListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();
+ int which = currentListAdapter.hasFilteredItem()
+ ? currentListAdapter.getFilteredPosition()
+ : listView.getCheckedItemPosition();
+ boolean hasIndexBeenFiltered = !currentListAdapter.hasFilteredItem();
+ startSelected(which, id == com.android.internal.R.id.button_always, hasIndexBeenFiltered);
+ }
+
+ public void startSelected(int which, boolean always, boolean hasIndexBeenFiltered) {
+ if (isFinishing()) {
+ return;
+ }
+ ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .resolveInfoForPosition(which, hasIndexBeenFiltered);
+ if (mLogic.getResolvingHome() && hasManagedProfile() && !supportsManagedProfiles(ri)) {
+ String launcherName = ri.activityInfo.loadLabel(getPackageManager()).toString();
+ Toast.makeText(this,
+ mDevicePolicyResources.getWorkProfileNotSupportedMessage(launcherName),
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .targetInfoForPosition(which, hasIndexBeenFiltered);
+ if (target == null) {
+ return;
+ }
+ if (onTargetSelected(target, always)) {
+ if (always && mLogic.getSupportsAlwaysUseOption()) {
+ MetricsLogger.action(
+ this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_ALWAYS);
+ } else if (mLogic.getSupportsAlwaysUseOption()) {
+ MetricsLogger.action(
+ this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_JUST_ONCE);
+ } else {
+ MetricsLogger.action(
+ this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_TAP);
+ }
+ MetricsLogger.action(this,
+ mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()
+ ? MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_APP_FEATURED
+ : MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_NONE_FEATURED);
+ finish();
+ }
+ }
+
+ /**
+ * Replace me in subclasses!
+ */
+ @Override // ResolverListCommunicator
+ public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
+ return defIntent;
+ }
+
+ protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildCompleted) {
+ final ItemClickListener listener = new ItemClickListener();
+ setupAdapterListView((ListView) mMultiProfilePagerAdapter.getActiveAdapterView(), listener);
+ if (shouldShowTabs() && mIsIntentPicker) {
+ final ResolverDrawerLayout rdl = findViewById(com.android.internal.R.id.contentPanel);
+ if (rdl != null) {
+ rdl.setMaxCollapsedHeight(getResources()
+ .getDimensionPixelSize(useLayoutWithDefault()
+ ? R.dimen.resolver_max_collapsed_height_with_default_with_tabs
+ : R.dimen.resolver_max_collapsed_height_with_tabs));
+ }
+ }
+ }
+
+ protected boolean onTargetSelected(TargetInfo target, boolean always) {
+ final ResolveInfo ri = target.getResolveInfo();
+ final Intent intent = target != null ? target.getResolvedIntent() : null;
+
+ if (intent != null && (mLogic.getSupportsAlwaysUseOption()
+ || mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem())
+ && mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredResolveList() != null) {
+ // Build a reasonable intent filter, based on what matched.
+ IntentFilter filter = new IntentFilter();
+ Intent filterIntent;
+
+ if (intent.getSelector() != null) {
+ filterIntent = intent.getSelector();
+ } else {
+ filterIntent = intent;
+ }
+
+ String action = filterIntent.getAction();
+ if (action != null) {
+ filter.addAction(action);
+ }
+ Set<String> categories = filterIntent.getCategories();
+ if (categories != null) {
+ for (String cat : categories) {
+ filter.addCategory(cat);
+ }
+ }
+ filter.addCategory(Intent.CATEGORY_DEFAULT);
+
+ int cat = ri.match & IntentFilter.MATCH_CATEGORY_MASK;
+ Uri data = filterIntent.getData();
+ if (cat == IntentFilter.MATCH_CATEGORY_TYPE) {
+ String mimeType = filterIntent.resolveType(this);
+ if (mimeType != null) {
+ try {
+ filter.addDataType(mimeType);
+ } catch (IntentFilter.MalformedMimeTypeException e) {
+ Log.w("ResolverActivity", e);
+ filter = null;
+ }
+ }
+ }
+ if (data != null && data.getScheme() != null) {
+ // We need the data specification if there was no type,
+ // OR if the scheme is not one of our magical "file:"
+ // or "content:" schemes (see IntentFilter for the reason).
+ if (cat != IntentFilter.MATCH_CATEGORY_TYPE
+ || (!"file".equals(data.getScheme())
+ && !"content".equals(data.getScheme()))) {
+ filter.addDataScheme(data.getScheme());
+
+ // Look through the resolved filter to determine which part
+ // of it matched the original Intent.
+ Iterator<PatternMatcher> pIt = ri.filter.schemeSpecificPartsIterator();
+ if (pIt != null) {
+ String ssp = data.getSchemeSpecificPart();
+ while (ssp != null && pIt.hasNext()) {
+ PatternMatcher p = pIt.next();
+ if (p.match(ssp)) {
+ filter.addDataSchemeSpecificPart(p.getPath(), p.getType());
+ break;
+ }
+ }
+ }
+ Iterator<IntentFilter.AuthorityEntry> aIt = ri.filter.authoritiesIterator();
+ if (aIt != null) {
+ while (aIt.hasNext()) {
+ IntentFilter.AuthorityEntry a = aIt.next();
+ if (a.match(data) >= 0) {
+ int port = a.getPort();
+ filter.addDataAuthority(a.getHost(),
+ port >= 0 ? Integer.toString(port) : null);
+ break;
+ }
+ }
+ }
+ pIt = ri.filter.pathsIterator();
+ if (pIt != null) {
+ String path = data.getPath();
+ while (path != null && pIt.hasNext()) {
+ PatternMatcher p = pIt.next();
+ if (p.match(path)) {
+ filter.addDataPath(p.getPath(), p.getType());
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (filter != null) {
+ final int N = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getUnfilteredResolveList().size();
+ ComponentName[] set;
+ // If we don't add back in the component for forwarding the intent to a managed
+ // profile, the preferred activity may not be updated correctly (as the set of
+ // components we tell it we knew about will have changed).
+ final boolean needToAddBackProfileForwardingComponent =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null;
+ if (!needToAddBackProfileForwardingComponent) {
+ set = new ComponentName[N];
+ } else {
+ set = new ComponentName[N + 1];
+ }
+
+ int bestMatch = 0;
+ for (int i=0; i<N; i++) {
+ ResolveInfo r = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getUnfilteredResolveList().get(i).getResolveInfoAt(0);
+ set[i] = new ComponentName(r.activityInfo.packageName,
+ r.activityInfo.name);
+ if (r.match > bestMatch) bestMatch = r.match;
+ }
+
+ if (needToAddBackProfileForwardingComponent) {
+ set[N] = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getOtherProfile().getResolvedComponentName();
+ final int otherProfileMatch = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getOtherProfile().getResolveInfo().match;
+ if (otherProfileMatch > bestMatch) bestMatch = otherProfileMatch;
+ }
+
+ if (always) {
+ final int userId = getUserId();
+ final PackageManager pm = getPackageManager();
+
+ // Set the preferred Activity
+ pm.addUniquePreferredActivity(filter, bestMatch, set, intent.getComponent());
+
+ if (ri.handleAllWebDataURI) {
+ // Set default Browser if needed
+ final String packageName = pm.getDefaultBrowserPackageNameAsUser(userId);
+ if (TextUtils.isEmpty(packageName)) {
+ pm.setDefaultBrowserPackageNameAsUser(ri.activityInfo.packageName, userId);
+ }
+ }
+ } else {
+ try {
+ mMultiProfilePagerAdapter.getActiveListAdapter()
+ .mResolverListController.setLastChosen(intent, filter, bestMatch);
+ } catch (RemoteException re) {
+ Log.d(TAG, "Error calling setLastChosenActivity\n" + re);
+ }
+ }
+ }
+ }
+
+ if (target != null) {
+ safelyStartActivity(target);
+
+ // Rely on the ActivityManager to pop up a dialog regarding app suspension
+ // and return false
+ if (target.isSuspended()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public void onActivityStarted(TargetInfo cti) {
+ // Do nothing
+ }
+
+ @Override // ResolverListCommunicator
+ public boolean shouldGetActivityMetadata() {
+ return false;
+ }
+
+ public boolean shouldAutoLaunchSingleChoice(TargetInfo target) {
+ return !target.isSuspended();
+ }
+
+ // TODO: this method takes an unused `UserHandle` because the override in `ChooserActivity` uses
+ // that data to set up other components as dependencies of the controller. In reality, these
+ // methods don't require polymorphism, because they're only invoked from within their respective
+ // concrete class; `ResolverActivity` will never call this method expecting to get a
+ // `ChooserListController` (subclass) result, because `ResolverActivity` only invokes this
+ // method as part of handling `createMultiProfilePagerAdapter()`, which is itself overridden in
+ // `ChooserActivity`. A future refactoring could better express the coupling between the adapter
+ // and controller types; in the meantime, structuring as an override (with matching signatures)
+ // shows that these methods are *structurally* related, and helps to prevent any regressions in
+ // the future if resolver *were* to make any (non-overridden) calls to a version that used a
+ // different signature (and thus didn't return the subclass type).
+ @VisibleForTesting
+ protected ResolverListController createListController(UserHandle userHandle) {
+ ResolverRankerServiceResolverComparator resolverComparator =
+ new ResolverRankerServiceResolverComparator(
+ this,
+ mLogic.getTargetIntent(),
+ mLogic.getReferrerPackageName(),
+ null,
+ null,
+ getResolverRankerServiceUserHandleList(userHandle),
+ null);
+ return new ResolverListController(
+ this,
+ mPm,
+ mLogic.getTargetIntent(),
+ mLogic.getReferrerPackageName(),
+ requireAnnotatedUserHandles().userIdOfCallingApp,
+ resolverComparator,
+ getQueryIntentsUser(userHandle));
+ }
+
+ /**
+ * Finishing procedures to be performed after the list has been rebuilt.
+ * </p>Subclasses must call postRebuildListInternal at the end of postRebuildList.
+ * @param rebuildCompleted
+ * @return <code>true</code> if the activity is finishing and creation should halt.
+ */
+ protected boolean postRebuildList(boolean rebuildCompleted) {
+ return postRebuildListInternal(rebuildCompleted);
+ }
+
+ void onHorizontalSwipeStateChanged(int state) {}
+
+ /**
+ * Callback called when user changes the profile tab.
+ * <p>This method is intended to be overridden by subclasses.
+ */
+ protected void onProfileTabSelected() { }
+
+ /**
+ * Add a label to signify that the user can pick a different app.
+ * @param adapter The adapter used to provide data to item views.
+ */
+ public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) {
+ final boolean useHeader = adapter.hasFilteredItem();
+ if (useHeader) {
+ FrameLayout stub = findViewById(com.android.internal.R.id.stub);
+ stub.setVisibility(View.VISIBLE);
+ TextView textView = (TextView) LayoutInflater.from(this).inflate(
+ R.layout.resolver_different_item_header, null, false);
+ if (shouldShowTabs()) {
+ textView.setGravity(Gravity.CENTER);
+ }
+ stub.addView(textView);
+ }
+ }
+
+ protected void resetButtonBar() {
+ if (!mLogic.getSupportsAlwaysUseOption()) {
+ return;
+ }
+ final ViewGroup buttonLayout = findViewById(com.android.internal.R.id.button_bar);
+ if (buttonLayout == null) {
+ Log.e(TAG, "Layout unexpectedly does not have a button bar");
+ return;
+ }
+ ResolverListAdapter activeListAdapter =
+ mMultiProfilePagerAdapter.getActiveListAdapter();
+ View buttonBarDivider = findViewById(com.android.internal.R.id.resolver_button_bar_divider);
+ if (!useLayoutWithDefault()) {
+ int inset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0;
+ buttonLayout.setPadding(buttonLayout.getPaddingLeft(), buttonLayout.getPaddingTop(),
+ buttonLayout.getPaddingRight(), getResources().getDimensionPixelSize(
+ R.dimen.resolver_button_bar_spacing) + inset);
+ }
+ if (activeListAdapter.isTabLoaded()
+ && mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)
+ && !useLayoutWithDefault()) {
+ buttonLayout.setVisibility(View.INVISIBLE);
+ if (buttonBarDivider != null) {
+ buttonBarDivider.setVisibility(View.INVISIBLE);
+ }
+ setButtonBarIgnoreOffset(/* ignoreOffset */ false);
+ return;
+ }
+ if (buttonBarDivider != null) {
+ buttonBarDivider.setVisibility(View.VISIBLE);
+ }
+ buttonLayout.setVisibility(View.VISIBLE);
+ setButtonBarIgnoreOffset(/* ignoreOffset */ true);
+
+ mOnceButton = (Button) buttonLayout.findViewById(com.android.internal.R.id.button_once);
+ mAlwaysButton = (Button) buttonLayout.findViewById(com.android.internal.R.id.button_always);
+
+ resetAlwaysOrOnceButtonBar();
+ }
+
+ protected String getMetricsCategory() {
+ return METRICS_CATEGORY_RESOLVER;
+ }
+
+ @Override // ResolverListCommunicator
+ public final void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
+ if (!mMultiProfilePagerAdapter.onHandlePackagesChanged(
+ listAdapter,
+ mLogic.getWorkProfileAvailabilityManager().isWaitingToEnableWorkProfile())) {
+ // We no longer have any items... just finish the activity.
+ finish();
+ }
+ }
+
+ protected void maybeLogProfileChange() {}
+
+ // @NonFinalForTesting
+ @VisibleForTesting
+ protected MyUserIdProvider createMyUserIdProvider() {
+ return new MyUserIdProvider();
+ }
+
+ // @NonFinalForTesting
+ @VisibleForTesting
+ protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() {
+ return new CrossProfileIntentsChecker(getContentResolver());
+ }
+
+ protected Unit onWorkProfileStatusUpdated() {
+ if (mMultiProfilePagerAdapter.getCurrentUserHandle().equals(
+ requireAnnotatedUserHandles().workProfileUserHandle)) {
+ mMultiProfilePagerAdapter.rebuildActiveTab(true);
+ } else {
+ mMultiProfilePagerAdapter.clearInactiveProfileCache();
+ }
+ return Unit.INSTANCE;
+ }
+
+ // @NonFinalForTesting
+ @VisibleForTesting
+ protected ResolverListAdapter createResolverListAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean filterLastUsed,
+ UserHandle userHandle,
+ TargetDataLoader targetDataLoader) {
+ UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()
+ && userHandle.equals(requireAnnotatedUserHandles().personalProfileUserHandle)
+ ? requireAnnotatedUserHandles().cloneProfileUserHandle : userHandle;
+ return new ResolverListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ resolutionList,
+ filterLastUsed,
+ createListController(userHandle),
+ userHandle,
+ mLogic.getTargetIntent(),
+ this,
+ initialIntentsUserSpace,
+ targetDataLoader);
+ }
+
+ private LatencyTracker getLatencyTracker() {
+ return LatencyTracker.getInstance(this);
+ }
+
+ /**
+ * Get the string resource to be used as a label for the link to the resolver activity for an
+ * action.
+ *
+ * @param action The action to resolve
+ *
+ * @return The string resource to be used as a label
+ */
+ public static @StringRes int getLabelRes(String action) {
+ return ActionTitle.forAction(action).labelRes;
+ }
+
+ protected final EmptyStateProvider createEmptyStateProvider(
+ @Nullable UserHandle workProfileUserHandle) {
+ final EmptyStateProvider blockerEmptyStateProvider = createBlockerEmptyStateProvider();
+
+ final EmptyStateProvider workProfileOffEmptyStateProvider =
+ new WorkProfilePausedEmptyStateProvider(this, workProfileUserHandle,
+ mLogic.getWorkProfileAvailabilityManager(),
+ /* onSwitchOnWorkSelectedListener= */
+ () -> {
+ if (mOnSwitchOnWorkSelectedListener != null) {
+ mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected();
+ }
+ },
+ getMetricsCategory());
+
+ final EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider(
+ this,
+ workProfileUserHandle,
+ requireAnnotatedUserHandles().personalProfileUserHandle,
+ getMetricsCategory(),
+ requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch
+ );
+
+ // Return composite provider, the order matters (the higher, the more priority)
+ return new CompositeEmptyStateProvider(
+ blockerEmptyStateProvider,
+ workProfileOffEmptyStateProvider,
+ noAppsEmptyStateProvider
+ );
+ }
+
+ private ResolverMultiProfilePagerAdapter
+ createResolverMultiProfilePagerAdapterForOneProfile(
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ ResolverListAdapter adapter = createResolverListAdapter(
+ /* context */ this,
+ mLogic.getPayloadIntents(),
+ initialIntents,
+ resolutionList,
+ filterLastUsed,
+ /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle,
+ targetDataLoader);
+ return new ResolverMultiProfilePagerAdapter(
+ /* context */ this,
+ adapter,
+ createEmptyStateProvider(/* workProfileUserHandle= */ null),
+ /* workProfileQuietModeChecker= */ () -> false,
+ /* workProfileUserHandle= */ null,
+ requireAnnotatedUserHandles().cloneProfileUserHandle);
+ }
+
+ private UserHandle getIntentUser() {
+ return getIntent().hasExtra(EXTRA_CALLING_USER)
+ ? getIntent().getParcelableExtra(EXTRA_CALLING_USER)
+ : requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch;
+ }
+
+ private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles(
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ // In the edge case when we have 0 apps in the current profile and >1 apps in the other,
+ // the intent resolver is started in the other profile. Since this is the only case when
+ // this happens, we check for it here and set the current profile's tab.
+ int selectedProfile = getCurrentProfile();
+ UserHandle intentUser = getIntentUser();
+ if (!requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch.equals(intentUser)) {
+ if (requireAnnotatedUserHandles().personalProfileUserHandle.equals(intentUser)) {
+ selectedProfile = PROFILE_PERSONAL;
+ } else if (requireAnnotatedUserHandles().workProfileUserHandle.equals(intentUser)) {
+ selectedProfile = PROFILE_WORK;
+ }
+ } else {
+ int selectedProfileExtra = getSelectedProfileExtra();
+ if (selectedProfileExtra != -1) {
+ selectedProfile = selectedProfileExtra;
+ }
+ }
+ // We only show the default app for the profile of the current user. The filterLastUsed
+ // flag determines whether to show a default app and that app is not shown in the
+ // resolver list. So filterLastUsed should be false for the other profile.
+ ResolverListAdapter personalAdapter = createResolverListAdapter(
+ /* context */ this,
+ mLogic.getPayloadIntents(),
+ selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
+ resolutionList,
+ (filterLastUsed && UserHandle.myUserId()
+ == requireAnnotatedUserHandles().personalProfileUserHandle.getIdentifier()),
+ /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle,
+ targetDataLoader);
+ UserHandle workProfileUserHandle = requireAnnotatedUserHandles().workProfileUserHandle;
+ ResolverListAdapter workAdapter = createResolverListAdapter(
+ /* context */ this,
+ mLogic.getPayloadIntents(),
+ selectedProfile == PROFILE_WORK ? initialIntents : null,
+ resolutionList,
+ (filterLastUsed && UserHandle.myUserId()
+ == workProfileUserHandle.getIdentifier()),
+ /* userHandle */ workProfileUserHandle,
+ targetDataLoader);
+ return new ResolverMultiProfilePagerAdapter(
+ /* context */ this,
+ personalAdapter,
+ workAdapter,
+ createEmptyStateProvider(workProfileUserHandle),
+ () -> mLogic.getWorkProfileAvailabilityManager().isQuietModeEnabled(),
+ selectedProfile,
+ workProfileUserHandle,
+ requireAnnotatedUserHandles().cloneProfileUserHandle);
+ }
+
+ /**
+ * Returns {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK} if the {@link
+ * #EXTRA_SELECTED_PROFILE} extra was supplied, or {@code -1} if no extra was supplied.
+ * @throws IllegalArgumentException if the value passed to the {@link #EXTRA_SELECTED_PROFILE}
+ * extra is not {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}
+ */
+ final int getSelectedProfileExtra() {
+ int selectedProfile = -1;
+ if (getIntent().hasExtra(EXTRA_SELECTED_PROFILE)) {
+ selectedProfile = getIntent().getIntExtra(EXTRA_SELECTED_PROFILE, /* defValue = */ -1);
+ if (selectedProfile != PROFILE_PERSONAL && selectedProfile != PROFILE_WORK) {
+ throw new IllegalArgumentException(EXTRA_SELECTED_PROFILE + " has invalid value "
+ + selectedProfile + ". Must be either ResolverActivity.PROFILE_PERSONAL or "
+ + "ResolverActivity.PROFILE_WORK.");
+ }
+ }
+ return selectedProfile;
+ }
+
+ protected final @Profile int getCurrentProfile() {
+ UserHandle launchUser = requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch;
+ UserHandle personalUser = requireAnnotatedUserHandles().personalProfileUserHandle;
+ return launchUser.equals(personalUser) ? PROFILE_PERSONAL : PROFILE_WORK;
+ }
+
+ private AnnotatedUserHandles requireAnnotatedUserHandles() {
+ return requireNonNull(mLogic.getAnnotatedUserHandles());
+ }
+
+ private boolean hasWorkProfile() {
+ return requireAnnotatedUserHandles().workProfileUserHandle != null;
+ }
+
+ private boolean hasCloneProfile() {
+ return requireAnnotatedUserHandles().cloneProfileUserHandle != null;
+ }
+
+ protected final boolean isLaunchedAsCloneProfile() {
+ UserHandle launchUser = requireAnnotatedUserHandles().userHandleSharesheetLaunchedAs;
+ UserHandle cloneUser = requireAnnotatedUserHandles().cloneProfileUserHandle;
+ return hasCloneProfile() && launchUser.equals(cloneUser);
+ }
+
+ protected final boolean shouldShowTabs() {
+ return hasWorkProfile();
+ }
+
+ protected final void onProfileClick(View v) {
+ final DisplayResolveInfo dri =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile();
+ if (dri == null) {
+ return;
+ }
+
+ // Do not show the profile switch message anymore.
+ mLogic.clearProfileSwitchMessage();
+
+ onTargetSelected(dri, false);
+ finish();
+ }
+
+ private void updateIntentPickerPaddings() {
+ View titleCont = findViewById(com.android.internal.R.id.title_container);
+ titleCont.setPadding(
+ titleCont.getPaddingLeft(),
+ titleCont.getPaddingTop(),
+ titleCont.getPaddingRight(),
+ getResources().getDimensionPixelSize(R.dimen.resolver_title_padding_bottom));
+ View buttonBar = findViewById(com.android.internal.R.id.button_bar);
+ buttonBar.setPadding(
+ buttonBar.getPaddingLeft(),
+ getResources().getDimensionPixelSize(R.dimen.resolver_button_bar_spacing),
+ buttonBar.getPaddingRight(),
+ getResources().getDimensionPixelSize(R.dimen.resolver_button_bar_spacing));
+ }
+
+ private void maybeLogCrossProfileTargetLaunch(TargetInfo cti, UserHandle currentUserHandle) {
+ if (!hasWorkProfile() || currentUserHandle.equals(getUser())) {
+ return;
+ }
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_CROSS_PROFILE_TARGET_OPENED)
+ .setBoolean(
+ currentUserHandle.equals(
+ requireAnnotatedUserHandles().personalProfileUserHandle))
+ .setStrings(getMetricsCategory(),
+ cti.isInDirectShareMetricsCategory() ? "direct_share" : "other_target")
+ .write();
+ }
+
+ @Override // ResolverListCommunicator
+ public final void sendVoiceChoicesIfNeeded() {
+ if (!isVoiceInteraction()) {
+ // Clearly not needed.
+ return;
+ }
+
+ int count = mMultiProfilePagerAdapter.getActiveListAdapter().getCount();
+ final Option[] options = new Option[count];
+ for (int i = 0; i < options.length; i++) {
+ TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter().getItem(i);
+ if (target == null) {
+ // If this occurs, a new set of targets is being loaded. Let that complete,
+ // and have the next call to send voice choices proceed instead.
+ return;
+ }
+ options[i] = optionForChooserTarget(target, i);
+ }
+
+ mPickOptionRequest = new PickTargetOptionRequest(
+ new Prompt(getTitle()), options, null);
+ getVoiceInteractor().submitRequest(mPickOptionRequest);
+ }
+
+ final Option optionForChooserTarget(TargetInfo target, int index) {
+ return new Option(getOrLoadDisplayLabel(target), index);
+ }
+
+ @Override // ResolverListCommunicator
+ public final void updateProfileViewButton() {
+ if (mProfileView == null) {
+ return;
+ }
+
+ final DisplayResolveInfo dri =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile();
+ if (dri != null && !shouldShowTabs()) {
+ mProfileView.setVisibility(View.VISIBLE);
+ View text = mProfileView.findViewById(com.android.internal.R.id.profile_button);
+ if (!(text instanceof TextView)) {
+ text = mProfileView.findViewById(com.android.internal.R.id.text1);
+ }
+ ((TextView) text).setText(dri.getDisplayLabel());
+ } else {
+ mProfileView.setVisibility(View.GONE);
+ }
+ }
+
+ protected final CharSequence getTitleForAction(Intent intent, int defaultTitleRes) {
+ final ActionTitle title = mLogic.getResolvingHome()
+ ? ActionTitle.HOME
+ : ActionTitle.forAction(intent.getAction());
+
+ // While there may already be a filtered item, we can only use it in the title if the list
+ // is already sorted and all information relevant to it is already in the list.
+ final boolean named =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getFilteredPosition() >= 0;
+ if (title == ActionTitle.DEFAULT && defaultTitleRes != 0) {
+ return getString(defaultTitleRes);
+ } else {
+ return named
+ ? getString(
+ title.namedTitleRes,
+ getOrLoadDisplayLabel(
+ mMultiProfilePagerAdapter
+ .getActiveListAdapter().getFilteredItem()))
+ : getString(title.titleRes);
+ }
+ }
+
+ final void dismiss() {
+ if (!isFinishing()) {
+ finish();
+ }
+ }
+
+ @Override
+ protected final void onRestart() {
+ super.onRestart();
+ if (!mRegistered) {
+ mPersonalPackageMonitor.register(
+ this,
+ getMainLooper(),
+ requireAnnotatedUserHandles().personalProfileUserHandle,
+ false);
+ if (hasWorkProfile()) {
+ if (mWorkPackageMonitor == null) {
+ mWorkPackageMonitor = createPackageMonitor(
+ mMultiProfilePagerAdapter.getWorkListAdapter());
+ }
+ mWorkPackageMonitor.register(
+ this,
+ getMainLooper(),
+ requireAnnotatedUserHandles().workProfileUserHandle,
+ false);
+ }
+ mRegistered = true;
+ }
+ WorkProfileAvailabilityManager workProfileAvailabilityManager =
+ mLogic.getWorkProfileAvailabilityManager();
+ if (hasWorkProfile() && workProfileAvailabilityManager.isWaitingToEnableWorkProfile()) {
+ if (workProfileAvailabilityManager.isQuietModeEnabled()) {
+ workProfileAvailabilityManager.markWorkProfileEnabledBroadcastReceived();
+ }
+ }
+ mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
+ updateProfileViewButton();
+ }
+
+ @Override
+ protected final void onStart() {
+ super.onStart();
+
+ this.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+ if (hasWorkProfile()) {
+ mLogic.getWorkProfileAvailabilityManager().registerWorkProfileStateReceiver(this);
+ }
+ }
+
+ @Override
+ protected final void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ if (viewPager != null) {
+ outState.putInt(LAST_SHOWN_TAB_KEY, viewPager.getCurrentItem());
+ }
+ }
+
+ private boolean hasManagedProfile() {
+ UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
+ if (userManager == null) {
+ return false;
+ }
+
+ try {
+ List<UserInfo> profiles = userManager.getProfiles(getUserId());
+ for (UserInfo userInfo : profiles) {
+ if (userInfo != null && userInfo.isManagedProfile()) {
+ return true;
+ }
+ }
+ } catch (SecurityException e) {
+ return false;
+ }
+ return false;
+ }
+
+ private boolean supportsManagedProfiles(ResolveInfo resolveInfo) {
+ try {
+ ApplicationInfo appInfo = getPackageManager().getApplicationInfo(
+ resolveInfo.activityInfo.packageName, 0 /* default flags */);
+ return appInfo.targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP;
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ private void setAlwaysButtonEnabled(boolean hasValidSelection, int checkedPos,
+ boolean filtered) {
+ if (!mMultiProfilePagerAdapter.getCurrentUserHandle().equals(getUser())) {
+ // Never allow the inactive profile to always open an app.
+ mAlwaysButton.setEnabled(false);
+ return;
+ }
+ // In case of clonedProfile being active, we do not allow the 'Always' option in the
+ // disambiguation dialog of Personal Profile as the package manager cannot distinguish
+ // between cross-profile preferred activities.
+ if (hasCloneProfile() && (mMultiProfilePagerAdapter.getCurrentPage() == PROFILE_PERSONAL)) {
+ mAlwaysButton.setEnabled(false);
+ return;
+ }
+ boolean enabled = false;
+ ResolveInfo ri = null;
+ if (hasValidSelection) {
+ ri = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .resolveInfoForPosition(checkedPos, filtered);
+ if (ri == null) {
+ Log.e(TAG, "Invalid position supplied to setAlwaysButtonEnabled");
+ return;
+ } else if (ri.targetUserId != UserHandle.USER_CURRENT) {
+ Log.e(TAG, "Attempted to set selection to resolve info for another user");
+ return;
+ } else {
+ enabled = true;
+ }
+
+ mAlwaysButton.setText(getResources()
+ .getString(R.string.activity_resolver_use_always));
+ }
+
+ if (ri != null) {
+ ActivityInfo activityInfo = ri.activityInfo;
+
+ boolean hasRecordPermission =
+ mPm.checkPermission(android.Manifest.permission.RECORD_AUDIO,
+ activityInfo.packageName)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (!hasRecordPermission) {
+ // OK, we know the record permission, is this a capture device
+ boolean hasAudioCapture =
+ getIntent().getBooleanExtra(
+ ResolverActivity.EXTRA_IS_AUDIO_CAPTURE_DEVICE, false);
+ enabled = !hasAudioCapture;
+ }
+ }
+ mAlwaysButton.setEnabled(enabled);
+ }
+
+ @Override // ResolverListCommunicator
+ public final void onPostListReady(ResolverListAdapter listAdapter, boolean doPostProcessing,
+ boolean rebuildCompleted) {
+ if (isAutolaunching()) {
+ return;
+ }
+ if (mIsIntentPicker) {
+ ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
+ .setUseLayoutWithDefault(useLayoutWithDefault());
+ }
+ if (mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(listAdapter)) {
+ mMultiProfilePagerAdapter.showEmptyResolverListEmptyState(listAdapter);
+ } else {
+ mMultiProfilePagerAdapter.showListView(listAdapter);
+ }
+ // showEmptyResolverListEmptyState can mark the tab as loaded,
+ // which is a precondition for auto launching
+ if (rebuildCompleted && maybeAutolaunchActivity()) {
+ return;
+ }
+ if (doPostProcessing) {
+ maybeCreateHeader(listAdapter);
+ resetButtonBar();
+ onListRebuilt(listAdapter, rebuildCompleted);
+ }
+ }
+
+ /** Start the activity specified by the {@link TargetInfo}.*/
+ public final void safelyStartActivity(TargetInfo cti) {
+ // In case cloned apps are present, we would want to start those apps in cloned user
+ // space, which will not be same as the adapter's userHandle. resolveInfo.userHandle
+ // identifies the correct user space in such cases.
+ UserHandle activityUserHandle = cti.getResolveInfo().userHandle;
+ safelyStartActivityAsUser(cti, activityUserHandle, null);
+ }
+
+ /**
+ * Start activity as a fixed user handle.
+ * @param cti TargetInfo to be launched.
+ * @param user User to launch this activity as.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
+ public final void safelyStartActivityAsUser(TargetInfo cti, UserHandle user) {
+ safelyStartActivityAsUser(cti, user, null);
+ }
+
+ protected final void safelyStartActivityAsUser(
+ TargetInfo cti, UserHandle user, @Nullable Bundle options) {
+ // We're dispatching intents that might be coming from legacy apps, so
+ // don't kill ourselves.
+ StrictMode.disableDeathOnFileUriExposure();
+ try {
+ safelyStartActivityInternal(cti, user, options);
+ } finally {
+ StrictMode.enableDeathOnFileUriExposure();
+ }
+ }
+
+ @VisibleForTesting
+ protected void safelyStartActivityInternal(
+ TargetInfo cti, UserHandle user, @Nullable Bundle options) {
+ // If the target is suspended, the activity will not be successfully launched.
+ // Do not unregister from package manager updates in this case
+ if (!cti.isSuspended() && mRegistered) {
+ if (mPersonalPackageMonitor != null) {
+ mPersonalPackageMonitor.unregister();
+ }
+ if (mWorkPackageMonitor != null) {
+ mWorkPackageMonitor.unregister();
+ }
+ mRegistered = false;
+ }
+ // If needed, show that intent is forwarded
+ // from managed profile to owner or other way around.
+ String profileSwitchMessage = mLogic.getProfileSwitchMessage();
+ if (profileSwitchMessage != null) {
+ Toast.makeText(this, profileSwitchMessage, Toast.LENGTH_LONG).show();
+ }
+ try {
+ if (cti.startAsCaller(this, options, user.getIdentifier())) {
+ onActivityStarted(cti);
+ maybeLogCrossProfileTargetLaunch(cti, user);
+ }
+ } catch (RuntimeException e) {
+ Slog.wtf(TAG,
+ "Unable to launch as uid " + requireAnnotatedUserHandles().userIdOfCallingApp
+ + " package " + getLaunchedFromPackage() + ", while running in "
+ + ActivityThread.currentProcessName(), e);
+ }
+ }
+
+ final void showTargetDetails(ResolveInfo ri) {
+ Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(Uri.fromParts("package", ri.activityInfo.packageName, null))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+ startActivityAsUser(in, mMultiProfilePagerAdapter.getCurrentUserHandle());
+ }
+
+ /**
+ * Sets up the content view.
+ * @return <code>true</code> if the activity is finishing and creation should halt.
+ */
+ private boolean configureContentView(TargetDataLoader targetDataLoader) {
+ if (mMultiProfilePagerAdapter.getActiveListAdapter() == null) {
+ throw new IllegalStateException("mMultiProfilePagerAdapter.getCurrentListAdapter() "
+ + "cannot be null.");
+ }
+ Trace.beginSection("configureContentView");
+ // We partially rebuild the inactive adapter to determine if we should auto launch
+ // isTabLoaded will be true here if the empty state screen is shown instead of the list.
+ // To date, we really only care about "partially rebuilding" tabs for work and/or personal.
+ boolean rebuildCompleted = mMultiProfilePagerAdapter.rebuildTabs(shouldShowTabs());
+
+ if (shouldUseMiniResolver()) {
+ configureMiniResolverContent(targetDataLoader);
+ Trace.endSection();
+ return false;
+ }
+
+ if (useLayoutWithDefault()) {
+ mLayoutId = R.layout.resolver_list_with_default;
+ } else {
+ mLayoutId = getLayoutResource();
+ }
+ setContentView(mLayoutId);
+ mMultiProfilePagerAdapter.setupViewPager(findViewById(com.android.internal.R.id.profile_pager));
+ boolean result = postRebuildList(rebuildCompleted);
+ Trace.endSection();
+ return result;
+ }
+
+ /**
+ * Mini resolver is shown when the user is choosing between browser[s] in this profile and a
+ * single app in the other profile (see shouldUseMiniResolver()). It shows the single app icon
+ * and asks the user if they'd like to open that cross-profile app or use the in-profile
+ * browser.
+ */
+ private void configureMiniResolverContent(TargetDataLoader targetDataLoader) {
+ mLayoutId = R.layout.miniresolver;
+ setContentView(mLayoutId);
+
+ // TODO: try to dedupe and use the pager's `getActiveProfile()` instead of the activity
+ // `getCurrentProfile()` (or align them if they're not currently equivalent). If they truly
+ // need to be distinct here, then `getCurrentProfile()` should at *least* get a more
+ // specific name -- but note that checking `getCurrentProfile()` here, then following
+ // `getActiveProfile()` to find the "in/active adapter," is exactly the legacy behavior.
+ boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK;
+
+ ResolverListAdapter sameProfileAdapter =
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getPersonalListAdapter()
+ : mMultiProfilePagerAdapter.getWorkListAdapter();
+
+ ResolverListAdapter inactiveAdapter =
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getWorkListAdapter()
+ : mMultiProfilePagerAdapter.getPersonalListAdapter();
+
+ DisplayResolveInfo sameProfileResolveInfo = sameProfileAdapter.getFirstDisplayResolveInfo();
+
+ final DisplayResolveInfo otherProfileResolveInfo =
+ inactiveAdapter.getFirstDisplayResolveInfo();
+
+ // Load the icon asynchronously
+ ImageView icon = findViewById(com.android.internal.R.id.icon);
+ targetDataLoader.loadAppTargetIcon(
+ otherProfileResolveInfo,
+ inactiveAdapter.getUserHandle(),
+ (drawable) -> {
+ if (!isDestroyed()) {
+ otherProfileResolveInfo.getDisplayIconHolder().setDisplayIcon(drawable);
+ new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo);
+ }
+ });
+
+ ((TextView) findViewById(com.android.internal.R.id.open_cross_profile)).setText(
+ getResources().getString(
+ inWorkProfile
+ ? R.string.miniresolver_open_in_personal
+ : R.string.miniresolver_open_in_work,
+ getOrLoadDisplayLabel(otherProfileResolveInfo)));
+ ((Button) findViewById(com.android.internal.R.id.use_same_profile_browser)).setText(
+ inWorkProfile ? R.string.miniresolver_use_work_browser
+ : R.string.miniresolver_use_personal_browser);
+
+ findViewById(com.android.internal.R.id.use_same_profile_browser).setOnClickListener(
+ v -> {
+ safelyStartActivity(sameProfileResolveInfo);
+ finish();
+ });
+
+ findViewById(com.android.internal.R.id.button_open).setOnClickListener(v -> {
+ Intent intent = otherProfileResolveInfo.getResolvedIntent();
+ safelyStartActivityAsUser(otherProfileResolveInfo, inactiveAdapter.getUserHandle());
+ finish();
+ });
+ }
+
+ private boolean isTwoPagePersonalAndWorkConfiguration() {
+ return (mMultiProfilePagerAdapter.getCount() == 2)
+ && mMultiProfilePagerAdapter.hasPageForProfile(PROFILE_PERSONAL)
+ && mMultiProfilePagerAdapter.hasPageForProfile(PROFILE_WORK);
+ }
+
+ /**
+ * Mini resolver should be used when all of the following are true:
+ * 1. This is the intent picker (ResolverActivity).
+ * 2. There are exactly two tabs, for the "personal" and "work" profiles.
+ * 3. This profile only has web browser matches.
+ * 4. The other profile has a single non-browser match.
+ */
+ private boolean shouldUseMiniResolver() {
+ if (!mIsIntentPicker) {
+ return false;
+ }
+ if (!isTwoPagePersonalAndWorkConfiguration()) {
+ return false;
+ }
+
+ ResolverListAdapter sameProfileAdapter =
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getPersonalListAdapter()
+ : mMultiProfilePagerAdapter.getWorkListAdapter();
+
+ ResolverListAdapter otherProfileAdapter =
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getWorkListAdapter()
+ : mMultiProfilePagerAdapter.getPersonalListAdapter();
+
+ if (sameProfileAdapter.getDisplayResolveInfoCount() == 0) {
+ Log.d(TAG, "No targets in the current profile");
+ return false;
+ }
+
+ if (otherProfileAdapter.getDisplayResolveInfoCount() != 1) {
+ Log.d(TAG, "Other-profile count: " + otherProfileAdapter.getDisplayResolveInfoCount());
+ return false;
+ }
+
+ if (otherProfileAdapter.allResolveInfosHandleAllWebDataUri()) {
+ Log.d(TAG, "Other profile is a web browser");
+ return false;
+ }
+
+ if (!sameProfileAdapter.allResolveInfosHandleAllWebDataUri()) {
+ Log.d(TAG, "Non-browser found in this profile");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Finishing procedures to be performed after the list has been rebuilt.
+ * @param rebuildCompleted
+ * @return <code>true</code> if the activity is finishing and creation should halt.
+ */
+ final boolean postRebuildListInternal(boolean rebuildCompleted) {
+ int count = mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();
+
+ // We only rebuild asynchronously when we have multiple elements to sort. In the case where
+ // we're already done, we can check if we should auto-launch immediately.
+ if (rebuildCompleted && maybeAutolaunchActivity()) {
+ return true;
+ }
+
+ setupViewVisibilities();
+
+ if (shouldShowTabs()) {
+ setupProfileTabs();
+ }
+
+ return false;
+ }
+
+ private int isPermissionGranted(String permission, int uid) {
+ return ActivityManager.checkComponentPermission(permission, uid,
+ /* owningUid= */-1, /* exported= */ true);
+ }
+
+ /**
+ * @return {@code true} if a resolved target is autolaunched, otherwise {@code false}
+ */
+ private boolean maybeAutolaunchActivity() {
+ int numberOfProfiles = mMultiProfilePagerAdapter.getItemCount();
+ if (numberOfProfiles == 1 && maybeAutolaunchIfSingleTarget()) {
+ return true;
+ } else if (maybeAutolaunchIfCrossProfileSupported()) {
+ // TODO(b/280988288): If the ChooserActivity is shown we should consider showing the
+ // correct intent-picker UIs (e.g., mini-resolver) if it was launched without
+ // ACTION_SEND.
+ return true;
+ }
+ return false;
+ }
+
+ private boolean maybeAutolaunchIfSingleTarget() {
+ int count = mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();
+ if (count != 1) {
+ return false;
+ }
+
+ if (mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null) {
+ return false;
+ }
+
+ // Only one target, so we're a candidate to auto-launch!
+ final TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .targetInfoForPosition(0, false);
+ if (shouldAutoLaunchSingleChoice(target)) {
+ safelyStartActivity(target);
+ finish();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * When we have just a personal and a work profile, we auto launch in the following scenario:
+ * - There is 1 resolved target on each profile
+ * - That target is the same app on both profiles
+ * - The target app has permission to communicate cross profiles
+ * - The target app has declared it supports cross-profile communication via manifest metadata
+ */
+ private boolean maybeAutolaunchIfCrossProfileSupported() {
+ if (!isTwoPagePersonalAndWorkConfiguration()) {
+ return false;
+ }
+
+ ResolverListAdapter activeListAdapter =
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getPersonalListAdapter()
+ : mMultiProfilePagerAdapter.getWorkListAdapter();
+
+ ResolverListAdapter inactiveListAdapter =
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getWorkListAdapter()
+ : mMultiProfilePagerAdapter.getPersonalListAdapter();
+
+ if (!activeListAdapter.isTabLoaded() || !inactiveListAdapter.isTabLoaded()) {
+ return false;
+ }
+
+ if ((activeListAdapter.getUnfilteredCount() != 1)
+ || (inactiveListAdapter.getUnfilteredCount() != 1)) {
+ return false;
+ }
+
+ TargetInfo activeProfileTarget = activeListAdapter.targetInfoForPosition(0, false);
+ TargetInfo inactiveProfileTarget = inactiveListAdapter.targetInfoForPosition(0, false);
+ if (!Objects.equals(
+ activeProfileTarget.getResolvedComponentName(),
+ inactiveProfileTarget.getResolvedComponentName())) {
+ return false;
+ }
+
+ if (!shouldAutoLaunchSingleChoice(activeProfileTarget)) {
+ return false;
+ }
+
+ String packageName = activeProfileTarget.getResolvedComponentName().getPackageName();
+ if (!canAppInteractCrossProfiles(packageName)) {
+ return false;
+ }
+
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET)
+ .setBoolean(activeListAdapter.getUserHandle()
+ .equals(requireAnnotatedUserHandles().personalProfileUserHandle))
+ .setStrings(getMetricsCategory())
+ .write();
+ safelyStartActivity(activeProfileTarget);
+ finish();
+ return true;
+ }
+
+ /**
+ * Returns whether the package has the necessary permissions to interact across profiles on
+ * behalf of a given user.
+ *
+ * <p>This means meeting the following condition:
+ * <ul>
+ * <li>The app's {@link ApplicationInfo#crossProfile} flag must be true, and at least
+ * one of the following conditions must be fulfilled</li>
+ * <li>{@code Manifest.permission.INTERACT_ACROSS_USERS_FULL} granted.</li>
+ * <li>{@code Manifest.permission.INTERACT_ACROSS_USERS} granted.</li>
+ * <li>{@code Manifest.permission.INTERACT_ACROSS_PROFILES} granted, or the corresponding
+ * AppOps {@code android:interact_across_profiles} is set to "allow".</li>
+ * </ul>
+ *
+ */
+ private boolean canAppInteractCrossProfiles(String packageName) {
+ ApplicationInfo applicationInfo;
+ try {
+ applicationInfo = getPackageManager().getApplicationInfo(packageName, 0);
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Package " + packageName + " does not exist on current user.");
+ return false;
+ }
+ if (!applicationInfo.crossProfile) {
+ return false;
+ }
+
+ int packageUid = applicationInfo.uid;
+
+ if (isPermissionGranted(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+ packageUid) == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+ if (isPermissionGranted(android.Manifest.permission.INTERACT_ACROSS_USERS, packageUid)
+ == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+ if (PermissionChecker.checkPermissionForPreflight(this, INTERACT_ACROSS_PROFILES,
+ PID_UNKNOWN, packageUid, packageName) == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isAutolaunching() {
+ return !mRegistered && isFinishing();
+ }
+
+ private void setupProfileTabs() {
+ maybeHideDivider();
+ TabHost tabHost = findViewById(com.android.internal.R.id.profile_tabhost);
+ tabHost.setup();
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ viewPager.setSaveEnabled(false);
+
+ Button personalButton = (Button) getLayoutInflater().inflate(
+ R.layout.resolver_profile_tab_button, tabHost.getTabWidget(), false);
+ personalButton.setText(mDevicePolicyResources.getPersonalTabLabel());
+ personalButton.setContentDescription(
+ mDevicePolicyResources.getPersonalTabAccessibilityLabel());
+
+ TabHost.TabSpec tabSpec = tabHost.newTabSpec(TAB_TAG_PERSONAL)
+ .setContent(com.android.internal.R.id.profile_pager)
+ .setIndicator(personalButton);
+ tabHost.addTab(tabSpec);
+
+ Button workButton = (Button) getLayoutInflater().inflate(
+ R.layout.resolver_profile_tab_button, tabHost.getTabWidget(), false);
+ workButton.setText(mDevicePolicyResources.getWorkTabLabel());
+ workButton.setContentDescription(mDevicePolicyResources.getWorkTabAccessibilityLabel());
+
+ tabSpec = tabHost.newTabSpec(TAB_TAG_WORK)
+ .setContent(com.android.internal.R.id.profile_pager)
+ .setIndicator(workButton);
+ tabHost.addTab(tabSpec);
+
+ TabWidget tabWidget = tabHost.getTabWidget();
+ tabWidget.setVisibility(View.VISIBLE);
+ updateActiveTabStyle(tabHost);
+
+ tabHost.setOnTabChangedListener(tabId -> {
+ updateActiveTabStyle(tabHost);
+ if (TAB_TAG_PERSONAL.equals(tabId)) {
+ viewPager.setCurrentItem(0);
+ } else {
+ viewPager.setCurrentItem(1);
+ }
+ setupViewVisibilities();
+ maybeLogProfileChange();
+ onProfileTabSelected();
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_SWITCH_TABS)
+ .setInt(viewPager.getCurrentItem())
+ .setStrings(getMetricsCategory())
+ .write();
+ });
+
+ viewPager.setVisibility(View.VISIBLE);
+ tabHost.setCurrentTab(mMultiProfilePagerAdapter.getCurrentPage());
+ mMultiProfilePagerAdapter.setOnProfileSelectedListener(
+ new MultiProfilePagerAdapter.OnProfileSelectedListener() {
+ @Override
+ public void onProfileSelected(int index) {
+ tabHost.setCurrentTab(index);
+ resetButtonBar();
+ resetCheckedItem();
+ }
+
+ @Override
+ public void onProfilePageStateChanged(int state) {
+ onHorizontalSwipeStateChanged(state);
+ }
+ });
+ mOnSwitchOnWorkSelectedListener = () -> {
+ final View workTab = tabHost.getTabWidget().getChildAt(1);
+ workTab.setFocusable(true);
+ workTab.setFocusableInTouchMode(true);
+ workTab.requestFocus();
+ };
+ }
+
+ private void maybeHideDivider() {
+ if (!mIsIntentPicker) {
+ return;
+ }
+ final View divider = findViewById(com.android.internal.R.id.divider);
+ if (divider == null) {
+ return;
+ }
+ divider.setVisibility(View.GONE);
+ }
+
+ private void resetCheckedItem() {
+ if (!mIsIntentPicker) {
+ return;
+ }
+ mLastSelected = ListView.INVALID_POSITION;
+ ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
+ .clearCheckedItemsInInactiveProfiles();
+ }
+
+ private static int getAttrColor(Context context, int attr) {
+ TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
+ int colorAccent = ta.getColor(0, 0);
+ ta.recycle();
+ return colorAccent;
+ }
+
+ private void updateActiveTabStyle(TabHost tabHost) {
+ int currentTab = tabHost.getCurrentTab();
+ TextView selected = (TextView) tabHost.getTabWidget().getChildAt(currentTab);
+ TextView unselected = (TextView) tabHost.getTabWidget().getChildAt(1 - currentTab);
+ selected.setSelected(true);
+ unselected.setSelected(false);
+ }
+
+ private void setupViewVisibilities() {
+ ResolverListAdapter activeListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();
+ if (!mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)) {
+ addUseDifferentAppLabelIfNecessary(activeListAdapter);
+ }
+ }
+
+ /**
+ * Updates the button bar container {@code ignoreOffset} layout param.
+ * <p>Setting this to {@code true} means that the button bar will be glued to the bottom of
+ * the screen.
+ */
+ private void setButtonBarIgnoreOffset(boolean ignoreOffset) {
+ View buttonBarContainer = findViewById(com.android.internal.R.id.button_bar_container);
+ if (buttonBarContainer != null) {
+ ResolverDrawerLayout.LayoutParams layoutParams =
+ (ResolverDrawerLayout.LayoutParams) buttonBarContainer.getLayoutParams();
+ layoutParams.ignoreOffset = ignoreOffset;
+ buttonBarContainer.setLayoutParams(layoutParams);
+ }
+ }
+
+ private void setupAdapterListView(ListView listView, ItemClickListener listener) {
+ listView.setOnItemClickListener(listener);
+ listView.setOnItemLongClickListener(listener);
+
+ if (mLogic.getSupportsAlwaysUseOption()) {
+ listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
+ }
+ }
+
+ /**
+ * Configure the area above the app selection list (title, content preview, etc).
+ */
+ private void maybeCreateHeader(ResolverListAdapter listAdapter) {
+ if (mHeaderCreatorUser != null
+ && !listAdapter.getUserHandle().equals(mHeaderCreatorUser)) {
+ return;
+ }
+ if (!shouldShowTabs()
+ && listAdapter.getCount() == 0 && listAdapter.getPlaceholderCount() == 0) {
+ final TextView titleView = findViewById(com.android.internal.R.id.title);
+ if (titleView != null) {
+ titleView.setVisibility(View.GONE);
+ }
+ }
+
+
+ CharSequence title = mLogic.getTitle() != null
+ ? mLogic.getTitle()
+ : getTitleForAction(mLogic.getTargetIntent(), mLogic.getDefaultTitleResId());
+
+ if (!TextUtils.isEmpty(title)) {
+ final TextView titleView = findViewById(com.android.internal.R.id.title);
+ if (titleView != null) {
+ titleView.setText(title);
+ }
+ setTitle(title);
+ }
+
+ final ImageView iconView = findViewById(com.android.internal.R.id.icon);
+ if (iconView != null) {
+ listAdapter.loadFilteredItemIconTaskAsync(iconView);
+ }
+ mHeaderCreatorUser = listAdapter.getUserHandle();
+ }
+
+ private void resetAlwaysOrOnceButtonBar() {
+ // Disable both buttons initially
+ setAlwaysButtonEnabled(false, ListView.INVALID_POSITION, false);
+ mOnceButton.setEnabled(false);
+
+ int filteredPosition = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getFilteredPosition();
+ if (useLayoutWithDefault() && filteredPosition != ListView.INVALID_POSITION) {
+ setAlwaysButtonEnabled(true, filteredPosition, false);
+ mOnceButton.setEnabled(true);
+ // Focus the button if we already have the default option
+ mOnceButton.requestFocus();
+ return;
+ }
+
+ // When the items load in, if an item was already selected, enable the buttons
+ ListView currentAdapterView = (ListView) mMultiProfilePagerAdapter.getActiveAdapterView();
+ if (currentAdapterView != null
+ && currentAdapterView.getCheckedItemPosition() != ListView.INVALID_POSITION) {
+ setAlwaysButtonEnabled(true, currentAdapterView.getCheckedItemPosition(), true);
+ mOnceButton.setEnabled(true);
+ }
+ }
+
+ @Override // ResolverListCommunicator
+ public final boolean useLayoutWithDefault() {
+ // We only use the default app layout when the profile of the active user has a
+ // filtered item. We always show the same default app even in the inactive user profile.
+ boolean adapterForCurrentUserHasFilteredItem =
+ mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+ requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch
+ ).hasFilteredItem();
+ return mLogic.getSupportsAlwaysUseOption() && adapterForCurrentUserHasFilteredItem;
+ }
+
+ /**
+ * If {@code retainInOnStop} is set to true, we will not finish ourselves when onStop gets
+ * called and we are launched in a new task.
+ */
+ protected final void setRetainInOnStop(boolean retainInOnStop) {
+ mRetainInOnStop = retainInOnStop;
+ }
+
+ final class ItemClickListener implements AdapterView.OnItemClickListener,
+ AdapterView.OnItemLongClickListener {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final ListView listView = parent instanceof ListView ? (ListView) parent : null;
+ if (listView != null) {
+ position -= listView.getHeaderViewsCount();
+ }
+ if (position < 0) {
+ // Header views don't count.
+ return;
+ }
+ // If we're still loading, we can't yet enable the buttons.
+ if (mMultiProfilePagerAdapter.getActiveListAdapter()
+ .resolveInfoForPosition(position, true) == null) {
+ return;
+ }
+ ListView currentAdapterView =
+ (ListView) mMultiProfilePagerAdapter.getActiveAdapterView();
+ final int checkedPos = currentAdapterView.getCheckedItemPosition();
+ final boolean hasValidSelection = checkedPos != ListView.INVALID_POSITION;
+ if (!useLayoutWithDefault()
+ && (!hasValidSelection || mLastSelected != checkedPos)
+ && mAlwaysButton != null) {
+ setAlwaysButtonEnabled(hasValidSelection, checkedPos, true);
+ mOnceButton.setEnabled(hasValidSelection);
+ if (hasValidSelection) {
+ currentAdapterView.smoothScrollToPosition(checkedPos);
+ mOnceButton.requestFocus();
+ }
+ mLastSelected = checkedPos;
+ } else {
+ startSelected(position, false, true);
+ }
+ }
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ final ListView listView = parent instanceof ListView ? (ListView) parent : null;
+ if (listView != null) {
+ position -= listView.getHeaderViewsCount();
+ }
+ if (position < 0) {
+ // Header views don't count.
+ return false;
+ }
+ ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .resolveInfoForPosition(position, true);
+ showTargetDetails(ri);
+ return true;
+ }
+
+ }
+
+ /** Determine whether a given match result is considered "specific" in our application. */
+ public static final boolean isSpecificUriMatch(int match) {
+ match = (match & IntentFilter.MATCH_CATEGORY_MASK);
+ return match >= IntentFilter.MATCH_CATEGORY_HOST
+ && match <= IntentFilter.MATCH_CATEGORY_PATH;
+ }
+
+ static final class PickTargetOptionRequest extends PickOptionRequest {
+ public PickTargetOptionRequest(@Nullable Prompt prompt, Option[] options,
+ @Nullable Bundle extras) {
+ super(prompt, options, extras);
+ }
+
+ @Override
+ public void onCancel() {
+ super.onCancel();
+ final ResolverActivity ra = (ResolverActivity) getActivity();
+ if (ra != null) {
+ ra.mPickOptionRequest = null;
+ ra.finish();
+ }
+ }
+
+ @Override
+ public void onPickOptionResult(boolean finished, Option[] selections, Bundle result) {
+ super.onPickOptionResult(finished, selections, result);
+ if (selections.length != 1) {
+ // TODO In a better world we would filter the UI presented here and let the
+ // user refine. Maybe later.
+ return;
+ }
+
+ final ResolverActivity ra = (ResolverActivity) getActivity();
+ if (ra != null) {
+ final TargetInfo ti = ra.mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getItem(selections[0].getIndex());
+ if (ra.onTargetSelected(ti, false)) {
+ ra.mPickOptionRequest = null;
+ ra.finish();
+ }
+ }
+ }
+ }
+ /**
+ * Returns the {@link UserHandle} to use when querying resolutions for intents in a
+ * {@link ResolverListController} configured for the provided {@code userHandle}.
+ */
+ protected final UserHandle getQueryIntentsUser(UserHandle userHandle) {
+ return requireAnnotatedUserHandles().getQueryIntentsUser(userHandle);
+ }
+
+ /**
+ * Returns the {@link List} of {@link UserHandle} to pass on to the
+ * {@link ResolverRankerServiceResolverComparator} as per the provided {@code userHandle}.
+ */
+ @VisibleForTesting(visibility = PROTECTED)
+ public final List<UserHandle> getResolverRankerServiceUserHandleList(UserHandle userHandle) {
+ return getResolverRankerServiceUserHandleListInternal(userHandle);
+ }
+
+ @VisibleForTesting
+ protected List<UserHandle> getResolverRankerServiceUserHandleListInternal(
+ UserHandle userHandle) {
+ List<UserHandle> userList = new ArrayList<>();
+ userList.add(userHandle);
+ // Add clonedProfileUserHandle to the list only if we are:
+ // a. Building the Personal Tab.
+ // b. CloneProfile exists on the device.
+ if (userHandle.equals(requireAnnotatedUserHandles().personalProfileUserHandle)
+ && hasCloneProfile()) {
+ userList.add(requireAnnotatedUserHandles().cloneProfileUserHandle);
+ }
+ return userList;
+ }
+
+ private CharSequence getOrLoadDisplayLabel(TargetInfo info) {
+ if (info.isDisplayResolveInfo()) {
+ mLogic.getTargetDataLoader().getOrLoadLabel((DisplayResolveInfo) info);
+ }
+ CharSequence displayLabel = info.getDisplayLabel();
+ return displayLabel == null ? "" : displayLabel;
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt b/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt
new file mode 100644
index 0000000..0e2b25e
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt
@@ -0,0 +1,81 @@
+package com.android.intentresolver.v2
+
+import android.content.Intent
+import androidx.activity.ComponentActivity
+import androidx.annotation.OpenForTesting
+import com.android.intentresolver.R
+import com.android.intentresolver.icons.DefaultTargetDataLoader
+import com.android.intentresolver.icons.TargetDataLoader
+import com.android.intentresolver.v2.util.mutableLazy
+
+/** Activity logic for [ResolverActivity]. */
+@OpenForTesting
+open class ResolverActivityLogic(
+ tag: String,
+ activityProvider: () -> ComponentActivity,
+ onWorkProfileStatusUpdated: () -> Unit,
+) :
+ ActivityLogic,
+ CommonActivityLogic by CommonActivityLogicImpl(
+ tag,
+ activityProvider,
+ onWorkProfileStatusUpdated,
+ ) {
+
+ override val targetIntent: Intent by lazy {
+ val intent = Intent(activity.intent)
+ intent.setComponent(null)
+ // The resolver activity is set to be hidden from recent tasks.
+ // we don't want this attribute to be propagated to the next activity
+ // being launched. Note that if the original Intent also had this
+ // flag set, we are now losing it. That should be a very rare case
+ // and we can live with this.
+ intent.setFlags(intent.flags and Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS.inv())
+
+ // If FLAG_ACTIVITY_LAUNCH_ADJACENT was set, ResolverActivity was opened in the alternate
+ // side, which means we want to open the target app on the same side as ResolverActivity.
+ if (intent.flags and Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT != 0) {
+ intent.setFlags(intent.flags and Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT.inv())
+ }
+ intent
+ }
+
+ override val resolvingHome: Boolean by lazy {
+ targetIntent.action == Intent.ACTION_MAIN &&
+ targetIntent.categories.singleOrNull() == Intent.CATEGORY_HOME
+ }
+
+ override val title: CharSequence? = null
+
+ override val defaultTitleResId: Int = 0
+
+ override val initialIntents: List<Intent>? = null
+
+ override val supportsAlwaysUseOption: Boolean = true
+
+ override val targetDataLoader: TargetDataLoader by lazy {
+ DefaultTargetDataLoader(
+ activity,
+ activity.lifecycle,
+ activity.intent.getBooleanExtra(
+ ResolverActivity.EXTRA_IS_AUDIO_CAPTURE_DEVICE,
+ /* defaultValue = */ false,
+ ),
+ )
+ }
+
+ override val themeResId: Int = R.style.Theme_DeviceDefault_Resolver
+
+ private val _profileSwitchMessage = mutableLazy { forwardMessageFor(targetIntent) }
+ override val profileSwitchMessage: String? by _profileSwitchMessage
+
+ override val payloadIntents: List<Intent> by lazy { listOf(targetIntent) }
+
+ override fun preInitialization() {
+ // Do nothing
+ }
+
+ override fun clearProfileSwitchMessage() {
+ _profileSwitchMessage.setLazy(null)
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java
new file mode 100644
index 0000000..d96fd15
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2019 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.intentresolver.v2;
+
+import android.content.Context;
+import android.os.UserHandle;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+import androidx.viewpager.widget.PagerAdapter;
+
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+
+/**
+ * A {@link PagerAdapter} which describes the work and personal profile intent resolver screens.
+ */
+@VisibleForTesting
+public class ResolverMultiProfilePagerAdapter extends
+ MultiProfilePagerAdapter<ListView, ResolverListAdapter, ResolverListAdapter> {
+ private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier;
+
+ public ResolverMultiProfilePagerAdapter(
+ Context context,
+ ResolverListAdapter adapter,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle) {
+ this(
+ context,
+ ImmutableList.of(adapter),
+ emptyStateProvider,
+ workProfileQuietModeChecker,
+ /* defaultProfile= */ 0,
+ workProfileUserHandle,
+ cloneProfileUserHandle,
+ new BottomPaddingOverrideSupplier());
+ }
+
+ public ResolverMultiProfilePagerAdapter(Context context,
+ ResolverListAdapter personalAdapter,
+ ResolverListAdapter workAdapter,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ @Profile int defaultProfile,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle) {
+ this(
+ context,
+ ImmutableList.of(personalAdapter, workAdapter),
+ emptyStateProvider,
+ workProfileQuietModeChecker,
+ defaultProfile,
+ workProfileUserHandle,
+ cloneProfileUserHandle,
+ new BottomPaddingOverrideSupplier());
+ }
+
+ private ResolverMultiProfilePagerAdapter(
+ Context context,
+ ImmutableList<ResolverListAdapter> listAdapters,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ @Profile int defaultProfile,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle,
+ BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {
+ super(
+ listAdapter -> listAdapter,
+ (listView, bindAdapter) -> listView.setAdapter(bindAdapter),
+ listAdapters,
+ emptyStateProvider,
+ workProfileQuietModeChecker,
+ defaultProfile,
+ workProfileUserHandle,
+ cloneProfileUserHandle,
+ () -> (ViewGroup) LayoutInflater.from(context).inflate(
+ R.layout.resolver_list_per_profile, null, false),
+ bottomPaddingOverrideSupplier);
+ mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier;
+ }
+
+ public void setUseLayoutWithDefault(boolean useLayoutWithDefault) {
+ mBottomPaddingOverrideSupplier.setUseLayoutWithDefault(useLayoutWithDefault);
+ }
+
+ /** Un-check any item(s) that may be checked in any of our inactive adapter(s). */
+ public void clearCheckedItemsInInactiveProfiles() {
+ // TODO: apply to all inactive adapters; for now we just have the one.
+ ListView inactiveListView = getInactiveAdapterView();
+ if (inactiveListView.getCheckedItemCount() > 0) {
+ inactiveListView.setItemChecked(inactiveListView.getCheckedItemPosition(), false);
+ }
+ }
+
+ private static class BottomPaddingOverrideSupplier implements Supplier<Optional<Integer>> {
+ private boolean mUseLayoutWithDefault;
+
+ public void setUseLayoutWithDefault(boolean useLayoutWithDefault) {
+ mUseLayoutWithDefault = useLayoutWithDefault;
+ }
+
+ @Override
+ public Optional<Integer> get() {
+ return mUseLayoutWithDefault ? Optional.empty() : Optional.of(0);
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/data/BroadcastFlow.kt b/java/src/com/android/intentresolver/v2/data/BroadcastFlow.kt
new file mode 100644
index 0000000..1a58afc
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/data/BroadcastFlow.kt
@@ -0,0 +1,46 @@
+package com.android.intentresolver.v2.data
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.UserHandle
+import android.util.Log
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.channels.onFailure
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+
+private const val TAG = "BroadcastFlow"
+
+/**
+ * Returns a [callbackFlow] that, when collected, registers a broadcast receiver and emits a new
+ * value whenever broadcast matching _filter_ is received. The result value will be computed using
+ * [transform] and emitted if non-null.
+ */
+internal fun <T> broadcastFlow(
+ context: Context,
+ filter: IntentFilter,
+ user: UserHandle,
+ transform: (Intent) -> T?
+): Flow<T> = callbackFlow {
+ val receiver =
+ object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ transform(intent)?.also { result ->
+ trySend(result).onFailure { Log.e(TAG, "Failed to send $result", it) }
+ }
+ ?: Log.w(TAG, "Ignored broadcast $intent")
+ }
+ }
+
+ context.registerReceiverAsUser(
+ receiver,
+ user,
+ IntentFilter(filter),
+ null,
+ null,
+ Context.RECEIVER_NOT_EXPORTED
+ )
+ awaitClose { context.unregisterReceiver(receiver) }
+}
diff --git a/java/src/com/android/intentresolver/v2/data/model/User.kt b/java/src/com/android/intentresolver/v2/data/model/User.kt
new file mode 100644
index 0000000..504b04c
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/data/model/User.kt
@@ -0,0 +1,50 @@
+package com.android.intentresolver.v2.data.model
+
+import android.annotation.UserIdInt
+import android.os.UserHandle
+import com.android.intentresolver.v2.data.model.User.Type
+import com.android.intentresolver.v2.data.model.User.Type.FULL
+import com.android.intentresolver.v2.data.model.User.Type.PROFILE
+
+/**
+ * A User represents the owner of a distinct set of content.
+ * * maps 1:1 to a UserHandle or UserId (Int) value.
+ * * refers to either [Full][Type.FULL], or a [Profile][Type.PROFILE] user, as indicated by the
+ * [type] property.
+ *
+ * See
+ * [Users for system developers](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/Users.md)
+ *
+ * ```
+ * val users = listOf(
+ * User(id = 0, role = PERSONAL),
+ * User(id = 10, role = WORK),
+ * User(id = 11, role = CLONE),
+ * User(id = 12, role = PRIVATE),
+ * )
+ * ```
+ */
+data class User(
+ @UserIdInt val id: Int,
+ val role: Role,
+) {
+ val handle: UserHandle = UserHandle.of(id)
+
+ val type: Type
+ get() = role.type
+
+ enum class Type {
+ FULL,
+ PROFILE
+ }
+
+ enum class Role(
+ /** The type of the role user. */
+ val type: Type
+ ) {
+ PERSONAL(FULL),
+ PRIVATE(PROFILE),
+ WORK(PROFILE),
+ CLONE(PROFILE)
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt b/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt
new file mode 100644
index 0000000..7debdf0
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.data.repository
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB
+import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB_ACCESSIBILITY
+import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED
+import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB
+import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY
+import android.content.res.Resources
+import com.android.intentresolver.R
+import com.android.intentresolver.inject.ApplicationOwned
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class DevicePolicyResources @Inject constructor(
+ @ApplicationOwned private val resources: Resources,
+ devicePolicyManager: DevicePolicyManager
+) {
+ private val policyResources = devicePolicyManager.resources
+
+ val personalTabLabel by lazy {
+ requireNotNull(policyResources.getString(RESOLVER_PERSONAL_TAB) {
+ resources.getString(R.string.resolver_personal_tab)
+ })
+ }
+
+ val workTabLabel by lazy {
+ requireNotNull(policyResources.getString(RESOLVER_WORK_TAB) {
+ resources.getString(R.string.resolver_work_tab)
+ })
+ }
+
+ val personalTabAccessibilityLabel by lazy {
+ requireNotNull(policyResources.getString(RESOLVER_PERSONAL_TAB_ACCESSIBILITY) {
+ resources.getString(R.string.resolver_personal_tab_accessibility)
+ })
+ }
+
+ val workTabAccessibilityLabel by lazy {
+ requireNotNull(policyResources.getString(RESOLVER_WORK_TAB_ACCESSIBILITY) {
+ resources.getString(R.string.resolver_work_tab_accessibility)
+ })
+ }
+
+ fun getWorkProfileNotSupportedMessage(launcherName: String): String {
+ return requireNotNull(policyResources.getString(RESOLVER_WORK_PROFILE_NOT_SUPPORTED, {
+ resources.getString(
+ R.string.activity_resolver_work_profiles_support,
+ launcherName)
+ }, launcherName))
+ }
+} \ No newline at end of file
diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt b/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt
new file mode 100644
index 0000000..fc82efe
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt
@@ -0,0 +1,29 @@
+package com.android.intentresolver.v2.data.repository
+
+import android.content.pm.UserInfo
+import com.android.intentresolver.v2.data.model.User
+import com.android.intentresolver.v2.data.model.User.Role
+
+/** Maps the UserInfo to one of the defined [Roles][User.Role], if possible. */
+fun UserInfo.getSupportedUserRole(): Role? =
+ when {
+ isFull -> Role.PERSONAL
+ isManagedProfile -> Role.WORK
+ isCloneProfile -> Role.CLONE
+ isPrivateProfile -> Role.PRIVATE
+ else -> null
+ }
+
+/**
+ * Creates a [User], based on values from a [UserInfo].
+ *
+ * ```
+ * val users: List<User> =
+ * getEnabledProfiles(user).map(::toUser).filterNotNull()
+ * ```
+ *
+ * @return a [User] if the [UserInfo] matched a supported [Role], otherwise null
+ */
+fun UserInfo.toUser(): User? {
+ return getSupportedUserRole()?.let { role -> User(userHandle.identifier, role) }
+}
diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt b/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt
new file mode 100644
index 0000000..dc809b4
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt
@@ -0,0 +1,261 @@
+package com.android.intentresolver.v2.data.repository
+
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE
+import android.content.Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE
+import android.content.Intent.ACTION_PROFILE_ADDED
+import android.content.Intent.ACTION_PROFILE_AVAILABLE
+import android.content.Intent.ACTION_PROFILE_REMOVED
+import android.content.Intent.ACTION_PROFILE_UNAVAILABLE
+import android.content.Intent.EXTRA_QUIET_MODE
+import android.content.Intent.EXTRA_USER
+import android.content.IntentFilter
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.intentresolver.inject.Background
+import com.android.intentresolver.inject.Main
+import com.android.intentresolver.inject.ProfileParent
+import com.android.intentresolver.v2.data.broadcastFlow
+import com.android.intentresolver.v2.data.model.User
+import com.android.intentresolver.v2.data.repository.UserRepositoryImpl.UserEvent
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNot
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.runningFold
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+interface UserRepository {
+ /**
+ * A [Flow] user profile groups. Each map contains the context user along with all members of
+ * the profile group. This includes the (Full) parent user, if the context user is a profile.
+ */
+ val users: Flow<Map<UserHandle, User>>
+
+ /**
+ * A [Flow] of availability. Only profile users may become unavailable.
+ *
+ * Availability is currently defined as not being in [quietMode][UserInfo.isQuietModeEnabled].
+ */
+ fun isAvailable(user: User): Flow<Boolean>
+
+ /**
+ * Request that availability be updated to the requested state. This currently includes toggling
+ * quiet mode as needed. This may involve additional background actions, such as starting or
+ * stopping a profile user (along with their many associated processes).
+ *
+ * If successful, the change will be applied after the call returns and can be observed using
+ * [UserRepository.isAvailable] for the given user.
+ *
+ * No actions are taken if the user is already in requested state.
+ *
+ * @throws IllegalArgumentException if called for an unsupported user type
+ */
+ suspend fun requestState(user: User, available: Boolean)
+}
+
+private const val TAG = "UserRepository"
+
+private data class UserWithState(val user: User, val available: Boolean)
+
+private typealias UserStateMap = Map<UserHandle, UserWithState>
+
+/** Tracks and publishes state for the parent user and associated profiles. */
+class UserRepositoryImpl
+@VisibleForTesting
+constructor(
+ private val profileParent: UserHandle,
+ private val userManager: UserManager,
+ /** A flow of events which represent user-state changes from [UserManager]. */
+ private val userEvents: Flow<UserEvent>,
+ scope: CoroutineScope,
+ private val backgroundDispatcher: CoroutineDispatcher
+) : UserRepository {
+ @Inject
+ constructor(
+ @ApplicationContext context: Context,
+ @ProfileParent profileParent: UserHandle,
+ userManager: UserManager,
+ @Main scope: CoroutineScope,
+ @Background background: CoroutineDispatcher
+ ) : this(
+ profileParent,
+ userManager,
+ userEvents = userBroadcastFlow(context, profileParent),
+ scope,
+ background
+ )
+
+ data class UserEvent(val action: String, val user: UserHandle, val quietMode: Boolean = false)
+
+ /**
+ * An exception which indicates that an inconsistency exists between the user state map and the
+ * rest of the system.
+ */
+ internal class UserStateException(
+ override val message: String,
+ val event: UserEvent,
+ override val cause: Throwable? = null
+ ) : RuntimeException("$message: event=$event", cause)
+
+ private val usersWithState: Flow<UserStateMap> =
+ userEvents
+ .onStart { emit(UserEvent(INITIALIZE, profileParent)) }
+ .onEach { Log.i("UserDataSource", "userEvent: $it") }
+ .runningFold<UserEvent, UserStateMap>(emptyMap()) { users, event ->
+ try {
+ // Handle an action by performing some operation, then returning a new map
+ when (event.action) {
+ INITIALIZE -> createNewUserStateMap(profileParent)
+ ACTION_PROFILE_ADDED -> handleProfileAdded(event, users)
+ ACTION_PROFILE_REMOVED -> handleProfileRemoved(event, users)
+ ACTION_MANAGED_PROFILE_UNAVAILABLE,
+ ACTION_MANAGED_PROFILE_AVAILABLE,
+ ACTION_PROFILE_AVAILABLE,
+ ACTION_PROFILE_UNAVAILABLE -> handleAvailability(event, users)
+ else -> {
+ Log.w(TAG, "Unhandled event: $event)")
+ users
+ }
+ }
+ } catch (e: UserStateException) {
+ Log.e(TAG, "An error occurred handling an event: ${e.event}", e)
+ Log.e(TAG, "Attempting to recover...")
+ createNewUserStateMap(profileParent)
+ }
+ }
+ .onEach { Log.i("UserDataSource", "userStateMap: $it") }
+ .stateIn(scope, SharingStarted.Eagerly, emptyMap())
+ .filterNot { it.isEmpty() }
+
+ override val users: Flow<Map<UserHandle, User>> =
+ usersWithState.map { map -> map.mapValues { it.value.user } }.distinctUntilChanged()
+
+ private val availability: Flow<Map<UserHandle, Boolean>> =
+ usersWithState.map { map -> map.mapValues { it.value.available } }.distinctUntilChanged()
+
+ override fun isAvailable(user: User): Flow<Boolean> {
+ return isAvailable(user.handle)
+ }
+
+ @VisibleForTesting
+ fun isAvailable(handle: UserHandle): Flow<Boolean> {
+ return availability.map { it[handle] ?: false }
+ }
+
+ override suspend fun requestState(user: User, available: Boolean) {
+ require(user.type == User.Type.PROFILE) { "Only profile users are supported" }
+ return requestState(user.handle, available)
+ }
+
+ @VisibleForTesting
+ suspend fun requestState(user: UserHandle, available: Boolean) {
+ return withContext(backgroundDispatcher) {
+ Log.i(TAG, "requestQuietModeEnabled: ${!available} for user $user")
+ userManager.requestQuietModeEnabled(/* enableQuietMode = */ !available, user)
+ }
+ }
+
+ private fun handleAvailability(event: UserEvent, current: UserStateMap): UserStateMap {
+ val userEntry =
+ current[event.user]
+ ?: throw UserStateException("User was not present in the map", event)
+ return current + (event.user to userEntry.copy(available = !event.quietMode))
+ }
+
+ private fun handleProfileRemoved(event: UserEvent, current: UserStateMap): UserStateMap {
+ if (!current.containsKey(event.user)) {
+ throw UserStateException("User was not present in the map", event)
+ }
+ return current.filterKeys { it != event.user }
+ }
+
+ private suspend fun handleProfileAdded(event: UserEvent, current: UserStateMap): UserStateMap {
+ val user =
+ try {
+ requireNotNull(readUser(event.user))
+ } catch (e: Exception) {
+ throw UserStateException("Failed to read user from UserManager", event, e)
+ }
+ return current + (event.user to UserWithState(user, !event.quietMode))
+ }
+
+ private suspend fun createNewUserStateMap(user: UserHandle): UserStateMap {
+ val profiles = readProfileGroup(user)
+ return profiles
+ .mapNotNull { userInfo ->
+ userInfo.toUser()?.let { user -> UserWithState(user, userInfo.isAvailable()) }
+ }
+ .associateBy { it.user.handle }
+ }
+
+ private suspend fun readProfileGroup(handle: UserHandle): List<UserInfo> {
+ return withContext(backgroundDispatcher) {
+ @Suppress("DEPRECATION") userManager.getEnabledProfiles(handle.identifier)
+ }
+ .toList()
+ }
+
+ /** Read [UserInfo] from [UserManager], or null if not found or an unsupported type. */
+ private suspend fun readUser(user: UserHandle): User? {
+ val userInfo =
+ withContext(backgroundDispatcher) { userManager.getUserInfo(user.identifier) }
+ return userInfo?.let { info ->
+ info.getSupportedUserRole()?.let { role -> User(info.id, role) }
+ }
+ }
+}
+
+/** Used with [broadcastFlow] to transform a UserManager broadcast action into a [UserEvent]. */
+private fun Intent.toUserEvent(): UserEvent? {
+ val action = action
+ val user = extras?.getParcelable(EXTRA_USER, UserHandle::class.java)
+ val quietMode = extras?.getBoolean(EXTRA_QUIET_MODE, false) ?: false
+ return if (user == null || action == null) {
+ null
+ } else {
+ UserEvent(action, user, quietMode)
+ }
+}
+
+const val INITIALIZE = "INITIALIZE"
+
+private fun createFilter(actions: Iterable<String>): IntentFilter {
+ return IntentFilter().apply { actions.forEach(::addAction) }
+}
+
+private fun UserInfo?.isAvailable(): Boolean {
+ return this?.isQuietModeEnabled != true
+}
+
+private fun userBroadcastFlow(context: Context, profileParent: UserHandle): Flow<UserEvent> {
+ val userActions =
+ setOf(
+ ACTION_PROFILE_ADDED,
+ ACTION_PROFILE_REMOVED,
+
+ // Quiet mode enabled/disabled for managed
+ // From: UserController.broadcastProfileAvailabilityChanges
+ // In response to setQuietModeEnabled
+ ACTION_MANAGED_PROFILE_AVAILABLE, // quiet mode, sent for manage profiles only
+ ACTION_MANAGED_PROFILE_UNAVAILABLE, // quiet mode, sent for manage profiles only
+
+ // Quiet mode toggled for profile type, requires flag 'android.os.allow_private_profile
+ // true'
+ ACTION_PROFILE_AVAILABLE, // quiet mode,
+ ACTION_PROFILE_UNAVAILABLE, // quiet mode, sent for any profile type
+ )
+ return broadcastFlow(context, createFilter(userActions), profileParent, Intent::toUserEvent)
+}
diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt b/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt
new file mode 100644
index 0000000..94f985e
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt
@@ -0,0 +1,34 @@
+package com.android.intentresolver.v2.data.repository
+
+import android.content.Context
+import android.os.UserHandle
+import android.os.UserManager
+import com.android.intentresolver.inject.ApplicationUser
+import com.android.intentresolver.inject.ProfileParent
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+interface UserRepositoryModule {
+ companion object {
+ @Provides
+ @Singleton
+ @ApplicationUser
+ fun applicationUser(@ApplicationContext context: Context): UserHandle = context.user
+
+ @Provides
+ @Singleton
+ @ProfileParent
+ fun profileParent(@ApplicationUser user: UserHandle, userManager: UserManager): UserHandle {
+ return userManager.getProfileParent(user) ?: user
+ }
+ }
+
+ @Binds @Singleton fun userRepository(impl: UserRepositoryImpl): UserRepository
+}
diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt b/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt
new file mode 100644
index 0000000..7ee78d9
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt
@@ -0,0 +1,46 @@
+package com.android.intentresolver.v2.data.repository
+
+import android.content.Context
+import androidx.core.content.getSystemService
+import com.android.intentresolver.v2.data.model.User
+
+/**
+ * Provides cached instances of a [system service][Context.getSystemService] created with
+ * [the context of a specified user][Context.createContextAsUser].
+ *
+ * System services which have only `@UserHandleAware` APIs operate on the user id available from
+ * [Context.getUser], the context used to retrieve the service. This utility helps adapt a per-user
+ * API model to work in multi-user manner.
+ *
+ * Example usage:
+ * ```
+ * val usageStats = userScopedService<UsageStatsManager>(context)
+ *
+ * fun getStatsForUser(
+ * user: User,
+ * from: Long,
+ * to: Long
+ * ): UsageStats {
+ * return usageStats.forUser(user)
+ * .queryUsageStats(INTERVAL_BEST, from, to)
+ * }
+ * ```
+ */
+interface UserScopedService<T> {
+ fun forUser(user: User): T
+}
+
+inline fun <reified T> userScopedService(context: Context): UserScopedService<T> {
+ return object : UserScopedService<T> {
+ private val map = mutableMapOf<User, T>()
+
+ override fun forUser(user: User): T {
+ return synchronized(this) {
+ map.getOrPut(user) {
+ val userContext = context.createContextAsUser(user.handle, 0)
+ requireNotNull(userContext.getSystemService())
+ }
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java b/java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java
new file mode 100644
index 0000000..2f1e1b5
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.emptystate;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+
+/**
+ * Helper for building `MultiProfilePagerAdapter` tab UIs for profile tabs that are "blocked" by
+ * some empty-state status.
+ */
+public class EmptyStateUiHelper {
+ private final Supplier<Optional<Integer>> mContainerBottomPaddingOverrideSupplier;
+ private final View mEmptyStateView;
+ private final View mListView;
+ private final View mEmptyStateContainerView;
+ private final TextView mEmptyStateTitleView;
+ private final TextView mEmptyStateSubtitleView;
+ private final Button mEmptyStateButtonView;
+ private final View mEmptyStateProgressView;
+ private final View mEmptyStateEmptyView;
+
+ public EmptyStateUiHelper(
+ ViewGroup rootView,
+ int listViewResourceId,
+ Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
+ mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier;
+ mEmptyStateView =
+ rootView.requireViewById(com.android.internal.R.id.resolver_empty_state);
+ mListView = rootView.requireViewById(listViewResourceId);
+ mEmptyStateContainerView = mEmptyStateView.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_container);
+ mEmptyStateTitleView = mEmptyStateView.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_title);
+ mEmptyStateSubtitleView = mEmptyStateView.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_subtitle);
+ mEmptyStateButtonView = mEmptyStateView.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_button);
+ mEmptyStateProgressView = mEmptyStateView.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_progress);
+ mEmptyStateEmptyView = mEmptyStateView.requireViewById(com.android.internal.R.id.empty);
+ }
+
+ /**
+ * Display the described empty state.
+ * @param emptyState the data describing the cause of this empty-state condition.
+ * @param buttonOnClick handler for a button that the user might be able to use to circumvent
+ * the empty-state condition. If null, no button will be displayed.
+ */
+ public void showEmptyState(EmptyState emptyState, View.OnClickListener buttonOnClick) {
+ resetViewVisibilities();
+ setupContainerPadding();
+
+ String title = emptyState.getTitle();
+ if (title != null) {
+ mEmptyStateTitleView.setVisibility(View.VISIBLE);
+ mEmptyStateTitleView.setText(title);
+ } else {
+ mEmptyStateTitleView.setVisibility(View.GONE);
+ }
+
+ String subtitle = emptyState.getSubtitle();
+ if (subtitle != null) {
+ mEmptyStateSubtitleView.setVisibility(View.VISIBLE);
+ mEmptyStateSubtitleView.setText(subtitle);
+ } else {
+ mEmptyStateSubtitleView.setVisibility(View.GONE);
+ }
+
+ mEmptyStateEmptyView.setVisibility(
+ emptyState.useDefaultEmptyView() ? View.VISIBLE : View.GONE);
+ // TODO: The EmptyState API says that if `useDefaultEmptyView()` is true, we'll ignore the
+ // state's specified title/subtitle; where (if anywhere) is that implemented?
+
+ mEmptyStateButtonView.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE);
+ mEmptyStateButtonView.setOnClickListener(buttonOnClick);
+
+ // Don't show the main list view when we're showing an empty state.
+ mListView.setVisibility(View.GONE);
+ }
+
+ /** Sets up the padding of the view containing the empty state screens. */
+ public void setupContainerPadding() {
+ Optional<Integer> bottomPaddingOverride = mContainerBottomPaddingOverrideSupplier.get();
+ bottomPaddingOverride.ifPresent(paddingBottom ->
+ mEmptyStateContainerView.setPadding(
+ mEmptyStateContainerView.getPaddingLeft(),
+ mEmptyStateContainerView.getPaddingTop(),
+ mEmptyStateContainerView.getPaddingRight(),
+ paddingBottom));
+ }
+
+ public void showSpinner() {
+ mEmptyStateTitleView.setVisibility(View.INVISIBLE);
+ // TODO: subtitle?
+ mEmptyStateButtonView.setVisibility(View.INVISIBLE);
+ mEmptyStateProgressView.setVisibility(View.VISIBLE);
+ mEmptyStateEmptyView.setVisibility(View.GONE);
+ }
+
+ public void hide() {
+ mEmptyStateView.setVisibility(View.GONE);
+ mListView.setVisibility(View.VISIBLE);
+ }
+
+ // TODO: this is exposed for testing so we can thoroughly prepare initial conditions that let us
+ // observe the resulting change. In reality it's only invoked as part of `showEmptyState()` and
+ // we could consider setting up narrower "realistic" preconditions to make assertions about the
+ // higher-level operation.
+ @VisibleForTesting
+ void resetViewVisibilities() {
+ mEmptyStateTitleView.setVisibility(View.VISIBLE);
+ mEmptyStateSubtitleView.setVisibility(View.VISIBLE);
+ mEmptyStateButtonView.setVisibility(View.INVISIBLE);
+ mEmptyStateProgressView.setVisibility(View.GONE);
+ mEmptyStateEmptyView.setVisibility(View.GONE);
+ mEmptyStateView.setVisibility(View.VISIBLE);
+ }
+}
+
diff --git a/java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java
new file mode 100644
index 0000000..e9d1bb3
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2.emptystate;
+
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_PERSONAL_APPS;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS;
+
+import android.app.admin.DevicePolicyEventLogger;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+import android.stats.devicepolicy.nano.DevicePolicyEnums;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.ResolvedComponentInfo;
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.internal.R;
+
+import java.util.List;
+
+/**
+ * Chooser/ResolverActivity empty state provider that returns empty state which is shown when
+ * there are no apps available.
+ */
+public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider {
+
+ @NonNull
+ private final Context mContext;
+ @Nullable
+ private final UserHandle mWorkProfileUserHandle;
+ @Nullable
+ private final UserHandle mPersonalProfileUserHandle;
+ @NonNull
+ private final String mMetricsCategory;
+ @NonNull
+ private final UserHandle mTabOwnerUserHandleForLaunch;
+
+ public NoAppsAvailableEmptyStateProvider(@NonNull Context context,
+ @Nullable UserHandle workProfileUserHandle,
+ @Nullable UserHandle personalProfileUserHandle, @NonNull String metricsCategory,
+ @NonNull UserHandle tabOwnerUserHandleForLaunch) {
+ mContext = context;
+ mWorkProfileUserHandle = workProfileUserHandle;
+ mPersonalProfileUserHandle = personalProfileUserHandle;
+ mMetricsCategory = metricsCategory;
+ mTabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch;
+ }
+
+ @Nullable
+ @Override
+ @SuppressWarnings("ReferenceEquality")
+ public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ UserHandle listUserHandle = resolverListAdapter.getUserHandle();
+
+ if (mWorkProfileUserHandle != null
+ && (mTabOwnerUserHandleForLaunch.equals(listUserHandle)
+ || !hasAppsInOtherProfile(resolverListAdapter))) {
+
+ String title;
+ if (listUserHandle == mPersonalProfileUserHandle) {
+ title = mContext.getSystemService(
+ DevicePolicyManager.class).getResources().getString(
+ RESOLVER_NO_PERSONAL_APPS,
+ () -> mContext.getString(R.string.resolver_no_personal_apps_available));
+ } else {
+ title = mContext.getSystemService(
+ DevicePolicyManager.class).getResources().getString(
+ RESOLVER_NO_WORK_APPS,
+ () -> mContext.getString(R.string.resolver_no_work_apps_available));
+ }
+
+ return new NoAppsAvailableEmptyState(
+ title, mMetricsCategory,
+ /* isPersonalProfile= */ listUserHandle == mPersonalProfileUserHandle
+ );
+ } else if (mWorkProfileUserHandle == null) {
+ // Return default empty state without tracking
+ return new DefaultEmptyState();
+ }
+
+ return null;
+ }
+
+ private boolean hasAppsInOtherProfile(ResolverListAdapter adapter) {
+ if (mWorkProfileUserHandle == null) {
+ return false;
+ }
+ List<ResolvedComponentInfo> resolversForIntent =
+ adapter.getResolversForUser(mTabOwnerUserHandleForLaunch);
+ for (ResolvedComponentInfo info : resolversForIntent) {
+ ResolveInfo resolveInfo = info.getResolveInfoAt(0);
+ if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static class DefaultEmptyState implements EmptyState {
+ @Override
+ public boolean useDefaultEmptyView() {
+ return true;
+ }
+ }
+
+ public static class NoAppsAvailableEmptyState implements EmptyState {
+
+ @NonNull
+ private final String mTitle;
+
+ @NonNull
+ private final String mMetricsCategory;
+
+ private final boolean mIsPersonalProfile;
+
+ public NoAppsAvailableEmptyState(@NonNull String title, @NonNull String metricsCategory,
+ boolean isPersonalProfile) {
+ mTitle = title;
+ mMetricsCategory = metricsCategory;
+ mIsPersonalProfile = isPersonalProfile;
+ }
+
+ @NonNull
+ @Override
+ public String getTitle() {
+ return mTitle;
+ }
+
+ @Override
+ public void onEmptyStateShown() {
+ DevicePolicyEventLogger.createEvent(
+ DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_APPS_RESOLVED)
+ .setStrings(mMetricsCategory)
+ .setBoolean(/*isPersonalProfile*/ mIsPersonalProfile)
+ .write();
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/emptystate/NoCrossProfileEmptyStateProvider.java b/java/src/com/android/intentresolver/v2/emptystate/NoCrossProfileEmptyStateProvider.java
new file mode 100644
index 0000000..b744c58
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/emptystate/NoCrossProfileEmptyStateProvider.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2.emptystate;
+
+import android.app.admin.DevicePolicyEventLogger;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+
+/**
+ * Empty state provider that does not allow cross profile sharing, it will return a blocker
+ * in case if the profile of the current tab is not the same as the profile of the calling app.
+ */
+public class NoCrossProfileEmptyStateProvider implements EmptyStateProvider {
+
+ private final UserHandle mPersonalProfileUserHandle;
+ private final EmptyState mNoWorkToPersonalEmptyState;
+ private final EmptyState mNoPersonalToWorkEmptyState;
+ private final CrossProfileIntentsChecker mCrossProfileIntentsChecker;
+ private final UserHandle mTabOwnerUserHandleForLaunch;
+
+ public NoCrossProfileEmptyStateProvider(UserHandle personalUserHandle,
+ EmptyState noWorkToPersonalEmptyState,
+ EmptyState noPersonalToWorkEmptyState,
+ CrossProfileIntentsChecker crossProfileIntentsChecker,
+ UserHandle tabOwnerUserHandleForLaunch) {
+ mPersonalProfileUserHandle = personalUserHandle;
+ mNoWorkToPersonalEmptyState = noWorkToPersonalEmptyState;
+ mNoPersonalToWorkEmptyState = noPersonalToWorkEmptyState;
+ mCrossProfileIntentsChecker = crossProfileIntentsChecker;
+ mTabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch;
+ }
+
+ @Nullable
+ @Override
+ public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ boolean shouldShowBlocker =
+ !mTabOwnerUserHandleForLaunch.equals(resolverListAdapter.getUserHandle())
+ && !mCrossProfileIntentsChecker
+ .hasCrossProfileIntents(resolverListAdapter.getIntents(),
+ mTabOwnerUserHandleForLaunch.getIdentifier(),
+ resolverListAdapter.getUserHandle().getIdentifier());
+
+ if (!shouldShowBlocker) {
+ return null;
+ }
+
+ if (resolverListAdapter.getUserHandle().equals(mPersonalProfileUserHandle)) {
+ return mNoWorkToPersonalEmptyState;
+ } else {
+ return mNoPersonalToWorkEmptyState;
+ }
+ }
+
+
+ /**
+ * Empty state that gets strings from the device policy manager and tracks events into
+ * event logger of the device policy events.
+ */
+ public static class DevicePolicyBlockerEmptyState implements EmptyState {
+
+ @NonNull
+ private final Context mContext;
+ private final String mDevicePolicyStringTitleId;
+ @StringRes
+ private final int mDefaultTitleResource;
+ private final String mDevicePolicyStringSubtitleId;
+ @StringRes
+ private final int mDefaultSubtitleResource;
+ private final int mEventId;
+ @NonNull
+ private final String mEventCategory;
+
+ public DevicePolicyBlockerEmptyState(@NonNull Context context,
+ String devicePolicyStringTitleId, @StringRes int defaultTitleResource,
+ String devicePolicyStringSubtitleId, @StringRes int defaultSubtitleResource,
+ int devicePolicyEventId, @NonNull String devicePolicyEventCategory) {
+ mContext = context;
+ mDevicePolicyStringTitleId = devicePolicyStringTitleId;
+ mDefaultTitleResource = defaultTitleResource;
+ mDevicePolicyStringSubtitleId = devicePolicyStringSubtitleId;
+ mDefaultSubtitleResource = defaultSubtitleResource;
+ mEventId = devicePolicyEventId;
+ mEventCategory = devicePolicyEventCategory;
+ }
+
+ @Nullable
+ @Override
+ public String getTitle() {
+ return mContext.getSystemService(DevicePolicyManager.class).getResources().getString(
+ mDevicePolicyStringTitleId,
+ () -> mContext.getString(mDefaultTitleResource));
+ }
+
+ @Nullable
+ @Override
+ public String getSubtitle() {
+ return mContext.getSystemService(DevicePolicyManager.class).getResources().getString(
+ mDevicePolicyStringSubtitleId,
+ () -> mContext.getString(mDefaultSubtitleResource));
+ }
+
+ @Override
+ public void onEmptyStateShown() {
+ DevicePolicyEventLogger.createEvent(mEventId)
+ .setStrings(mEventCategory)
+ .write();
+ }
+
+ @Override
+ public boolean shouldSkipDataRebuild() {
+ return true;
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/emptystate/WorkProfilePausedEmptyStateProvider.java b/java/src/com/android/intentresolver/v2/emptystate/WorkProfilePausedEmptyStateProvider.java
new file mode 100644
index 0000000..a6fee3e
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/emptystate/WorkProfilePausedEmptyStateProvider.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2.emptystate;
+
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE;
+
+import android.app.admin.DevicePolicyEventLogger;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.os.UserHandle;
+import android.stats.devicepolicy.nano.DevicePolicyEnums;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener;
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.WorkProfileAvailabilityManager;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+
+/**
+ * Chooser/ResolverActivity empty state provider that returns empty state which is shown when
+ * work profile is paused and we need to show a button to enable it.
+ */
+public class WorkProfilePausedEmptyStateProvider implements EmptyStateProvider {
+
+ private final UserHandle mWorkProfileUserHandle;
+ private final WorkProfileAvailabilityManager mWorkProfileAvailability;
+ private final String mMetricsCategory;
+ private final OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
+ private final Context mContext;
+
+ public WorkProfilePausedEmptyStateProvider(@NonNull Context context,
+ @Nullable UserHandle workProfileUserHandle,
+ @NonNull WorkProfileAvailabilityManager workProfileAvailability,
+ @Nullable OnSwitchOnWorkSelectedListener onSwitchOnWorkSelectedListener,
+ @NonNull String metricsCategory) {
+ mContext = context;
+ mWorkProfileUserHandle = workProfileUserHandle;
+ mWorkProfileAvailability = workProfileAvailability;
+ mMetricsCategory = metricsCategory;
+ mOnSwitchOnWorkSelectedListener = onSwitchOnWorkSelectedListener;
+ }
+
+ @Nullable
+ @Override
+ public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ if (!resolverListAdapter.getUserHandle().equals(mWorkProfileUserHandle)
+ || !mWorkProfileAvailability.isQuietModeEnabled()
+ || resolverListAdapter.getCount() == 0) {
+ return null;
+ }
+
+ final String title = mContext.getSystemService(DevicePolicyManager.class)
+ .getResources().getString(RESOLVER_WORK_PAUSED_TITLE,
+ () -> mContext.getString(R.string.resolver_turn_on_work_apps));
+
+ return new WorkProfileOffEmptyState(title, (tab) -> {
+ tab.showSpinner();
+ if (mOnSwitchOnWorkSelectedListener != null) {
+ mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected();
+ }
+ mWorkProfileAvailability.requestQuietModeEnabled(false);
+ }, mMetricsCategory);
+ }
+
+ public static class WorkProfileOffEmptyState implements EmptyState {
+
+ private final String mTitle;
+ private final ClickListener mOnClick;
+ private final String mMetricsCategory;
+
+ public WorkProfileOffEmptyState(String title, @NonNull ClickListener onClick,
+ @NonNull String metricsCategory) {
+ mTitle = title;
+ mOnClick = onClick;
+ mMetricsCategory = metricsCategory;
+ }
+
+ @Nullable
+ @Override
+ public String getTitle() {
+ return mTitle;
+ }
+
+ @Nullable
+ @Override
+ public ClickListener getButtonClickListener() {
+ return mOnClick;
+ }
+
+ @Override
+ public void onEmptyStateShown() {
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_EMPTY_STATE_WORK_APPS_DISABLED)
+ .setStrings(mMetricsCategory)
+ .write();
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt b/java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt
new file mode 100644
index 0000000..4e8783f
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.icons
+
+import android.content.Context
+import androidx.lifecycle.Lifecycle
+import com.android.intentresolver.icons.DefaultTargetDataLoader
+import com.android.intentresolver.icons.TargetDataLoader
+import com.android.intentresolver.inject.ActivityOwned
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityComponent
+import dagger.hilt.android.qualifiers.ActivityContext
+import dagger.hilt.android.scopes.ActivityScoped
+
+@Module
+@InstallIn(ActivityComponent::class)
+object TargetDataLoaderModule {
+ @Provides
+ @ActivityScoped
+ fun targetDataLoader(
+ @ActivityContext context: Context,
+ @ActivityOwned lifecycle: Lifecycle,
+ ): TargetDataLoader = DefaultTargetDataLoader(context, lifecycle, isAudioCaptureDevice = false)
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt b/java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt
new file mode 100644
index 0000000..5855e2f
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import com.android.intentresolver.ChooserRequestParameters
+
+/** A class that is able to identify components that should be hidden from the user. */
+interface FilterableComponents {
+ /** Whether this component should hidden from the user. */
+ fun isComponentFiltered(name: ComponentName): Boolean
+}
+
+/** A class that never filters components. */
+class NoComponentFiltering : FilterableComponents {
+ override fun isComponentFiltered(name: ComponentName): Boolean = false
+}
+
+/** A class that filters components by chooser request filter. */
+class ChooserRequestFilteredComponents(
+ private val chooserRequestParameters: ChooserRequestParameters,
+) : FilterableComponents {
+ override fun isComponentFiltered(name: ComponentName): Boolean =
+ chooserRequestParameters.filteredComponentNames.contains(name)
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt b/java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt
new file mode 100644
index 0000000..bb9394b
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt
@@ -0,0 +1,70 @@
+package com.android.intentresolver.v2.listcontroller
+
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import com.android.intentresolver.ResolvedComponentInfo
+
+/** A class for translating [Intent]s to [ResolvedComponentInfo]s. */
+interface IntentResolver {
+ /**
+ * Get data about all the ways the user with the specified handle can resolve any of the
+ * provided `intents`.
+ */
+ fun getResolversForIntentAsUser(
+ shouldGetResolvedFilter: Boolean,
+ shouldGetActivityMetadata: Boolean,
+ shouldGetOnlyDefaultActivities: Boolean,
+ intents: List<Intent>,
+ userHandle: UserHandle,
+ ): List<ResolvedComponentInfo>
+}
+
+/** Resolves [Intent]s using the [packageManager], deduping using the given [ResolveListDeduper]. */
+class IntentResolverImpl(
+ private val packageManager: PackageManager,
+ resolveListDeduper: ResolveListDeduper,
+) : IntentResolver, ResolveListDeduper by resolveListDeduper {
+ override fun getResolversForIntentAsUser(
+ shouldGetResolvedFilter: Boolean,
+ shouldGetActivityMetadata: Boolean,
+ shouldGetOnlyDefaultActivities: Boolean,
+ intents: List<Intent>,
+ userHandle: UserHandle,
+ ): List<ResolvedComponentInfo> {
+ val baseFlags =
+ ((if (shouldGetOnlyDefaultActivities) PackageManager.MATCH_DEFAULT_ONLY else 0) or
+ PackageManager.MATCH_DIRECT_BOOT_AWARE or
+ PackageManager.MATCH_DIRECT_BOOT_UNAWARE or
+ (if (shouldGetResolvedFilter) PackageManager.GET_RESOLVED_FILTER else 0) or
+ (if (shouldGetActivityMetadata) PackageManager.GET_META_DATA else 0) or
+ PackageManager.MATCH_CLONE_PROFILE)
+ return getResolversForIntentAsUserInternal(
+ intents,
+ userHandle,
+ baseFlags,
+ )
+ }
+
+ private fun getResolversForIntentAsUserInternal(
+ intents: List<Intent>,
+ userHandle: UserHandle,
+ baseFlags: Int,
+ ): List<ResolvedComponentInfo> = buildList {
+ for (intent in intents) {
+ var flags = baseFlags
+ if (intent.isWebIntent || intent.flags and Intent.FLAG_ACTIVITY_MATCH_EXTERNAL != 0) {
+ flags = flags or PackageManager.MATCH_INSTANT
+ }
+ // Because of AIDL bug, queryIntentActivitiesAsUser can't accept subclasses of Intent.
+ val fixedIntent =
+ if (intent.javaClass != Intent::class.java) {
+ Intent(intent)
+ } else {
+ intent
+ }
+ val infos = packageManager.queryIntentActivitiesAsUser(fixedIntent, flags, userHandle)
+ addToResolveListWithDedupe(this, fixedIntent, infos)
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt b/java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt
new file mode 100644
index 0000000..b285652
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.listcontroller
+
+import android.app.AppGlobals
+import android.content.ContentResolver
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.IPackageManager
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.RemoteException
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/** Class that stores and retrieves the most recently chosen resolutions. */
+interface LastChosenManager {
+
+ /** Returns the most recently chosen resolution. */
+ suspend fun getLastChosen(): ResolveInfo
+
+ /** Sets the most recently chosen resolution. */
+ suspend fun setLastChosen(intent: Intent, filter: IntentFilter, match: Int)
+}
+
+/**
+ * Stores and retrieves the most recently chosen resolutions using the [PackageManager] provided by
+ * the [packageManagerProvider].
+ */
+class PackageManagerLastChosenManager(
+ private val contentResolver: ContentResolver,
+ private val bgDispatcher: CoroutineDispatcher,
+ private val targetIntent: Intent,
+ private val packageManagerProvider: () -> IPackageManager = AppGlobals::getPackageManager,
+) : LastChosenManager {
+
+ @Throws(RemoteException::class)
+ override suspend fun getLastChosen(): ResolveInfo {
+ return withContext(bgDispatcher) {
+ packageManagerProvider()
+ .getLastChosenActivity(
+ targetIntent,
+ targetIntent.resolveTypeIfNeeded(contentResolver),
+ PackageManager.MATCH_DEFAULT_ONLY,
+ )
+ }
+ }
+
+ @Throws(RemoteException::class)
+ override suspend fun setLastChosen(intent: Intent, filter: IntentFilter, match: Int) {
+ return withContext(bgDispatcher) {
+ packageManagerProvider()
+ .setLastChosenActivity(
+ intent,
+ intent.resolveType(contentResolver),
+ PackageManager.MATCH_DEFAULT_ONLY,
+ filter,
+ match,
+ intent.component,
+ )
+ }
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/TestApplication.kt b/java/src/com/android/intentresolver/v2/listcontroller/ListController.kt
index 849cfba..4ddab75 100644
--- a/java/tests/src/com/android/intentresolver/TestApplication.kt
+++ b/java/src/com/android/intentresolver/v2/listcontroller/ListController.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 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.
@@ -14,14 +14,8 @@
* limitations under the License.
*/
-package com.android.intentresolver
+package com.android.intentresolver.v2.listcontroller
-import android.app.Application
-import android.content.Context
-import android.os.UserHandle
-
-class TestApplication : Application() {
-
- // return the current context as a work profile doesn't really exist in these tests
- override fun createContextAsUser(user: UserHandle, flags: Int): Context = this
-} \ No newline at end of file
+/** Controller for managing lists of [com.android.intentresolver.ResolvedComponentInfo]s. */
+interface ListController :
+ LastChosenManager, IntentResolver, ResolvedComponentFiltering, ResolvedComponentSorting
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt b/java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt
new file mode 100644
index 0000000..cae2af9
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt
@@ -0,0 +1,34 @@
+package com.android.intentresolver.v2.listcontroller
+
+import android.app.ActivityManager
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/** Class for checking if a permission has been granted. */
+interface PermissionChecker {
+ /** Checks if the given [permission] has been granted. */
+ suspend fun checkComponentPermission(
+ permission: String,
+ uid: Int,
+ owningUid: Int,
+ exported: Boolean,
+ ): Int
+}
+
+/**
+ * Class for checking if a permission has been granted using the static
+ * [ActivityManager.checkComponentPermission].
+ */
+class ActivityManagerPermissionChecker(
+ private val bgDispatcher: CoroutineDispatcher,
+) : PermissionChecker {
+ override suspend fun checkComponentPermission(
+ permission: String,
+ uid: Int,
+ owningUid: Int,
+ exported: Boolean,
+ ): Int =
+ withContext(bgDispatcher) {
+ ActivityManager.checkComponentPermission(permission, uid, owningUid, exported)
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt b/java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt
new file mode 100644
index 0000000..8be45ba
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.SharedPreferences
+
+/** A class that is able to identify components that should be pinned for the user. */
+interface PinnableComponents {
+ /** Whether this component is pinned by the user. */
+ fun isComponentPinned(name: ComponentName): Boolean
+}
+
+/** A class that never pins components. */
+class NoComponentPinning : PinnableComponents {
+ override fun isComponentPinned(name: ComponentName): Boolean = false
+}
+
+/** A class that determines pinnable components by user preferences. */
+class SharedPreferencesPinnedComponents(
+ private val pinnedSharedPreferences: SharedPreferences,
+) : PinnableComponents {
+ override fun isComponentPinned(name: ComponentName): Boolean =
+ pinnedSharedPreferences.getBoolean(name.flattenToString(), false)
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt b/java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt
new file mode 100644
index 0000000..f0b4bf3
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt
@@ -0,0 +1,69 @@
+package com.android.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ResolveInfo
+import android.util.Log
+import com.android.intentresolver.ResolvedComponentInfo
+
+/** A class for adding [ResolveInfo]s to a list of [ResolvedComponentInfo]s without duplicates. */
+interface ResolveListDeduper {
+ /**
+ * Adds [ResolveInfo]s in [from] to [ResolvedComponentInfo]s in [into], creating new
+ * [ResolvedComponentInfo]s when there is not already a corresponding one.
+ *
+ * This method may be destructive to both the given [into] list and the underlying
+ * [ResolvedComponentInfo]s.
+ */
+ fun addToResolveListWithDedupe(
+ into: MutableList<ResolvedComponentInfo>,
+ intent: Intent,
+ from: List<ResolveInfo>,
+ )
+}
+
+/**
+ * Default implementation for adding [ResolveInfo]s to a list of [ResolvedComponentInfo]s without
+ * duplicates. Uses the given [PinnableComponents] to determine the pinning state of newly created
+ * [ResolvedComponentInfo]s.
+ */
+class ResolveListDeduperImpl(pinnableComponents: PinnableComponents) :
+ ResolveListDeduper, PinnableComponents by pinnableComponents {
+ override fun addToResolveListWithDedupe(
+ into: MutableList<ResolvedComponentInfo>,
+ intent: Intent,
+ from: List<ResolveInfo>,
+ ) {
+ from.forEach { newInfo ->
+ if (newInfo.userHandle == null) {
+ Log.w(TAG, "Skipping ResolveInfo with no userHandle: $newInfo")
+ return@forEach
+ }
+ val oldInfo = into.firstOrNull { isSameResolvedComponent(newInfo, it) }
+ // If existing resolution found, add to existing and filter out
+ if (oldInfo != null) {
+ oldInfo.add(intent, newInfo)
+ } else {
+ with(newInfo.activityInfo) {
+ into.add(
+ ResolvedComponentInfo(
+ ComponentName(packageName, name),
+ intent,
+ newInfo,
+ )
+ .apply { isPinned = isComponentPinned(name) },
+ )
+ }
+ }
+ }
+ }
+
+ private fun isSameResolvedComponent(a: ResolveInfo, b: ResolvedComponentInfo): Boolean {
+ val ai = a.activityInfo
+ return ai.packageName == b.name.packageName && ai.name == b.name.className
+ }
+
+ companion object {
+ const val TAG = "ResolveListDeduper"
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt b/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt
new file mode 100644
index 0000000..e78bff0
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt
@@ -0,0 +1,121 @@
+package com.android.intentresolver.v2.listcontroller
+
+import android.content.pm.PackageManager
+import android.util.Log
+import com.android.intentresolver.ResolvedComponentInfo
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+
+/** Provides filtering methods for lists of [ResolvedComponentInfo]. */
+interface ResolvedComponentFiltering {
+ /**
+ * Returns a list with all the [ResolvedComponentInfo] in [inputList], less the ones that are
+ * not eligible.
+ */
+ suspend fun filterIneligibleActivities(
+ inputList: List<ResolvedComponentInfo>,
+ ): List<ResolvedComponentInfo>
+
+ /** Filter out any low priority items. */
+ fun filterLowPriority(inputList: List<ResolvedComponentInfo>): List<ResolvedComponentInfo>
+}
+
+/**
+ * Default instantiation of the filtering methods for lists of [ResolvedComponentInfo].
+ *
+ * Binder calls are performed on the given [bgDispatcher] and permissions are checked as if launched
+ * from the given [launchedFromUid] UID. Component filtering is handled by the given
+ * [FilterableComponents] and permission checking is handled by the given [PermissionChecker].
+ */
+class ResolvedComponentFilteringImpl(
+ private val launchedFromUid: Int,
+ filterableComponents: FilterableComponents,
+ permissionChecker: PermissionChecker,
+) :
+ ResolvedComponentFiltering,
+ PermissionChecker by permissionChecker,
+ FilterableComponents by filterableComponents {
+ constructor(
+ bgDispatcher: CoroutineDispatcher,
+ launchedFromUid: Int,
+ filterableComponents: FilterableComponents,
+ ) : this(
+ launchedFromUid = launchedFromUid,
+ filterableComponents = filterableComponents,
+ permissionChecker = ActivityManagerPermissionChecker(bgDispatcher),
+ )
+
+ /**
+ * Filter out items that are filtered by [FilterableComponents] or do not have the necessary
+ * permissions.
+ */
+ override suspend fun filterIneligibleActivities(
+ inputList: List<ResolvedComponentInfo>,
+ ): List<ResolvedComponentInfo> = coroutineScope {
+ inputList
+ .map {
+ val activityInfo = it.getResolveInfoAt(0).activityInfo
+ if (isComponentFiltered(activityInfo.componentName)) {
+ CompletableDeferred(value = null)
+ } else {
+ // Do all permission checks in parallel
+ async {
+ val granted =
+ checkComponentPermission(
+ activityInfo.permission,
+ launchedFromUid,
+ activityInfo.applicationInfo.uid,
+ activityInfo.exported,
+ ) == PackageManager.PERMISSION_GRANTED
+ if (granted) it else null
+ }
+ }
+ }
+ .awaitAll()
+ .filterNotNull()
+ }
+
+ /**
+ * Filters out all elements starting with the first elements with a different priority or
+ * default status than the first element.
+ */
+ override fun filterLowPriority(
+ inputList: List<ResolvedComponentInfo>,
+ ): List<ResolvedComponentInfo> {
+ val firstResolveInfo = inputList[0].getResolveInfoAt(0)
+ // Only display the first matches that are either of equal
+ // priority or have asked to be default options.
+ val firstDiffIndex =
+ inputList.indexOfFirst { resolvedComponentInfo ->
+ val resolveInfo = resolvedComponentInfo.getResolveInfoAt(0)
+ if (firstResolveInfo == resolveInfo) {
+ false
+ } else {
+ if (DEBUG) {
+ Log.v(
+ TAG,
+ "${firstResolveInfo?.activityInfo?.name}=" +
+ "${firstResolveInfo?.priority}/${firstResolveInfo?.isDefault}" +
+ " vs ${resolveInfo?.activityInfo?.name}=" +
+ "${resolveInfo?.priority}/${resolveInfo?.isDefault}"
+ )
+ }
+ firstResolveInfo!!.priority != resolveInfo!!.priority ||
+ firstResolveInfo.isDefault != resolveInfo.isDefault
+ }
+ }
+ return if (firstDiffIndex == -1) {
+ inputList
+ } else {
+ inputList.subList(0, firstDiffIndex)
+ }
+ }
+
+ companion object {
+ private const val TAG = "ResolvedComponentFilter"
+ private const val DEBUG = false
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt b/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt
new file mode 100644
index 0000000..8ab41ef
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt
@@ -0,0 +1,108 @@
+package com.android.intentresolver.v2.listcontroller
+
+import android.os.UserHandle
+import android.util.Log
+import com.android.intentresolver.ResolvedComponentInfo
+import com.android.intentresolver.chooser.DisplayResolveInfo
+import com.android.intentresolver.chooser.TargetInfo
+import com.android.intentresolver.model.AbstractResolverComparator
+import java.util.concurrent.atomic.AtomicReference
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/** Provides sorting methods for lists of [ResolvedComponentInfo]. */
+interface ResolvedComponentSorting {
+ /** Returns the a copy of the [inputList] sorted by app share score. */
+ suspend fun sorted(inputList: List<ResolvedComponentInfo>?): List<ResolvedComponentInfo>?
+
+ /** Returns the app share score of the [target]. */
+ fun getScore(target: DisplayResolveInfo): Float
+
+ /** Returns the app share score of the [targetInfo]. */
+ fun getScore(targetInfo: TargetInfo): Float
+
+ /** Updates the model about [targetInfo]. */
+ suspend fun updateModel(targetInfo: TargetInfo)
+
+ /** Updates the model about Activity selection. */
+ suspend fun updateChooserCounts(packageName: String, user: UserHandle, action: String)
+
+ /** Cleans up resources. Nothing should be called after calling this. */
+ fun destroy()
+}
+
+/**
+ * Provides sorting methods using the given [resolverComparator].
+ *
+ * Long calculations and binder calls are performed on the given [bgDispatcher].
+ */
+class ResolvedComponentSortingImpl(
+ private val bgDispatcher: CoroutineDispatcher,
+ private val resolverComparator: AbstractResolverComparator,
+) : ResolvedComponentSorting {
+
+ private val computeComplete = AtomicReference<CompletableDeferred<Unit>?>(null)
+
+ @Throws(InterruptedException::class)
+ private suspend fun computeIfNeeded(inputList: List<ResolvedComponentInfo>) {
+ if (computeComplete.compareAndSet(null, CompletableDeferred())) {
+ resolverComparator.setCallBack { computeComplete.get()!!.complete(Unit) }
+ resolverComparator.compute(inputList)
+ }
+ with(computeComplete.get()!!) { if (isCompleted) return else return await() }
+ }
+
+ override suspend fun sorted(
+ inputList: List<ResolvedComponentInfo>?,
+ ): List<ResolvedComponentInfo>? {
+ if (inputList.isNullOrEmpty()) return inputList
+
+ return withContext(bgDispatcher) {
+ try {
+ val beforeRank = System.currentTimeMillis()
+ computeIfNeeded(inputList)
+ val sorted = inputList.sortedWith(resolverComparator)
+ val afterRank = System.currentTimeMillis()
+ if (DEBUG) {
+ Log.d(TAG, "Time Cost: ${afterRank - beforeRank}")
+ }
+ sorted
+ } catch (e: InterruptedException) {
+ Log.e(TAG, "Compute & Sort was interrupted: $e")
+ null
+ }
+ }
+ }
+
+ override fun getScore(target: DisplayResolveInfo): Float {
+ return resolverComparator.getScore(target)
+ }
+
+ override fun getScore(targetInfo: TargetInfo): Float {
+ return resolverComparator.getScore(targetInfo)
+ }
+
+ override suspend fun updateModel(targetInfo: TargetInfo) {
+ withContext(bgDispatcher) { resolverComparator.updateModel(targetInfo) }
+ }
+
+ override suspend fun updateChooserCounts(
+ packageName: String,
+ user: UserHandle,
+ action: String,
+ ) {
+ withContext(bgDispatcher) {
+ resolverComparator.updateChooserCounts(packageName, user, action)
+ }
+ }
+
+ override fun destroy() {
+ resolverComparator.destroy()
+ }
+
+ companion object {
+ private const val TAG = "ResolvedComponentSort"
+ private const val DEBUG = false
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt b/java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt
new file mode 100644
index 0000000..efbf053
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt
@@ -0,0 +1,35 @@
+package com.android.intentresolver.v2.platform
+
+import android.content.ComponentName
+import android.content.res.Resources
+import androidx.annotation.StringRes
+import com.android.intentresolver.R
+import com.android.intentresolver.inject.ApplicationOwned
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import java.util.Optional
+import javax.inject.Qualifier
+import javax.inject.Singleton
+
+internal fun Resources.componentName(@StringRes resId: Int): ComponentName? {
+ check(getResourceTypeName(resId) == "string") { "resId must be a string" }
+ return ComponentName.unflattenFromString(getString(resId))
+}
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ImageEditor
+
+@Module
+@InstallIn(SingletonComponent::class)
+object ImageEditorModule {
+ /**
+ * The name of the preferred Activity to launch for editing images. This is added to Intents to
+ * edit images using Intent.ACTION_EDIT.
+ */
+ @Provides
+ @Singleton
+ @ImageEditor
+ fun imageEditorComponent(@ApplicationOwned resources: Resources) =
+ Optional.ofNullable(resources.componentName(R.string.config_systemImageEditor))
+}
diff --git a/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt b/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt
new file mode 100644
index 0000000..25ee919
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt
@@ -0,0 +1,32 @@
+package com.android.intentresolver.v2.platform
+
+import android.content.ComponentName
+import android.content.res.Resources
+import android.provider.Settings.Secure.NEARBY_SHARING_COMPONENT
+import com.android.intentresolver.R
+import com.android.intentresolver.inject.ApplicationOwned
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import java.util.Optional
+import javax.inject.Qualifier
+import javax.inject.Singleton
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class NearbyShare
+
+@Module
+@InstallIn(SingletonComponent::class)
+object NearbyShareModule {
+
+ @Provides
+ @Singleton
+ @NearbyShare
+ fun nearbyShareComponent(@ApplicationOwned resources: Resources, settings: SecureSettings) =
+ Optional.ofNullable(
+ ComponentName.unflattenFromString(
+ settings.getString(NEARBY_SHARING_COMPONENT)?.ifEmpty { null }
+ ?: resources.getString(R.string.config_defaultNearbySharingComponent),
+ )
+ )
+}
diff --git a/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt b/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt
new file mode 100644
index 0000000..531152b
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt
@@ -0,0 +1,30 @@
+package com.android.intentresolver.v2.platform
+
+import android.content.ContentResolver
+import android.provider.Settings
+import javax.inject.Inject
+
+/**
+ * Implements [SecureSettings] backed by Settings.Secure and a ContentResolver.
+ *
+ * These methods make Binder calls and may block, so use on the Main thread should be avoided.
+ */
+class PlatformSecureSettings @Inject constructor(private val resolver: ContentResolver) :
+ SecureSettings {
+
+ override fun getString(name: String): String? {
+ return Settings.Secure.getString(resolver, name)
+ }
+
+ override fun getInt(name: String): Int? {
+ return runCatching { Settings.Secure.getInt(resolver, name) }.getOrNull()
+ }
+
+ override fun getLong(name: String): Long? {
+ return runCatching { Settings.Secure.getLong(resolver, name) }.getOrNull()
+ }
+
+ override fun getFloat(name: String): Float? {
+ return runCatching { Settings.Secure.getFloat(resolver, name) }.getOrNull()
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt b/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt
new file mode 100644
index 0000000..62ee8ae
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt
@@ -0,0 +1,25 @@
+package com.android.intentresolver.v2.platform
+
+import android.provider.Settings.SettingNotFoundException
+
+/**
+ * A component which provides access to values from [android.provider.Settings.Secure].
+ *
+ * All methods return nullable types instead of throwing [SettingNotFoundException] which yields
+ * cleaner, more idiomatic Kotlin code:
+ *
+ * // apply a default: val foo = settings.getInt(FOO) ?: DEFAULT_FOO
+ *
+ * // assert if missing: val required = settings.getInt(REQUIRED_VALUE) ?: error("required value
+ * missing")
+ */
+interface SecureSettings {
+
+ fun getString(name: String): String?
+
+ fun getInt(name: String): Int?
+
+ fun getLong(name: String): Long?
+
+ fun getFloat(name: String): Float?
+}
diff --git a/java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt b/java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt
new file mode 100644
index 0000000..18f4702
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt
@@ -0,0 +1,14 @@
+package com.android.intentresolver.v2.platform
+
+import dagger.Binds
+import dagger.Module
+import dagger.Reusable
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+interface SecureSettingsModule {
+
+ @Binds @Reusable fun secureSettings(settings: PlatformSecureSettings): SecureSettings
+}
diff --git a/java/src/com/android/intentresolver/v2/ui/ActionTitle.java b/java/src/com/android/intentresolver/v2/ui/ActionTitle.java
new file mode 100644
index 0000000..271c6f3
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ui/ActionTitle.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.ui;
+
+import android.content.Intent;
+import android.provider.MediaStore;
+
+import androidx.annotation.StringRes;
+
+import com.android.intentresolver.R;
+import com.android.intentresolver.v2.ResolverActivity;
+
+/**
+ * Provides a set of related resources for different use cases.
+ */
+public enum ActionTitle {
+ VIEW(Intent.ACTION_VIEW,
+ R.string.whichViewApplication,
+ R.string.whichViewApplicationNamed,
+ R.string.whichViewApplicationLabel),
+ EDIT(Intent.ACTION_EDIT,
+ R.string.whichEditApplication,
+ R.string.whichEditApplicationNamed,
+ R.string.whichEditApplicationLabel),
+ SEND(Intent.ACTION_SEND,
+ R.string.whichSendApplication,
+ R.string.whichSendApplicationNamed,
+ R.string.whichSendApplicationLabel),
+ SENDTO(Intent.ACTION_SENDTO,
+ R.string.whichSendToApplication,
+ R.string.whichSendToApplicationNamed,
+ R.string.whichSendToApplicationLabel),
+ SEND_MULTIPLE(Intent.ACTION_SEND_MULTIPLE,
+ R.string.whichSendApplication,
+ R.string.whichSendApplicationNamed,
+ R.string.whichSendApplicationLabel),
+ CAPTURE_IMAGE(MediaStore.ACTION_IMAGE_CAPTURE,
+ R.string.whichImageCaptureApplication,
+ R.string.whichImageCaptureApplicationNamed,
+ R.string.whichImageCaptureApplicationLabel),
+ DEFAULT(null,
+ R.string.whichApplication,
+ R.string.whichApplicationNamed,
+ R.string.whichApplicationLabel),
+ HOME(Intent.ACTION_MAIN,
+ R.string.whichHomeApplication,
+ R.string.whichHomeApplicationNamed,
+ R.string.whichHomeApplicationLabel);
+
+ // titles for layout that deals with http(s) intents
+ public static final int BROWSABLE_TITLE_RES = R.string.whichOpenLinksWith;
+ public static final int BROWSABLE_HOST_TITLE_RES = R.string.whichOpenHostLinksWith;
+ public static final int BROWSABLE_HOST_APP_TITLE_RES = R.string.whichOpenHostLinksWithApp;
+ public static final int BROWSABLE_APP_TITLE_RES = R.string.whichOpenLinksWithApp;
+
+ public final String action;
+ public final int titleRes;
+ public final int namedTitleRes;
+ public final @StringRes int labelRes;
+
+ ActionTitle(String action, int titleRes, int namedTitleRes, @StringRes int labelRes) {
+ this.action = action;
+ this.titleRes = titleRes;
+ this.namedTitleRes = namedTitleRes;
+ this.labelRes = labelRes;
+ }
+
+ public static ActionTitle forAction(String action) {
+ for (ActionTitle title : values()) {
+ if (title != HOME && action != null && action.equals(title.action)) {
+ return title;
+ }
+ }
+ return DEFAULT;
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/util/MutableLazy.kt b/java/src/com/android/intentresolver/v2/util/MutableLazy.kt
new file mode 100644
index 0000000..4ce9b7f
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/util/MutableLazy.kt
@@ -0,0 +1,36 @@
+package com.android.intentresolver.v2.util
+
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.reflect.KProperty
+
+/** A lazy delegate that can be changed to a new lazy or null at any time. */
+class MutableLazy<T>(initializer: () -> T?) : Lazy<T?> {
+
+ override val value: T?
+ get() = lazy.get()?.value
+
+ private var lazy: AtomicReference<Lazy<T?>?> = AtomicReference(lazy(initializer))
+
+ override fun isInitialized(): Boolean = lazy.get()?.isInitialized() != false
+
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): T? =
+ lazy.get()?.getValue(thisRef, property)
+
+ /** Replace the existing lazy logic with the [newLazy] */
+ fun setLazy(newLazy: Lazy<T?>?) {
+ lazy.set(newLazy)
+ }
+
+ /** Replace the existing lazy logic with a [Lazy] created from the [newInitializer]. */
+ fun setLazy(newInitializer: () -> T?) {
+ lazy.set(lazy(newInitializer))
+ }
+
+ /** Set the lazy logic to null. */
+ fun clear() {
+ lazy.set(null)
+ }
+}
+
+/** Constructs a [MutableLazy] using the given [initializer] */
+fun <T> mutableLazy(initializer: () -> T?) = MutableLazy(initializer)
diff --git a/java/src/com/android/intentresolver/v2/validation/Findings.kt b/java/src/com/android/intentresolver/v2/validation/Findings.kt
new file mode 100644
index 0000000..9a3cc9c
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/Findings.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.validation
+
+import android.util.Log
+import com.android.intentresolver.v2.validation.Importance.CRITICAL
+import com.android.intentresolver.v2.validation.Importance.WARNING
+import kotlin.reflect.KClass
+
+sealed interface Finding {
+ val importance: Importance
+ val message: String
+}
+
+enum class Importance {
+ CRITICAL,
+ WARNING,
+}
+
+val Finding.logcatPriority
+ get() =
+ when (importance) {
+ CRITICAL -> Log.ERROR
+ else -> Log.WARN
+ }
+
+private fun formatMessage(key: String? = null, msg: String) = buildString {
+ key?.also { append("['$key']: ") }
+ append(msg)
+}
+
+data class IgnoredValue(
+ val key: String,
+ val reason: String,
+) : Finding {
+ override val importance = WARNING
+
+ override val message: String
+ get() = formatMessage(key, "Ignored. $reason")
+}
+
+data class RequiredValueMissing(
+ val key: String,
+ val allowedType: KClass<*>,
+) : Finding {
+
+ override val importance = CRITICAL
+
+ override val message: String
+ get() =
+ formatMessage(
+ key,
+ "expected value of ${allowedType.simpleName}, " + "but no value was present"
+ )
+}
+
+data class WrongElementType(
+ val key: String,
+ override val importance: Importance,
+ val container: KClass<*>,
+ val actualType: KClass<*>,
+ val expectedType: KClass<*>
+) : Finding {
+ override val message: String
+ get() =
+ formatMessage(
+ key,
+ "${container.simpleName} expected with elements of " +
+ "${expectedType.simpleName} " +
+ "but found ${actualType.simpleName} values instead"
+ )
+}
+
+data class ValueIsWrongType(
+ val key: String,
+ override val importance: Importance,
+ val actualType: KClass<*>,
+ val allowedTypes: List<KClass<*>>,
+) : Finding {
+
+ override val message: String
+ get() =
+ formatMessage(
+ key,
+ "expected value of ${allowedTypes.map(KClass<*>::simpleName)} " +
+ "but was ${actualType.simpleName}"
+ )
+}
+
+data class UncaughtException(val thrown: Throwable, val key: String? = null) : Finding {
+ override val importance: Importance
+ get() = CRITICAL
+ override val message: String
+ get() =
+ formatMessage(
+ key,
+ "An unhandled exception was caught during validation: " +
+ thrown.stackTraceToString()
+ )
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/Validation.kt b/java/src/com/android/intentresolver/v2/validation/Validation.kt
new file mode 100644
index 0000000..4693960
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/Validation.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.validation
+
+import com.android.intentresolver.v2.validation.Importance.CRITICAL
+import com.android.intentresolver.v2.validation.Importance.WARNING
+
+/**
+ * Provides a mechanism for validating a result from a set of properties.
+ *
+ * The results of validation are provided as [findings].
+ */
+interface Validation {
+ val findings: List<Finding>
+
+ /**
+ * Require a valid property.
+ *
+ * If [property] is not valid, this [Validation] will be immediately completed as [Invalid].
+ *
+ * @param property the required property
+ * @return a valid **T**
+ */
+ @Throws(InvalidResultError::class) fun <T> required(property: Validator<T>): T
+
+ /**
+ * Request an optional value for a property.
+ *
+ * If [property] is not valid, this [Validation] will be immediately completed as [Invalid].
+ *
+ * @param property the required property
+ * @return a valid **T**
+ */
+ fun <T> optional(property: Validator<T>): T?
+
+ /**
+ * Report a property as __ignored__.
+ *
+ * The presence of any value will report a warning citing [reason].
+ */
+ fun <T> ignored(property: Validator<T>, reason: String)
+}
+
+/** Performs validation for a specific key -> value pair. */
+interface Validator<T> {
+ val key: String
+
+ /**
+ * Performs validation on a specific value from [source].
+ *
+ * @param source a source for reading the property value. Values are intentionally untyped
+ * (Any?) to avoid upstream code from making type assertions through type inference. Types are
+ * asserted later using a [Validator].
+ * @param importance the importance of any findings
+ */
+ fun validate(source: (String) -> Any?, importance: Importance): ValidationResult<T>
+}
+
+internal class InvalidResultError internal constructor() : Error()
+
+/**
+ * Perform a number of validations on the source, assembling and returning a Result.
+ *
+ * When an exception is thrown by [validate], it is caught here. In response, a failed
+ * [ValidationResult] is returned containing a [CRITICAL] [Finding] for the exception.
+ *
+ * @param validate perform validations and return a [ValidationResult]
+ */
+fun <T> validateFrom(source: (String) -> Any?, validate: Validation.() -> T): ValidationResult<T> {
+ val validation = ValidationImpl(source)
+ return runCatching { validate(validation) }
+ .fold(
+ onSuccess = { result -> Valid(result, validation.findings) },
+ onFailure = {
+ when (it) {
+ // A validator has interrupted validation. Return the findings.
+ is InvalidResultError -> Invalid(validation.findings)
+
+ // Some other exception was thrown from [validate],
+ else -> Invalid(findings = listOf(UncaughtException(it)))
+ }
+ }
+ )
+}
+
+private class ValidationImpl(val source: (String) -> Any?) : Validation {
+ override val findings = mutableListOf<Finding>()
+
+ override fun <T> optional(property: Validator<T>): T? = validate(property, WARNING)
+
+ override fun <T> required(property: Validator<T>): T {
+ return validate(property, CRITICAL) ?: throw InvalidResultError()
+ }
+
+ override fun <T> ignored(property: Validator<T>, reason: String) {
+ val result = property.validate(source, WARNING)
+ if (result.value != null) {
+ // Note: Any findings about the value (result.findings) are ignored.
+ findings += IgnoredValue(property.key, reason)
+ }
+ }
+
+ private fun <T> validate(property: Validator<T>, importance: Importance): T? {
+ return runCatching { property.validate(source, importance) }
+ .fold(
+ onSuccess = { result ->
+ findings += result.findings
+ result.value
+ },
+ onFailure = {
+ findings += UncaughtException(it, property.key)
+ null
+ }
+ )
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt
new file mode 100644
index 0000000..092cabe
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.validation
+
+import android.util.Log
+
+sealed interface ValidationResult<T> {
+ val value: T?
+ val findings: List<Finding>
+
+ fun isSuccess() = value != null
+
+ fun getOrThrow(): T =
+ checkNotNull(value) { "The result was invalid: " + findings.joinToString(separator = "\n") }
+
+ fun <T> reportToLogcat(tag: String) {
+ findings.forEach { Log.println(it.logcatPriority, tag, it.toString()) }
+ }
+}
+
+data class Valid<T>(override val value: T?, override val findings: List<Finding> = emptyList()) :
+ ValidationResult<T>
+
+data class Invalid<T>(override val findings: List<Finding>) : ValidationResult<T> {
+ override val value: T? = null
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt
new file mode 100644
index 0000000..3cefeb1
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.validation.types
+
+import android.content.Intent
+import android.net.Uri
+import com.android.intentresolver.v2.validation.Importance
+import com.android.intentresolver.v2.validation.RequiredValueMissing
+import com.android.intentresolver.v2.validation.Valid
+import com.android.intentresolver.v2.validation.ValidationResult
+import com.android.intentresolver.v2.validation.Validator
+import com.android.intentresolver.v2.validation.ValueIsWrongType
+
+class IntentOrUri(override val key: String) : Validator<Intent> {
+
+ override fun validate(
+ source: (String) -> Any?,
+ importance: Importance
+ ): ValidationResult<Intent> {
+
+ return when (val value = source(key)) {
+ // An intent, return it.
+ is Intent -> Valid(value)
+
+ // A Uri was supplied.
+ // Unfortunately, converting Uri -> Intent requires a toString().
+ is Uri -> Valid(Intent.parseUri(value.toString(), Intent.URI_INTENT_SCHEME))
+
+ // No value present.
+ null -> createResult(importance, RequiredValueMissing(key, Intent::class))
+
+ // Some other type.
+ else -> {
+ return createResult(
+ importance,
+ ValueIsWrongType(
+ key,
+ importance,
+ actualType = value::class,
+ allowedTypes = listOf(Intent::class, Uri::class)
+ )
+ )
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt
new file mode 100644
index 0000000..c6c4abb
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.validation.types
+
+import com.android.intentresolver.v2.validation.Importance
+import com.android.intentresolver.v2.validation.RequiredValueMissing
+import com.android.intentresolver.v2.validation.Valid
+import com.android.intentresolver.v2.validation.ValidationResult
+import com.android.intentresolver.v2.validation.Validator
+import com.android.intentresolver.v2.validation.ValueIsWrongType
+import com.android.intentresolver.v2.validation.WrongElementType
+import kotlin.reflect.KClass
+import kotlin.reflect.cast
+
+class ParceledArray<T : Any>(
+ override val key: String,
+ private val elementType: KClass<T>,
+) : Validator<List<T>> {
+
+ override fun validate(
+ source: (String) -> Any?,
+ importance: Importance
+ ): ValidationResult<List<T>> {
+
+ return when (val value: Any? = source(key)) {
+ // No value present.
+ null -> createResult(importance, RequiredValueMissing(key, elementType))
+
+ // A parcel does not transfer the element type information for parcelable
+ // arrays. This leads to a restored type of Array<Parcelable>, which is
+ // incompatible with Array<T : Parcelable>.
+
+ // To handle this safely, treat as Array<*>, assert contents of the expected
+ // parcelable type, and return as a list.
+
+ is Array<*> -> {
+ val invalid = value.filterNotNull().firstOrNull { !elementType.isInstance(it) }
+ when (invalid) {
+ // No invalid elements, result is ok.
+ null -> Valid(value.map { elementType.cast(it) })
+
+ // At least one incorrect element type found.
+ else ->
+ createResult(
+ importance,
+ WrongElementType(
+ key,
+ importance,
+ actualType = invalid::class,
+ container = Array::class,
+ expectedType = elementType
+ )
+ )
+ }
+ }
+
+ // The value is not an Array at all.
+ else ->
+ createResult(
+ importance,
+ ValueIsWrongType(
+ key,
+ importance,
+ actualType = value::class,
+ allowedTypes = listOf(elementType)
+ )
+ )
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt
new file mode 100644
index 0000000..3287b84
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.validation.types
+
+import com.android.intentresolver.v2.validation.Importance
+import com.android.intentresolver.v2.validation.RequiredValueMissing
+import com.android.intentresolver.v2.validation.Valid
+import com.android.intentresolver.v2.validation.ValidationResult
+import com.android.intentresolver.v2.validation.Validator
+import com.android.intentresolver.v2.validation.ValueIsWrongType
+import kotlin.reflect.KClass
+import kotlin.reflect.cast
+
+class SimpleValue<T : Any>(
+ override val key: String,
+ private val expected: KClass<T>,
+) : Validator<T> {
+
+ override fun validate(source: (String) -> Any?, importance: Importance): ValidationResult<T> {
+ val value: Any? = source(key)
+ return when {
+ // The value is present and of the expected type.
+ expected.isInstance(value) -> return Valid(expected.cast(value))
+
+ // No value is present.
+ value == null -> createResult(importance, RequiredValueMissing(key, expected))
+
+ // The value is some other type.
+ else ->
+ createResult(
+ importance,
+ ValueIsWrongType(
+ key,
+ importance,
+ actualType = value::class,
+ allowedTypes = listOf(expected)
+ )
+ )
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/types/Validators.kt b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt
new file mode 100644
index 0000000..4e6e5df
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.validation.types
+
+import com.android.intentresolver.v2.validation.Finding
+import com.android.intentresolver.v2.validation.Importance
+import com.android.intentresolver.v2.validation.Importance.CRITICAL
+import com.android.intentresolver.v2.validation.Importance.WARNING
+import com.android.intentresolver.v2.validation.Invalid
+import com.android.intentresolver.v2.validation.Valid
+import com.android.intentresolver.v2.validation.ValidationResult
+import com.android.intentresolver.v2.validation.Validator
+
+inline fun <reified T : Any> value(key: String): Validator<T> {
+ return SimpleValue(key, T::class)
+}
+
+inline fun <reified T : Any> array(key: String): Validator<List<T>> {
+ return ParceledArray(key, T::class)
+}
+
+/**
+ * Convenience function to wrap a finding in an appropriate result type.
+ *
+ * An error [finding] is suppressed when [importance] == [WARNING]
+ */
+internal fun <T> createResult(importance: Importance, finding: Finding): ValidationResult<T> {
+ return when (importance) {
+ WARNING -> Valid(null, listOf(finding).filter { it.importance == WARNING })
+ CRITICAL -> Invalid(listOf(finding))
+ }
+}
diff --git a/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt b/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt
new file mode 100644
index 0000000..26464ca
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt
@@ -0,0 +1,90 @@
+package com.android.intentresolver.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.LinearLayout
+import androidx.core.view.ScrollingView
+import androidx.core.view.marginBottom
+import androidx.core.view.marginLeft
+import androidx.core.view.marginRight
+import androidx.core.view.marginTop
+import androidx.core.widget.NestedScrollView
+
+/**
+ * A narrowly tailored [NestedScrollView] to be used inside [ResolverDrawerLayout] and help to
+ * orchestrate content preview scrolling. It expects one [LinearLayout] child with
+ * [LinearLayout.VERTICAL] orientation. If the child has more than one child, the first its child
+ * will be made scrollable (it is expected to be a content preview view).
+ */
+class ChooserNestedScrollView : NestedScrollView {
+ constructor(context: Context) : super(context)
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int
+ ) : super(context, attrs, defStyleAttr)
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val content =
+ getChildAt(0) as? LinearLayout ?: error("Exactly one child, LinerLayout, is expected")
+ require(content.orientation == LinearLayout.VERTICAL) { "VERTICAL orientation is expected" }
+ require(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
+ "Expected to have an exact width"
+ }
+
+ val lp = content.layoutParams ?: error("LayoutParams is missing")
+ val contentWidthSpec =
+ getChildMeasureSpec(
+ widthMeasureSpec,
+ paddingLeft + content.marginLeft + content.marginRight + paddingRight,
+ lp.width
+ )
+ val contentHeightSpec =
+ getChildMeasureSpec(
+ heightMeasureSpec,
+ paddingTop + content.marginTop + content.marginBottom + paddingBottom,
+ lp.height
+ )
+ content.measure(contentWidthSpec, contentHeightSpec)
+
+ if (content.childCount > 1) {
+ // We expect that the first child should be scrollable up
+ val child = content.getChildAt(0)
+ val height =
+ MeasureSpec.getSize(heightMeasureSpec) +
+ child.measuredHeight +
+ child.marginTop +
+ child.marginBottom
+
+ content.measure(
+ contentWidthSpec,
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec))
+ )
+ }
+ setMeasuredDimension(
+ MeasureSpec.getSize(widthMeasureSpec),
+ minOf(
+ MeasureSpec.getSize(heightMeasureSpec),
+ paddingTop +
+ content.marginTop +
+ content.measuredHeight +
+ content.marginBottom +
+ paddingBottom
+ )
+ )
+ }
+
+ override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
+ // let the parent scroll
+ super.onNestedPreScroll(target, dx, dy, consumed, type)
+ // scroll ourselves, if recycler has not scrolled
+ val delta = dy - consumed[1]
+ if (delta > 0 && target is ScrollingView && !target.canScrollVertically(-1)) {
+ val preScrollY = scrollY
+ scrollBy(0, delta)
+ consumed[1] += scrollY - preScrollY
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
index de76a1d..2c8140d 100644
--- a/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
+++ b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
@@ -19,7 +19,6 @@ package com.android.intentresolver.widget;
import static android.content.res.Resources.ID_NULL;
-import android.annotation.IdRes;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
@@ -45,6 +44,10 @@ import android.view.animation.AnimationUtils;
import android.widget.AbsListView;
import android.widget.OverScroller;
+import androidx.annotation.IdRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.view.ScrollingView;
import androidx.recyclerview.widget.RecyclerView;
import com.android.intentresolver.R;
@@ -131,6 +134,9 @@ public class ResolverDrawerLayout extends ViewGroup {
private AbsListView mNestedListChild;
private RecyclerView mNestedRecyclerChild;
+ @Nullable
+ private final ScrollablePreviewFlingLogicDelegate mFlingLogicDelegate;
+
private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener =
new ViewTreeObserver.OnTouchModeChangeListener() {
@Override
@@ -167,6 +173,12 @@ public class ResolverDrawerLayout extends ViewGroup {
mIgnoreOffsetTopLimitViewId = a.getResourceId(
R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit, ID_NULL);
}
+ mFlingLogicDelegate =
+ a.getBoolean(
+ R.styleable.ResolverDrawerLayout_useScrollablePreviewNestedFlingLogic,
+ false)
+ ? new ScrollablePreviewFlingLogicDelegate() {}
+ : null;
a.recycle();
mScrollIndicatorDrawable = mContext.getDrawable(
@@ -832,6 +844,9 @@ public class ResolverDrawerLayout extends ViewGroup {
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+ if (mFlingLogicDelegate != null) {
+ return mFlingLogicDelegate.onNestedPreFling(this, target, velocityX, velocityY);
+ }
if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) {
smoothScrollTo(0, velocityY);
return true;
@@ -841,9 +856,12 @@ public class ResolverDrawerLayout extends ViewGroup {
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+ if (mFlingLogicDelegate != null) {
+ return mFlingLogicDelegate.onNestedFling(this, target, velocityX, velocityY, consumed);
+ }
// TODO: find a more suitable way to fix it.
// RecyclerView started reporting `consumed` as true whenever a scrolling is enabled,
- // previously the value was based whether the fling can be performed in given direction
+ // previously the value was based on whether the fling can be performed in given direction
// i.e. whether it is at the top or at the bottom. isRecyclerViewAtTheTop method is a
// workaround that restores the legacy functionality.
boolean shouldConsume = (Math.abs(velocityY) > mMinFlingVelocity)
@@ -885,6 +903,13 @@ public class ResolverDrawerLayout extends ViewGroup {
&& firstChild.getTop() >= recyclerView.getPaddingTop();
}
+ private static boolean isFlingTargetAtTop(View target) {
+ if (target instanceof ScrollingView) {
+ return !target.canScrollVertically(-1);
+ }
+ return false;
+ }
+
private boolean performAccessibilityActionCommon(int action) {
switch (action) {
case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
@@ -974,7 +999,7 @@ public class ResolverDrawerLayout extends ViewGroup {
}
@Override
- public void onDrawForeground(Canvas canvas) {
+ public void onDrawForeground(@NonNull Canvas canvas) {
if (mScrollIndicatorDrawable != null) {
mScrollIndicatorDrawable.draw(canvas);
}
@@ -1299,4 +1324,74 @@ public class ResolverDrawerLayout extends ViewGroup {
}
return mMetricsLogger;
}
+
+ /**
+ * Controlled by
+ * {@link com.android.intentresolver.Flags#FLAG_SCROLLABLE_PREVIEW}
+ */
+ private interface ScrollablePreviewFlingLogicDelegate {
+ default boolean onNestedPreFling(
+ ResolverDrawerLayout drawer, View target, float velocityX, float velocityY) {
+ boolean shouldScroll = !drawer.getShowAtTop() && velocityY > drawer.mMinFlingVelocity
+ && drawer.mCollapseOffset != 0;
+ if (shouldScroll) {
+ drawer.smoothScrollTo(0, velocityY);
+ return true;
+ }
+ boolean shouldDismiss = (Math.abs(velocityY) > drawer.mMinFlingVelocity)
+ && velocityY < 0
+ && isFlingTargetAtTop(target);
+ if (shouldDismiss) {
+ if (drawer.getShowAtTop()) {
+ drawer.smoothScrollTo(drawer.mCollapsibleHeight, velocityY);
+ } else {
+ if (drawer.isDismissable()
+ && drawer.mCollapseOffset > drawer.mCollapsibleHeight) {
+ drawer.smoothScrollTo(drawer.mHeightUsed, velocityY);
+ drawer.mDismissOnScrollerFinished = true;
+ } else {
+ drawer.smoothScrollTo(drawer.mCollapsibleHeight, velocityY);
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ default boolean onNestedFling(
+ ResolverDrawerLayout drawer,
+ View target,
+ float velocityX,
+ float velocityY,
+ boolean consumed) {
+ // TODO: find a more suitable way to fix it.
+ // RecyclerView started reporting `consumed` as true whenever a scrolling is enabled,
+ // previously the value was based on whether the fling can be performed in given
+ // direction i.e. whether it is at the top or at the bottom. isRecyclerViewAtTheTop
+ // method is a workaround that restores the legacy functionality.
+ boolean shouldConsume = (Math.abs(velocityY) > drawer.mMinFlingVelocity) && !consumed;
+ if (shouldConsume) {
+ if (drawer.getShowAtTop()) {
+ if (drawer.isDismissable() && velocityY > 0) {
+ drawer.abortAnimation();
+ drawer.dismiss();
+ } else {
+ drawer.smoothScrollTo(
+ velocityY < 0 ? drawer.mCollapsibleHeight : 0, velocityY);
+ }
+ } else {
+ if (drawer.isDismissable()
+ && velocityY < 0
+ && drawer.mCollapseOffset > drawer.mCollapsibleHeight) {
+ drawer.smoothScrollTo(drawer.mHeightUsed, velocityY);
+ drawer.mDismissOnScrollerFinished = true;
+ } else {
+ drawer.smoothScrollTo(
+ velocityY > 0 ? 0 : drawer.mCollapsibleHeight, velocityY);
+ }
+ }
+ }
+ return shouldConsume;
+ }
+ }
}
diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
index 3bbafc4..7fe1609 100644
--- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
+++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
@@ -26,11 +26,16 @@ import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.animation.AlphaAnimation
+import android.view.animation.Animation
+import android.view.animation.Animation.AnimationListener
+import android.view.animation.DecelerateInterpolator
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.ViewCompat
+import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.intentresolver.R
@@ -45,6 +50,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
private const val TRANSITION_NAME = "screenshot_preview_image"
private const val PLURALS_COUNT = "count"
@@ -65,7 +71,6 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
defStyleAttr: Int
) : super(context, attrs, defStyleAttr) {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
- adapter = Adapter(context)
context
.obtainStyledAttributes(attrs, R.styleable.ScrollableImagePreviewView, defStyleAttr, 0)
@@ -98,11 +103,14 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
)
.toInt()
}
- addItemDecoration(SpacingDecoration(innerSpacing, outerSpacing))
+ super.addItemDecoration(SpacingDecoration(innerSpacing, outerSpacing))
maxWidthHint =
a.getDimensionPixelSize(R.styleable.ScrollableImagePreviewView_maxWidthHint, -1)
}
+ val itemAnimator = ItemAnimator()
+ super.setItemAnimator(itemAnimator)
+ super.setAdapter(Adapter(context, itemAnimator.getAddDuration()))
}
private var batchLoader: BatchPreviewLoader? = null
@@ -167,6 +175,14 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
return null
}
+ override fun setAdapter(adapter: RecyclerView.Adapter<*>?) {
+ error("This method is not supported")
+ }
+
+ override fun setItemAnimator(animator: RecyclerView.ItemAnimator?) {
+ error("This method is not supported")
+ }
+
fun setImageLoader(imageLoader: CachingImageLoader) {
previewAdapter.imageLoader = imageLoader
}
@@ -269,7 +285,10 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
File
}
- private class Adapter(private val context: Context) : RecyclerView.Adapter<ViewHolder>() {
+ private class Adapter(
+ private val context: Context,
+ private val fadeInDurationMs: Long,
+ ) : RecyclerView.Adapter<ViewHolder>() {
private val previews = ArrayList<Preview>()
private val imagePreviewDescription =
context.resources.getString(R.string.image_preview_a11y_description)
@@ -311,15 +330,17 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
if (newPreviews.isEmpty()) return
val insertPos = previews.size
val hadOtherItem = hasOtherItem
- val wasEmpty = previews.isEmpty()
+ val oldItemCount = getItemCount()
previews.addAll(newPreviews)
if (firstImagePos < 0) {
val pos = newPreviews.indexOfFirst { it.type == PreviewType.Image }
if (pos >= 0) firstImagePos = insertPos + pos
}
- if (wasEmpty) {
- // we don't want any item animation in that case
- notifyDataSetChanged()
+ if (insertPos == 0) {
+ if (oldItemCount > 0) {
+ notifyItemRangeRemoved(0, oldItemCount)
+ }
+ notifyItemRangeInserted(insertPos, getItemCount())
} else {
notifyItemRangeInserted(insertPos, newPreviews.size)
when {
@@ -366,6 +387,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
vh.bind(
previews[position],
imageLoader ?: error("ImageLoader is missing"),
+ fadeInDurationMs,
isSharedTransitionElement = position == firstImagePos,
previewReadyCallback =
if (
@@ -416,10 +438,13 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
fun bind(
preview: Preview,
imageLoader: CachingImageLoader,
+ fadeInDurationMs: Long,
isSharedTransitionElement: Boolean,
previewReadyCallback: ((String) -> Unit)?
) {
image.setImageDrawable(null)
+ image.alpha = 1f
+ image.clearAnimation()
(image.layoutParams as? ConstraintLayout.LayoutParams)?.let { params ->
params.dimensionRatio = preview.aspectRatioString
}
@@ -453,11 +478,11 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
}
resetScope().launch {
loadImage(preview, imageLoader)
- if (preview.type == PreviewType.Image) {
- previewReadyCallback?.let { callback ->
- image.waitForPreDraw()
- callback(TRANSITION_NAME)
- }
+ if (preview.type == PreviewType.Image && previewReadyCallback != null) {
+ image.waitForPreDraw()
+ previewReadyCallback(TRANSITION_NAME)
+ } else if (image.isAttachedToWindow()) {
+ fadeInPreview(fadeInDurationMs)
}
}
}
@@ -473,6 +498,30 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
image.setImageBitmap(bitmap)
}
+ private suspend fun fadeInPreview(durationMs: Long) =
+ suspendCancellableCoroutine { continuation ->
+ val animation =
+ AlphaAnimation(0f, 1f).apply {
+ duration = durationMs
+ interpolator = DecelerateInterpolator()
+ setAnimationListener(
+ object : AnimationListener {
+ override fun onAnimationStart(animation: Animation?) = Unit
+ override fun onAnimationRepeat(animation: Animation?) = Unit
+
+ override fun onAnimationEnd(animation: Animation?) {
+ continuation.resumeWith(Result.success(Unit))
+ }
+ }
+ )
+ }
+ image.startAnimation(animation)
+ continuation.invokeOnCancellation {
+ image.clearAnimation()
+ image.alpha = 1f
+ }
+ }
+
private fun resetScope(): CoroutineScope =
CoroutineScope(Dispatchers.Main.immediate).also {
scope?.cancel()
@@ -521,6 +570,70 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
}
}
+ /**
+ * ItemAnimator to handle a special case of addng first image items into the view. The view is
+ * used with wrap_content width spec thus after adding the first views it, generally, changes
+ * its size and position breaking the animation. This class handles that by preserving loading
+ * idicator position in this special case.
+ */
+ private inner class ItemAnimator() : DefaultItemAnimator() {
+ private var animatedVH: ViewHolder? = null
+ private var originalTranslation = 0f
+
+ override fun recordPreLayoutInformation(
+ state: State,
+ viewHolder: RecyclerView.ViewHolder,
+ changeFlags: Int,
+ payloads: MutableList<Any>
+ ): ItemHolderInfo {
+ return super.recordPreLayoutInformation(state, viewHolder, changeFlags, payloads).let {
+ holderInfo ->
+ if (viewHolder is LoadingItemViewHolder && getChildCount() == 1) {
+ LoadingItemHolderInfo(holderInfo, parentLeft = left)
+ } else {
+ holderInfo
+ }
+ }
+ }
+
+ override fun animateDisappearance(
+ viewHolder: RecyclerView.ViewHolder,
+ preLayoutInfo: ItemHolderInfo,
+ postLayoutInfo: ItemHolderInfo?
+ ): Boolean {
+ if (viewHolder is LoadingItemViewHolder && preLayoutInfo is LoadingItemHolderInfo) {
+ val view = viewHolder.itemView
+ animatedVH = viewHolder
+ originalTranslation = view.getTranslationX()
+ view.setTranslationX(
+ (preLayoutInfo.parentLeft - left + preLayoutInfo.left).toFloat() - view.left
+ )
+ }
+ return super.animateDisappearance(viewHolder, preLayoutInfo, postLayoutInfo)
+ }
+
+ override fun onRemoveFinished(viewHolder: RecyclerView.ViewHolder) {
+ if (animatedVH === viewHolder) {
+ viewHolder.itemView.setTranslationX(originalTranslation)
+ animatedVH = null
+ }
+ super.onRemoveFinished(viewHolder)
+ }
+
+ private inner class LoadingItemHolderInfo(
+ holderInfo: ItemHolderInfo,
+ val parentLeft: Int,
+ ) : ItemHolderInfo() {
+ init {
+ left = holderInfo.left
+ top = holderInfo.top
+ right = holderInfo.right
+ bottom = holderInfo.bottom
+ changeFlags = holderInfo.changeFlags
+ }
+ }
+ }
+
@VisibleForTesting
class BatchPreviewLoader(
private val imageLoader: CachingImageLoader,
diff --git a/java/tests/Android.bp b/java/tests/Android.bp
deleted file mode 100644
index e10ca72..0000000
--- a/java/tests/Android.bp
+++ /dev/null
@@ -1,47 +0,0 @@
-package {
- // See: http://go/android-license-faq
- default_applicable_licenses: ["packages_modules_IntentResolver_license"],
-}
-
-android_test {
- name: "IntentResolverUnitTests",
-
- // Include all test java files.
- srcs: [
- "src/**/*.java",
- "src/**/*.kt",
- ],
-
- libs: [
- "android.test.runner",
- "android.test.base",
- "android.test.mock",
- "framework",
- "framework-res",
- ],
-
- static_libs: [
- "IntentResolver-core",
- "androidx.test.core",
- "androidx.test.rules",
- "androidx.test.ext.junit",
- "androidx.test.ext.truth",
- "androidx.test.espresso.contrib",
- "androidx.test.espresso.core",
- "androidx.test.rules",
- "androidx.lifecycle_lifecycle-common-java8",
- "androidx.lifecycle_lifecycle-extensions",
- "androidx.lifecycle_lifecycle-runtime-ktx",
- "androidx.lifecycle_lifecycle-runtime-testing",
- "kotlinx_coroutines_test",
- "mockito-target-minus-junit4",
- "testables",
- "truth",
- ],
- plugins: ["dagger2-compiler"],
- test_suites: ["general-tests"],
- sdk_version: "core_platform",
- compile_multilib: "both",
-
- dont_merge_manifests: true,
-}
diff --git a/java/tests/src/com/android/intentresolver/FeatureFlagRule.kt b/java/tests/src/com/android/intentresolver/FeatureFlagRule.kt
deleted file mode 100644
index 3fa01bc..0000000
--- a/java/tests/src/com/android/intentresolver/FeatureFlagRule.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2023 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.intentresolver
-
-import com.android.systemui.flags.BooleanFlag
-import org.junit.rules.TestRule
-import org.junit.runner.Description
-import org.junit.runners.model.Statement
-
-/**
- * Ignores tests annotated with [RequireFeatureFlags] which flag requirements does not
- * meet in the active flag set.
- * @param flags active flag set
- */
-internal class FeatureFlagRule(flags: Map<BooleanFlag, Boolean>) : TestRule {
- private val flags = flags.entries.fold(HashMap<String, Boolean>()) { map, (key, value) ->
- map.apply {
- put(key.name, value)
- }
- }
- private val skippingStatement = object : Statement() {
- override fun evaluate() = Unit
- }
-
- override fun apply(base: Statement, description: Description): Statement {
- val annotation = description.annotations.firstOrNull {
- it is RequireFeatureFlags
- } as? RequireFeatureFlags
- ?: return base
-
- if (annotation.flags.size != annotation.values.size) {
- error("${description.className}#${description.methodName}: inconsistent number of" +
- " flags and values in $annotation")
- }
- for (i in annotation.flags.indices) {
- val flag = annotation.flags[i]
- val value = annotation.values[i]
- if (flags.getOrDefault(flag, !value) != value) return skippingStatement
- }
- return base
- }
-}
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt
deleted file mode 100644
index fe13a21..0000000
--- a/java/tests/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt
+++ /dev/null
@@ -1,225 +0,0 @@
-/*
- * Copyright (C) 2023 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.intentresolver.contentpreview
-
-import android.net.Uri
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import android.widget.TextView
-import androidx.lifecycle.testing.TestLifecycleOwner
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
-import com.android.intentresolver.R
-import com.android.intentresolver.mock
-import com.android.intentresolver.whenever
-import com.android.intentresolver.widget.ActionRow
-import com.google.common.truth.Truth.assertThat
-import java.util.function.Consumer
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.never
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-
-private const val HEADLINE_IMAGES = "Image Headline"
-private const val HEADLINE_VIDEOS = "Video Headline"
-private const val HEADLINE_FILES = "Files Headline"
-private const val SHARED_TEXT = "Some text to share"
-
-@RunWith(AndroidJUnit4::class)
-class FilesPlusTextContentPreviewUiTest {
- private val lifecycleOwner = TestLifecycleOwner()
- private val actionFactory =
- object : ChooserContentPreviewUi.ActionFactory {
- override fun getEditButtonRunnable(): Runnable? = null
- override fun getCopyButtonRunnable(): Runnable? = null
- override fun createCustomActions(): List<ActionRow.Action> = emptyList()
- override fun getModifyShareAction(): ActionRow.Action? = null
- override fun getExcludeSharedTextAction(): Consumer<Boolean> = Consumer<Boolean> {}
- }
- private val imageLoader = mock<ImageLoader>()
- private val headlineGenerator =
- mock<HeadlineGenerator> {
- whenever(getImagesHeadline(anyInt())).thenReturn(HEADLINE_IMAGES)
- whenever(getVideosHeadline(anyInt())).thenReturn(HEADLINE_VIDEOS)
- whenever(getFilesHeadline(anyInt())).thenReturn(HEADLINE_FILES)
- }
-
- private val context
- get() = getInstrumentation().getContext()
-
- @Test
- fun test_displayImagesPlusTextWithoutUriMetadata_showImagesHeadline() {
- val sharedFileCount = 2
- val previewView = testLoadingHeadline("image/*", sharedFileCount)
-
- verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
- verifyPreviewHeadline(previewView, HEADLINE_IMAGES)
- verifySharedText(previewView)
- }
-
- @Test
- fun test_displayVideosPlusTextWithoutUriMetadata_showVideosHeadline() {
- val sharedFileCount = 2
- val previewView = testLoadingHeadline("video/*", sharedFileCount)
-
- verify(headlineGenerator, times(1)).getVideosHeadline(sharedFileCount)
- verifyPreviewHeadline(previewView, HEADLINE_VIDEOS)
- verifySharedText(previewView)
- }
-
- @Test
- fun test_displayDocsPlusTextWithoutUriMetadata_showFilesHeadline() {
- val sharedFileCount = 2
- val previewView = testLoadingHeadline("application/pdf", sharedFileCount)
-
- verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
- verifyPreviewHeadline(previewView, HEADLINE_FILES)
- verifySharedText(previewView)
- }
-
- @Test
- fun test_displayMixedContentPlusTextWithoutUriMetadata_showFilesHeadline() {
- val sharedFileCount = 2
- val previewView = testLoadingHeadline("*/*", sharedFileCount)
-
- verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
- verifyPreviewHeadline(previewView, HEADLINE_FILES)
- verifySharedText(previewView)
- }
-
- @Test
- fun test_displayImagesPlusTextWithUriMetadataSet_showImagesHeadline() {
- val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "image/jpeg")
- val sharedFileCount = loadedFileMetadata.size
- val previewView = testLoadingHeadline("image/*", sharedFileCount, loadedFileMetadata)
-
- verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
- verifyPreviewHeadline(previewView, HEADLINE_IMAGES)
- verifySharedText(previewView)
- }
-
- @Test
- fun test_displayVideosPlusTextWithUriMetadataSet_showVideosHeadline() {
- val loadedFileMetadata = createFileInfosWithMimeTypes("video/mp4", "video/mp4")
- val sharedFileCount = loadedFileMetadata.size
- val previewView = testLoadingHeadline("video/*", sharedFileCount, loadedFileMetadata)
-
- verify(headlineGenerator, times(1)).getVideosHeadline(sharedFileCount)
- verifyPreviewHeadline(previewView, HEADLINE_VIDEOS)
- verifySharedText(previewView)
- }
-
- @Test
- fun test_displayImagesAndVideosPlusTextWithUriMetadataSet_showFilesHeadline() {
- val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "video/mp4")
- val sharedFileCount = loadedFileMetadata.size
- val previewView = testLoadingHeadline("*/*", sharedFileCount, loadedFileMetadata)
-
- verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
- verifyPreviewHeadline(previewView, HEADLINE_FILES)
- verifySharedText(previewView)
- }
-
- @Test
- fun test_displayDocsPlusTextWithUriMetadataSet_showFilesHeadline() {
- val loadedFileMetadata = createFileInfosWithMimeTypes("application/pdf", "application/pdf")
- val sharedFileCount = loadedFileMetadata.size
- val previewView =
- testLoadingHeadline("application/pdf", sharedFileCount, loadedFileMetadata)
-
- verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
- verifyPreviewHeadline(previewView, HEADLINE_FILES)
- verifySharedText(previewView)
- }
-
- @Test
- fun test_uriMetadataIsMoreSpecificThanIntentMimeType_headlineGetsUpdated() {
- val sharedFileCount = 2
- val testSubject =
- FilesPlusTextContentPreviewUi(
- lifecycleOwner.lifecycle,
- /*isSingleImage=*/ false,
- sharedFileCount,
- SHARED_TEXT,
- /*intentMimeType=*/ "*/*",
- actionFactory,
- imageLoader,
- DefaultMimeTypeClassifier,
- headlineGenerator
- )
- val layoutInflater = LayoutInflater.from(context)
- val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup
-
- val previewView =
- testSubject.display(context.resources, LayoutInflater.from(context), gridLayout)
-
- verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
- verify(headlineGenerator, never()).getImagesHeadline(sharedFileCount)
- verifyPreviewHeadline(previewView, HEADLINE_FILES)
-
- testSubject.updatePreviewMetadata(createFileInfosWithMimeTypes("image/png", "image/jpg"))
-
- verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
- verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
- verifyPreviewHeadline(previewView, HEADLINE_IMAGES)
- }
-
- private fun testLoadingHeadline(
- intentMimeType: String,
- sharedFileCount: Int,
- loadedFileMetadata: List<FileInfo>? = null
- ): ViewGroup? {
- val testSubject =
- FilesPlusTextContentPreviewUi(
- lifecycleOwner.lifecycle,
- /*isSingleImage=*/ false,
- sharedFileCount,
- SHARED_TEXT,
- intentMimeType,
- actionFactory,
- imageLoader,
- DefaultMimeTypeClassifier,
- headlineGenerator
- )
- val layoutInflater = LayoutInflater.from(context)
- val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup
-
- loadedFileMetadata?.let(testSubject::updatePreviewMetadata)
- return testSubject.display(context.resources, LayoutInflater.from(context), gridLayout)
- }
-
- private fun createFileInfosWithMimeTypes(vararg mimeTypes: String): List<FileInfo> {
- val uri = Uri.parse("content://pkg.app/file")
- return mimeTypes.map { mimeType -> FileInfo.Builder(uri).withMimeType(mimeType).build() }
- }
-
- private fun verifyPreviewHeadline(previewView: ViewGroup?, expectedText: String) {
- assertThat(previewView).isNotNull()
- val headlineView = previewView?.findViewById<TextView>(R.id.headline)
- assertThat(headlineView).isNotNull()
- assertThat(headlineView?.text).isEqualTo(expectedText)
- }
-
- private fun verifySharedText(previewView: ViewGroup?) {
- assertThat(previewView).isNotNull()
- val textContentView = previewView?.findViewById<TextView>(R.id.content_preview_text)
- assertThat(textContentView).isNotNull()
- assertThat(textContentView?.text).isEqualTo(SHARED_TEXT)
- }
-}
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt
deleted file mode 100644
index e7de0b7..0000000
--- a/java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt
+++ /dev/null
@@ -1,166 +0,0 @@
-/*
- * Copyright (C) 2023 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.intentresolver.contentpreview
-
-import android.net.Uri
-import android.view.LayoutInflater
-import android.view.ViewGroup
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
-import com.android.intentresolver.R.layout.chooser_grid
-import com.android.intentresolver.mock
-import com.android.intentresolver.whenever
-import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback
-import kotlin.coroutines.EmptyCoroutineContext
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.asFlow
-import kotlinx.coroutines.flow.takeWhile
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.runTest
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-
-@RunWith(AndroidJUnit4::class)
-class UnifiedContentPreviewUiTest {
- private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher())
- private val actionFactory =
- mock<ChooserContentPreviewUi.ActionFactory> {
- whenever(createCustomActions()).thenReturn(emptyList())
- }
- private val imageLoader = mock<ImageLoader>()
- private val headlineGenerator =
- mock<HeadlineGenerator> {
- whenever(getImagesHeadline(anyInt())).thenReturn("Image Headline")
- whenever(getVideosHeadline(anyInt())).thenReturn("Video Headline")
- whenever(getFilesHeadline(anyInt())).thenReturn("Files Headline")
- }
-
- private val context
- get() = getInstrumentation().getContext()
-
- @Test
- fun test_displayImagesWithoutUriMetadata_showImagesHeadline() {
- testLoadingHeadline("image/*", files = null)
-
- verify(headlineGenerator, times(1)).getImagesHeadline(2)
- }
-
- @Test
- fun test_displayVideosWithoutUriMetadata_showImagesHeadline() {
- testLoadingHeadline("video/*", files = null)
-
- verify(headlineGenerator, times(1)).getVideosHeadline(2)
- }
-
- @Test
- fun test_displayDocumentsWithoutUriMetadata_showImagesHeadline() {
- testLoadingHeadline("application/pdf", files = null)
-
- verify(headlineGenerator, times(1)).getFilesHeadline(2)
- }
-
- @Test
- fun test_displayMixedContentWithoutUriMetadata_showImagesHeadline() {
- testLoadingHeadline("*/*", files = null)
-
- verify(headlineGenerator, times(1)).getFilesHeadline(2)
- }
-
- @Test
- fun test_displayImagesWithUriMetadataSet_showImagesHeadline() {
- val uri = Uri.parse("content://pkg.app/image.png")
- val files =
- listOf(
- FileInfo.Builder(uri).withMimeType("image/png").build(),
- FileInfo.Builder(uri).withMimeType("image/jpeg").build(),
- )
- testLoadingHeadline("image/*", files)
-
- verify(headlineGenerator, times(1)).getImagesHeadline(2)
- }
-
- @Test
- fun test_displayVideosWithUriMetadataSet_showImagesHeadline() {
- val uri = Uri.parse("content://pkg.app/image.png")
- val files =
- listOf(
- FileInfo.Builder(uri).withMimeType("video/mp4").build(),
- FileInfo.Builder(uri).withMimeType("video/mp4").build(),
- )
- testLoadingHeadline("video/*", files)
-
- verify(headlineGenerator, times(1)).getVideosHeadline(2)
- }
-
- @Test
- fun test_displayImagesAndVideosWithUriMetadataSet_showImagesHeadline() {
- val uri = Uri.parse("content://pkg.app/image.png")
- val files =
- listOf(
- FileInfo.Builder(uri).withMimeType("image/png").build(),
- FileInfo.Builder(uri).withMimeType("video/mp4").build(),
- )
- testLoadingHeadline("*/*", files)
-
- verify(headlineGenerator, times(1)).getFilesHeadline(2)
- }
-
- @Test
- fun test_displayDocumentsWithUriMetadataSet_showImagesHeadline() {
- val uri = Uri.parse("content://pkg.app/image.png")
- val files =
- listOf(
- FileInfo.Builder(uri).withMimeType("application/pdf").build(),
- FileInfo.Builder(uri).withMimeType("application/pdf").build(),
- )
- testLoadingHeadline("application/pdf", files)
-
- verify(headlineGenerator, times(1)).getFilesHeadline(2)
- }
-
- private fun testLoadingHeadline(intentMimeType: String, files: List<FileInfo>?) {
- testScope.runTest {
- val endMarker = FileInfo.Builder(Uri.EMPTY).build()
- val emptySourceFlow = MutableSharedFlow<FileInfo>(replay = 1)
- val testSubject =
- UnifiedContentPreviewUi(
- testScope,
- /*isSingleImage=*/ false,
- intentMimeType,
- actionFactory,
- imageLoader,
- DefaultMimeTypeClassifier,
- object : TransitionElementStatusCallback {
- override fun onTransitionElementReady(name: String) = Unit
- override fun onAllTransitionElementsReady() = Unit
- },
- files?.let { it.asFlow() } ?: emptySourceFlow.takeWhile { it !== endMarker },
- /*itemCount=*/ 2,
- headlineGenerator
- )
- val layoutInflater = LayoutInflater.from(context)
- val gridLayout = layoutInflater.inflate(chooser_grid, null, false) as ViewGroup
-
- testSubject.display(context.resources, LayoutInflater.from(context), gridLayout)
- emptySourceFlow.tryEmit(endMarker)
- }
- }
-}
diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
deleted file mode 100644
index 9b4a805..0000000
--- a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
+++ /dev/null
@@ -1,482 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver.shortcuts
-
-import android.app.prediction.AppPredictor
-import android.content.ComponentName
-import android.content.Context
-import android.content.IntentFilter
-import android.content.pm.ApplicationInfo
-import android.content.pm.PackageManager
-import android.content.pm.PackageManager.ApplicationInfoFlags
-import android.content.pm.ShortcutManager
-import android.os.UserHandle
-import android.os.UserManager
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.testing.TestLifecycleOwner
-import androidx.test.filters.SmallTest
-import com.android.intentresolver.any
-import com.android.intentresolver.argumentCaptor
-import com.android.intentresolver.capture
-import com.android.intentresolver.chooser.DisplayResolveInfo
-import com.android.intentresolver.createAppTarget
-import com.android.intentresolver.createShareShortcutInfo
-import com.android.intentresolver.createShortcutInfo
-import com.android.intentresolver.mock
-import com.android.intentresolver.whenever
-import java.util.function.Consumer
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.test.TestCoroutineScheduler
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
-import org.junit.After
-import org.junit.Assert.assertArrayEquals
-import org.junit.Assert.assertEquals
-import org.junit.Assert.assertFalse
-import org.junit.Assert.assertTrue
-import org.junit.Before
-import org.junit.Test
-import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.atLeastOnce
-import org.mockito.Mockito.never
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-class ShortcutLoaderTest {
- private val appInfo =
- ApplicationInfo().apply {
- enabled = true
- flags = 0
- }
- private val pm =
- mock<PackageManager> {
- whenever(getApplicationInfo(any(), any<ApplicationInfoFlags>())).thenReturn(appInfo)
- }
- private val userManager =
- mock<UserManager> {
- whenever(isUserRunning(any<UserHandle>())).thenReturn(true)
- whenever(isUserUnlocked(any<UserHandle>())).thenReturn(true)
- whenever(isQuietModeEnabled(any<UserHandle>())).thenReturn(false)
- }
- private val context =
- mock<Context> {
- whenever(packageManager).thenReturn(pm)
- whenever(createContextAsUser(any(), anyInt())).thenReturn(this)
- whenever(getSystemService(Context.USER_SERVICE)).thenReturn(userManager)
- }
- private val scheduler = TestCoroutineScheduler()
- private val dispatcher = UnconfinedTestDispatcher(scheduler)
- private val lifecycleOwner = TestLifecycleOwner()
- private val intentFilter = mock<IntentFilter>()
- private val appPredictor = mock<ShortcutLoader.AppPredictorProxy>()
- private val callback = mock<Consumer<ShortcutLoader.Result>>()
- private val componentName = ComponentName("pkg", "Class")
- private val appTarget =
- mock<DisplayResolveInfo> { whenever(resolvedComponentName).thenReturn(componentName) }
- private val appTargets = arrayOf(appTarget)
- private val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1)
-
- @Before
- fun setup() {
- Dispatchers.setMain(dispatcher)
- lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
- }
-
- @After
- fun cleanup() {
- lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
- Dispatchers.resetMain()
- }
-
- @Test
- fun test_loadShortcutsWithAppPredictor_resultIntegrity() {
- val testSubject =
- ShortcutLoader(
- context,
- lifecycleOwner.lifecycle,
- appPredictor,
- UserHandle.of(0),
- true,
- intentFilter,
- dispatcher,
- callback
- )
-
- testSubject.updateAppTargets(appTargets)
-
- val matchingAppTarget = createAppTarget(matchingShortcutInfo)
- val shortcuts =
- listOf(
- matchingAppTarget,
- // an AppTarget that does not belong to any resolved application; should be ignored
- createAppTarget(
- createShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
- )
- )
- val appPredictorCallbackCaptor = argumentCaptor<AppPredictor.Callback>()
- verify(appPredictor, atLeastOnce())
- .registerPredictionUpdates(any(), capture(appPredictorCallbackCaptor))
- appPredictorCallbackCaptor.value.onTargetsAvailable(shortcuts)
-
- val resultCaptor = argumentCaptor<ShortcutLoader.Result>()
- verify(callback, times(1)).accept(capture(resultCaptor))
-
- val result = resultCaptor.value
- assertTrue("An app predictor result is expected", result.isFromAppPredictor)
- assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets)
- assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size)
- assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget)
- for (shortcut in result.shortcutsByApp[0].shortcuts) {
- assertEquals(
- "Wrong AppTarget in the cache",
- matchingAppTarget,
- result.directShareAppTargetCache[shortcut]
- )
- assertEquals(
- "Wrong ShortcutInfo in the cache",
- matchingShortcutInfo,
- result.directShareShortcutInfoCache[shortcut]
- )
- }
- }
-
- @Test
- fun test_loadShortcutsWithShortcutManager_resultIntegrity() {
- val shortcutManagerResult =
- listOf(
- ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
- // mismatching shortcut
- createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
- )
- val shortcutManager =
- mock<ShortcutManager> {
- whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
- }
- whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
- val testSubject =
- ShortcutLoader(
- context,
- lifecycleOwner.lifecycle,
- null,
- UserHandle.of(0),
- true,
- intentFilter,
- dispatcher,
- callback
- )
-
- testSubject.updateAppTargets(appTargets)
-
- val resultCaptor = argumentCaptor<ShortcutLoader.Result>()
- verify(callback, times(1)).accept(capture(resultCaptor))
-
- val result = resultCaptor.value
- assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor)
- assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets)
- assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size)
- assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget)
- for (shortcut in result.shortcutsByApp[0].shortcuts) {
- assertTrue(
- "AppTargets are not expected the cache of a ShortcutManager result",
- result.directShareAppTargetCache.isEmpty()
- )
- assertEquals(
- "Wrong ShortcutInfo in the cache",
- matchingShortcutInfo,
- result.directShareShortcutInfoCache[shortcut]
- )
- }
- }
-
- @Test
- fun test_appPredictorReturnsEmptyList_fallbackToShortcutManager() {
- val shortcutManagerResult =
- listOf(
- ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
- // mismatching shortcut
- createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
- )
- val shortcutManager =
- mock<ShortcutManager> {
- whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
- }
- whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
- val testSubject =
- ShortcutLoader(
- context,
- lifecycleOwner.lifecycle,
- appPredictor,
- UserHandle.of(0),
- true,
- intentFilter,
- dispatcher,
- callback
- )
-
- testSubject.updateAppTargets(appTargets)
-
- verify(appPredictor, times(1)).requestPredictionUpdate()
- val appPredictorCallbackCaptor = argumentCaptor<AppPredictor.Callback>()
- verify(appPredictor, times(1))
- .registerPredictionUpdates(any(), capture(appPredictorCallbackCaptor))
- appPredictorCallbackCaptor.value.onTargetsAvailable(emptyList())
-
- val resultCaptor = argumentCaptor<ShortcutLoader.Result>()
- verify(callback, times(1)).accept(capture(resultCaptor))
-
- val result = resultCaptor.value
- assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor)
- assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets)
- assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size)
- assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget)
- for (shortcut in result.shortcutsByApp[0].shortcuts) {
- assertTrue(
- "AppTargets are not expected the cache of a ShortcutManager result",
- result.directShareAppTargetCache.isEmpty()
- )
- assertEquals(
- "Wrong ShortcutInfo in the cache",
- matchingShortcutInfo,
- result.directShareShortcutInfoCache[shortcut]
- )
- }
- }
-
- @Test
- fun test_appPredictor_requestPredictionUpdateFailure_fallbackToShortcutManager() {
- val shortcutManagerResult =
- listOf(
- ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
- // mismatching shortcut
- createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
- )
- val shortcutManager =
- mock<ShortcutManager> {
- whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
- }
- whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
- whenever(appPredictor.requestPredictionUpdate())
- .thenThrow(IllegalStateException("Test exception"))
- val testSubject =
- ShortcutLoader(
- context,
- lifecycleOwner.lifecycle,
- appPredictor,
- UserHandle.of(0),
- true,
- intentFilter,
- dispatcher,
- callback
- )
-
- testSubject.updateAppTargets(appTargets)
-
- verify(appPredictor, times(1)).requestPredictionUpdate()
-
- val resultCaptor = argumentCaptor<ShortcutLoader.Result>()
- verify(callback, times(1)).accept(capture(resultCaptor))
-
- val result = resultCaptor.value
- assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor)
- assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets)
- assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size)
- assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget)
- for (shortcut in result.shortcutsByApp[0].shortcuts) {
- assertTrue(
- "AppTargets are not expected the cache of a ShortcutManager result",
- result.directShareAppTargetCache.isEmpty()
- )
- assertEquals(
- "Wrong ShortcutInfo in the cache",
- matchingShortcutInfo,
- result.directShareShortcutInfoCache[shortcut]
- )
- }
- }
-
- @Test
- fun test_ShortcutLoader_shortcutsRequestedIndependentlyFromAppTargets() {
- ShortcutLoader(
- context,
- lifecycleOwner.lifecycle,
- appPredictor,
- UserHandle.of(0),
- true,
- intentFilter,
- dispatcher,
- callback
- )
-
- verify(appPredictor, times(1)).requestPredictionUpdate()
- verify(callback, never()).accept(any())
- }
-
- @Test
- fun test_ShortcutLoader_noResultsWithoutAppTargets() {
- val shortcutManagerResult =
- listOf(
- ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
- // mismatching shortcut
- createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
- )
- val shortcutManager =
- mock<ShortcutManager> {
- whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
- }
- whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
- val testSubject =
- ShortcutLoader(
- context,
- lifecycleOwner.lifecycle,
- null,
- UserHandle.of(0),
- true,
- intentFilter,
- dispatcher,
- callback
- )
-
- verify(shortcutManager, times(1)).getShareTargets(any())
- verify(callback, never()).accept(any())
-
- testSubject.reset()
-
- verify(shortcutManager, times(2)).getShareTargets(any())
- verify(callback, never()).accept(any())
-
- testSubject.updateAppTargets(appTargets)
-
- verify(shortcutManager, times(2)).getShareTargets(any())
- verify(callback, times(1)).accept(any())
- }
-
- @Test
- fun test_OnLifecycleDestroyed_unsubscribeFromAppPredictor() {
- ShortcutLoader(
- context,
- lifecycleOwner.lifecycle,
- appPredictor,
- UserHandle.of(0),
- true,
- intentFilter,
- dispatcher,
- callback
- )
-
- verify(appPredictor, never()).unregisterPredictionUpdates(any())
-
- lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
-
- verify(appPredictor, times(1)).unregisterPredictionUpdates(any())
- }
-
- @Test
- fun test_workProfileNotRunning_doNotCallServices() {
- testDisabledWorkProfileDoNotCallSystem(isUserRunning = false)
- }
-
- @Test
- fun test_workProfileLocked_doNotCallServices() {
- testDisabledWorkProfileDoNotCallSystem(isUserUnlocked = false)
- }
-
- @Test
- fun test_workProfileQuiteModeEnabled_doNotCallServices() {
- testDisabledWorkProfileDoNotCallSystem(isQuietModeEnabled = true)
- }
-
- @Test
- fun test_mainProfileNotRunning_callServicesAnyway() {
- testAlwaysCallSystemForMainProfile(isUserRunning = false)
- }
-
- @Test
- fun test_mainProfileLocked_callServicesAnyway() {
- testAlwaysCallSystemForMainProfile(isUserUnlocked = false)
- }
-
- @Test
- fun test_mainProfileQuiteModeEnabled_callServicesAnyway() {
- testAlwaysCallSystemForMainProfile(isQuietModeEnabled = true)
- }
-
- private fun testDisabledWorkProfileDoNotCallSystem(
- isUserRunning: Boolean = true,
- isUserUnlocked: Boolean = true,
- isQuietModeEnabled: Boolean = false
- ) {
- val userHandle = UserHandle.of(10)
- with(userManager) {
- whenever(isUserRunning(userHandle)).thenReturn(isUserRunning)
- whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked)
- whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled)
- }
- whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager)
- val appPredictor = mock<ShortcutLoader.AppPredictorProxy>()
- val callback = mock<Consumer<ShortcutLoader.Result>>()
- val testSubject =
- ShortcutLoader(
- context,
- lifecycleOwner.lifecycle,
- appPredictor,
- userHandle,
- false,
- intentFilter,
- dispatcher,
- callback
- )
-
- testSubject.updateAppTargets(arrayOf<DisplayResolveInfo>(mock()))
-
- verify(appPredictor, never()).requestPredictionUpdate()
- }
-
- private fun testAlwaysCallSystemForMainProfile(
- isUserRunning: Boolean = true,
- isUserUnlocked: Boolean = true,
- isQuietModeEnabled: Boolean = false
- ) {
- val userHandle = UserHandle.of(10)
- with(userManager) {
- whenever(isUserRunning(userHandle)).thenReturn(isUserRunning)
- whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked)
- whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled)
- }
- whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager)
- val appPredictor = mock<ShortcutLoader.AppPredictorProxy>()
- val callback = mock<Consumer<ShortcutLoader.Result>>()
- val testSubject =
- ShortcutLoader(
- context,
- lifecycleOwner.lifecycle,
- appPredictor,
- userHandle,
- true,
- intentFilter,
- dispatcher,
- callback
- )
-
- testSubject.updateAppTargets(arrayOf<DisplayResolveInfo>(mock()))
-
- verify(appPredictor, times(1)).requestPredictionUpdate()
- }
-}
diff --git a/tests/README.md b/tests/README.md
new file mode 100644
index 0000000..a3f9055
--- /dev/null
+++ b/tests/README.md
@@ -0,0 +1,33 @@
+# Automated testing
+
+IntentResolver test code is organized into sub-modules by scope and purpose.
+
+TreeHugger execution is controlled via [TEST_MAPPING](../TEST_MAPPING).
+
+## [Unit Tests](unit)
+
+Instrumentation tests which run on devices or emulators, but are otherwise isolated from the system. Scope of verification is limited to a single component at a time. These tests are extremely fast and should provide the most detailed and granular failure information.
+
+**Use cases**: The first choice for all new code. Fakes and other reusable test code should be placed in [shared](shared).
+
+## [Integration Tests](integration)
+
+Emulator tests which verify operation of the foundational components backed by android platform APIs. These tests are required for coverage because components tested here are replaced with fakes in other test suites.
+
+**Use cases**: Larger tests which require device preparation and setup to test production code using real dependencies. Implement these when verification is needed of interactions with live system services or applications using real data.
+
+## [Activity Tests](activity)
+
+Instrumentation tests which launch target activity code directly in the instrumentation context. These operate mostly production code end to end and provide a blend of UI assertions and verification using injected mocks and fakes.
+
+Originally from `frameworks/base/core/tests`, these cover the widest range of code but are historically the most flaky, brittle and with the least informative failures.
+
+Use Hilt's [@TestInstallIn](https://developer.android.com/training/dependency-injection/hilt-testing) to replace dependencies with alternates as needed. Test modules should be added here, while the fakes and other utilities used in these tests are found in [tests/shared](shared).
+
+**Use cases**: New tests and expansion of existing tests should be considered only as last resort for otherwise untestable code.
+
+## [Shared](shared)
+
+Testing code as a common dependency available to all the above test types.
+
+**Use cases**: Fakes, reusable assertions, or other test setup code. Tests for code here should be placed in [tests/unit](unit).
diff --git a/tests/activity/Android.bp b/tests/activity/Android.bp
new file mode 100644
index 0000000..f69caf0
--- /dev/null
+++ b/tests/activity/Android.bp
@@ -0,0 +1,68 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "IntentResolver-tests-activity",
+ manifest: "AndroidManifest.xml",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ "android.test.mock",
+ "framework",
+ "framework-res",
+ ],
+
+ resource_dirs: ["res"],
+ test_config: "AndroidTest.xml",
+ static_libs: [
+ "androidx.test.core",
+ "androidx.test.ext.junit",
+ "androidx.test.ext.truth",
+ "androidx.test.espresso.contrib",
+ "androidx.test.espresso.core",
+ "androidx.test.rules",
+ "androidx.test.runner",
+ "androidx.lifecycle_lifecycle-common-java8",
+ "androidx.lifecycle_lifecycle-extensions",
+ "androidx.lifecycle_lifecycle-runtime-testing",
+ "hilt_android_testing",
+ "IntentResolver-core",
+ "IntentResolver-tests-shared",
+ "junit",
+ "kotlinx_coroutines_test",
+ "mockito-target-minus-junit4",
+ "testables",
+ "truth",
+ "truth-java8-extension",
+ "flag-junit",
+ "platform-test-annotations",
+ ],
+ plugins: ["dagger2-compiler"],
+ test_suites: ["general-tests"],
+ sdk_version: "core_platform",
+ min_sdk_version: "current",
+ target_sdk_version: "current",
+ platform_apis: true,
+}
diff --git a/java/tests/AndroidManifest.xml b/tests/activity/AndroidManifest.xml
index 05830c4..be05e99 100644
--- a/java/tests/AndroidManifest.xml
+++ b/tests/activity/AndroidManifest.xml
@@ -13,11 +13,8 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.intentresolver.tests">
-
- <uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
+ package="com.android.intentresolver.tests">
<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL"/>
<uses-permission android:name="android.permission.QUERY_USERS"/>
@@ -25,19 +22,20 @@
<uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG"/>
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
- <application android:name="com.android.intentresolver.TestApplication">
+ <application android:name="dagger.hilt.android.testing.HiltTestApplication">
<uses-library android:name="android.test.runner" />
<activity android:name="com.android.intentresolver.ChooserWrapperActivity" />
<activity android:name="com.android.intentresolver.ResolverWrapperActivity" />
+ <activity android:name="com.android.intentresolver.v2.ChooserWrapperActivity" />
+ <activity android:name="com.android.intentresolver.v2.ResolverWrapperActivity" />
<provider
android:authorities="com.android.intentresolver.tests"
android:name="com.android.intentresolver.TestContentProvider"
android:grantUriPermissions="true" />
</application>
- <instrumentation android:name="android.testing.TestableInstrumentation"
- android:targetPackage="com.android.intentresolver.tests"
- android:label="Tests for IntentResolver">
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.intentresolver.tests">
</instrumentation>
</manifest>
diff --git a/tests/activity/AndroidTest.xml b/tests/activity/AndroidTest.xml
new file mode 100644
index 0000000..6c9d495
--- /dev/null
+++ b/tests/activity/AndroidTest.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Run IntentResolver Tests.">
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="IntentResolver-tests-activity.apk" />
+ </target_preparer>
+
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
+ <option name="run-command" value="wm dismiss-keyguard" />
+ </target_preparer>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.intentresolver.tests" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
diff --git a/java/tests/res/drawable/test320x240.png b/tests/activity/res/drawable/test320x240.png
index 9b5800d..9b5800d 100644
--- a/java/tests/res/drawable/test320x240.png
+++ b/tests/activity/res/drawable/test320x240.png
Binary files differ
diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java
index 84f5124..3ee80c1 100644
--- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
+++ b/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java
@@ -26,11 +26,9 @@ import android.content.res.Resources;
import android.database.Cursor;
import android.os.UserHandle;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.contentpreview.ImageLoader;
-import com.android.intentresolver.flags.FeatureFlagRepository;
-import com.android.intentresolver.logging.EventLog;
+import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
import com.android.intentresolver.shortcuts.ShortcutLoader;
import java.util.function.Consumer;
@@ -65,19 +63,15 @@ public class ChooserActivityOverrideData {
public Cursor resolverCursor;
public boolean resolverForceException;
public ImageLoader imageLoader;
- public EventLog mEventLog;
public int alternateProfileSetting;
public Resources resources;
- public UserHandle workProfileUserHandle;
- public UserHandle cloneProfileUserHandle;
- public UserHandle tabOwnerUserHandleForLaunch;
+ public AnnotatedUserHandles annotatedUserHandles;
public boolean hasCrossProfileIntents;
public boolean isQuietModeEnabled;
public Integer myUserId;
public WorkProfileAvailabilityManager mWorkProfileAvailability;
public CrossProfileIntentsChecker mCrossProfileIntentsChecker;
public PackageManager packageManager;
- public FeatureFlagRepository featureFlagRepository;
public void reset() {
onSafelyStartInternalCallback = null;
@@ -88,12 +82,13 @@ public class ChooserActivityOverrideData {
resolverForceException = false;
resolverListController = mock(ChooserActivity.ChooserListController.class);
workResolverListController = mock(ChooserActivity.ChooserListController.class);
- mEventLog = mock(EventLog.class);
alternateProfileSetting = 0;
resources = null;
- workProfileUserHandle = null;
- cloneProfileUserHandle = null;
- tabOwnerUserHandleForLaunch = null;
+ annotatedUserHandles = AnnotatedUserHandles.newBuilder()
+ .setUserIdOfCallingApp(1234) // Must be non-negative.
+ .setUserHandleSharesheetLaunchedAs(UserHandle.SYSTEM)
+ .setPersonalProfileUserHandle(UserHandle.SYSTEM)
+ .build();
hasCrossProfileIntents = true;
isQuietModeEnabled = false;
myUserId = null;
@@ -127,7 +122,6 @@ public class ChooserActivityOverrideData {
mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class);
when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt()))
.thenAnswer(invocation -> hasCrossProfileIntents);
- featureFlagRepository = null;
}
private ChooserActivityOverrideData() {}
diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java
index 8608cf7..4ea0681 100644
--- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
+++ b/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java
@@ -16,7 +16,6 @@
package com.android.intentresolver;
-import android.annotation.Nullable;
import android.app.prediction.AppPredictor;
import android.app.usage.UsageStatsManager;
import android.content.ComponentName;
@@ -32,15 +31,15 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.UserHandle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
-import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
import com.android.intentresolver.grid.ChooserGridAdapter;
import com.android.intentresolver.icons.TargetDataLoader;
-import com.android.intentresolver.logging.EventLog;
import com.android.intentresolver.shortcuts.ShortcutLoader;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -51,8 +50,7 @@ import java.util.function.Consumer;
* Simple wrapper around chooser activity to be able to initiate it under test. For more
* information, see {@code com.android.internal.app.ChooserWrapperActivity}.
*/
-public class ChooserWrapperActivity
- extends com.android.intentresolver.ChooserActivity implements IChooserWrapper {
+public class ChooserWrapperActivity extends ChooserActivity implements IChooserWrapper {
static final ChooserActivityOverrideData sOverrides = ChooserActivityOverrideData.getInstance();
private UsageStatsManager mUsm;
@@ -73,7 +71,7 @@ public class ChooserWrapperActivity
ResolverListController resolverListController,
UserHandle userHandle,
Intent targetIntent,
- ChooserRequestParameters chooserRequest,
+ Intent referrrerFillInIntent,
int maxTargetsPerRow,
TargetDataLoader targetDataLoader) {
PackageManager packageManager =
@@ -88,13 +86,14 @@ public class ChooserWrapperActivity
createListController(userHandle),
userHandle,
targetIntent,
+ referrrerFillInIntent,
this,
packageManager,
getEventLog(),
- chooserRequest,
maxTargetsPerRow,
userHandle,
- targetDataLoader);
+ targetDataLoader,
+ null);
}
@Override
@@ -206,11 +205,6 @@ public class ChooserWrapperActivity
}
@Override
- public EventLog getEventLog() {
- return sOverrides.mEventLog;
- }
-
- @Override
public Cursor queryResolver(ContentResolver resolver, Uri uri) {
if (sOverrides.resolverCursor != null) {
return sOverrides.resolverCursor;
@@ -232,21 +226,23 @@ public class ChooserWrapperActivity
}
@Override
- public DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri,
- CharSequence pLabel, CharSequence pInfo, Intent replacementIntent,
- @Nullable TargetPresentationGetter resolveInfoPresentationGetter) {
+ public DisplayResolveInfo createTestDisplayResolveInfo(
+ Intent originalIntent,
+ ResolveInfo pri,
+ CharSequence pLabel,
+ CharSequence pInfo,
+ Intent replacementIntent) {
return DisplayResolveInfo.newDisplayResolveInfo(
originalIntent,
pri,
pLabel,
pInfo,
- replacementIntent,
- resolveInfoPresentationGetter);
+ replacementIntent);
}
@Override
- protected UserHandle getWorkProfileUserHandle() {
- return sOverrides.workProfileUserHandle;
+ protected AnnotatedUserHandles computeAnnotatedUserHandles() {
+ return sOverrides.annotatedUserHandles;
}
@Override
@@ -254,18 +250,11 @@ public class ChooserWrapperActivity
return mMultiProfilePagerAdapter.getCurrentUserHandle();
}
- @Override
- protected UserHandle getTabOwnerUserHandleForLaunch() {
- if (sOverrides.tabOwnerUserHandleForLaunch == null) {
- return super.getTabOwnerUserHandleForLaunch();
- }
- return sOverrides.tabOwnerUserHandleForLaunch;
- }
-
+ @NonNull
@Override
public Context createContextAsUser(UserHandle user, int flags) {
// return the current context as a work profile doesn't really exist in these tests
- return getApplicationContext();
+ return this;
}
@Override
@@ -283,12 +272,4 @@ public class ChooserWrapperActivity
return super.createShortcutLoader(
context, appPredictor, userHandle, targetIntentFilter, callback);
}
-
- @Override
- protected FeatureFlagRepository createFeatureFlagRepository() {
- if (sOverrides.featureFlagRepository != null) {
- return sOverrides.featureFlagRepository;
- }
- return super.createFeatureFlagRepository();
- }
}
diff --git a/java/tests/src/com/android/intentresolver/IChooserWrapper.java b/tests/activity/src/com/android/intentresolver/IChooserWrapper.java
index 3326d7f..481cf3b 100644
--- a/java/tests/src/com/android/intentresolver/IChooserWrapper.java
+++ b/tests/activity/src/com/android/intentresolver/IChooserWrapper.java
@@ -16,14 +16,14 @@
package com.android.intentresolver;
-import android.annotation.Nullable;
import android.app.usage.UsageStatsManager;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.os.UserHandle;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.chooser.DisplayResolveInfo;
-import com.android.intentresolver.logging.EventLog;
import java.util.concurrent.Executor;
@@ -38,10 +38,12 @@ public interface IChooserWrapper {
ChooserListAdapter getWorkListAdapter();
boolean getIsSelected();
UsageStatsManager getUsageStatsManager();
- DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri,
- CharSequence pLabel, CharSequence pInfo, Intent replacementIntent,
- @Nullable TargetPresentationGetter resolveInfoPresentationGetter);
+ DisplayResolveInfo createTestDisplayResolveInfo(
+ Intent originalIntent,
+ ResolveInfo pri,
+ CharSequence pLabel,
+ CharSequence pInfo,
+ @Nullable Intent replacementIntent);
UserHandle getCurrentUserHandle();
- EventLog getEventLog();
Executor getMainExecutor();
}
diff --git a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java b/tests/activity/src/com/android/intentresolver/ResolverActivityTest.java
index 7233fd3..dde2f98 100644
--- a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java
+++ b/tests/activity/src/com/android/intentresolver/ResolverActivityTest.java
@@ -77,12 +77,8 @@ public class ResolverActivityTest {
private static final UserHandle PERSONAL_USER_HANDLE = androidx.test.platform.app
.InstrumentationRegistry.getInstrumentation().getTargetContext().getUser();
- protected Intent getConcreteIntentForLaunch(Intent clientIntent) {
- clientIntent.setClass(
- androidx.test.platform.app.InstrumentationRegistry.getInstrumentation().getTargetContext(),
- ResolverWrapperActivity.class);
- return clientIntent;
- }
+ private static final UserHandle WORK_PROFILE_USER_HANDLE = UserHandle.of(10);
+ private static final UserHandle CLONE_PROFILE_USER_HANDLE = UserHandle.of(11);
@Rule
public ActivityTestRule<ResolverWrapperActivity> mActivityRule =
@@ -238,9 +234,9 @@ public class ResolverActivityTest {
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10,
PERSONAL_USER_HANDLE);
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
- sOverrides.workProfileUserHandle);
+ WORK_PROFILE_USER_HANDLE);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
ResolveInfo toChoose = personalResolvedComponentInfos.get(1).getResolveInfoAt(0);
@@ -350,7 +346,7 @@ public class ResolverActivityTest {
@Test
public void testWorkTab_displayedWhenWorkProfileUserAvailable() {
Intent sendIntent = createSendImageIntent();
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
mActivityRule.launchActivity(sendIntent);
waitForIdle();
@@ -373,9 +369,9 @@ public class ResolverActivityTest {
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3, /* userId = */ 10,
PERSONAL_USER_HANDLE);
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
- sOverrides.workProfileUserHandle);
+ WORK_PROFILE_USER_HANDLE);
setupResolverControllers(personalResolvedComponentInfos,
new ArrayList<>(workResolvedComponentInfos));
Intent sendIntent = createSendImageIntent();
@@ -393,12 +389,12 @@ public class ResolverActivityTest {
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10,
PERSONAL_USER_HANDLE);
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
- sOverrides.workProfileUserHandle);
+ WORK_PROFILE_USER_HANDLE);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
waitForIdle();
@@ -412,9 +408,9 @@ public class ResolverActivityTest {
public void testWorkTab_personalTabUsesExpectedAdapter() {
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE);
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
- sOverrides.workProfileUserHandle);
+ WORK_PROFILE_USER_HANDLE);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
@@ -428,12 +424,12 @@ public class ResolverActivityTest {
@Test
public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10,
PERSONAL_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
- sOverrides.workProfileUserHandle);
+ WORK_PROFILE_USER_HANDLE);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
@@ -448,12 +444,12 @@ public class ResolverActivityTest {
@Test
public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() throws InterruptedException {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10,
PERSONAL_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
- sOverrides.workProfileUserHandle);
+ WORK_PROFILE_USER_HANDLE);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
ResolveInfo[] chosen = new ResolveInfo[1];
@@ -480,11 +476,11 @@ public class ResolverActivityTest {
@Test
public void testWorkTab_noPersonalApps_workTabHasExpectedNumberOfTargets()
throws InterruptedException {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
- sOverrides.workProfileUserHandle);
+ WORK_PROFILE_USER_HANDLE);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
@@ -499,11 +495,11 @@ public class ResolverActivityTest {
@Test
public void testWorkTab_headerIsVisibleInPersonalTab() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
- sOverrides.workProfileUserHandle);
+ WORK_PROFILE_USER_HANDLE);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createOpenWebsiteIntent();
@@ -517,11 +513,11 @@ public class ResolverActivityTest {
@Test
public void testWorkTab_switchTabs_headerStaysSame() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
- sOverrides.workProfileUserHandle);
+ WORK_PROFILE_USER_HANDLE);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createOpenWebsiteIntent();
@@ -543,12 +539,12 @@ public class ResolverActivityTest {
@Test
public void testWorkTab_noPersonalApps_canStartWorkApps()
throws InterruptedException {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3, /* userId= */ 10,
PERSONAL_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
- sOverrides.workProfileUserHandle);
+ WORK_PROFILE_USER_HANDLE);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
ResolveInfo[] chosen = new ResolveInfo[1];
@@ -576,14 +572,13 @@ public class ResolverActivityTest {
@Test
public void testWorkTab_crossProfileIntentsDisabled_personalToWork_emptyStateShown() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
int workProfileTargets = 4;
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10,
PERSONAL_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos =
- createResolvedComponentsForTest(workProfileTargets,
- sOverrides.workProfileUserHandle);
+ createResolvedComponentsForTest(workProfileTargets, WORK_PROFILE_USER_HANDLE);
sOverrides.hasCrossProfileIntents = false;
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
@@ -602,14 +597,13 @@ public class ResolverActivityTest {
@Test
public void testWorkTab_workProfileDisabled_emptyStateShown() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
int workProfileTargets = 4;
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10,
PERSONAL_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos =
- createResolvedComponentsForTest(workProfileTargets,
- sOverrides.workProfileUserHandle);
+ createResolvedComponentsForTest(workProfileTargets, WORK_PROFILE_USER_HANDLE);
sOverrides.isQuietModeEnabled = true;
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
@@ -628,11 +622,11 @@ public class ResolverActivityTest {
@Test
public void testWorkTab_noWorkAppsAvailable_emptyStateShown() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos =
- createResolvedComponentsForTest(0, sOverrides.workProfileUserHandle);
+ createResolvedComponentsForTest(0, WORK_PROFILE_USER_HANDLE);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
sendIntent.setType("TestType");
@@ -650,11 +644,11 @@ public class ResolverActivityTest {
@Test
public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos =
- createResolvedComponentsForTest(0, sOverrides.workProfileUserHandle);
+ createResolvedComponentsForTest(0, WORK_PROFILE_USER_HANDLE);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
sendIntent.setType("TestType");
@@ -674,11 +668,11 @@ public class ResolverActivityTest {
@Test
public void testMiniResolver() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTest(1, PERSONAL_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos =
- createResolvedComponentsForTest(1, sOverrides.workProfileUserHandle);
+ createResolvedComponentsForTest(1, WORK_PROFILE_USER_HANDLE);
// Personal profile only has a browser
personalResolvedComponentInfos.get(0).getResolveInfoAt(0).handleAllWebDataURI = true;
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
@@ -692,11 +686,11 @@ public class ResolverActivityTest {
@Test
public void testMiniResolver_noCurrentProfileTarget() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTest(0, PERSONAL_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos =
- createResolvedComponentsForTest(1, sOverrides.workProfileUserHandle);
+ createResolvedComponentsForTest(1, WORK_PROFILE_USER_HANDLE);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
sendIntent.setType("TestType");
@@ -720,11 +714,11 @@ public class ResolverActivityTest {
@Test
public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos =
- createResolvedComponentsForTest(0, sOverrides.workProfileUserHandle);
+ createResolvedComponentsForTest(0, WORK_PROFILE_USER_HANDLE);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
sendIntent.setType("TestType");
@@ -743,14 +737,13 @@ public class ResolverActivityTest {
@Test
public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_doesNotAutoLaunch() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
int workProfileTargets = 4;
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10,
PERSONAL_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos =
- createResolvedComponentsForTest(workProfileTargets,
- sOverrides.workProfileUserHandle);
+ createResolvedComponentsForTest(workProfileTargets, WORK_PROFILE_USER_HANDLE);
sOverrides.hasCrossProfileIntents = false;
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
@@ -769,7 +762,7 @@ public class ResolverActivityTest {
@Test
public void testLayoutWithDefault_withWorkTab_neverShown() throws RemoteException {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
// In this case we prefer the other profile and don't display anything about the last
// chosen activity.
@@ -794,54 +787,53 @@ public class ResolverActivityTest {
@Test
public void testClonedProfilePresent_personalAdapterIsSetWithPersonalProfile() {
// enable cloneProfile
- markCloneProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true);
List<ResolvedComponentInfo> resolvedComponentInfos =
createResolvedComponentsWithCloneProfileForTest(
3,
PERSONAL_USER_HANDLE,
- sOverrides.cloneProfileUserHandle);
+ CLONE_PROFILE_USER_HANDLE);
setupResolverControllers(resolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
waitForIdle();
- assertThat(activity.getCurrentUserHandle(), is(activity.getPersonalProfileUserHandle()));
+ assertThat(activity.getCurrentUserHandle(), is(PERSONAL_USER_HANDLE));
assertThat(activity.getAdapter().getCount(), is(3));
}
@Test
public void testClonedProfilePresent_personalTabUsesExpectedAdapter() {
- markWorkProfileUserAvailable();
// enable cloneProfile
- markCloneProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ true);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsWithCloneProfileForTest(
3,
PERSONAL_USER_HANDLE,
- sOverrides.cloneProfileUserHandle);
+ CLONE_PROFILE_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
- sOverrides.workProfileUserHandle);
+ WORK_PROFILE_USER_HANDLE);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
waitForIdle();
- assertThat(activity.getCurrentUserHandle(), is(activity.getPersonalProfileUserHandle()));
+ assertThat(activity.getCurrentUserHandle(), is(PERSONAL_USER_HANDLE));
assertThat(activity.getAdapter().getCount(), is(3));
}
@Test
public void testClonedProfilePresent_layoutWithDefault_neverShown() throws Exception {
// enable cloneProfile
- markCloneProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true);
Intent sendIntent = createSendImageIntent();
List<ResolvedComponentInfo> resolvedComponentInfos =
createResolvedComponentsWithCloneProfileForTest(
2,
PERSONAL_USER_HANDLE,
- sOverrides.cloneProfileUserHandle);
+ CLONE_PROFILE_USER_HANDLE);
setupResolverControllers(resolvedComponentInfos);
when(sOverrides.resolverListController.getLastChosen())
@@ -859,13 +851,13 @@ public class ResolverActivityTest {
@Test
public void testClonedProfilePresent_alwaysButtonDisabled() throws Exception {
// enable cloneProfile
- markCloneProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true);
Intent sendIntent = createSendImageIntent();
List<ResolvedComponentInfo> resolvedComponentInfos =
createResolvedComponentsWithCloneProfileForTest(
3,
PERSONAL_USER_HANDLE,
- sOverrides.cloneProfileUserHandle);
+ CLONE_PROFILE_USER_HANDLE);
setupResolverControllers(resolvedComponentInfos);
when(sOverrides.resolverListController.getLastChosen())
@@ -892,17 +884,16 @@ public class ResolverActivityTest {
@Test
public void testClonedProfilePresent_personalProfileActivityIsStartedInCorrectUser()
throws Exception {
- markWorkProfileUserAvailable();
// enable cloneProfile
- markCloneProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ true);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsWithCloneProfileForTest(
3,
PERSONAL_USER_HANDLE,
- sOverrides.cloneProfileUserHandle);
+ CLONE_PROFILE_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos =
- createResolvedComponentsForTest(3, sOverrides.workProfileUserHandle);
+ createResolvedComponentsForTest(3, WORK_PROFILE_USER_HANDLE);
sOverrides.hasCrossProfileIntents = false;
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
@@ -928,17 +919,16 @@ public class ResolverActivityTest {
@Test
public void testClonedProfilePresent_workProfileActivityIsStartedInCorrectUser()
throws Exception {
- markWorkProfileUserAvailable();
// enable cloneProfile
- markCloneProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ true);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsWithCloneProfileForTest(
3,
PERSONAL_USER_HANDLE,
- sOverrides.cloneProfileUserHandle);
+ CLONE_PROFILE_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos =
- createResolvedComponentsForTest(3, sOverrides.workProfileUserHandle);
+ createResolvedComponentsForTest(3, WORK_PROFILE_USER_HANDLE);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
sendIntent.setType("TestType");
@@ -967,12 +957,12 @@ public class ResolverActivityTest {
public void testClonedProfilePresent_personalProfileResolverComparatorHasCorrectUsers()
throws Exception {
// enable cloneProfile
- markCloneProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true);
List<ResolvedComponentInfo> resolvedComponentInfos =
createResolvedComponentsWithCloneProfileForTest(
3,
PERSONAL_USER_HANDLE,
- sOverrides.cloneProfileUserHandle);
+ CLONE_PROFILE_USER_HANDLE);
setupResolverControllers(resolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
@@ -981,8 +971,8 @@ public class ResolverActivityTest {
List<UserHandle> result = activity
.getResolverRankerServiceUserHandleList(PERSONAL_USER_HANDLE);
- assertThat(result.containsAll(Lists.newArrayList(PERSONAL_USER_HANDLE,
- sOverrides.cloneProfileUserHandle)), is(true));
+ assertThat(result.containsAll(
+ Lists.newArrayList(PERSONAL_USER_HANDLE, CLONE_PROFILE_USER_HANDLE)), is(true));
}
private Intent createSendImageIntent() {
@@ -1059,8 +1049,19 @@ public class ResolverActivityTest {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
- private void markWorkProfileUserAvailable() {
- ResolverWrapperActivity.sOverrides.workProfileUserHandle = UserHandle.of(10);
+ private void markOtherProfileAvailability(boolean workAvailable, boolean cloneAvailable) {
+ AnnotatedUserHandles.Builder handles = AnnotatedUserHandles.newBuilder();
+ handles
+ .setUserIdOfCallingApp(1234) // Must be non-negative.
+ .setUserHandleSharesheetLaunchedAs(PERSONAL_USER_HANDLE)
+ .setPersonalProfileUserHandle(PERSONAL_USER_HANDLE);
+ if (workAvailable) {
+ handles.setWorkProfileUserHandle(WORK_PROFILE_USER_HANDLE);
+ }
+ if (cloneAvailable) {
+ handles.setCloneProfileUserHandle(CLONE_PROFILE_USER_HANDLE);
+ }
+ sOverrides.annotatedUserHandles = handles.build();
}
private void setupResolverControllers(
@@ -1068,10 +1069,6 @@ public class ResolverActivityTest {
setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>());
}
- private void markCloneProfileUserAvailable() {
- ResolverWrapperActivity.sOverrides.cloneProfileUserHandle = UserHandle.of(11);
- }
-
private void setupResolverControllers(
List<ResolvedComponentInfo> personalResolvedComponentInfos,
List<ResolvedComponentInfo> workResolvedComponentInfos) {
diff --git a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java b/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java
index 401ede2..d1adfba 100644
--- a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java
+++ b/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java
@@ -21,7 +21,6 @@ import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
-import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
@@ -32,12 +31,14 @@ import android.os.UserHandle;
import android.util.Pair;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.test.espresso.idling.CountingIdlingResource;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.SelectableTargetInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
+import com.android.intentresolver.icons.LabelInfo;
import com.android.intentresolver.icons.TargetDataLoader;
import java.util.List;
@@ -57,13 +58,6 @@ public class ResolverWrapperActivity extends ResolverActivity {
super(/* isIntentPicker= */ true);
}
- // ResolverActivity inspects the launched-from UID at onCreate and needs to see some
- // non-negative value in the test.
- @Override
- public int getLaunchedFromUid() {
- return 1234;
- }
-
public CountingIdlingResource getLabelIdlingResource() {
return mLabelIdlingResource;
}
@@ -161,17 +155,15 @@ public class ResolverWrapperActivity extends ResolverActivity {
}
@Override
- protected UserHandle getWorkProfileUserHandle() {
- return sOverrides.workProfileUserHandle;
+ protected AnnotatedUserHandles computeAnnotatedUserHandles() {
+ return sOverrides.annotatedUserHandles;
}
-
- @Override
- protected UserHandle getCloneProfileUserHandle() {
- return sOverrides.cloneProfileUserHandle;
- }
-
@Override
- public void startActivityAsUser(Intent intent, Bundle options, UserHandle user) {
+ public void startActivityAsUser(
+ @NonNull Intent intent,
+ Bundle options,
+ @NonNull UserHandle user
+ ) {
super.startActivityAsUser(intent, options, user);
}
@@ -193,9 +185,7 @@ public class ResolverWrapperActivity extends ResolverActivity {
public ResolverListController resolverListController;
public ResolverListController workResolverListController;
public Boolean isVoiceInteraction;
- public UserHandle workProfileUserHandle;
- public UserHandle cloneProfileUserHandle;
- public UserHandle tabOwnerUserHandleForLaunch;
+ public AnnotatedUserHandles annotatedUserHandles;
public Integer myUserId;
public boolean hasCrossProfileIntents;
public boolean isQuietModeEnabled;
@@ -208,9 +198,11 @@ public class ResolverWrapperActivity extends ResolverActivity {
createPackageManager = null;
resolverListController = mock(ResolverListController.class);
workResolverListController = mock(ResolverListController.class);
- workProfileUserHandle = null;
- cloneProfileUserHandle = null;
- tabOwnerUserHandleForLaunch = null;
+ annotatedUserHandles = AnnotatedUserHandles.newBuilder()
+ .setUserIdOfCallingApp(1234) // Must be non-negative.
+ .setUserHandleSharesheetLaunchedAs(UserHandle.SYSTEM)
+ .setPersonalProfileUserHandle(UserHandle.SYSTEM)
+ .build();
myUserId = null;
hasCrossProfileIntents = true;
isQuietModeEnabled = false;
@@ -275,7 +267,7 @@ public class ResolverWrapperActivity extends ResolverActivity {
@Override
public void loadLabel(
@NonNull DisplayResolveInfo info,
- @NonNull Consumer<CharSequence[]> callback) {
+ @NonNull Consumer<LabelInfo> callback) {
mLabelIdlingResource.increment();
mTargetDataLoader.loadLabel(
info,
@@ -285,10 +277,9 @@ public class ResolverWrapperActivity extends ResolverActivity {
});
}
- @NonNull
@Override
- public TargetPresentationGetter createPresentationGetter(@NonNull ResolveInfo info) {
- return mTargetDataLoader.createPresentationGetter(info);
+ public void getOrLoadLabel(@NonNull DisplayResolveInfo info) {
+ mTargetDataLoader.getOrLoadLabel(info);
}
}
}
diff --git a/java/tests/src/com/android/intentresolver/TestContentProvider.kt b/tests/activity/src/com/android/intentresolver/TestContentProvider.kt
index 426f9af..426f9af 100644
--- a/java/tests/src/com/android/intentresolver/TestContentProvider.kt
+++ b/tests/activity/src/com/android/intentresolver/TestContentProvider.kt
diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityTest.java
index b8b5740..f597d7f 100644
--- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
+++ b/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityTest.java
@@ -25,6 +25,7 @@ import static androidx.test.espresso.action.ViewActions.swipeUp;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
import static androidx.test.espresso.matcher.ViewMatchers.hasSibling;
+import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
@@ -39,6 +40,7 @@ import static com.android.intentresolver.ChooserListAdapter.SHORTCUT_TARGET_SCOR
import static com.android.intentresolver.MatcherUtils.first;
import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
import static junit.framework.Assert.assertNull;
@@ -47,10 +49,9 @@ import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.CoreMatchers.not;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
@@ -82,20 +83,30 @@ import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
+import android.graphics.Typeface;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserHandle;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
import android.provider.DeviceConfig;
import android.service.chooser.ChooserAction;
import android.service.chooser.ChooserTarget;
-import android.util.HashedStringCache;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
import android.util.Pair;
import android.util.SparseArray;
import android.view.View;
import android.view.WindowManager;
+import android.widget.TextView;
-import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.recyclerview.widget.GridLayoutManager;
@@ -109,10 +120,13 @@ import androidx.test.rule.ActivityTestRule;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.contentpreview.ImageLoader;
import com.android.intentresolver.logging.EventLog;
+import com.android.intentresolver.logging.FakeEventLog;
import com.android.intentresolver.shortcuts.ShortcutLoader;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.systemui.flags.BooleanFlag;
+
+import dagger.hilt.android.testing.HiltAndroidRule;
+import dagger.hilt.android.testing.HiltAndroidTest;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
@@ -121,8 +135,6 @@ import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
-import org.junit.rules.RuleChain;
-import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.mockito.ArgumentCaptor;
@@ -136,38 +148,30 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
/**
- * Instrumentation tests for the IntentResolver module's Sharesheet (ChooserActivity).
- * TODO: remove methods that supported running these tests against arbitrary ChooserActivity
- * subclasses. Those were left over from an earlier version where IntentResolver's ChooserActivity
- * inherited from the framework version at com.android.internal.app.ChooserActivity, and this test
- * file inherited from the framework's version as well. Once the migration to the IntentResolver
- * package is complete, that aspect of the test design can revert to match the style of the
- * framework tests prior to ag/16482932.
- * TODO: this can simply be renamed to "ChooserActivityTest" if that's ever unambiguous (i.e., if
- * there's no risk of confusion with the framework tests that currently share the same name).
+ * Instrumentation tests for ChooserActivity.
+ * <p>
+ * Legacy test suite migrated from framework CoreTests.
+ * <p>
*/
@RunWith(Parameterized.class)
+@HiltAndroidTest
public class UnbundledChooserActivityTest {
- /* --------
- * Subclasses should copy the following section verbatim (or alternatively could specify some
- * additional @Parameterized.Parameters, as long as the correct parameters are used to
- * initialize the ChooserActivityTest). The subclasses should also be @RunWith the
- * `Parameterized` runner.
- * --------
- */
+ private static FakeEventLog getEventLog(ChooserWrapperActivity activity) {
+ return (FakeEventLog) activity.mEventLog;
+ }
private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry
.getInstrumentation().getTargetContext().getUser();
+ private static final UserHandle WORK_PROFILE_USER_HANDLE = UserHandle.of(10);
+ private static final UserHandle CLONE_PROFILE_USER_HANDLE = UserHandle.of(11);
+
private static final Function<PackageManager, PackageManager> DEFAULT_PM = pm -> pm;
private static final Function<PackageManager, PackageManager> NO_APP_PREDICTION_SERVICE_PM =
pm -> {
@@ -176,56 +180,34 @@ public class UnbundledChooserActivityTest {
return mock;
};
- private static final List<BooleanFlag> ALL_FLAGS =
- Arrays.asList();
-
- private static final Map<BooleanFlag, Boolean> ALL_FLAGS_OFF =
- createAllFlagsOverride(false);
- private static final Map<BooleanFlag, Boolean> ALL_FLAGS_ON =
- createAllFlagsOverride(true);
-
@Parameterized.Parameters
public static Collection packageManagers() {
- if (ALL_FLAGS.isEmpty()) {
- // No flags to toggle between, so just two configurations.
- return Arrays.asList(new Object[][] {
- // Default PackageManager and all flags off
- { DEFAULT_PM, ALL_FLAGS_OFF},
- // No App Prediction Service and all flags off
- { NO_APP_PREDICTION_SERVICE_PM, ALL_FLAGS_OFF },
- });
- }
return Arrays.asList(new Object[][] {
- // Default PackageManager and all flags off
- { DEFAULT_PM, ALL_FLAGS_OFF},
- // Default PackageManager and all flags on
- { DEFAULT_PM, ALL_FLAGS_ON},
- // No App Prediction Service and all flags off
- { NO_APP_PREDICTION_SERVICE_PM, ALL_FLAGS_OFF },
- // No App Prediction Service and all flags on
- { NO_APP_PREDICTION_SERVICE_PM, ALL_FLAGS_ON }
+ // Default PackageManager
+ { DEFAULT_PM },
+ // No App Prediction Service
+ { NO_APP_PREDICTION_SERVICE_PM}
});
}
- private static Map<BooleanFlag, Boolean> createAllFlagsOverride(boolean value) {
- HashMap<BooleanFlag, Boolean> overrides = new HashMap<>(ALL_FLAGS.size());
- for (BooleanFlag flag : ALL_FLAGS) {
- overrides.put(flag, value);
- }
- return overrides;
- }
+ private static final String TEST_MIME_TYPE = "application/TestType";
- /* --------
- * Subclasses can override the following methods to customize test behavior.
- * --------
- */
+ private static final int CONTENT_PREVIEW_IMAGE = 1;
+ private static final int CONTENT_PREVIEW_FILE = 2;
+ private static final int CONTENT_PREVIEW_TEXT = 3;
- /**
- * Perform any necessary per-test initialization steps (subclasses may add additional steps
- * before and/or after calling up to the superclass implementation).
- */
- @CallSuper
- protected void setup() {
+ @Rule(order = 0)
+ public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ @Rule(order = 1)
+ public HiltAndroidRule mHiltAndroidRule = new HiltAndroidRule(this);
+
+ @Rule(order = 2)
+ public ActivityTestRule<ChooserWrapperActivity> mActivityRule =
+ new ActivityTestRule<>(ChooserWrapperActivity.class, false, false);
+
+ @Before
+ public void setUp() {
// TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the
// permissions we require (which we'll read from the manifest at runtime).
InstrumentationRegistry
@@ -234,87 +216,14 @@ public class UnbundledChooserActivityTest {
.adoptShellPermissionIdentity();
cleanOverrideData();
- ChooserActivityOverrideData.getInstance().featureFlagRepository =
- new TestFeatureFlagRepository(mFlags);
- }
-
- /**
- * Given an intent that was constructed in a test, perform any additional configuration to
- * specify the appropriate concrete ChooserActivity subclass. The activity launched by this
- * intent must descend from android.intentresolver.ChooserActivity (for our ActivityTestRule), and
- * must also implement the android.intentresolver.IChooserWrapper interface (since test code will
- * assume the ability to make unsafe downcasts).
- */
- protected Intent getConcreteIntentForLaunch(Intent clientIntent) {
- clientIntent.setClass(
- InstrumentationRegistry.getInstrumentation().getTargetContext(),
- com.android.intentresolver.ChooserWrapperActivity.class);
- return clientIntent;
- }
-
- /**
- * Whether {@code #testIsAppPredictionServiceAvailable} should verify the behavior after
- * changing the availability conditions at runtime. In the unbundled chooser, the availability
- * is cached at start and will never be re-evaluated.
- * TODO: remove when we no longer want to test the system's on-the-fly evaluation.
- */
- protected boolean shouldTestTogglingAppPredictionServiceAvailabilityAtRuntime() {
- return false;
- }
-
- /* --------
- * The code in this section is unorthodox and can be simplified/reverted when we no longer need
- * to support the parallel chooser implementations.
- * --------
- */
-
- @Rule
- public final TestRule mRule;
-
- // Shared test code references the activity under test as ChooserActivity, the common ancestor
- // of any (inheritance-based) chooser implementation. For testing purposes, that activity will
- // usually be cast to IChooserWrapper to expose instrumentation.
- private ActivityTestRule<ChooserActivity> mActivityRule =
- new ActivityTestRule<>(ChooserActivity.class, false, false) {
- @Override
- public ChooserActivity launchActivity(Intent clientIntent) {
- return super.launchActivity(getConcreteIntentForLaunch(clientIntent));
- }
- };
-
- @Before
- public final void doPolymorphicSetup() {
- // The base class needs a @Before-annotated setup for when it runs against the system
- // chooser, while subclasses need to be able to specify their own setup behavior. Notably
- // the unbundled chooser, running in user-space, needs to take additional steps before it
- // can run #cleanOverrideData() (which writes to DeviceConfig).
- setup();
+ mHiltAndroidRule.inject();
}
- /* --------
- * Subclasses can ignore the remaining code and inherit the full suite of tests.
- * --------
- */
-
- private static final String TEST_MIME_TYPE = "application/TestType";
-
- private static final int CONTENT_PREVIEW_IMAGE = 1;
- private static final int CONTENT_PREVIEW_FILE = 2;
- private static final int CONTENT_PREVIEW_TEXT = 3;
-
private final Function<PackageManager, PackageManager> mPackageManagerOverride;
- private final Map<BooleanFlag, Boolean> mFlags;
-
public UnbundledChooserActivityTest(
- Function<PackageManager, PackageManager> packageManagerOverride,
- Map<BooleanFlag, Boolean> flags) {
+ Function<PackageManager, PackageManager> packageManagerOverride) {
mPackageManagerOverride = packageManagerOverride;
- mFlags = flags;
-
- mRule = RuleChain
- .outerRule(new FeatureFlagRule(flags))
- .around(mActivityRule);
}
private void setDeviceConfigProperty(
@@ -384,6 +293,58 @@ public class UnbundledChooserActivityTest {
}
@Test
+ public void test_shareRichTextWithRichTitle_richTextAndRichTitleDisplayed() {
+ CharSequence title = new SpannableStringBuilder()
+ .append("Rich", new UnderlineSpan(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
+ .append(
+ "Title",
+ new ForegroundColorSpan(Color.RED),
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+ CharSequence sharedText = new SpannableStringBuilder()
+ .append(
+ "Rich",
+ new BackgroundColorSpan(Color.YELLOW),
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
+ .append(
+ "Text",
+ new StyleSpan(Typeface.ITALIC),
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText);
+ sendIntent.putExtra(Intent.EXTRA_TITLE, title);
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withId(com.android.internal.R.id.content_preview_title))
+ .check((view, e) -> {
+ assertThat(view).isInstanceOf(TextView.class);
+ CharSequence text = ((TextView) view).getText();
+ assertThat(text).isInstanceOf(Spanned.class);
+ Spanned spanned = (Spanned) text;
+ assertThat(spanned.getSpans(0, spanned.length(), Object.class))
+ .hasLength(2);
+ assertThat(spanned.getSpans(0, 4, UnderlineSpan.class)).hasLength(1);
+ assertThat(spanned.getSpans(4, spanned.length(), ForegroundColorSpan.class))
+ .hasLength(1);
+ });
+
+ onView(withId(com.android.internal.R.id.content_preview_text))
+ .check((view, e) -> {
+ assertThat(view).isInstanceOf(TextView.class);
+ CharSequence text = ((TextView) view).getText();
+ assertThat(text).isInstanceOf(Spanned.class);
+ Spanned spanned = (Spanned) text;
+ assertThat(spanned.getSpans(0, spanned.length(), Object.class))
+ .hasLength(2);
+ assertThat(spanned.getSpans(0, 4, BackgroundColorSpan.class)).hasLength(1);
+ assertThat(spanned.getSpans(4, spanned.length(), StyleSpan.class)).hasLength(1);
+ });
+ }
+
+ @Test
public void emptyPreviewTitleAndThumbnail() throws InterruptedException {
Intent sendIntent = createSendTextIntentWithPreview(null, null);
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -435,7 +396,7 @@ public class UnbundledChooserActivityTest {
String previewTitle = "My Content Preview Title";
Uri uri = Uri.parse(
"android.resource://com.android.frameworks.coretests/"
- + R.drawable.test320x240);
+ + com.android.intentresolver.tests.R.drawable.test320x240);
Intent sendIntent = createSendTextIntentWithPreview(previewTitle, uri);
ChooserActivityOverrideData.getInstance().imageLoader =
createImageLoader(uri, createBitmap());
@@ -547,8 +508,8 @@ public class UnbundledChooserActivityTest {
};
ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0);
DisplayResolveInfo testDri =
- activity.createTestDisplayResolveInfo(sendIntent, toChoose, "testLabel", "testInfo",
- sendIntent, /* resolveInfoPresentationGetter */ null);
+ activity.createTestDisplayResolveInfo(
+ sendIntent, toChoose, "testLabel", "testInfo", sendIntent);
onView(withText(toChoose.activityInfo.name))
.perform(click());
waitForIdle();
@@ -607,7 +568,7 @@ public class UnbundledChooserActivityTest {
createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
ResolveInfo toChoose = personalResolvedComponentInfos.get(1).getResolveInfoAt(0);
Intent sendIntent = createSendTextIntent();
@@ -899,15 +860,16 @@ public class UnbundledChooserActivityTest {
setupResolverControllers(resolvedComponentInfos);
- final IChooserWrapper activity = (IChooserWrapper)
+ ChooserWrapperActivity activity =
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
onView(withId(R.id.copy)).check(matches(isDisplayed()));
onView(withId(R.id.copy)).perform(click());
-
- EventLog logger = activity.getEventLog();
- verify(logger, times(1)).logActionSelected(eq(EventLog.SELECTION_TYPE_COPY));
+ FakeEventLog eventLog = getEventLog(activity);
+ assertThat(eventLog.getActionSelected())
+ .isEqualTo(new FakeEventLog.ActionSelected(
+ /* targetType = */ EventLog.SELECTION_TYPE_COPY));
}
@Test
@@ -918,8 +880,7 @@ public class UnbundledChooserActivityTest {
setupResolverControllers(resolvedComponentInfos);
- final IChooserWrapper activity = (IChooserWrapper)
- mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
onView(withId(com.android.internal.R.id.chooser_nearby_button))
@@ -942,8 +903,7 @@ public class UnbundledChooserActivityTest {
setupResolverControllers(resolvedComponentInfos);
- final IChooserWrapper activity = (IChooserWrapper)
- mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
onView(withId(com.android.internal.R.id.chooser_edit_button)).check(matches(isDisplayed()));
@@ -1009,10 +969,10 @@ public class UnbundledChooserActivityTest {
.check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)));
}
- @Test
- public void testSlowUriMetadata_fallbackToFilePreview() throws InterruptedException {
+ @Test(timeout = 4_000)
+ public void testSlowUriMetadata_fallbackToFilePreview() {
Uri uri = createTestContentProviderUri(
- "application/pdf", "image/png", /*streamTypeTimeout=*/4_000);
+ "application/pdf", "image/png", /*streamTypeTimeout=*/8_000);
ArrayList<Uri> uris = new ArrayList<>(1);
uris.add(uri);
Intent sendIntent = createSendUriIntentWithPreview(uris);
@@ -1022,8 +982,9 @@ public class UnbundledChooserActivityTest {
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
setupResolverControllers(resolvedComponentInfos);
- assertThat(launchActivityWithTimeout(Intent.createChooser(sendIntent, null), 2_000))
- .isTrue();
+ // The preview type resolution is expected to timeout and default to file preview, otherwise
+ // the test should timeout.
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed()));
@@ -1031,11 +992,10 @@ public class UnbundledChooserActivityTest {
onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed()));
}
- @Test
- public void testSendManyFilesWithSmallMetadataDelayAndOneImage_fallbackToFilePreviewUi()
- throws InterruptedException {
+ @Test(timeout = 4_000)
+ public void testSendManyFilesWithSmallMetadataDelayAndOneImage_fallbackToFilePreviewUi() {
Uri fileUri = createTestContentProviderUri(
- "application/pdf", "application/pdf", /*streamTypeTimeout=*/150);
+ "application/pdf", "application/pdf", /*streamTypeTimeout=*/300);
Uri imageUri = createTestContentProviderUri("application/pdf", "image/png");
ArrayList<Uri> uris = new ArrayList<>(50);
for (int i = 0; i < 49; i++) {
@@ -1048,8 +1008,9 @@ public class UnbundledChooserActivityTest {
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
setupResolverControllers(resolvedComponentInfos);
- assertThat(launchActivityWithTimeout(Intent.createChooser(sendIntent, null), 2_000))
- .isTrue();
+ // The preview type resolution is expected to timeout and default to file preview, otherwise
+ // the test should timeout.
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
@@ -1094,15 +1055,14 @@ public class UnbundledChooserActivityTest {
});
}
- @Test
- public void testPartiallyLoadedMetadata_previewIsShownForTheLoadedPart()
- throws InterruptedException {
+ @Test(timeout = 4_000)
+ public void testPartiallyLoadedMetadata_previewIsShownForTheLoadedPart() {
Uri imgOneUri = createTestContentProviderUri("image/png", null);
Uri imgTwoUri = createTestContentProviderUri("image/png", null)
.buildUpon()
.path("image-2.png")
.build();
- Uri docUri = createTestContentProviderUri("application/pdf", "image/png", 3_000);
+ Uri docUri = createTestContentProviderUri("application/pdf", "image/png", 8_000);
ArrayList<Uri> uris = new ArrayList<>(2);
// two large previews to fill the screen and be presented right away and one
// document that would be delayed by the URI metadata reading
@@ -1121,8 +1081,11 @@ public class UnbundledChooserActivityTest {
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
setupResolverControllers(resolvedComponentInfos);
- assertThat(launchActivityWithTimeout(Intent.createChooser(sendIntent, null), 1_000))
- .isTrue();
+ // the preview type is expected to be resolved quickly based on the first provided URI
+ // metadata. If, instead, it is dependent on the third URI metadata, the test should either
+ // timeout or (more probably due to inner timeout) default to file preview type; anyway the
+ // test will fail.
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
onView(withId(R.id.scrollable_image_preview))
@@ -1174,6 +1137,50 @@ public class UnbundledChooserActivityTest {
}
@Test
+ public void test_shareImageWithRichText_RichTextIsDisplayed() {
+ final Uri uri = createTestContentProviderUri("image/png", null);
+ final CharSequence sharedText = new SpannableStringBuilder()
+ .append(
+ "text-",
+ new StyleSpan(Typeface.BOLD_ITALIC),
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
+ .append(
+ Long.toString(System.currentTimeMillis()),
+ new ForegroundColorSpan(Color.RED),
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createBitmap());
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withText(sharedText.toString()))
+ .check(matches(isDisplayed()))
+ .check((view, e) -> {
+ if (e != null) {
+ throw e;
+ }
+ assertThat(view).isInstanceOf(TextView.class);
+ CharSequence text = ((TextView) view).getText();
+ assertThat(text).isInstanceOf(Spanned.class);
+ Spanned spanned = (Spanned) text;
+ Object[] spans = spanned.getSpans(0, text.length(), Object.class);
+ assertThat(spans).hasLength(2);
+ assertThat(spanned.getSpans(0, 5, StyleSpan.class)).hasLength(1);
+ assertThat(spanned.getSpans(5, text.length(), ForegroundColorSpan.class))
+ .hasLength(1);
+ });
+ }
+
+ @Test
public void testTextPreviewWhenTextIsSharedWithMultipleImages() {
final Uri uri = createTestContentProviderUri("image/png", null);
final String sharedText = "text-" + System.currentTimeMillis();
@@ -1210,12 +1217,15 @@ public class UnbundledChooserActivityTest {
Intent sendIntent = createSendTextIntent();
sendIntent.setType(TEST_MIME_TYPE);
- final IChooserWrapper activity = (IChooserWrapper)
+ ChooserWrapperActivity activity =
mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
- EventLog logger = activity.getEventLog();
waitForIdle();
- verify(logger).logChooserActivityShown(eq(false), eq(TEST_MIME_TYPE), anyLong());
+ FakeEventLog eventLog = getEventLog(activity);
+ FakeEventLog.ChooserActivityShown event = eventLog.getChooserActivityShown();
+ assertThat(event).isNotNull();
+ assertThat(event.isWorkProfile()).isFalse();
+ assertThat(event.getTargetMimeType()).isEqualTo(TEST_MIME_TYPE);
}
@Test
@@ -1225,25 +1235,31 @@ public class UnbundledChooserActivityTest {
ChooserActivityOverrideData.getInstance().alternateProfileSetting =
MetricsEvent.MANAGED_PROFILE;
- final IChooserWrapper activity = (IChooserWrapper)
+ ChooserWrapperActivity activity =
mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
- EventLog logger = activity.getEventLog();
waitForIdle();
- verify(logger).logChooserActivityShown(eq(true), eq(TEST_MIME_TYPE), anyLong());
+ FakeEventLog eventLog = getEventLog(activity);
+ FakeEventLog.ChooserActivityShown event = eventLog.getChooserActivityShown();
+ assertThat(event).isNotNull();
+ assertThat(event.isWorkProfile()).isTrue();
+ assertThat(event.getTargetMimeType()).isEqualTo(TEST_MIME_TYPE);
}
@Test
public void testEmptyPreviewLogging() {
Intent sendIntent = createSendTextIntentWithPreview(null, null);
- final IChooserWrapper activity = (IChooserWrapper)
- mActivityRule.launchActivity(
- Intent.createChooser(sendIntent, "empty preview logger test"));
- EventLog logger = activity.getEventLog();
+ ChooserWrapperActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent,
+ "empty preview logger test"));
waitForIdle();
- verify(logger).logChooserActivityShown(eq(false), eq(null), anyLong());
+ FakeEventLog eventLog = getEventLog(activity);
+ FakeEventLog.ChooserActivityShown event = eventLog.getChooserActivityShown();
+ assertThat(event).isNotNull();
+ assertThat(event.isWorkProfile()).isFalse();
+ assertThat(event.getTargetMimeType()).isNull();
}
@Test
@@ -1254,13 +1270,14 @@ public class UnbundledChooserActivityTest {
setupResolverControllers(resolvedComponentInfos);
- final IChooserWrapper activity = (IChooserWrapper)
+ ChooserWrapperActivity activity =
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
- // Second invocation is from onCreate
- EventLog logger = activity.getEventLog();
- Mockito.verify(logger, times(1)).logActionShareWithPreview(eq(CONTENT_PREVIEW_TEXT));
+ FakeEventLog eventLog = getEventLog(activity);
+ assertThat(eventLog.getActionShareWithPreview())
+ .isEqualTo(new FakeEventLog.ActionShareWithPreview(
+ /* previewType = */ CONTENT_PREVIEW_TEXT));
}
@Test
@@ -1278,11 +1295,14 @@ public class UnbundledChooserActivityTest {
setupResolverControllers(resolvedComponentInfos);
- final IChooserWrapper activity = (IChooserWrapper)
+ ChooserWrapperActivity activity =
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
- EventLog logger = activity.getEventLog();
- Mockito.verify(logger, times(1)).logActionShareWithPreview(eq(CONTENT_PREVIEW_IMAGE));
+
+ FakeEventLog eventLog = getEventLog(activity);
+ assertThat(eventLog.getActionShareWithPreview())
+ .isEqualTo(new FakeEventLog.ActionShareWithPreview(
+ /* previewType = */ CONTENT_PREVIEW_IMAGE));
}
@Test
@@ -1405,8 +1425,7 @@ public class UnbundledChooserActivityTest {
ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE),
"testLabel",
"testInfo",
- sendIntent,
- /* resolveInfoPresentationGetter */ null);
+ sendIntent);
final ChooserListAdapter adapter = activity.getAdapter();
assertThat(adapter.getBaseScore(null, 0), is(CALLER_TARGET_SCORE_BOOST));
@@ -1431,7 +1450,7 @@ public class UnbundledChooserActivityTest {
createShortcutLoaderFactory();
// Start activity
- final IChooserWrapper activity = (IChooserWrapper)
+ ChooserWrapperActivity activity =
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
@@ -1481,22 +1500,15 @@ public class UnbundledChooserActivityTest {
.perform(click());
waitForIdle();
- ArgumentCaptor<HashedStringCache.HashResult> hashCaptor =
- ArgumentCaptor.forClass(HashedStringCache.HashResult.class);
- verify(activity.getEventLog(), times(1)).logShareTargetSelected(
- eq(EventLog.SELECTION_TYPE_SERVICE),
- /* packageName= */ any(),
- /* positionPicked= */ anyInt(),
- /* directTargetAlsoRanked= */ eq(-1),
- /* numCallerProvided= */ anyInt(),
- /* directTargetHashed= */ hashCaptor.capture(),
- /* isPinned= */ anyBoolean(),
- /* successfullySelected= */ anyBoolean(),
- /* selectionCost= */ anyLong());
- String hashedName = hashCaptor.getValue().hashedString;
- assertThat(
- "Hash is not predictable but must be obfuscated",
- hashedName, is(not(name)));
+ FakeEventLog eventLog = getEventLog(activity);
+ assertThat(eventLog.getShareTargetSelected()).hasSize(1);
+ FakeEventLog.ShareTargetSelected call = eventLog.getShareTargetSelected().get(0);
+ assertThat(call.getTargetType()).isEqualTo(EventLog.SELECTION_TYPE_SERVICE);
+ assertThat(call.getDirectTargetAlsoRanked()).isEqualTo(-1);
+ var hashResult = call.getDirectTargetHashed();
+ var hash = hashResult == null ? "" : hashResult.hashedString;
+ assertWithMessage("Hash is not predictable but must be obfuscated")
+ .that(hash).isNotEqualTo(name);
}
// This test is too long and too slow and should not be taken as an example for future tests.
@@ -1512,7 +1524,7 @@ public class UnbundledChooserActivityTest {
createShortcutLoaderFactory();
// Start activity
- final IChooserWrapper activity = (IChooserWrapper)
+ ChooserWrapperActivity activity =
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
@@ -1564,16 +1576,12 @@ public class UnbundledChooserActivityTest {
.perform(click());
waitForIdle();
- verify(activity.getEventLog(), times(1)).logShareTargetSelected(
- eq(EventLog.SELECTION_TYPE_SERVICE),
- /* packageName= */ any(),
- /* positionPicked= */ anyInt(),
- /* directTargetAlsoRanked= */ eq(0),
- /* numCallerProvided= */ anyInt(),
- /* directTargetHashed= */ any(),
- /* isPinned= */ anyBoolean(),
- /* successfullySelected= */ anyBoolean(),
- /* selectionCost= */ anyLong());
+ FakeEventLog eventLog = getEventLog(activity);
+ assertThat(eventLog.getShareTargetSelected()).hasSize(1);
+ FakeEventLog.ShareTargetSelected call = eventLog.getShareTargetSelected().get(0);
+
+ assertThat(call.getTargetType()).isEqualTo(EventLog.SELECTION_TYPE_SERVICE);
+ assertThat(call.getDirectTargetAlsoRanked()).isEqualTo(0);
}
@Test
@@ -1733,7 +1741,7 @@ public class UnbundledChooserActivityTest {
// We need app targets for direct targets to get displayed
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
setupResolverControllers(resolvedComponentInfos, resolvedComponentInfos);
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
// set caller-provided target
Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null);
@@ -1835,11 +1843,13 @@ public class UnbundledChooserActivityTest {
broadcastInvoked.countDown();
}
};
- testContext.registerReceiver(testReceiver, new IntentFilter(testAction));
+ testContext.registerReceiver(testReceiver, new IntentFilter(testAction),
+ Context.RECEIVER_EXPORTED);
try {
onView(withText(customActionLabel)).perform(click());
- broadcastInvoked.await();
+ assertTrue("Timeout waiting for broadcast",
+ broadcastInvoked.await(5000, TimeUnit.MILLISECONDS));
} finally {
testContext.unregisterReceiver(testReceiver);
}
@@ -1875,11 +1885,14 @@ public class UnbundledChooserActivityTest {
broadcastInvoked.countDown();
}
};
- testContext.registerReceiver(testReceiver, new IntentFilter(modifyShareAction));
+ testContext.registerReceiver(testReceiver, new IntentFilter(modifyShareAction),
+ Context.RECEIVER_EXPORTED);
try {
onView(withText(label)).perform(click());
- broadcastInvoked.await();
+ assertTrue("Timeout waiting for broadcast",
+ broadcastInvoked.await(5000, TimeUnit.MILLISECONDS));
+
} finally {
testContext.unregisterReceiver(testReceiver);
}
@@ -1940,19 +1953,18 @@ public class UnbundledChooserActivityTest {
ResolveInfo ri = ResolverDataProvider.createResolveInfo(16, 0, PERSONAL_USER_HANDLE);
// Start activity
- final IChooserWrapper wrapper = (IChooserWrapper)
+ ChooserWrapperActivity activity =
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
// Insert the direct share target
Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
directShareToShortcutInfos.put(serviceTargets.get(0), null);
InstrumentationRegistry.getInstrumentation().runOnMainSync(
- () -> wrapper.getAdapter().addServiceResults(
- wrapper.createTestDisplayResolveInfo(sendIntent,
+ () -> activity.getAdapter().addServiceResults(
+ activity.createTestDisplayResolveInfo(sendIntent,
ri,
"testLabel",
"testInfo",
- sendIntent,
- /* resolveInfoPresentationGetter */ null),
+ sendIntent),
serviceTargets,
TARGET_TYPE_CHOOSER_TARGET,
directShareToShortcutInfos,
@@ -1962,11 +1974,11 @@ public class UnbundledChooserActivityTest {
assertThat(
String.format("Chooser should have %d targets (%d apps, 1 direct, 15 A-Z)",
appTargetsExpected + 16, appTargetsExpected),
- wrapper.getAdapter().getCount(), is(appTargetsExpected + 16));
+ activity.getAdapter().getCount(), is(appTargetsExpected + 16));
assertThat("Chooser should have exactly one selectable direct target",
- wrapper.getAdapter().getSelectableServiceTargetCount(), is(1));
+ activity.getAdapter().getSelectableServiceTargetCount(), is(1));
assertThat("The resolver info must match the resolver info used to create the target",
- wrapper.getAdapter().getItem(0).getResolveInfo(), is(ri));
+ activity.getAdapter().getItem(0).getResolveInfo(), is(ri));
// Click on the direct target
String name = serviceTargets.get(0).getTitle().toString();
@@ -1974,25 +1986,23 @@ public class UnbundledChooserActivityTest {
.perform(click());
waitForIdle();
- EventLog logger = wrapper.getEventLog();
- verify(logger, times(1)).logShareTargetSelected(
- eq(EventLog.SELECTION_TYPE_SERVICE),
- /* packageName= */ any(),
- /* positionPicked= */ anyInt(),
- // The packages sholdn't match for app target and direct target:
- /* directTargetAlsoRanked= */ eq(-1),
- /* numCallerProvided= */ anyInt(),
- /* directTargetHashed= */ any(),
- /* isPinned= */ anyBoolean(),
- /* successfullySelected= */ anyBoolean(),
- /* selectionCost= */ anyLong());
+ FakeEventLog eventLog = getEventLog(activity);
+ var invocations = eventLog.getShareTargetSelected();
+ assertWithMessage("Only one ShareTargetSelected event logged")
+ .that(invocations).hasSize(1);
+ FakeEventLog.ShareTargetSelected call = invocations.get(0);
+ assertWithMessage("targetType should be SELECTION_TYPE_SERVICE")
+ .that(call.getTargetType()).isEqualTo(EventLog.SELECTION_TYPE_SERVICE);
+ assertWithMessage(
+ "The packages shouldn't match for app target and direct target")
+ .that(call.getDirectTargetAlsoRanked()).isEqualTo(-1);
}
@Test
public void testWorkTab_displayedWhenWorkProfileUserAvailable() {
Intent sendIntent = createSendTextIntent();
sendIntent.setType(TEST_MIME_TYPE);
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
waitForIdle();
@@ -2024,7 +2034,7 @@ public class UnbundledChooserActivityTest {
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendTextIntent();
sendIntent.setType(TEST_MIME_TYPE);
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
@@ -2039,7 +2049,7 @@ public class UnbundledChooserActivityTest {
@Test
public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
int workProfileTargets = 4;
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
@@ -2060,7 +2070,7 @@ public class UnbundledChooserActivityTest {
@Test @Ignore
public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
int workProfileTargets = 4;
@@ -2091,7 +2101,7 @@ public class UnbundledChooserActivityTest {
@Test
public void testWorkTab_crossProfileIntentsDisabled_personalToWork_emptyStateShown() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
int workProfileTargets = 4;
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
@@ -2115,7 +2125,7 @@ public class UnbundledChooserActivityTest {
@Test
public void testWorkTab_workProfileDisabled_emptyStateShown() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
int workProfileTargets = 4;
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
@@ -2139,7 +2149,7 @@ public class UnbundledChooserActivityTest {
@Test
public void testWorkTab_noWorkAppsAvailable_emptyStateShown() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTest(3);
List<ResolvedComponentInfo> workResolvedComponentInfos =
@@ -2159,10 +2169,46 @@ public class UnbundledChooserActivityTest {
.check(matches(isDisplayed()));
}
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_SCROLLABLE_PREVIEW)
+ public void testWorkTab_previewIsScrollable() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(300);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+
+ Uri uri = createTestContentProviderUri("image/png", null);
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createWideBitmap());
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Scrollable preview test"));
+ waitForIdle();
+
+ onView(withId(com.android.intentresolver.R.id.scrollable_image_preview))
+ .check(matches(isDisplayed()));
+
+ onView(withId(com.android.internal.R.id.contentPanel)).perform(swipeUp());
+ waitForIdle();
+
+ onView(withId(com.android.intentresolver.R.id.chooser_headline_row_container))
+ .check(matches(isCompletelyDisplayed()));
+ onView(withId(com.android.intentresolver.R.id.headline))
+ .check(matches(isDisplayed()));
+ onView(withId(com.android.intentresolver.R.id.scrollable_image_preview))
+ .check(matches(not(isDisplayed())));
+ }
+
@Ignore // b/220067877
@Test
public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTest(3);
List<ResolvedComponentInfo> workResolvedComponentInfos =
@@ -2186,7 +2232,7 @@ public class UnbundledChooserActivityTest {
@Test
public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTest(3);
List<ResolvedComponentInfo> workResolvedComponentInfos =
@@ -2258,7 +2304,7 @@ public class UnbundledChooserActivityTest {
};
// Start activity
- final IChooserWrapper activity = (IChooserWrapper)
+ ChooserWrapperActivity activity =
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
@@ -2306,18 +2352,10 @@ public class UnbundledChooserActivityTest {
.perform(click());
waitForIdle();
- EventLog logger = activity.getEventLog();
- ArgumentCaptor<Integer> typeCaptor = ArgumentCaptor.forClass(Integer.class);
- verify(logger, times(1)).logShareTargetSelected(
- eq(EventLog.SELECTION_TYPE_SERVICE),
- /* packageName= */ any(),
- /* positionPicked= */ anyInt(),
- /* directTargetAlsoRanked= */ anyInt(),
- /* numCallerProvided= */ anyInt(),
- /* directTargetHashed= */ any(),
- /* isPinned= */ anyBoolean(),
- /* successfullySelected= */ anyBoolean(),
- /* selectionCost= */ anyLong());
+ FakeEventLog eventLog = getEventLog(activity);
+ assertThat(eventLog.getShareTargetSelected()).hasSize(1);
+ FakeEventLog.ShareTargetSelected call = eventLog.getShareTargetSelected().get(0);
+ assertThat(call.getTargetType()).isEqualTo(EventLog.SELECTION_TYPE_SERVICE);
}
@Test
@@ -2420,7 +2458,7 @@ public class UnbundledChooserActivityTest {
@Test @Ignore("b/222124533")
public void testSwitchProfileLogging() throws InterruptedException {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
int workProfileTargets = 4;
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
@@ -2443,7 +2481,7 @@ public class UnbundledChooserActivityTest {
@Test
public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_doesNotAutoLaunch() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
int workProfileTargets = 4;
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10);
@@ -2495,7 +2533,7 @@ public class UnbundledChooserActivityTest {
@Test
public void testWorkTab_withInitialIntents_workTabDoesNotIncludePersonalInitialIntents() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
int workProfileTargets = 1;
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10);
@@ -2525,7 +2563,7 @@ public class UnbundledChooserActivityTest {
@Test
public void testWorkTab_xProfileIntentsDisabled_personalToWork_nonSendIntent_emptyStateShown() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
int workProfileTargets = 4;
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
@@ -2559,7 +2597,7 @@ public class UnbundledChooserActivityTest {
@Test
public void testWorkTab_noWorkAppsAvailable_nonSendIntent_emptyStateShown() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTest(3);
List<ResolvedComponentInfo> workResolvedComponentInfos =
@@ -2620,7 +2658,7 @@ public class UnbundledChooserActivityTest {
@Test
public void test_query_shortcut_loader_for_the_selected_tab() {
- markWorkProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
List<ResolvedComponentInfo> workResolvedComponentInfos =
@@ -2653,12 +2691,12 @@ public class UnbundledChooserActivityTest {
@Test
public void testClonedProfilePresent_personalAdapterIsSetWithPersonalProfile() {
// enable cloneProfile
- markCloneProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true);
List<ResolvedComponentInfo> resolvedComponentInfos =
createResolvedComponentsWithCloneProfileForTest(
3,
PERSONAL_USER_HANDLE,
- ChooserActivityOverrideData.getInstance().cloneProfileUserHandle);
+ CLONE_PROFILE_USER_HANDLE);
setupResolverControllers(resolvedComponentInfos);
Intent sendIntent = createSendTextIntent();
@@ -2672,8 +2710,7 @@ public class UnbundledChooserActivityTest {
@Test
public void testClonedProfilePresent_personalTabUsesExpectedAdapter() {
- markWorkProfileUserAvailable();
- markCloneProfileUserAvailable();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ true);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTest(3);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(
@@ -2714,7 +2751,7 @@ public class UnbundledChooserActivityTest {
final ChooserActivity activity = mActivityRule.launchActivity(
Intent.createChooser(new Intent("ACTION_FOO"), "foo"));
waitForIdle();
- assertThat(activity).isInstanceOf(com.android.intentresolver.ChooserWrapperActivity.class);
+ assertThat(activity).isInstanceOf(ChooserWrapperActivity.class);
}
private ResolveInfo createFakeResolveInfo() {
@@ -2899,35 +2936,6 @@ public class UnbundledChooserActivityTest {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
- private boolean launchActivityWithTimeout(Intent intent, long timeout)
- throws InterruptedException {
- final int initialState = 0;
- final int completedState = 1;
- final int timeoutState = 2;
- final AtomicInteger state = new AtomicInteger(initialState);
- final CountDownLatch cdl = new CountDownLatch(1);
-
- ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
- try {
- executor.execute(() -> {
- mActivityRule.launchActivity(intent);
- state.compareAndSet(initialState, completedState);
- cdl.countDown();
- });
- executor.schedule(
- () -> {
- state.compareAndSet(initialState, timeoutState);
- cdl.countDown();
- },
- timeout,
- TimeUnit.MILLISECONDS);
- cdl.await();
- return state.get() == completedState;
- } finally {
- executor.shutdownNow();
- }
- }
-
private Bitmap createBitmap() {
return createBitmap(200, 200);
}
@@ -2994,12 +3002,19 @@ public class UnbundledChooserActivityTest {
return shortcuts;
}
- private void markWorkProfileUserAvailable() {
- ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(10);
- }
-
- private void markCloneProfileUserAvailable() {
- ChooserActivityOverrideData.getInstance().cloneProfileUserHandle = UserHandle.of(11);
+ private void markOtherProfileAvailability(boolean workAvailable, boolean cloneAvailable) {
+ AnnotatedUserHandles.Builder handles = AnnotatedUserHandles.newBuilder();
+ handles
+ .setUserIdOfCallingApp(1234) // Must be non-negative.
+ .setUserHandleSharesheetLaunchedAs(PERSONAL_USER_HANDLE)
+ .setPersonalProfileUserHandle(PERSONAL_USER_HANDLE);
+ if (workAvailable) {
+ handles.setWorkProfileUserHandle(WORK_PROFILE_USER_HANDLE);
+ }
+ if (cloneAvailable) {
+ handles.setCloneProfileUserHandle(CLONE_PROFILE_USER_HANDLE);
+ }
+ ChooserWrapperActivity.sOverrides.annotatedUserHandles = handles.build();
}
private void setupResolverControllers(
diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java b/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java
index 92bccb7..da879f7 100644
--- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java
+++ b/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java
@@ -64,15 +64,22 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.List;
+import dagger.hilt.android.testing.HiltAndroidRule;
+import dagger.hilt.android.testing.HiltAndroidTest;
+
@DeviceFilter.MediumType
@RunWith(Parameterized.class)
+@HiltAndroidTest
public class UnbundledChooserActivityWorkProfileTest {
private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry
.getInstrumentation().getTargetContext().getUser();
private static final UserHandle WORK_USER_HANDLE = UserHandle.of(10);
- @Rule
+ @Rule(order = 0)
+ public HiltAndroidRule mHiltAndroidRule = new HiltAndroidRule(this);
+
+ @Rule(order = 1)
public ActivityTestRule<ChooserWrapperActivity> mActivityRule =
new ActivityTestRule<>(ChooserWrapperActivity.class, false,
false);
@@ -98,7 +105,6 @@ public class UnbundledChooserActivityWorkProfileTest {
public void testBlocker() {
setUpPersonalAndWorkComponentInfos();
sOverrides.hasCrossProfileIntents = mTestCase.hasCrossProfileIntents();
- sOverrides.tabOwnerUserHandleForLaunch = mTestCase.getMyUserHandle();
launchActivity(mTestCase.getIsSendAction());
switchToTab(mTestCase.getTab());
@@ -261,7 +267,12 @@ public class UnbundledChooserActivityWorkProfileTest {
}
private void setUpPersonalAndWorkComponentInfos() {
- markWorkProfileUserAvailable();
+ ChooserWrapperActivity.sOverrides.annotatedUserHandles = AnnotatedUserHandles.newBuilder()
+ .setUserIdOfCallingApp(1234) // Must be non-negative.
+ .setUserHandleSharesheetLaunchedAs(mTestCase.getMyUserHandle())
+ .setPersonalProfileUserHandle(PERSONAL_USER_HANDLE)
+ .setWorkProfileUserHandle(WORK_USER_HANDLE)
+ .build();
int workProfileTargets = 4;
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTestWithOtherProfile(3,
@@ -301,10 +312,6 @@ public class UnbundledChooserActivityWorkProfileTest {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
- private void markWorkProfileUserAvailable() {
- ChooserWrapperActivity.sOverrides.workProfileUserHandle = WORK_USER_HANDLE;
- }
-
private void assertCantAccessWorkAppsBlockerDisplayed() {
onView(withText(R.string.resolver_cross_profile_blocked))
.check(matches(isDisplayed()));
diff --git a/tests/activity/src/com/android/intentresolver/logging/TestEventLogModule.kt b/tests/activity/src/com/android/intentresolver/logging/TestEventLogModule.kt
new file mode 100644
index 0000000..cd808af
--- /dev/null
+++ b/tests/activity/src/com/android/intentresolver/logging/TestEventLogModule.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.logging
+
+import com.android.internal.logging.InstanceId
+import com.android.internal.logging.InstanceIdSequence
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.android.components.ActivityComponent
+import dagger.hilt.android.scopes.ActivityScoped
+import dagger.hilt.testing.TestInstallIn
+
+/** Binds a [FakeEventLog] as [EventLog] in tests. */
+@Module
+@TestInstallIn(components = [ActivityComponent::class], replaces = [EventLogModule::class])
+interface TestEventLogModule {
+
+ @Binds @ActivityScoped fun fakeEventLog(impl: FakeEventLog): EventLog
+
+ companion object {
+ @Provides
+ fun instanceId(sequence: InstanceIdSequence): InstanceId = sequence.newInstanceId()
+ }
+}
diff --git a/tests/activity/src/com/android/intentresolver/v2/ChooserActivityOverrideData.java b/tests/activity/src/com/android/intentresolver/v2/ChooserActivityOverrideData.java
new file mode 100644
index 0000000..32eabbe
--- /dev/null
+++ b/tests/activity/src/com/android/intentresolver/v2/ChooserActivityOverrideData.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.os.UserHandle;
+
+import com.android.intentresolver.AnnotatedUserHandles;
+import com.android.intentresolver.WorkProfileAvailabilityManager;
+import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.contentpreview.ImageLoader;
+import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
+import com.android.intentresolver.shortcuts.ShortcutLoader;
+
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+import kotlin.jvm.functions.Function2;
+
+/**
+ * Singleton providing overrides to be applied by any {@code IChooserWrapper} used in testing.
+ * We cannot directly mock the activity created since instrumentation creates it, so instead we use
+ * this singleton to modify behavior.
+ */
+public class ChooserActivityOverrideData {
+ private static ChooserActivityOverrideData sInstance = null;
+
+ public static ChooserActivityOverrideData getInstance() {
+ if (sInstance == null) {
+ sInstance = new ChooserActivityOverrideData();
+ }
+ return sInstance;
+ }
+
+ @SuppressWarnings("Since15")
+ public Function<PackageManager, PackageManager> createPackageManager;
+ public Function<TargetInfo, Boolean> onSafelyStartInternalCallback;
+ public Function<TargetInfo, Boolean> onSafelyStartCallback;
+ public Function2<UserHandle, Consumer<ShortcutLoader.Result>, ShortcutLoader>
+ shortcutLoaderFactory = (userHandle, callback) -> null;
+ public ChooserActivity.ChooserListController resolverListController;
+ public ChooserActivity.ChooserListController workResolverListController;
+ public Boolean isVoiceInteraction;
+ public Cursor resolverCursor;
+ public boolean resolverForceException;
+ public ImageLoader imageLoader;
+ public int alternateProfileSetting;
+ public Resources resources;
+ public AnnotatedUserHandles annotatedUserHandles;
+ public boolean hasCrossProfileIntents;
+ public boolean isQuietModeEnabled;
+ public Integer myUserId;
+ public WorkProfileAvailabilityManager mWorkProfileAvailability;
+ public CrossProfileIntentsChecker mCrossProfileIntentsChecker;
+ public PackageManager packageManager;
+
+ public void reset() {
+ onSafelyStartInternalCallback = null;
+ isVoiceInteraction = null;
+ createPackageManager = null;
+ imageLoader = null;
+ resolverCursor = null;
+ resolverForceException = false;
+ resolverListController = mock(ChooserActivity.ChooserListController.class);
+ workResolverListController = mock(ChooserActivity.ChooserListController.class);
+ alternateProfileSetting = 0;
+ resources = null;
+ annotatedUserHandles = AnnotatedUserHandles.newBuilder()
+ .setUserIdOfCallingApp(1234) // Must be non-negative.
+ .setUserHandleSharesheetLaunchedAs(UserHandle.SYSTEM)
+ .setPersonalProfileUserHandle(UserHandle.SYSTEM)
+ .build();
+ hasCrossProfileIntents = true;
+ isQuietModeEnabled = false;
+ myUserId = null;
+ packageManager = null;
+ mWorkProfileAvailability = new WorkProfileAvailabilityManager(null, null, null) {
+ @Override
+ public boolean isQuietModeEnabled() {
+ return isQuietModeEnabled;
+ }
+
+ @Override
+ public boolean isWorkProfileUserUnlocked() {
+ return true;
+ }
+
+ @Override
+ public void requestQuietModeEnabled(boolean enabled) {
+ isQuietModeEnabled = enabled;
+ }
+
+ @Override
+ public void markWorkProfileEnabledBroadcastReceived() {}
+
+ @Override
+ public boolean isWaitingToEnableWorkProfile() {
+ return false;
+ }
+ };
+ shortcutLoaderFactory = ((userHandle, resultConsumer) -> null);
+
+ mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class);
+ when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt()))
+ .thenAnswer(invocation -> hasCrossProfileIntents);
+ }
+
+ private ChooserActivityOverrideData() {}
+}
+
diff --git a/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java b/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java
new file mode 100644
index 0000000..a7930f8
--- /dev/null
+++ b/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * 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.intentresolver.v2;
+
+import android.annotation.Nullable;
+import android.app.prediction.AppPredictor;
+import android.app.usage.UsageStatsManager;
+import android.content.ContentResolver;
+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.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.UserHandle;
+
+import androidx.lifecycle.ViewModelProvider;
+
+import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.IChooserWrapper;
+import com.android.intentresolver.ResolverListController;
+import com.android.intentresolver.TestContentPreviewViewModel;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
+import com.android.intentresolver.grid.ChooserGridAdapter;
+import com.android.intentresolver.icons.TargetDataLoader;
+import com.android.intentresolver.shortcuts.ShortcutLoader;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Simple wrapper around chooser activity to be able to initiate it under test. For more
+ * information, see {@code com.android.internal.app.ChooserWrapperActivity}.
+ */
+public class ChooserWrapperActivity extends ChooserActivity implements IChooserWrapper {
+ static final ChooserActivityOverrideData sOverrides = ChooserActivityOverrideData.getInstance();
+ private UsageStatsManager mUsm;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setLogic(new TestChooserActivityLogic(
+ "ChooserWrapper",
+ () -> this,
+ this::onWorkProfileStatusUpdated,
+ () -> mTargetDataLoader,
+ this::onPreinitialization,
+ sOverrides));
+ }
+
+ // ResolverActivity (the base class of ChooserActivity) inspects the launched-from UID at
+ // onCreate and needs to see some non-negative value in the test.
+ @Override
+ public int getLaunchedFromUid() {
+ return 1234;
+ }
+
+ @Override
+ public ChooserListAdapter createChooserListAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ ResolverListController resolverListController,
+ UserHandle userHandle,
+ Intent targetIntent,
+ Intent referrerFillInIntent,
+ int maxTargetsPerRow,
+ TargetDataLoader targetDataLoader) {
+ PackageManager packageManager =
+ sOverrides.packageManager == null ? context.getPackageManager()
+ : sOverrides.packageManager;
+ return new ChooserListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ createListController(userHandle),
+ userHandle,
+ targetIntent,
+ referrerFillInIntent,
+ this,
+ packageManager,
+ getEventLog(),
+ maxTargetsPerRow,
+ userHandle,
+ targetDataLoader,
+ null);
+ }
+
+ @Override
+ public ChooserListAdapter getAdapter() {
+ return mChooserMultiProfilePagerAdapter.getActiveListAdapter();
+ }
+
+ @Override
+ public ChooserListAdapter getPersonalListAdapter() {
+ return ((ChooserGridAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(0))
+ .getListAdapter();
+ }
+
+ @Override
+ public ChooserListAdapter getWorkListAdapter() {
+ if (mMultiProfilePagerAdapter.getInactiveListAdapter() == null) {
+ return null;
+ }
+ return ((ChooserGridAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(1))
+ .getListAdapter();
+ }
+
+ @Override
+ public boolean getIsSelected() {
+ return mIsSuccessfullySelected;
+ }
+
+ @Override
+ public UsageStatsManager getUsageStatsManager() {
+ if (mUsm == null) {
+ mUsm = getSystemService(UsageStatsManager.class);
+ }
+ return mUsm;
+ }
+
+ @Override
+ public boolean isVoiceInteraction() {
+ if (sOverrides.isVoiceInteraction != null) {
+ return sOverrides.isVoiceInteraction;
+ }
+ return super.isVoiceInteraction();
+ }
+
+ @Override
+ protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() {
+ if (sOverrides.mCrossProfileIntentsChecker != null) {
+ return sOverrides.mCrossProfileIntentsChecker;
+ }
+ return super.createCrossProfileIntentsChecker();
+ }
+
+ @Override
+ public void safelyStartActivityInternal(TargetInfo cti, UserHandle user,
+ @Nullable Bundle options) {
+ if (sOverrides.onSafelyStartInternalCallback != null
+ && sOverrides.onSafelyStartInternalCallback.apply(cti)) {
+ return;
+ }
+ super.safelyStartActivityInternal(cti, user, options);
+ }
+
+ @Override
+ protected ChooserListController createListController(UserHandle userHandle) {
+ if (userHandle == UserHandle.SYSTEM) {
+ return sOverrides.resolverListController;
+ }
+ return sOverrides.workResolverListController;
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ if (sOverrides.createPackageManager != null) {
+ return sOverrides.createPackageManager.apply(super.getPackageManager());
+ }
+ return super.getPackageManager();
+ }
+
+ @Override
+ public Resources getResources() {
+ if (sOverrides.resources != null) {
+ return sOverrides.resources;
+ }
+ return super.getResources();
+ }
+
+ @Override
+ protected ViewModelProvider.Factory createPreviewViewModelFactory() {
+ return TestContentPreviewViewModel.Companion.wrap(
+ super.createPreviewViewModelFactory(),
+ sOverrides.imageLoader);
+ }
+
+ @Override
+ public Cursor queryResolver(ContentResolver resolver, Uri uri) {
+ if (sOverrides.resolverCursor != null) {
+ return sOverrides.resolverCursor;
+ }
+
+ if (sOverrides.resolverForceException) {
+ throw new SecurityException("Test exception handling");
+ }
+
+ return super.queryResolver(resolver, uri);
+ }
+
+ @Override
+ protected boolean isWorkProfile() {
+ if (sOverrides.alternateProfileSetting != 0) {
+ return sOverrides.alternateProfileSetting == MetricsEvent.MANAGED_PROFILE;
+ }
+ return super.isWorkProfile();
+ }
+
+ @Override
+ public DisplayResolveInfo createTestDisplayResolveInfo(
+ Intent originalIntent,
+ ResolveInfo pri,
+ CharSequence pLabel,
+ CharSequence pInfo,
+ Intent replacementIntent) {
+ return DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent,
+ pri,
+ pLabel,
+ pInfo,
+ replacementIntent);
+ }
+
+ @Override
+ public UserHandle getCurrentUserHandle() {
+ return mMultiProfilePagerAdapter.getCurrentUserHandle();
+ }
+
+ @Override
+ public Context createContextAsUser(UserHandle user, int flags) {
+ // return the current context as a work profile doesn't really exist in these tests
+ return this;
+ }
+
+ @Override
+ protected ShortcutLoader createShortcutLoader(
+ Context context,
+ AppPredictor appPredictor,
+ UserHandle userHandle,
+ IntentFilter targetIntentFilter,
+ Consumer<ShortcutLoader.Result> callback) {
+ ShortcutLoader shortcutLoader =
+ sOverrides.shortcutLoaderFactory.invoke(userHandle, callback);
+ if (shortcutLoader != null) {
+ return shortcutLoader;
+ }
+ return super.createShortcutLoader(
+ context, appPredictor, userHandle, targetIntentFilter, callback);
+ }
+}
diff --git a/tests/activity/src/com/android/intentresolver/v2/ResolverActivityTest.java b/tests/activity/src/com/android/intentresolver/v2/ResolverActivityTest.java
new file mode 100644
index 0000000..f091183
--- /dev/null
+++ b/tests/activity/src/com/android/intentresolver/v2/ResolverActivityTest.java
@@ -0,0 +1,1105 @@
+/*
+ * Copyright (C) 2016 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.intentresolver.v2;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.action.ViewActions.swipeUp;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+import static com.android.intentresolver.MatcherUtils.first;
+import static com.android.intentresolver.v2.ResolverWrapperActivity.sOverrides;
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.espresso.Espresso;
+import androidx.test.espresso.NoMatchingViewException;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.intentresolver.AnnotatedUserHandles;
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolvedComponentInfo;
+import com.android.intentresolver.ResolverDataProvider;
+import com.android.intentresolver.widget.ResolverDrawerLayout;
+import com.google.android.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.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Resolver activity instrumentation tests
+ */
+@RunWith(AndroidJUnit4.class)
+public class ResolverActivityTest {
+
+ private static final UserHandle PERSONAL_USER_HANDLE = androidx.test.platform.app
+ .InstrumentationRegistry.getInstrumentation().getTargetContext().getUser();
+ private static final UserHandle WORK_PROFILE_USER_HANDLE = UserHandle.of(10);
+ private static final UserHandle CLONE_PROFILE_USER_HANDLE = UserHandle.of(11);
+
+ protected Intent getConcreteIntentForLaunch(Intent clientIntent) {
+ clientIntent.setClass(
+ androidx.test.platform.app.InstrumentationRegistry.getInstrumentation().getTargetContext(),
+ ResolverWrapperActivity.class);
+ return clientIntent;
+ }
+
+ @Rule
+ public ActivityTestRule<ResolverWrapperActivity> mActivityRule =
+ new ActivityTestRule<>(ResolverWrapperActivity.class, false, false);
+
+ @Before
+ public void setup() {
+ // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the
+ // permissions we require (which we'll read from the manifest at runtime).
+ androidx.test.platform.app.InstrumentationRegistry
+ .getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity();
+
+ sOverrides.reset();
+ }
+
+ @Test
+ public void twoOptionsAndUserSelectsOne() throws InterruptedException {
+ Intent sendIntent = createSendImageIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2,
+ PERSONAL_USER_HANDLE);
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ Espresso.registerIdlingResources(activity.getLabelIdlingResource());
+ waitForIdle();
+
+ assertThat(activity.getAdapter().getCount(), is(2));
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ sOverrides.onSafelyStartInternalCallback = result -> {
+ chosen[0] = result.first.getResolveInfo();
+ return true;
+ };
+
+ ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0);
+ onView(withText(toChoose.activityInfo.name))
+ .perform(click());
+ onView(withId(com.android.internal.R.id.button_once))
+ .perform(click());
+ waitForIdle();
+ assertThat(chosen[0], is(toChoose));
+ }
+
+ @Ignore // Failing - b/144929805
+ @Test
+ public void setMaxHeight() throws Exception {
+ Intent sendIntent = createSendImageIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2,
+ PERSONAL_USER_HANDLE);
+
+ setupResolverControllers(resolvedComponentInfos);
+ waitForIdle();
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ final View viewPager = activity.findViewById(com.android.internal.R.id.profile_pager);
+ final int initialResolverHeight = viewPager.getHeight();
+
+ activity.runOnUiThread(() -> {
+ ResolverDrawerLayout layout = (ResolverDrawerLayout)
+ activity.findViewById(
+ com.android.internal.R.id.contentPanel);
+ ((ResolverDrawerLayout.LayoutParams) viewPager.getLayoutParams()).maxHeight
+ = initialResolverHeight - 1;
+ // Force a relayout
+ layout.invalidate();
+ layout.requestLayout();
+ });
+ waitForIdle();
+ assertThat("Drawer should be capped at maxHeight",
+ viewPager.getHeight() == (initialResolverHeight - 1));
+
+ activity.runOnUiThread(() -> {
+ ResolverDrawerLayout layout = (ResolverDrawerLayout)
+ activity.findViewById(
+ com.android.internal.R.id.contentPanel);
+ ((ResolverDrawerLayout.LayoutParams) viewPager.getLayoutParams()).maxHeight
+ = initialResolverHeight + 1;
+ // Force a relayout
+ layout.invalidate();
+ layout.requestLayout();
+ });
+ waitForIdle();
+ assertThat("Drawer should not change height if its height is less than maxHeight",
+ viewPager.getHeight() == initialResolverHeight);
+ }
+
+ @Ignore // Failing - b/144929805
+ @Test
+ public void setShowAtTopToTrue() throws Exception {
+ Intent sendIntent = createSendImageIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2,
+ PERSONAL_USER_HANDLE);
+
+ setupResolverControllers(resolvedComponentInfos);
+ waitForIdle();
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ final View viewPager = activity.findViewById(com.android.internal.R.id.profile_pager);
+ final View divider = activity.findViewById(com.android.internal.R.id.divider);
+ final RelativeLayout profileView =
+ (RelativeLayout) activity.findViewById(com.android.internal.R.id.profile_button)
+ .getParent();
+ assertThat("Drawer should show at bottom by default",
+ profileView.getBottom() + divider.getHeight() == viewPager.getTop()
+ && profileView.getTop() > 0);
+
+ activity.runOnUiThread(() -> {
+ ResolverDrawerLayout layout = (ResolverDrawerLayout)
+ activity.findViewById(
+ com.android.internal.R.id.contentPanel);
+ layout.setShowAtTop(true);
+ });
+ waitForIdle();
+ assertThat("Drawer should show at top with new attribute",
+ profileView.getBottom() + divider.getHeight() == viewPager.getTop()
+ && profileView.getTop() == 0);
+ }
+
+ @Test
+ public void hasLastChosenActivity() throws Exception {
+ Intent sendIntent = createSendImageIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2,
+ PERSONAL_USER_HANDLE);
+ ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0);
+
+ setupResolverControllers(resolvedComponentInfos);
+ when(sOverrides.resolverListController.getLastChosen())
+ .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0));
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+
+ // The other entry is filtered to the last used slot
+ assertThat(activity.getAdapter().getCount(), is(1));
+ assertThat(activity.getAdapter().getPlaceholderCount(), is(1));
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ sOverrides.onSafelyStartInternalCallback = result -> {
+ chosen[0] = result.first.getResolveInfo();
+ return true;
+ };
+
+ onView(withId(com.android.internal.R.id.button_once)).perform(click());
+ waitForIdle();
+ assertThat(chosen[0], is(toChoose));
+ }
+
+ @Test
+ public void hasOtherProfileOneOption() throws Exception {
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10,
+ PERSONAL_USER_HANDLE);
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+ WORK_PROFILE_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+
+ ResolveInfo toChoose = personalResolvedComponentInfos.get(1).getResolveInfoAt(0);
+ Intent sendIntent = createSendImageIntent();
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ Espresso.registerIdlingResources(activity.getLabelIdlingResource());
+ waitForIdle();
+
+ // The other entry is filtered to the last used slot
+ assertThat(activity.getAdapter().getCount(), is(1));
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ sOverrides.onSafelyStartInternalCallback = result -> {
+ chosen[0] = result.first.getResolveInfo();
+ return true;
+ };
+ // Make a stable copy of the components as the original list may be modified
+ List<ResolvedComponentInfo> stableCopy =
+ createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10,
+ PERSONAL_USER_HANDLE);
+ // We pick the first one as there is another one in the work profile side
+ onView(first(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)))
+ .perform(click());
+ onView(withId(com.android.internal.R.id.button_once))
+ .perform(click());
+ waitForIdle();
+ assertThat(chosen[0], is(toChoose));
+ }
+
+ @Test
+ public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception {
+ Intent sendIntent = createSendImageIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE);
+ ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0);
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ Espresso.registerIdlingResources(activity.getLabelIdlingResource());
+ waitForIdle();
+
+ // The other entry is filtered to the other profile slot
+ assertThat(activity.getAdapter().getCount(), is(2));
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ sOverrides.onSafelyStartInternalCallback = result -> {
+ chosen[0] = result.first.getResolveInfo();
+ return true;
+ };
+
+ // Confirm that the button bar is disabled by default
+ onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled())));
+
+ // Make a stable copy of the components as the original list may be modified
+ List<ResolvedComponentInfo> stableCopy =
+ createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE);
+
+ onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))
+ .perform(click());
+ onView(withId(com.android.internal.R.id.button_once)).perform(click());
+ waitForIdle();
+ assertThat(chosen[0], is(toChoose));
+ }
+
+
+ @Test
+ public void hasLastChosenActivityAndOtherProfile() throws Exception {
+ // In this case we prefer the other profile and don't display anything about the last
+ // chosen activity.
+ Intent sendIntent = createSendImageIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE);
+ ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0);
+
+ setupResolverControllers(resolvedComponentInfos);
+ when(sOverrides.resolverListController.getLastChosen())
+ .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0));
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ Espresso.registerIdlingResources(activity.getLabelIdlingResource());
+ waitForIdle();
+
+ // The other entry is filtered to the other profile slot
+ assertThat(activity.getAdapter().getCount(), is(2));
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ sOverrides.onSafelyStartInternalCallback = result -> {
+ chosen[0] = result.first.getResolveInfo();
+ return true;
+ };
+
+ // Confirm that the button bar is disabled by default
+ onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled())));
+
+ // Make a stable copy of the components as the original list may be modified
+ List<ResolvedComponentInfo> stableCopy =
+ createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE);
+
+ onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))
+ .perform(click());
+ onView(withId(com.android.internal.R.id.button_once)).perform(click());
+ waitForIdle();
+ assertThat(chosen[0], is(toChoose));
+ }
+
+ @Test
+ public void testWorkTab_displayedWhenWorkProfileUserAvailable() {
+ Intent sendIntent = createSendImageIntent();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+
+ mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+
+ onView(withId(com.android.internal.R.id.tabs)).check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testWorkTab_hiddenWhenWorkProfileUserNotAvailable() {
+ Intent sendIntent = createSendImageIntent();
+
+ mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+
+ onView(withId(com.android.internal.R.id.tabs)).check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ public void testWorkTab_workTabListPopulatedBeforeGoingToTab() throws InterruptedException {
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId = */ 10,
+ PERSONAL_USER_HANDLE);
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+ WORK_PROFILE_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos,
+ new ArrayList<>(workResolvedComponentInfos));
+ Intent sendIntent = createSendImageIntent();
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+
+ assertThat(activity.getCurrentUserHandle().getIdentifier(), is(0));
+ // The work list adapter must be populated in advance before tapping the other tab
+ assertThat(activity.getWorkListAdapter().getCount(), is(4));
+ }
+
+ @Test
+ public void testWorkTab_workTabUsesExpectedAdapter() {
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10,
+ PERSONAL_USER_HANDLE);
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+ WORK_PROFILE_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+
+ assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10));
+ assertThat(activity.getWorkListAdapter().getCount(), is(4));
+ }
+
+ @Test
+ public void testWorkTab_personalTabUsesExpectedAdapter() {
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE);
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+ WORK_PROFILE_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+
+ assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10));
+ assertThat(activity.getPersonalListAdapter().getCount(), is(2));
+ }
+
+ @Test
+ public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10,
+ PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+ WORK_PROFILE_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+
+ onView(withText(R.string.resolver_work_tab))
+ .perform(click());
+ waitForIdle();
+ assertThat(activity.getWorkListAdapter().getCount(), is(4));
+ }
+
+ @Test
+ public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() throws InterruptedException {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10,
+ PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+ WORK_PROFILE_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ sOverrides.onSafelyStartInternalCallback = result -> {
+ chosen[0] = result.first.getResolveInfo();
+ return true;
+ };
+
+ mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ onView(withText(R.string.resolver_work_tab))
+ .perform(click());
+ waitForIdle();
+ onView(first(allOf(withText(workResolvedComponentInfos.get(0)
+ .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed())))
+ .perform(click());
+ onView(withId(com.android.internal.R.id.button_once))
+ .perform(click());
+
+ waitForIdle();
+ assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0)));
+ }
+
+ @Test
+ public void testWorkTab_noPersonalApps_workTabHasExpectedNumberOfTargets()
+ throws InterruptedException {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+ WORK_PROFILE_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ onView(withText(R.string.resolver_work_tab))
+ .perform(click());
+
+ waitForIdle();
+ assertThat(activity.getWorkListAdapter().getCount(), is(4));
+ }
+
+ @Test
+ public void testWorkTab_headerIsVisibleInPersonalTab() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+ WORK_PROFILE_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createOpenWebsiteIntent();
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ TextView headerText = activity.findViewById(com.android.internal.R.id.title);
+ String initialText = headerText.getText().toString();
+ assertFalse("Header text is empty.", initialText.isEmpty());
+ assertThat(headerText.getVisibility(), is(View.VISIBLE));
+ }
+
+ @Test
+ public void testWorkTab_switchTabs_headerStaysSame() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(1, PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+ WORK_PROFILE_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createOpenWebsiteIntent();
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ TextView headerText = activity.findViewById(com.android.internal.R.id.title);
+ String initialText = headerText.getText().toString();
+ onView(withText(R.string.resolver_work_tab))
+ .perform(click());
+
+ waitForIdle();
+ String currentText = headerText.getText().toString();
+ assertThat(headerText.getVisibility(), is(View.VISIBLE));
+ assertThat(String.format("Header text is not the same when switching tabs, personal profile"
+ + " header was %s but work profile header is %s", initialText, currentText),
+ TextUtils.equals(initialText, currentText));
+ }
+
+ @Test
+ public void testWorkTab_noPersonalApps_canStartWorkApps()
+ throws InterruptedException {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId= */ 10,
+ PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+ WORK_PROFILE_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ sOverrides.onSafelyStartInternalCallback = result -> {
+ chosen[0] = result.first.getResolveInfo();
+ return true;
+ };
+
+ mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ onView(withText(R.string.resolver_work_tab))
+ .perform(click());
+ waitForIdle();
+ onView(first(allOf(
+ withText(workResolvedComponentInfos.get(0)
+ .getResolveInfoAt(0).activityInfo.applicationInfo.name),
+ isDisplayed())))
+ .perform(click());
+ onView(withId(com.android.internal.R.id.button_once))
+ .perform(click());
+ waitForIdle();
+
+ assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0)));
+ }
+
+ @Test
+ public void testWorkTab_crossProfileIntentsDisabled_personalToWork_emptyStateShown() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10,
+ PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets, WORK_PROFILE_USER_HANDLE);
+ sOverrides.hasCrossProfileIntents = false;
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+ sendIntent.setType("TestType");
+
+ mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.contentPanel))
+ .perform(swipeUp());
+
+ onView(withText(R.string.resolver_cross_profile_blocked))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testWorkTab_workProfileDisabled_emptyStateShown() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10,
+ PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets, WORK_PROFILE_USER_HANDLE);
+ sOverrides.isQuietModeEnabled = true;
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+ sendIntent.setType("TestType");
+
+ mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.contentPanel))
+ .perform(swipeUp());
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+
+ onView(withText(R.string.resolver_turn_on_work_apps))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testWorkTab_noWorkAppsAvailable_emptyStateShown() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(0, WORK_PROFILE_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+ sendIntent.setType("TestType");
+
+ mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.contentPanel))
+ .perform(swipeUp());
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+
+ onView(withText(R.string.resolver_no_work_apps_available))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(0, WORK_PROFILE_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+ sendIntent.setType("TestType");
+ sOverrides.isQuietModeEnabled = true;
+ sOverrides.hasCrossProfileIntents = false;
+
+ mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.contentPanel))
+ .perform(swipeUp());
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+
+ onView(withText(R.string.resolver_cross_profile_blocked))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testMiniResolver() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(1, PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(1, WORK_PROFILE_USER_HANDLE);
+ // Personal profile only has a browser
+ personalResolvedComponentInfos.get(0).getResolveInfoAt(0).handleAllWebDataURI = true;
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+ sendIntent.setType("TestType");
+
+ mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.open_cross_profile)).check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testMiniResolver_noCurrentProfileTarget() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(0, PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(1, WORK_PROFILE_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+ sendIntent.setType("TestType");
+
+ mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+
+ // Need to ensure mini resolver doesn't trigger here.
+ assertNotMiniResolver();
+ }
+
+ private void assertNotMiniResolver() {
+ try {
+ onView(withId(com.android.internal.R.id.open_cross_profile))
+ .check(matches(isDisplayed()));
+ } catch (NoMatchingViewException e) {
+ return;
+ }
+ fail("Mini resolver present but shouldn't be");
+ }
+
+ @Test
+ public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(3, PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(0, WORK_PROFILE_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+ sendIntent.setType("TestType");
+ sOverrides.isQuietModeEnabled = true;
+
+ mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.contentPanel))
+ .perform(swipeUp());
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+
+ onView(withText(R.string.resolver_no_work_apps_available))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_doesNotAutoLaunch() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10,
+ PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets, WORK_PROFILE_USER_HANDLE);
+ sOverrides.hasCrossProfileIntents = false;
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+ sendIntent.setType("TestType");
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ sOverrides.onSafelyStartInternalCallback = result -> {
+ chosen[0] = result.first.getResolveInfo();
+ return true;
+ };
+
+ mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+
+ assertNull(chosen[0]);
+ }
+
+ @Test
+ public void testLayoutWithDefault_withWorkTab_neverShown() throws RemoteException {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+
+ // In this case we prefer the other profile and don't display anything about the last
+ // chosen activity.
+ Intent sendIntent = createSendImageIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos =
+ createResolvedComponentsForTest(2, PERSONAL_USER_HANDLE);
+
+ setupResolverControllers(resolvedComponentInfos);
+ when(sOverrides.resolverListController.getLastChosen())
+ .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0));
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ Espresso.registerIdlingResources(activity.getLabelIdlingResource());
+ waitForIdle();
+
+ // The other entry is filtered to the last used slot
+ assertThat(activity.getAdapter().hasFilteredItem(), is(false));
+ assertThat(activity.getAdapter().getCount(), is(2));
+ assertThat(activity.getAdapter().getPlaceholderCount(), is(2));
+ }
+
+ @Test
+ public void testClonedProfilePresent_personalAdapterIsSetWithPersonalProfile() {
+ // enable cloneProfile
+ markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true);
+ List<ResolvedComponentInfo> resolvedComponentInfos =
+ createResolvedComponentsWithCloneProfileForTest(
+ 3,
+ PERSONAL_USER_HANDLE,
+ CLONE_PROFILE_USER_HANDLE);
+ setupResolverControllers(resolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+
+ assertThat(activity.getCurrentUserHandle(), is(PERSONAL_USER_HANDLE));
+ assertThat(activity.getAdapter().getCount(), is(3));
+ }
+
+ @Test
+ public void testClonedProfilePresent_personalTabUsesExpectedAdapter() {
+ // enable cloneProfile
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ true);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsWithCloneProfileForTest(
+ 3,
+ PERSONAL_USER_HANDLE,
+ CLONE_PROFILE_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+ WORK_PROFILE_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+
+ assertThat(activity.getCurrentUserHandle(), is(PERSONAL_USER_HANDLE));
+ assertThat(activity.getAdapter().getCount(), is(3));
+ }
+
+ @Test
+ public void testClonedProfilePresent_layoutWithDefault_neverShown() throws Exception {
+ // enable cloneProfile
+ markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true);
+ Intent sendIntent = createSendImageIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos =
+ createResolvedComponentsWithCloneProfileForTest(
+ 2,
+ PERSONAL_USER_HANDLE,
+ CLONE_PROFILE_USER_HANDLE);
+
+ setupResolverControllers(resolvedComponentInfos);
+ when(sOverrides.resolverListController.getLastChosen())
+ .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0));
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ Espresso.registerIdlingResources(activity.getLabelIdlingResource());
+ waitForIdle();
+
+ assertThat(activity.getAdapter().hasFilteredItem(), is(false));
+ assertThat(activity.getAdapter().getCount(), is(2));
+ assertThat(activity.getAdapter().getPlaceholderCount(), is(2));
+ }
+
+ @Test
+ public void testClonedProfilePresent_alwaysButtonDisabled() throws Exception {
+ // enable cloneProfile
+ markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true);
+ Intent sendIntent = createSendImageIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos =
+ createResolvedComponentsWithCloneProfileForTest(
+ 3,
+ PERSONAL_USER_HANDLE,
+ CLONE_PROFILE_USER_HANDLE);
+
+ setupResolverControllers(resolvedComponentInfos);
+ when(sOverrides.resolverListController.getLastChosen())
+ .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0));
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+
+ // Confirm that the button bar is disabled by default
+ onView(withId(com.android.internal.R.id.button_once)).check(matches(not(isEnabled())));
+ onView(withId(com.android.internal.R.id.button_always)).check(matches(not(isEnabled())));
+
+ // Make a stable copy of the components as the original list may be modified
+ List<ResolvedComponentInfo> stableCopy =
+ createResolvedComponentsForTestWithOtherProfile(2, PERSONAL_USER_HANDLE);
+
+ onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))
+ .perform(click());
+
+ onView(withId(com.android.internal.R.id.button_once)).check(matches(isEnabled()));
+ onView(withId(com.android.internal.R.id.button_always)).check(matches(not(isEnabled())));
+ }
+
+ @Test
+ public void testClonedProfilePresent_personalProfileActivityIsStartedInCorrectUser()
+ throws Exception {
+ // enable cloneProfile
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ true);
+
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsWithCloneProfileForTest(
+ 3,
+ PERSONAL_USER_HANDLE,
+ CLONE_PROFILE_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(3, WORK_PROFILE_USER_HANDLE);
+ sOverrides.hasCrossProfileIntents = false;
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+ sendIntent.setType("TestType");
+ final UserHandle[] selectedActivityUserHandle = new UserHandle[1];
+ sOverrides.onSafelyStartInternalCallback = result -> {
+ selectedActivityUserHandle[0] = result.second;
+ return true;
+ };
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ onView(first(allOf(withText(personalResolvedComponentInfos.get(0)
+ .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed())))
+ .perform(click());
+ onView(withId(com.android.internal.R.id.button_once))
+ .perform(click());
+ waitForIdle();
+
+ assertThat(selectedActivityUserHandle[0], is(activity.getAdapter().getUserHandle()));
+ }
+
+ @Test
+ public void testClonedProfilePresent_workProfileActivityIsStartedInCorrectUser()
+ throws Exception {
+ // enable cloneProfile
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ true);
+
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsWithCloneProfileForTest(
+ 3,
+ PERSONAL_USER_HANDLE,
+ CLONE_PROFILE_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(3, WORK_PROFILE_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+ sendIntent.setType("TestType");
+ final UserHandle[] selectedActivityUserHandle = new UserHandle[1];
+ sOverrides.onSafelyStartInternalCallback = result -> {
+ selectedActivityUserHandle[0] = result.second;
+ return true;
+ };
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ onView(withText(R.string.resolver_work_tab))
+ .perform(click());
+ waitForIdle();
+ onView(first(allOf(withText(workResolvedComponentInfos.get(0)
+ .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed())))
+ .perform(click());
+ onView(withId(com.android.internal.R.id.button_once))
+ .perform(click());
+ waitForIdle();
+
+ assertThat(selectedActivityUserHandle[0], is(activity.getAdapter().getUserHandle()));
+ }
+
+ @Test
+ public void testClonedProfilePresent_personalProfileResolverComparatorHasCorrectUsers()
+ throws Exception {
+ // enable cloneProfile
+ markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true);
+ List<ResolvedComponentInfo> resolvedComponentInfos =
+ createResolvedComponentsWithCloneProfileForTest(
+ 3,
+ PERSONAL_USER_HANDLE,
+ CLONE_PROFILE_USER_HANDLE);
+ setupResolverControllers(resolvedComponentInfos);
+ Intent sendIntent = createSendImageIntent();
+
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ List<UserHandle> result = activity
+ .getResolverRankerServiceUserHandleList(PERSONAL_USER_HANDLE);
+
+ assertThat(result.containsAll(
+ Lists.newArrayList(PERSONAL_USER_HANDLE, CLONE_PROFILE_USER_HANDLE)), is(true));
+ }
+
+ private Intent createSendImageIntent() {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending");
+ sendIntent.setType("image/jpeg");
+ return sendIntent;
+ }
+
+ private Intent createOpenWebsiteIntent() {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_VIEW);
+ sendIntent.setData(Uri.parse("https://google.com"));
+ return sendIntent;
+ }
+
+ private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults,
+ UserHandle resolvedForUser) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < numberOfResults; i++) {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser));
+ }
+ return infoList;
+ }
+
+ private List<ResolvedComponentInfo> createResolvedComponentsWithCloneProfileForTest(
+ int numberOfResults,
+ UserHandle resolvedForPersonalUser,
+ UserHandle resolvedForClonedUser) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < 1; i++) {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfo(i,
+ resolvedForPersonalUser));
+ }
+ for (int i = 1; i < numberOfResults; i++) {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfo(i,
+ resolvedForClonedUser));
+ }
+ return infoList;
+ }
+
+ private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile(
+ int numberOfResults,
+ UserHandle resolvedForUser) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < numberOfResults; i++) {
+ if (i == 0) {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i,
+ resolvedForUser));
+ } else {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser));
+ }
+ }
+ return infoList;
+ }
+
+ private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile(
+ int numberOfResults, int userId, UserHandle resolvedForUser) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < numberOfResults; i++) {
+ if (i == 0) {
+ infoList.add(
+ ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId,
+ resolvedForUser));
+ } else {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser));
+ }
+ }
+ return infoList;
+ }
+
+ private void waitForIdle() {
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
+
+ private void markOtherProfileAvailability(boolean workAvailable, boolean cloneAvailable) {
+ AnnotatedUserHandles.Builder handles = AnnotatedUserHandles.newBuilder();
+ handles
+ .setUserIdOfCallingApp(1234) // Must be non-negative.
+ .setUserHandleSharesheetLaunchedAs(PERSONAL_USER_HANDLE)
+ .setPersonalProfileUserHandle(PERSONAL_USER_HANDLE);
+ if (workAvailable) {
+ handles.setWorkProfileUserHandle(WORK_PROFILE_USER_HANDLE);
+ }
+ if (cloneAvailable) {
+ handles.setCloneProfileUserHandle(CLONE_PROFILE_USER_HANDLE);
+ }
+ sOverrides.annotatedUserHandles = handles.build();
+ }
+
+ private void setupResolverControllers(
+ List<ResolvedComponentInfo> personalResolvedComponentInfos) {
+ setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>());
+ }
+
+ private void setupResolverControllers(
+ List<ResolvedComponentInfo> personalResolvedComponentInfos,
+ List<ResolvedComponentInfo> workResolvedComponentInfos) {
+ when(sOverrides.resolverListController.getResolversForIntentAsUser(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.SYSTEM)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ when(sOverrides.workResolverListController.getResolversForIntentAsUser(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.SYSTEM)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ when(sOverrides.workResolverListController.getResolversForIntentAsUser(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.of(10))))
+ .thenReturn(new ArrayList<>(workResolvedComponentInfos));
+ }
+}
diff --git a/tests/activity/src/com/android/intentresolver/v2/ResolverWrapperActivity.java b/tests/activity/src/com/android/intentresolver/v2/ResolverWrapperActivity.java
new file mode 100644
index 0000000..7ae5825
--- /dev/null
+++ b/tests/activity/src/com/android/intentresolver/v2/ResolverWrapperActivity.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2017 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.intentresolver.v2;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.Pair;
+
+import androidx.annotation.NonNull;
+import androidx.test.espresso.idling.CountingIdlingResource;
+
+import com.android.intentresolver.AnnotatedUserHandles;
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.ResolverListController;
+import com.android.intentresolver.WorkProfileAvailabilityManager;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.SelectableTargetInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
+import com.android.intentresolver.icons.LabelInfo;
+import com.android.intentresolver.icons.TargetDataLoader;
+
+import kotlin.Unit;
+
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/*
+ * Simple wrapper around chooser activity to be able to initiate it under test
+ */
+public class ResolverWrapperActivity extends ResolverActivity {
+ static final OverrideData sOverrides = new OverrideData();
+
+ private final CountingIdlingResource mLabelIdlingResource =
+ new CountingIdlingResource("LoadLabelTask");
+
+ public ResolverWrapperActivity() {
+ super(/* isIntentPicker= */ true);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setLogic(new TestResolverActivityLogic(
+ "ResolverWrapper",
+ () -> this,
+ () -> {
+ onWorkProfileStatusUpdated();
+ return Unit.INSTANCE;
+ },
+ sOverrides
+ ));
+ }
+
+ public CountingIdlingResource getLabelIdlingResource() {
+ return mLabelIdlingResource;
+ }
+
+ @Override
+ public ResolverListAdapter createResolverListAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ UserHandle userHandle,
+ TargetDataLoader targetDataLoader) {
+ return new ResolverListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ createListController(userHandle),
+ userHandle,
+ payloadIntents.get(0), // TODO: extract upstream
+ this,
+ userHandle,
+ new TargetDataLoaderWrapper(targetDataLoader, mLabelIdlingResource));
+ }
+
+ @Override
+ protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() {
+ if (sOverrides.mCrossProfileIntentsChecker != null) {
+ return sOverrides.mCrossProfileIntentsChecker;
+ }
+ return super.createCrossProfileIntentsChecker();
+ }
+
+ ResolverListAdapter getAdapter() {
+ return mMultiProfilePagerAdapter.getActiveListAdapter();
+ }
+
+ ResolverListAdapter getPersonalListAdapter() {
+ return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(0));
+ }
+
+ ResolverListAdapter getWorkListAdapter() {
+ if (mMultiProfilePagerAdapter.getInactiveListAdapter() == null) {
+ return null;
+ }
+ return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(1));
+ }
+
+ @Override
+ public boolean isVoiceInteraction() {
+ if (sOverrides.isVoiceInteraction != null) {
+ return sOverrides.isVoiceInteraction;
+ }
+ return super.isVoiceInteraction();
+ }
+
+ @Override
+ public void safelyStartActivityInternal(TargetInfo cti, UserHandle user,
+ @Nullable Bundle options) {
+ if (sOverrides.onSafelyStartInternalCallback != null
+ && sOverrides.onSafelyStartInternalCallback.apply(new Pair<>(cti, user))) {
+ return;
+ }
+ super.safelyStartActivityInternal(cti, user, options);
+ }
+
+ @Override
+ protected ResolverListController createListController(UserHandle userHandle) {
+ if (userHandle == UserHandle.SYSTEM) {
+ return sOverrides.resolverListController;
+ }
+ return sOverrides.workResolverListController;
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ if (sOverrides.createPackageManager != null) {
+ return sOverrides.createPackageManager.apply(super.getPackageManager());
+ }
+ return super.getPackageManager();
+ }
+
+ protected UserHandle getCurrentUserHandle() {
+ return mMultiProfilePagerAdapter.getCurrentUserHandle();
+ }
+
+ @Override
+ public void startActivityAsUser(Intent intent, Bundle options, UserHandle user) {
+ super.startActivityAsUser(intent, options, user);
+ }
+
+ @Override
+ protected List<UserHandle> getResolverRankerServiceUserHandleListInternal(UserHandle
+ userHandle) {
+ return super.getResolverRankerServiceUserHandleListInternal(userHandle);
+ }
+
+ /**
+ * We cannot directly mock the activity created since instrumentation creates it.
+ * <p>
+ * Instead, we use static instances of this object to modify behavior.
+ */
+ public static class OverrideData {
+ @SuppressWarnings("Since15")
+ public Function<PackageManager, PackageManager> createPackageManager;
+ public Function<Pair<TargetInfo, UserHandle>, Boolean> onSafelyStartInternalCallback;
+ public ResolverListController resolverListController;
+ public ResolverListController workResolverListController;
+ public Boolean isVoiceInteraction;
+ public AnnotatedUserHandles annotatedUserHandles;
+ public Integer myUserId;
+ public boolean hasCrossProfileIntents;
+ public boolean isQuietModeEnabled;
+ public WorkProfileAvailabilityManager mWorkProfileAvailability;
+ public CrossProfileIntentsChecker mCrossProfileIntentsChecker;
+
+ public void reset() {
+ onSafelyStartInternalCallback = null;
+ isVoiceInteraction = null;
+ createPackageManager = null;
+ resolverListController = mock(ResolverListController.class);
+ workResolverListController = mock(ResolverListController.class);
+ annotatedUserHandles = AnnotatedUserHandles.newBuilder()
+ .setUserIdOfCallingApp(1234) // Must be non-negative.
+ .setUserHandleSharesheetLaunchedAs(UserHandle.SYSTEM)
+ .setPersonalProfileUserHandle(UserHandle.SYSTEM)
+ .build();
+ myUserId = null;
+ hasCrossProfileIntents = true;
+ isQuietModeEnabled = false;
+
+ mWorkProfileAvailability = new WorkProfileAvailabilityManager(null, null, null) {
+ @Override
+ public boolean isQuietModeEnabled() {
+ return isQuietModeEnabled;
+ }
+
+ @Override
+ public boolean isWorkProfileUserUnlocked() {
+ return true;
+ }
+
+ @Override
+ public void requestQuietModeEnabled(boolean enabled) {
+ isQuietModeEnabled = enabled;
+ }
+
+ @Override
+ public void markWorkProfileEnabledBroadcastReceived() {}
+
+ @Override
+ public boolean isWaitingToEnableWorkProfile() {
+ return false;
+ }
+ };
+
+ mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class);
+ when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt()))
+ .thenAnswer(invocation -> hasCrossProfileIntents);
+ }
+ }
+
+ private static class TargetDataLoaderWrapper extends TargetDataLoader {
+ private final TargetDataLoader mTargetDataLoader;
+ private final CountingIdlingResource mLabelIdlingResource;
+
+ private TargetDataLoaderWrapper(
+ TargetDataLoader targetDataLoader, CountingIdlingResource labelIdlingResource) {
+ mTargetDataLoader = targetDataLoader;
+ mLabelIdlingResource = labelIdlingResource;
+ }
+
+ @Override
+ public void loadAppTargetIcon(
+ @NonNull DisplayResolveInfo info,
+ @NonNull UserHandle userHandle,
+ @NonNull Consumer<Drawable> callback) {
+ mTargetDataLoader.loadAppTargetIcon(info, userHandle, callback);
+ }
+
+ @Override
+ public void loadDirectShareIcon(
+ @NonNull SelectableTargetInfo info,
+ @NonNull UserHandle userHandle,
+ @NonNull Consumer<Drawable> callback) {
+ mTargetDataLoader.loadDirectShareIcon(info, userHandle, callback);
+ }
+
+ @Override
+ public void loadLabel(
+ @NonNull DisplayResolveInfo info,
+ @NonNull Consumer<LabelInfo> callback) {
+ mLabelIdlingResource.increment();
+ mTargetDataLoader.loadLabel(
+ info,
+ (result) -> {
+ mLabelIdlingResource.decrement();
+ callback.accept(result);
+ });
+ }
+
+ @Override
+ public void getOrLoadLabel(@NonNull DisplayResolveInfo info) {
+ mTargetDataLoader.getOrLoadLabel(info);
+ }
+ }
+}
diff --git a/tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt b/tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt
new file mode 100644
index 0000000..198b923
--- /dev/null
+++ b/tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt
@@ -0,0 +1,32 @@
+package com.android.intentresolver.v2
+
+import androidx.activity.ComponentActivity
+import com.android.intentresolver.AnnotatedUserHandles
+import com.android.intentresolver.WorkProfileAvailabilityManager
+import com.android.intentresolver.icons.TargetDataLoader
+
+/** Activity logic for use when testing [ChooserActivity]. */
+class TestChooserActivityLogic(
+ tag: String,
+ activityProvider: () -> ComponentActivity,
+ onWorkProfileStatusUpdated: () -> Unit,
+ targetDataLoaderProvider: () -> TargetDataLoader,
+ onPreinitialization: () -> Unit,
+ private val overrideData: ChooserActivityOverrideData,
+) :
+ ChooserActivityLogic(
+ tag,
+ activityProvider,
+ onWorkProfileStatusUpdated,
+ targetDataLoaderProvider,
+ onPreinitialization,
+ ) {
+
+ override val annotatedUserHandles: AnnotatedUserHandles? by lazy {
+ overrideData.annotatedUserHandles
+ }
+
+ override val workProfileAvailabilityManager: WorkProfileAvailabilityManager by lazy {
+ overrideData.mWorkProfileAvailability ?: super.workProfileAvailabilityManager
+ }
+}
diff --git a/tests/activity/src/com/android/intentresolver/v2/TestResolverActivityLogic.kt b/tests/activity/src/com/android/intentresolver/v2/TestResolverActivityLogic.kt
new file mode 100644
index 0000000..7581043
--- /dev/null
+++ b/tests/activity/src/com/android/intentresolver/v2/TestResolverActivityLogic.kt
@@ -0,0 +1,22 @@
+package com.android.intentresolver.v2
+
+import androidx.activity.ComponentActivity
+import com.android.intentresolver.AnnotatedUserHandles
+import com.android.intentresolver.WorkProfileAvailabilityManager
+
+/** Activity logic for use when testing [ResolverActivity]. */
+class TestResolverActivityLogic(
+ tag: String,
+ activityProvider: () -> ComponentActivity,
+ onWorkProfileStatusUpdated: () -> Unit,
+ private val overrideData: ResolverWrapperActivity.OverrideData,
+) : ResolverActivityLogic(tag, activityProvider, onWorkProfileStatusUpdated) {
+
+ override val annotatedUserHandles: AnnotatedUserHandles? by lazy {
+ overrideData.annotatedUserHandles
+ }
+
+ override val workProfileAvailabilityManager: WorkProfileAvailabilityManager by lazy {
+ overrideData.mWorkProfileAvailability ?: super.workProfileAvailabilityManager
+ }
+}
diff --git a/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityTest.java b/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityTest.java
new file mode 100644
index 0000000..5245f65
--- /dev/null
+++ b/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityTest.java
@@ -0,0 +1,3147 @@
+/*
+ * Copyright (C) 2016 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.intentresolver.v2;
+
+import static android.app.Activity.RESULT_OK;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.action.ViewActions.longClick;
+import static androidx.test.espresso.action.ViewActions.swipeUp;
+import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.hasSibling;
+import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withEffectiveVisibility;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_CHOOSER_TARGET;
+import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_DEFAULT;
+import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
+import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
+import static com.android.intentresolver.ChooserListAdapter.CALLER_TARGET_SCORE_BOOST;
+import static com.android.intentresolver.ChooserListAdapter.SHORTCUT_TARGET_SCORE_BOOST;
+import static com.android.intentresolver.MatcherUtils.first;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import static junit.framework.Assert.assertNull;
+
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.PendingIntent;
+import android.app.usage.UsageStatsManager;
+import android.content.BroadcastReceiver;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ClipboardManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager.ShareShortcutInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.platform.test.annotations.RequiresFlagsEnabled;
+import android.platform.test.flag.junit.CheckFlagsRule;
+import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.provider.DeviceConfig;
+import android.service.chooser.ChooserAction;
+import android.service.chooser.ChooserTarget;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.BackgroundColorSpan;
+import android.text.style.ForegroundColorSpan;
+import android.text.style.StyleSpan;
+import android.text.style.UnderlineSpan;
+import android.util.Pair;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.espresso.contrib.RecyclerViewActions;
+import androidx.test.espresso.matcher.BoundedDiagnosingMatcher;
+import androidx.test.espresso.matcher.ViewMatchers;
+import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.intentresolver.AnnotatedUserHandles;
+import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.Flags;
+import com.android.intentresolver.IChooserWrapper;
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolvedComponentInfo;
+import com.android.intentresolver.ResolverDataProvider;
+import com.android.intentresolver.TestContentProvider;
+import com.android.intentresolver.TestPreviewImageLoader;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.contentpreview.ImageLoader;
+import com.android.intentresolver.logging.EventLog;
+import com.android.intentresolver.logging.FakeEventLog;
+import com.android.intentresolver.shortcuts.ShortcutLoader;
+import com.android.intentresolver.v2.platform.ImageEditor;
+import com.android.intentresolver.v2.platform.ImageEditorModule;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import dagger.hilt.android.testing.BindValue;
+import dagger.hilt.android.testing.HiltAndroidRule;
+import dagger.hilt.android.testing.HiltAndroidTest;
+import dagger.hilt.android.testing.UninstallModules;
+
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+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.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+import java.util.function.Function;
+
+/**
+ * Instrumentation tests for ChooserActivity.
+ * <p>
+ * Legacy test suite migrated from framework CoreTests.
+ */
+@RunWith(Parameterized.class)
+@HiltAndroidTest
+@UninstallModules(ImageEditorModule.class)
+public class UnbundledChooserActivityTest {
+
+ private static FakeEventLog getEventLog(ChooserWrapperActivity activity) {
+ return (FakeEventLog) activity.mEventLog;
+ }
+
+ private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry
+ .getInstrumentation().getTargetContext().getUser();
+ private static final UserHandle WORK_PROFILE_USER_HANDLE = UserHandle.of(10);
+ private static final UserHandle CLONE_PROFILE_USER_HANDLE = UserHandle.of(11);
+
+ private static final Function<PackageManager, PackageManager> DEFAULT_PM = pm -> pm;
+ private static final Function<PackageManager, PackageManager> NO_APP_PREDICTION_SERVICE_PM =
+ pm -> {
+ PackageManager mock = Mockito.spy(pm);
+ when(mock.getAppPredictionServicePackageName()).thenReturn(null);
+ return mock;
+ };
+
+ @Parameterized.Parameters
+ public static Collection packageManagers() {
+ return Arrays.asList(new Object[][] {
+ // Default PackageManager
+ { DEFAULT_PM },
+ // No App Prediction Service
+ { NO_APP_PREDICTION_SERVICE_PM}
+ });
+ }
+
+ private static final String TEST_MIME_TYPE = "application/TestType";
+
+ private static final int CONTENT_PREVIEW_IMAGE = 1;
+ private static final int CONTENT_PREVIEW_FILE = 2;
+ private static final int CONTENT_PREVIEW_TEXT = 3;
+
+ @Rule(order = 0)
+ public CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule();
+
+ @Rule(order = 1)
+ public HiltAndroidRule mHiltAndroidRule = new HiltAndroidRule(this);
+
+ @Rule(order = 2)
+ public ActivityTestRule<ChooserWrapperActivity> mActivityRule =
+ new ActivityTestRule<>(ChooserWrapperActivity.class, false, false);
+
+ @Before
+ public void setUp() {
+ // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the
+ // permissions we require (which we'll read from the manifest at runtime).
+ InstrumentationRegistry
+ .getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity();
+
+ cleanOverrideData();
+ mHiltAndroidRule.inject();
+ }
+
+ private final Function<PackageManager, PackageManager> mPackageManagerOverride;
+
+ /** An arbitrary pre-installed activity that handles this type of intent. */
+ @BindValue
+ @ImageEditor
+ final Optional<ComponentName> mImageEditor = Optional.ofNullable(
+ ComponentName.unflattenFromString("com.google.android.apps.messaging/"
+ + ".ui.conversationlist.ShareIntentActivity"));
+
+ public UnbundledChooserActivityTest(
+ Function<PackageManager, PackageManager> packageManagerOverride) {
+ mPackageManagerOverride = packageManagerOverride;
+ }
+
+ private void setDeviceConfigProperty(
+ @NonNull String propertyName,
+ @NonNull String value) {
+ // TODO: consider running with {@link #runWithShellPermissionIdentity()} to more narrowly
+ // request WRITE_DEVICE_CONFIG permissions if we get rid of the broad grant we currently
+ // configure in {@link #setup()}.
+ // TODO: is it really appropriate that this is always set with makeDefault=true?
+ boolean valueWasSet = DeviceConfig.setProperty(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ propertyName,
+ value,
+ true /* makeDefault */);
+ if (!valueWasSet) {
+ throw new IllegalStateException(
+ "Could not set " + propertyName + " to " + value);
+ }
+ }
+
+ public void cleanOverrideData() {
+ ChooserActivityOverrideData.getInstance().reset();
+ ChooserActivityOverrideData.getInstance().createPackageManager = mPackageManagerOverride;
+
+ setDeviceConfigProperty(
+ SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
+ Boolean.toString(true));
+ }
+
+ @Test
+ public void customTitle() throws InterruptedException {
+ Intent viewIntent = createViewTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(
+ Intent.createChooser(viewIntent, "chooser test"));
+
+ waitForIdle();
+ assertThat(activity.getAdapter().getCount(), is(2));
+ assertThat(activity.getAdapter().getServiceTargetCount(), is(0));
+ onView(withId(android.R.id.title)).check(matches(withText("chooser test")));
+ }
+
+ @Test
+ public void customTitleIgnoredForSendIntents() throws InterruptedException {
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "chooser test"));
+ waitForIdle();
+ onView(withId(android.R.id.title))
+ .check(matches(withText(R.string.whichSendApplication)));
+ }
+
+ @Test
+ public void emptyTitle() throws InterruptedException {
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withId(android.R.id.title))
+ .check(matches(withText(R.string.whichSendApplication)));
+ }
+
+ @Test
+ public void test_shareRichTextWithRichTitle_richTextAndRichTitleDisplayed() {
+ CharSequence title = new SpannableStringBuilder()
+ .append("Rich", new UnderlineSpan(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
+ .append(
+ "Title",
+ new ForegroundColorSpan(Color.RED),
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
+ CharSequence sharedText = new SpannableStringBuilder()
+ .append(
+ "Rich",
+ new BackgroundColorSpan(Color.YELLOW),
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
+ .append(
+ "Text",
+ new StyleSpan(Typeface.ITALIC),
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText);
+ sendIntent.putExtra(Intent.EXTRA_TITLE, title);
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withId(com.android.internal.R.id.content_preview_title))
+ .check((view, e) -> {
+ assertThat(view).isInstanceOf(TextView.class);
+ CharSequence text = ((TextView) view).getText();
+ assertThat(text).isInstanceOf(Spanned.class);
+ Spanned spanned = (Spanned) text;
+ assertThat(spanned.getSpans(0, spanned.length(), Object.class))
+ .hasLength(2);
+ assertThat(spanned.getSpans(0, 4, UnderlineSpan.class)).hasLength(1);
+ assertThat(spanned.getSpans(4, spanned.length(), ForegroundColorSpan.class))
+ .hasLength(1);
+ });
+
+ onView(withId(com.android.internal.R.id.content_preview_text))
+ .check((view, e) -> {
+ assertThat(view).isInstanceOf(TextView.class);
+ CharSequence text = ((TextView) view).getText();
+ assertThat(text).isInstanceOf(Spanned.class);
+ Spanned spanned = (Spanned) text;
+ assertThat(spanned.getSpans(0, spanned.length(), Object.class))
+ .hasLength(2);
+ assertThat(spanned.getSpans(0, 4, BackgroundColorSpan.class)).hasLength(1);
+ assertThat(spanned.getSpans(4, spanned.length(), StyleSpan.class)).hasLength(1);
+ });
+ }
+
+ @Test
+ public void emptyPreviewTitleAndThumbnail() throws InterruptedException {
+ Intent sendIntent = createSendTextIntentWithPreview(null, null);
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.content_preview_title))
+ .check(matches(not(isDisplayed())));
+ onView(withId(com.android.internal.R.id.content_preview_thumbnail))
+ .check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ public void visiblePreviewTitleWithoutThumbnail() throws InterruptedException {
+ String previewTitle = "My Content Preview Title";
+ Intent sendIntent = createSendTextIntentWithPreview(previewTitle, null);
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.content_preview_title))
+ .check(matches(isDisplayed()));
+ onView(withId(com.android.internal.R.id.content_preview_title))
+ .check(matches(withText(previewTitle)));
+ onView(withId(com.android.internal.R.id.content_preview_thumbnail))
+ .check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ public void visiblePreviewTitleWithInvalidThumbnail() throws InterruptedException {
+ String previewTitle = "My Content Preview Title";
+ Intent sendIntent = createSendTextIntentWithPreview(previewTitle,
+ Uri.parse("tel:(+49)12345789"));
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.content_preview_title))
+ .check(matches(isDisplayed()));
+ onView(withId(com.android.internal.R.id.content_preview_thumbnail))
+ .check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ public void visiblePreviewTitleAndThumbnail() throws InterruptedException {
+ String previewTitle = "My Content Preview Title";
+ Uri uri = Uri.parse(
+ "android.resource://com.android.frameworks.coretests/"
+ + com.android.intentresolver.tests.R.drawable.test320x240);
+ Intent sendIntent = createSendTextIntentWithPreview(previewTitle, uri);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createBitmap());
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.content_preview_title))
+ .check(matches(isDisplayed()));
+ onView(withId(com.android.internal.R.id.content_preview_thumbnail))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test @Ignore
+ public void twoOptionsAndUserSelectsOne() throws InterruptedException {
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ assertThat(activity.getAdapter().getCount(), is(2));
+ onView(withId(com.android.internal.R.id.profile_button)).check(doesNotExist());
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0);
+ onView(withText(toChoose.activityInfo.name))
+ .perform(click());
+ waitForIdle();
+ assertThat(chosen[0], is(toChoose));
+ }
+
+ @Test @Ignore
+ public void fourOptionsStackedIntoOneTarget() throws InterruptedException {
+ Intent sendIntent = createSendTextIntent();
+
+ // create just enough targets to ensure the a-z list should be shown
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(1);
+
+ // next create 4 targets in a single app that should be stacked into a single target
+ String packageName = "xxx.yyy";
+ String appName = "aaa";
+ ComponentName cn = new ComponentName(packageName, appName);
+ Intent intent = new Intent("fakeIntent");
+ List<ResolvedComponentInfo> infosToStack = new ArrayList<>();
+ for (int i = 0; i < 4; i++) {
+ ResolveInfo resolveInfo = ResolverDataProvider.createResolveInfo(i,
+ UserHandle.USER_CURRENT, PERSONAL_USER_HANDLE);
+ resolveInfo.activityInfo.applicationInfo.name = appName;
+ resolveInfo.activityInfo.applicationInfo.packageName = packageName;
+ resolveInfo.activityInfo.packageName = packageName;
+ resolveInfo.activityInfo.name = "ccc" + i;
+ infosToStack.add(new ResolvedComponentInfo(cn, intent, resolveInfo));
+ }
+ resolvedComponentInfos.addAll(infosToStack);
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // expect 1 unique targets + 1 group + 4 ranked app targets
+ assertThat(activity.getAdapter().getCount(), is(6));
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ onView(allOf(withText(appName), hasSibling(withText("")))).perform(click());
+ waitForIdle();
+
+ // clicking will launch a dialog to choose the activity within the app
+ onView(withText(appName)).check(matches(isDisplayed()));
+ int i = 0;
+ for (ResolvedComponentInfo rci: infosToStack) {
+ onView(withText("ccc" + i)).check(matches(isDisplayed()));
+ ++i;
+ }
+ }
+
+ @Test @Ignore
+ public void updateChooserCountsAndModelAfterUserSelection() throws InterruptedException {
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ UsageStatsManager usm = activity.getUsageStatsManager();
+ verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1))
+ .topK(any(List.class), anyInt());
+ assertThat(activity.getIsSelected(), is(false));
+ ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
+ return true;
+ };
+ ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0);
+ DisplayResolveInfo testDri =
+ activity.createTestDisplayResolveInfo(
+ sendIntent, toChoose, "testLabel", "testInfo", sendIntent);
+ onView(withText(toChoose.activityInfo.name))
+ .perform(click());
+ waitForIdle();
+ verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1))
+ .updateChooserCounts(Mockito.anyString(), any(UserHandle.class),
+ Mockito.anyString());
+ verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1))
+ .updateModel(testDri);
+ assertThat(activity.getIsSelected(), is(true));
+ }
+
+ @Ignore // b/148158199
+ @Test
+ public void noResultsFromPackageManager() {
+ setupResolverControllers(null);
+ Intent sendIntent = createSendTextIntent();
+ final ChooserActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ final IChooserWrapper wrapper = (IChooserWrapper) activity;
+
+ waitForIdle();
+ assertThat(activity.isFinishing(), is(false));
+
+ onView(withId(android.R.id.empty)).check(matches(isDisplayed()));
+ onView(withId(com.android.internal.R.id.profile_pager)).check(matches(not(isDisplayed())));
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> wrapper.getAdapter().handlePackagesChanged()
+ );
+ // backward compatibility. looks like we finish when data is empty after package change
+ assertThat(activity.isFinishing(), is(true));
+ }
+
+ @Test
+ public void autoLaunchSingleResult() throws InterruptedException {
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(1);
+ setupResolverControllers(resolvedComponentInfos);
+
+ Intent sendIntent = createSendTextIntent();
+ final ChooserActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ assertThat(chosen[0], is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
+ assertThat(activity.isFinishing(), is(true));
+ }
+
+ @Test @Ignore
+ public void hasOtherProfileOneOption() {
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+
+ ResolveInfo toChoose = personalResolvedComponentInfos.get(1).getResolveInfoAt(0);
+ Intent sendIntent = createSendTextIntent();
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // The other entry is filtered to the other profile slot
+ assertThat(activity.getAdapter().getCount(), is(1));
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ // Make a stable copy of the components as the original list may be modified
+ List<ResolvedComponentInfo> stableCopy =
+ createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10);
+ waitForIdle();
+
+ onView(first(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)))
+ .perform(click());
+ waitForIdle();
+ assertThat(chosen[0], is(toChoose));
+ }
+
+ @Test @Ignore
+ public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception {
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3);
+ ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0);
+
+ setupResolverControllers(resolvedComponentInfos);
+ when(ChooserActivityOverrideData.getInstance().resolverListController.getLastChosen())
+ .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0));
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // The other entry is filtered to the other profile slot
+ assertThat(activity.getAdapter().getCount(), is(2));
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ // Make a stable copy of the components as the original list may be modified
+ List<ResolvedComponentInfo> stableCopy =
+ createResolvedComponentsForTestWithOtherProfile(3);
+ onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))
+ .perform(click());
+ waitForIdle();
+ assertThat(chosen[0], is(toChoose));
+ }
+
+ @Test @Ignore
+ public void hasLastChosenActivityAndOtherProfile() throws Exception {
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3);
+ ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0);
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // The other entry is filtered to the last used slot
+ assertThat(activity.getAdapter().getCount(), is(2));
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ // Make a stable copy of the components as the original list may be modified
+ List<ResolvedComponentInfo> stableCopy =
+ createResolvedComponentsForTestWithOtherProfile(3);
+ onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))
+ .perform(click());
+ waitForIdle();
+ assertThat(chosen[0], is(toChoose));
+ }
+
+ @Test
+ @Ignore("b/285309527")
+ public void testFilePlusTextSharing_ExcludeText() {
+ Uri uri = createTestContentProviderUri(null, "image/png");
+ Intent sendIntent = createSendImageIntent(uri);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createBitmap());
+ sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google");
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList(
+ ResolverDataProvider.createResolvedComponentInfo(
+ new ComponentName("org.imageviewer", "ImageTarget"),
+ sendIntent, PERSONAL_USER_HANDLE),
+ ResolverDataProvider.createResolvedComponentInfo(
+ new ComponentName("org.textviewer", "UriTarget"),
+ new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE)
+ );
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withId(R.id.include_text_action))
+ .check(matches(isDisplayed()))
+ .perform(click());
+ waitForIdle();
+
+ onView(withId(R.id.content_preview_text)).check(matches(withText("File only")));
+
+ AtomicReference<Intent> launchedIntentRef = new AtomicReference<>();
+ ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
+ launchedIntentRef.set(targetInfo.getTargetIntent());
+ return true;
+ };
+
+ onView(withText(resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.name))
+ .perform(click());
+ waitForIdle();
+ assertThat(launchedIntentRef.get().hasExtra(Intent.EXTRA_TEXT)).isFalse();
+ }
+
+ @Test
+ @Ignore("b/285309527")
+ public void testFilePlusTextSharing_RemoveAndAddBackText() {
+ Uri uri = createTestContentProviderUri("application/pdf", "image/png");
+ Intent sendIntent = createSendImageIntent(uri);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createBitmap());
+ final String text = "https://google.com/search?q=google";
+ sendIntent.putExtra(Intent.EXTRA_TEXT, text);
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList(
+ ResolverDataProvider.createResolvedComponentInfo(
+ new ComponentName("org.imageviewer", "ImageTarget"),
+ sendIntent, PERSONAL_USER_HANDLE),
+ ResolverDataProvider.createResolvedComponentInfo(
+ new ComponentName("org.textviewer", "UriTarget"),
+ new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE)
+ );
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withId(R.id.include_text_action))
+ .check(matches(isDisplayed()))
+ .perform(click());
+ waitForIdle();
+ onView(withId(R.id.content_preview_text)).check(matches(withText("File only")));
+
+ onView(withId(R.id.include_text_action))
+ .perform(click());
+ waitForIdle();
+
+ onView(withId(R.id.content_preview_text)).check(matches(withText(text)));
+
+ AtomicReference<Intent> launchedIntentRef = new AtomicReference<>();
+ ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
+ launchedIntentRef.set(targetInfo.getTargetIntent());
+ return true;
+ };
+
+ onView(withText(resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.name))
+ .perform(click());
+ waitForIdle();
+ assertThat(launchedIntentRef.get().getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(text);
+ }
+
+ @Test
+ @Ignore("b/285309527")
+ public void testFilePlusTextSharing_TextExclusionDoesNotAffectAlternativeIntent() {
+ Uri uri = createTestContentProviderUri("image/png", null);
+ Intent sendIntent = createSendImageIntent(uri);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createBitmap());
+ sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google");
+
+ Intent alternativeIntent = createSendTextIntent();
+ final String text = "alternative intent";
+ alternativeIntent.putExtra(Intent.EXTRA_TEXT, text);
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList(
+ ResolverDataProvider.createResolvedComponentInfo(
+ new ComponentName("org.imageviewer", "ImageTarget"),
+ sendIntent, PERSONAL_USER_HANDLE),
+ ResolverDataProvider.createResolvedComponentInfo(
+ new ComponentName("org.textviewer", "UriTarget"),
+ alternativeIntent, PERSONAL_USER_HANDLE)
+ );
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withId(R.id.include_text_action))
+ .check(matches(isDisplayed()))
+ .perform(click());
+ waitForIdle();
+
+ AtomicReference<Intent> launchedIntentRef = new AtomicReference<>();
+ ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
+ launchedIntentRef.set(targetInfo.getTargetIntent());
+ return true;
+ };
+
+ onView(withText(resolvedComponentInfos.get(1).getResolveInfoAt(0).activityInfo.name))
+ .perform(click());
+ waitForIdle();
+ assertThat(launchedIntentRef.get().getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(text);
+ }
+
+ @Test
+ @Ignore("b/285309527")
+ public void testImagePlusTextSharing_failedThumbnailAndExcludedText_textChanges() {
+ Uri uri = createTestContentProviderUri("image/png", null);
+ Intent sendIntent = createSendImageIntent(uri);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ new TestPreviewImageLoader(Collections.emptyMap());
+ sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google");
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList(
+ ResolverDataProvider.createResolvedComponentInfo(
+ new ComponentName("org.imageviewer", "ImageTarget"),
+ sendIntent, PERSONAL_USER_HANDLE),
+ ResolverDataProvider.createResolvedComponentInfo(
+ new ComponentName("org.textviewer", "UriTarget"),
+ new Intent("VIEW_TEXT"), PERSONAL_USER_HANDLE)
+ );
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withId(R.id.include_text_action))
+ .check(matches(isDisplayed()))
+ .perform(click());
+ waitForIdle();
+
+ onView(withId(R.id.image_view))
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)));
+ onView(withId(R.id.content_preview_text))
+ .check(matches(allOf(isDisplayed(), withText("Image only"))));
+ }
+
+ @Test
+ public void copyTextToClipboard() {
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ final ChooserActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withId(R.id.copy)).check(matches(isDisplayed()));
+ onView(withId(R.id.copy)).perform(click());
+ ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(
+ Context.CLIPBOARD_SERVICE);
+ ClipData clipData = clipboard.getPrimaryClip();
+ assertThat(clipData).isNotNull();
+ assertThat(clipData.getItemAt(0).getText()).isEqualTo("testing intent sending");
+
+ ClipDescription clipDescription = clipData.getDescription();
+ assertThat("text/plain", is(clipDescription.getMimeType(0)));
+
+ assertEquals(mActivityRule.getActivityResult().getResultCode(), RESULT_OK);
+ }
+
+ @Test
+ public void copyTextToClipboardLogging() {
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ ChooserWrapperActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withId(R.id.copy)).check(matches(isDisplayed()));
+ onView(withId(R.id.copy)).perform(click());
+ FakeEventLog eventLog = getEventLog(activity);
+ assertThat(eventLog.getActionSelected())
+ .isEqualTo(new FakeEventLog.ActionSelected(
+ /* targetType = */ EventLog.SELECTION_TYPE_COPY));
+ }
+
+ @Test
+ @Ignore
+ public void testNearbyShareLogging() throws Exception {
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withId(com.android.internal.R.id.chooser_nearby_button))
+ .check(matches(isDisplayed()));
+ onView(withId(com.android.internal.R.id.chooser_nearby_button)).perform(click());
+
+ // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
+ }
+
+ @Test @Ignore
+ public void testEditImageLogs() {
+
+ Uri uri = createTestContentProviderUri("image/png", null);
+ Intent sendIntent = createSendImageIntent(uri);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createBitmap());
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withId(com.android.internal.R.id.chooser_edit_button)).check(matches(isDisplayed()));
+ onView(withId(com.android.internal.R.id.chooser_edit_button)).perform(click());
+
+ // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
+ }
+
+
+ @Test
+ public void oneVisibleImagePreview() {
+ Uri uri = createTestContentProviderUri("image/png", null);
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createWideBitmap());
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withId(R.id.scrollable_image_preview))
+ .check((view, exception) -> {
+ if (exception != null) {
+ throw exception;
+ }
+ RecyclerView recyclerView = (RecyclerView) view;
+ assertThat(recyclerView.getAdapter().getItemCount(), is(1));
+ assertThat(recyclerView.getChildCount(), is(1));
+ View imageView = recyclerView.getChildAt(0);
+ Rect rect = new Rect();
+ boolean isPartiallyVisible = imageView.getGlobalVisibleRect(rect);
+ assertThat(
+ "image preview view is not fully visible",
+ isPartiallyVisible
+ && rect.width() == imageView.getWidth()
+ && rect.height() == imageView.getHeight());
+ });
+ }
+
+ @Test
+ public void allThumbnailsFailedToLoad_hidePreview() {
+ Uri uri = createTestContentProviderUri("image/jpg", null);
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ new TestPreviewImageLoader(Collections.emptyMap());
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withId(R.id.scrollable_image_preview))
+ .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE)));
+ }
+
+ @Test(timeout = 4_000)
+ public void testSlowUriMetadata_fallbackToFilePreview() {
+ Uri uri = createTestContentProviderUri(
+ "application/pdf", "image/png", /*streamTypeTimeout=*/8_000);
+ ArrayList<Uri> uris = new ArrayList<>(1);
+ uris.add(uri);
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createBitmap());
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ // The preview type resolution is expected to timeout and default to file preview, otherwise
+ // the test should timeout.
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed()));
+ onView(withId(R.id.content_preview_filename)).check(matches(withText("image.png")));
+ onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed()));
+ }
+
+ @Test(timeout = 4_000)
+ public void testSendManyFilesWithSmallMetadataDelayAndOneImage_fallbackToFilePreviewUi() {
+ Uri fileUri = createTestContentProviderUri(
+ "application/pdf", "application/pdf", /*streamTypeTimeout=*/300);
+ Uri imageUri = createTestContentProviderUri("application/pdf", "image/png");
+ ArrayList<Uri> uris = new ArrayList<>(50);
+ for (int i = 0; i < 49; i++) {
+ uris.add(fileUri);
+ }
+ uris.add(imageUri);
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(imageUri, createBitmap());
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+ // The preview type resolution is expected to timeout and default to file preview, otherwise
+ // the test should timeout.
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+
+ waitForIdle();
+
+ onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed()));
+ onView(withId(R.id.content_preview_filename)).check(matches(withText("image.png")));
+ onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testManyVisibleImagePreview_ScrollableImagePreview() {
+ Uri uri = createTestContentProviderUri("image/png", null);
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createBitmap());
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withId(R.id.scrollable_image_preview))
+ .perform(RecyclerViewActions.scrollToLastPosition())
+ .check((view, exception) -> {
+ if (exception != null) {
+ throw exception;
+ }
+ RecyclerView recyclerView = (RecyclerView) view;
+ assertThat(recyclerView.getAdapter().getItemCount(), is(uris.size()));
+ });
+ }
+
+ @Test(timeout = 4_000)
+ public void testPartiallyLoadedMetadata_previewIsShownForTheLoadedPart() {
+ Uri imgOneUri = createTestContentProviderUri("image/png", null);
+ Uri imgTwoUri = createTestContentProviderUri("image/png", null)
+ .buildUpon()
+ .path("image-2.png")
+ .build();
+ Uri docUri = createTestContentProviderUri("application/pdf", "image/png", 8_000);
+ ArrayList<Uri> uris = new ArrayList<>(2);
+ // two large previews to fill the screen and be presented right away and one
+ // document that would be delayed by the URI metadata reading
+ uris.add(imgOneUri);
+ uris.add(imgTwoUri);
+ uris.add(docUri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ Map<Uri, Bitmap> bitmaps = new HashMap<>();
+ bitmaps.put(imgOneUri, createWideBitmap(Color.RED));
+ bitmaps.put(imgTwoUri, createWideBitmap(Color.GREEN));
+ bitmaps.put(docUri, createWideBitmap(Color.BLUE));
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ new TestPreviewImageLoader(bitmaps);
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ // the preview type is expected to be resolved quickly based on the first provided URI
+ // metadata. If, instead, it is dependent on the third URI metadata, the test should either
+ // timeout or (more probably due to inner timeout) default to file preview type; anyway the
+ // test will fail.
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withId(R.id.scrollable_image_preview))
+ .check((view, exception) -> {
+ if (exception != null) {
+ throw exception;
+ }
+ RecyclerView recyclerView = (RecyclerView) view;
+ assertThat(recyclerView.getChildCount()).isAtLeast(1);
+ // the first view is a preview
+ View imageView = recyclerView.getChildAt(0).findViewById(R.id.image);
+ assertThat(imageView).isNotNull();
+ })
+ .perform(RecyclerViewActions.scrollToLastPosition())
+ .check((view, exception) -> {
+ if (exception != null) {
+ throw exception;
+ }
+ RecyclerView recyclerView = (RecyclerView) view;
+ assertThat(recyclerView.getChildCount()).isAtLeast(1);
+ // check that the last view is a loading indicator
+ View loadingIndicator =
+ recyclerView.getChildAt(recyclerView.getChildCount() - 1);
+ assertThat(loadingIndicator).isNotNull();
+ });
+ waitForIdle();
+ }
+
+ @Test
+ public void testImageAndTextPreview() {
+ final Uri uri = createTestContentProviderUri("image/png", null);
+ final String sharedText = "text-" + System.currentTimeMillis();
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createBitmap());
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withText(sharedText))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void test_shareImageWithRichText_RichTextIsDisplayed() {
+ final Uri uri = createTestContentProviderUri("image/png", null);
+ final CharSequence sharedText = new SpannableStringBuilder()
+ .append(
+ "text-",
+ new StyleSpan(Typeface.BOLD_ITALIC),
+ Spannable.SPAN_INCLUSIVE_EXCLUSIVE)
+ .append(
+ Long.toString(System.currentTimeMillis()),
+ new ForegroundColorSpan(Color.RED),
+ Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createBitmap());
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withText(sharedText.toString()))
+ .check(matches(isDisplayed()))
+ .check((view, e) -> {
+ if (e != null) {
+ throw e;
+ }
+ assertThat(view).isInstanceOf(TextView.class);
+ CharSequence text = ((TextView) view).getText();
+ assertThat(text).isInstanceOf(Spanned.class);
+ Spanned spanned = (Spanned) text;
+ Object[] spans = spanned.getSpans(0, text.length(), Object.class);
+ assertThat(spans).hasLength(2);
+ assertThat(spanned.getSpans(0, 5, StyleSpan.class)).hasLength(1);
+ assertThat(spanned.getSpans(5, text.length(), ForegroundColorSpan.class))
+ .hasLength(1);
+ });
+ }
+
+ @Test
+ public void testTextPreviewWhenTextIsSharedWithMultipleImages() {
+ final Uri uri = createTestContentProviderUri("image/png", null);
+ final String sharedText = "text-" + System.currentTimeMillis();
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createBitmap());
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntentAsUser(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ Mockito.any(UserHandle.class)))
+ .thenReturn(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withText(sharedText)).check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testOnCreateLogging() {
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ ChooserWrapperActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
+ waitForIdle();
+
+ FakeEventLog eventLog = getEventLog(activity);
+ FakeEventLog.ChooserActivityShown event = eventLog.getChooserActivityShown();
+ assertThat(event).isNotNull();
+ assertThat(event.isWorkProfile()).isFalse();
+ assertThat(event.getTargetMimeType()).isEqualTo(TEST_MIME_TYPE);
+ }
+
+ @Test
+ public void testOnCreateLoggingFromWorkProfile() {
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+ ChooserActivityOverrideData.getInstance().alternateProfileSetting =
+ MetricsEvent.MANAGED_PROFILE;
+
+ ChooserWrapperActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
+ waitForIdle();
+
+ FakeEventLog eventLog = getEventLog(activity);
+ FakeEventLog.ChooserActivityShown event = eventLog.getChooserActivityShown();
+ assertThat(event).isNotNull();
+ assertThat(event.isWorkProfile()).isTrue();
+ assertThat(event.getTargetMimeType()).isEqualTo(TEST_MIME_TYPE);
+ }
+
+ @Test
+ public void testEmptyPreviewLogging() {
+ Intent sendIntent = createSendTextIntentWithPreview(null, null);
+
+ ChooserWrapperActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent,
+ "empty preview logger test"));
+ waitForIdle();
+
+ FakeEventLog eventLog = getEventLog(activity);
+ FakeEventLog.ChooserActivityShown event = eventLog.getChooserActivityShown();
+ assertThat(event).isNotNull();
+ assertThat(event.isWorkProfile()).isFalse();
+ assertThat(event.getTargetMimeType()).isNull();
+ }
+
+ @Test
+ public void testTitlePreviewLogging() {
+ Intent sendIntent = createSendTextIntentWithPreview("TestTitle", null);
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ ChooserWrapperActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ FakeEventLog eventLog = getEventLog(activity);
+ assertThat(eventLog.getActionShareWithPreview())
+ .isEqualTo(new FakeEventLog.ActionShareWithPreview(
+ /* previewType = */ CONTENT_PREVIEW_TEXT));
+ }
+
+ @Test
+ public void testImagePreviewLogging() {
+ Uri uri = createTestContentProviderUri("image/png", null);
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createBitmap());
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ ChooserWrapperActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ FakeEventLog eventLog = getEventLog(activity);
+ assertThat(eventLog.getActionShareWithPreview())
+ .isEqualTo(new FakeEventLog.ActionShareWithPreview(
+ /* previewType = */ CONTENT_PREVIEW_IMAGE));
+ }
+
+ @Test
+ public void oneVisibleFilePreview() throws InterruptedException {
+ Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf");
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed()));
+ onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf")));
+ onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed()));
+ }
+
+
+ @Test
+ public void moreThanOneVisibleFilePreview() throws InterruptedException {
+ Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf");
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed()));
+ onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf")));
+ onView(withId(R.id.content_preview_more_files)).check(matches(isDisplayed()));
+ onView(withId(R.id.content_preview_more_files)).check(matches(withText("+ 2 more files")));
+ onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void contentProviderThrowSecurityException() throws InterruptedException {
+ Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf");
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ ChooserActivityOverrideData.getInstance().resolverForceException = true;
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed()));
+ onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf")));
+ onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void contentProviderReturnsNoColumns() throws InterruptedException {
+ Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf");
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ Cursor cursor = mock(Cursor.class);
+ when(cursor.getCount()).thenReturn(1);
+ Mockito.doNothing().when(cursor).close();
+ when(cursor.moveToFirst()).thenReturn(true);
+ when(cursor.getColumnIndex(Mockito.anyString())).thenReturn(-1);
+
+ ChooserActivityOverrideData.getInstance().resolverCursor = cursor;
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed()));
+ onView(withId(R.id.content_preview_filename)).check(matches(withText("app.pdf")));
+ onView(withId(R.id.content_preview_more_files)).check(matches(isDisplayed()));
+ onView(withId(R.id.content_preview_more_files)).check(matches(withText("+ 1 more file")));
+ onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testGetBaseScore() {
+ final float testBaseScore = 0.89f;
+
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getScore(Mockito.isA(DisplayResolveInfo.class)))
+ .thenReturn(testBaseScore);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ final DisplayResolveInfo testDri =
+ activity.createTestDisplayResolveInfo(
+ sendIntent,
+ ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE),
+ "testLabel",
+ "testInfo",
+ sendIntent);
+ final ChooserListAdapter adapter = activity.getAdapter();
+
+ assertThat(adapter.getBaseScore(null, 0), is(CALLER_TARGET_SCORE_BOOST));
+ assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_DEFAULT), is(testBaseScore));
+ assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_CHOOSER_TARGET), is(testBaseScore));
+ assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE),
+ is(testBaseScore * SHORTCUT_TARGET_SCORE_BOOST));
+ assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER),
+ is(testBaseScore * SHORTCUT_TARGET_SCORE_BOOST));
+ }
+
+ // This test is too long and too slow and should not be taken as an example for future tests.
+ @Test
+ public void testDirectTargetSelectionLogging() {
+ Intent sendIntent = createSendTextIntent();
+ // We need app targets for direct targets to get displayed
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ // create test shortcut loader factory, remember loaders and their callbacks
+ SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+ createShortcutLoaderFactory();
+
+ // Start activity
+ ChooserWrapperActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // verify that ShortcutLoader was queried
+ ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+ ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+ verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture());
+
+ // send shortcuts
+ assertThat(
+ "Wrong number of app targets",
+ appTargets.getValue().length,
+ is(resolvedComponentInfos.size()));
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(1, "");
+ ShortcutLoader.Result result = new ShortcutLoader.Result(
+ true,
+ appTargets.getValue(),
+ new ShortcutLoader.ShortcutResultInfo[] {
+ new ShortcutLoader.ShortcutResultInfo(
+ appTargets.getValue()[0],
+ serviceTargets
+ )
+ },
+ new HashMap<>(),
+ new HashMap<>()
+ );
+ activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+ waitForIdle();
+
+ final ChooserListAdapter activeAdapter = activity.getAdapter();
+ assertThat(
+ "Chooser should have 3 targets (2 apps, 1 direct)",
+ activeAdapter.getCount(),
+ is(3));
+ assertThat(
+ "Chooser should have exactly one selectable direct target",
+ activeAdapter.getSelectableServiceTargetCount(),
+ is(1));
+ assertThat(
+ "The resolver info must match the resolver info used to create the target",
+ activeAdapter.getItem(0).getResolveInfo(),
+ is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
+
+ // Click on the direct target
+ String name = serviceTargets.get(0).getTitle().toString();
+ onView(withText(name))
+ .perform(click());
+ waitForIdle();
+
+ FakeEventLog eventLog = getEventLog(activity);
+ assertThat(eventLog.getShareTargetSelected()).hasSize(1);
+ FakeEventLog.ShareTargetSelected call = eventLog.getShareTargetSelected().get(0);
+ assertThat(call.getTargetType()).isEqualTo(EventLog.SELECTION_TYPE_SERVICE);
+ assertThat(call.getDirectTargetAlsoRanked()).isEqualTo(-1);
+ var hashResult = call.getDirectTargetHashed();
+ var hash = hashResult == null ? "" : hashResult.hashedString;
+ assertWithMessage("Hash is not predictable but must be obfuscated")
+ .that(hash).isNotEqualTo(name);
+ }
+
+ // This test is too long and too slow and should not be taken as an example for future tests.
+ @Test
+ public void testDirectTargetLoggingWithRankedAppTarget() {
+ Intent sendIntent = createSendTextIntent();
+ // We need app targets for direct targets to get displayed
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ // create test shortcut loader factory, remember loaders and their callbacks
+ SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+ createShortcutLoaderFactory();
+
+ // Start activity
+ ChooserWrapperActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // verify that ShortcutLoader was queried
+ ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+ ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+ verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture());
+
+ // send shortcuts
+ assertThat(
+ "Wrong number of app targets",
+ appTargets.getValue().length,
+ is(resolvedComponentInfos.size()));
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(
+ 1,
+ resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+ ShortcutLoader.Result result = new ShortcutLoader.Result(
+ true,
+ appTargets.getValue(),
+ new ShortcutLoader.ShortcutResultInfo[] {
+ new ShortcutLoader.ShortcutResultInfo(
+ appTargets.getValue()[0],
+ serviceTargets
+ )
+ },
+ new HashMap<>(),
+ new HashMap<>()
+ );
+ activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+ waitForIdle();
+
+ final ChooserListAdapter activeAdapter = activity.getAdapter();
+ assertThat(
+ "Chooser should have 3 targets (2 apps, 1 direct)",
+ activeAdapter.getCount(),
+ is(3));
+ assertThat(
+ "Chooser should have exactly one selectable direct target",
+ activeAdapter.getSelectableServiceTargetCount(),
+ is(1));
+ assertThat(
+ "The resolver info must match the resolver info used to create the target",
+ activeAdapter.getItem(0).getResolveInfo(),
+ is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
+
+ // Click on the direct target
+ String name = serviceTargets.get(0).getTitle().toString();
+ onView(withText(name))
+ .perform(click());
+ waitForIdle();
+
+ FakeEventLog eventLog = getEventLog(activity);
+ assertThat(eventLog.getShareTargetSelected()).hasSize(1);
+ FakeEventLog.ShareTargetSelected call = eventLog.getShareTargetSelected().get(0);
+
+ assertThat(call.getTargetType()).isEqualTo(EventLog.SELECTION_TYPE_SERVICE);
+ assertThat(call.getDirectTargetAlsoRanked()).isEqualTo(0);
+ }
+
+ @Test
+ public void testShortcutTargetWithApplyAppLimits() {
+ // Set up resources
+ Resources resources = Mockito.spy(
+ InstrumentationRegistry.getInstrumentation().getContext().getResources());
+ ChooserActivityOverrideData.getInstance().resources = resources;
+ doReturn(1).when(resources).getInteger(R.integer.config_maxShortcutTargetsPerApp);
+ Intent sendIntent = createSendTextIntent();
+ // We need app targets for direct targets to get displayed
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ // create test shortcut loader factory, remember loaders and their callbacks
+ SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+ createShortcutLoaderFactory();
+
+ // Start activity
+ final IChooserWrapper activity = (IChooserWrapper) mActivityRule
+ .launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // verify that ShortcutLoader was queried
+ ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+ ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+ verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture());
+
+ // send shortcuts
+ assertThat(
+ "Wrong number of app targets",
+ appTargets.getValue().length,
+ is(resolvedComponentInfos.size()));
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(
+ 2,
+ resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+ ShortcutLoader.Result result = new ShortcutLoader.Result(
+ true,
+ appTargets.getValue(),
+ new ShortcutLoader.ShortcutResultInfo[] {
+ new ShortcutLoader.ShortcutResultInfo(
+ appTargets.getValue()[0],
+ serviceTargets
+ )
+ },
+ new HashMap<>(),
+ new HashMap<>()
+ );
+ activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+ waitForIdle();
+
+ final ChooserListAdapter activeAdapter = activity.getAdapter();
+ assertThat(
+ "Chooser should have 3 targets (2 apps, 1 direct)",
+ activeAdapter.getCount(),
+ is(3));
+ assertThat(
+ "Chooser should have exactly one selectable direct target",
+ activeAdapter.getSelectableServiceTargetCount(),
+ is(1));
+ assertThat(
+ "The resolver info must match the resolver info used to create the target",
+ activeAdapter.getItem(0).getResolveInfo(),
+ is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
+ assertThat(
+ "The display label must match",
+ activeAdapter.getItem(0).getDisplayLabel(),
+ is("testTitle0"));
+ }
+
+ @Test
+ public void testShortcutTargetWithoutApplyAppLimits() {
+ setDeviceConfigProperty(
+ SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
+ Boolean.toString(false));
+ // Set up resources
+ Resources resources = Mockito.spy(
+ InstrumentationRegistry.getInstrumentation().getContext().getResources());
+ ChooserActivityOverrideData.getInstance().resources = resources;
+ doReturn(1).when(resources).getInteger(R.integer.config_maxShortcutTargetsPerApp);
+ Intent sendIntent = createSendTextIntent();
+ // We need app targets for direct targets to get displayed
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ // create test shortcut loader factory, remember loaders and their callbacks
+ SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+ createShortcutLoaderFactory();
+
+ // Start activity
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // verify that ShortcutLoader was queried
+ ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+ ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+ verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture());
+
+ // send shortcuts
+ assertThat(
+ "Wrong number of app targets",
+ appTargets.getValue().length,
+ is(resolvedComponentInfos.size()));
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(
+ 2,
+ resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+ ShortcutLoader.Result result = new ShortcutLoader.Result(
+ true,
+ appTargets.getValue(),
+ new ShortcutLoader.ShortcutResultInfo[] {
+ new ShortcutLoader.ShortcutResultInfo(
+ appTargets.getValue()[0],
+ serviceTargets
+ )
+ },
+ new HashMap<>(),
+ new HashMap<>()
+ );
+ activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+ waitForIdle();
+
+ final ChooserListAdapter activeAdapter = activity.getAdapter();
+ assertThat(
+ "Chooser should have 4 targets (2 apps, 2 direct)",
+ activeAdapter.getCount(),
+ is(4));
+ assertThat(
+ "Chooser should have exactly two selectable direct target",
+ activeAdapter.getSelectableServiceTargetCount(),
+ is(2));
+ assertThat(
+ "The resolver info must match the resolver info used to create the target",
+ activeAdapter.getItem(0).getResolveInfo(),
+ is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
+ assertThat(
+ "The display label must match",
+ activeAdapter.getItem(0).getDisplayLabel(),
+ is("testTitle0"));
+ assertThat(
+ "The display label must match",
+ activeAdapter.getItem(1).getDisplayLabel(),
+ is("testTitle1"));
+ }
+
+ @Test
+ public void testLaunchWithCallerProvidedTarget() {
+ setDeviceConfigProperty(
+ SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
+ Boolean.toString(false));
+ // Set up resources
+ Resources resources = Mockito.spy(
+ InstrumentationRegistry.getInstrumentation().getContext().getResources());
+ ChooserActivityOverrideData.getInstance().resources = resources;
+ doReturn(1).when(resources).getInteger(R.integer.config_maxShortcutTargetsPerApp);
+
+ // We need app targets for direct targets to get displayed
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos, resolvedComponentInfos);
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+
+ // set caller-provided target
+ Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null);
+ String callerTargetLabel = "Caller Target";
+ ChooserTarget[] targets = new ChooserTarget[] {
+ new ChooserTarget(
+ callerTargetLabel,
+ Icon.createWithBitmap(createBitmap()),
+ 0.1f,
+ resolvedComponentInfos.get(0).name,
+ new Bundle())
+ };
+ chooserIntent.putExtra(Intent.EXTRA_CHOOSER_TARGETS, targets);
+
+ // create test shortcut loader factory, remember loaders and their callbacks
+ SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+ createShortcutLoaderFactory();
+
+ // Start activity
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(chooserIntent);
+ waitForIdle();
+
+ // verify that ShortcutLoader was queried
+ ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+ ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+ verify(shortcutLoaders.get(0).first, times(1)).updateAppTargets(appTargets.capture());
+
+ // send shortcuts
+ assertThat(
+ "Wrong number of app targets",
+ appTargets.getValue().length,
+ is(resolvedComponentInfos.size()));
+ ShortcutLoader.Result result = new ShortcutLoader.Result(
+ true,
+ appTargets.getValue(),
+ new ShortcutLoader.ShortcutResultInfo[0],
+ new HashMap<>(),
+ new HashMap<>());
+ activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+ waitForIdle();
+
+ final ChooserListAdapter activeAdapter = activity.getAdapter();
+ assertThat(
+ "Chooser should have 3 targets (2 apps, 1 direct)",
+ activeAdapter.getCount(),
+ is(3));
+ assertThat(
+ "Chooser should have exactly two selectable direct target",
+ activeAdapter.getSelectableServiceTargetCount(),
+ is(1));
+ assertThat(
+ "The display label must match",
+ activeAdapter.getItem(0).getDisplayLabel(),
+ is(callerTargetLabel));
+
+ // Switch to work profile and ensure that the target *doesn't* show up there.
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+
+ for (int i = 0; i < activity.getWorkListAdapter().getCount(); i++) {
+ assertThat(
+ "Chooser target should not show up in opposite profile",
+ activity.getWorkListAdapter().getItem(i).getDisplayLabel(),
+ not(callerTargetLabel));
+ }
+ }
+
+ @Test
+ public void testLaunchWithCustomAction() throws InterruptedException {
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ Context testContext = InstrumentationRegistry.getInstrumentation().getContext();
+ final String customActionLabel = "Custom Action";
+ final String testAction = "test-broadcast-receiver-action";
+ Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null);
+ chooserIntent.putExtra(
+ Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS,
+ new ChooserAction[] {
+ new ChooserAction.Builder(
+ Icon.createWithResource("", Resources.ID_NULL),
+ customActionLabel,
+ PendingIntent.getBroadcast(
+ testContext,
+ 123,
+ new Intent(testAction),
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT))
+ .build()
+ });
+ // Start activity
+ mActivityRule.launchActivity(chooserIntent);
+ waitForIdle();
+
+ final CountDownLatch broadcastInvoked = new CountDownLatch(1);
+ BroadcastReceiver testReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ broadcastInvoked.countDown();
+ }
+ };
+ testContext.registerReceiver(testReceiver, new IntentFilter(testAction),
+ Context.RECEIVER_EXPORTED);
+
+ try {
+ onView(withText(customActionLabel)).perform(click());
+ assertTrue("Timeout waiting for broadcast",
+ broadcastInvoked.await(5000, TimeUnit.MILLISECONDS));
+ } finally {
+ testContext.unregisterReceiver(testReceiver);
+ }
+ }
+
+ @Test
+ public void testLaunchWithShareModification() throws InterruptedException {
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ Context testContext = InstrumentationRegistry.getInstrumentation().getContext();
+ final String modifyShareAction = "test-broadcast-receiver-action";
+ Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null);
+ String label = "modify share";
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(
+ testContext,
+ 123,
+ new Intent(modifyShareAction),
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT);
+ ChooserAction action = new ChooserAction.Builder(Icon.createWithBitmap(
+ createBitmap()), label, pendingIntent).build();
+ chooserIntent.putExtra(
+ Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION,
+ action);
+ // Start activity
+ mActivityRule.launchActivity(chooserIntent);
+ waitForIdle();
+
+ final CountDownLatch broadcastInvoked = new CountDownLatch(1);
+ BroadcastReceiver testReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ broadcastInvoked.countDown();
+ }
+ };
+ testContext.registerReceiver(testReceiver, new IntentFilter(modifyShareAction),
+ Context.RECEIVER_EXPORTED);
+
+ try {
+ onView(withText(label)).perform(click());
+ assertTrue("Timeout waiting for broadcast",
+ broadcastInvoked.await(5000, TimeUnit.MILLISECONDS));
+
+ } finally {
+ testContext.unregisterReceiver(testReceiver);
+ }
+ }
+
+ @Test
+ public void testUpdateMaxTargetsPerRow_columnCountIsUpdated() throws InterruptedException {
+ updateMaxTargetsPerRowResource(/* targetsPerRow= */ 4);
+ givenAppTargets(/* appCount= */ 16);
+ Intent sendIntent = createSendTextIntent();
+ final ChooserActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+
+ updateMaxTargetsPerRowResource(/* targetsPerRow= */ 6);
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(() -> activity.onConfigurationChanged(
+ InstrumentationRegistry.getInstrumentation()
+ .getContext().getResources().getConfiguration()));
+
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.resolver_list))
+ .check(matches(withGridColumnCount(6)));
+ }
+
+ // This test is too long and too slow and should not be taken as an example for future tests.
+ @Test @Ignore
+ public void testDirectTargetLoggingWithAppTargetNotRankedPortrait()
+ throws InterruptedException {
+ testDirectTargetLoggingWithAppTargetNotRanked(Configuration.ORIENTATION_PORTRAIT, 4);
+ }
+
+ @Test @Ignore
+ public void testDirectTargetLoggingWithAppTargetNotRankedLandscape()
+ throws InterruptedException {
+ testDirectTargetLoggingWithAppTargetNotRanked(Configuration.ORIENTATION_LANDSCAPE, 8);
+ }
+
+ private void testDirectTargetLoggingWithAppTargetNotRanked(
+ int orientation, int appTargetsExpected) {
+ Configuration configuration =
+ new Configuration(InstrumentationRegistry.getInstrumentation().getContext()
+ .getResources().getConfiguration());
+ configuration.orientation = orientation;
+
+ Resources resources = Mockito.spy(
+ InstrumentationRegistry.getInstrumentation().getContext().getResources());
+ ChooserActivityOverrideData.getInstance().resources = resources;
+ doReturn(configuration).when(resources).getConfiguration();
+
+ Intent sendIntent = createSendTextIntent();
+ // We need app targets for direct targets to get displayed
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(15);
+ setupResolverControllers(resolvedComponentInfos);
+
+ // Create direct share target
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(1,
+ resolvedComponentInfos.get(14).getResolveInfoAt(0).activityInfo.packageName);
+ ResolveInfo ri = ResolverDataProvider.createResolveInfo(16, 0, PERSONAL_USER_HANDLE);
+
+ // Start activity
+ ChooserWrapperActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ // Insert the direct share target
+ Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
+ directShareToShortcutInfos.put(serviceTargets.get(0), null);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> activity.getAdapter().addServiceResults(
+ activity.createTestDisplayResolveInfo(sendIntent,
+ ri,
+ "testLabel",
+ "testInfo",
+ sendIntent),
+ serviceTargets,
+ TARGET_TYPE_CHOOSER_TARGET,
+ directShareToShortcutInfos,
+ /* directShareToAppTargets */ null)
+ );
+
+ assertThat(
+ String.format("Chooser should have %d targets (%d apps, 1 direct, 15 A-Z)",
+ appTargetsExpected + 16, appTargetsExpected),
+ activity.getAdapter().getCount(), is(appTargetsExpected + 16));
+ assertThat("Chooser should have exactly one selectable direct target",
+ activity.getAdapter().getSelectableServiceTargetCount(), is(1));
+ assertThat("The resolver info must match the resolver info used to create the target",
+ activity.getAdapter().getItem(0).getResolveInfo(), is(ri));
+
+ // Click on the direct target
+ String name = serviceTargets.get(0).getTitle().toString();
+ onView(withText(name))
+ .perform(click());
+ waitForIdle();
+
+ FakeEventLog eventLog = getEventLog(activity);
+ var invocations = eventLog.getShareTargetSelected();
+ assertWithMessage("Only one ShareTargetSelected event logged")
+ .that(invocations).hasSize(1);
+ FakeEventLog.ShareTargetSelected call = invocations.get(0);
+ assertWithMessage("targetType should be SELECTION_TYPE_SERVICE")
+ .that(call.getTargetType()).isEqualTo(EventLog.SELECTION_TYPE_SERVICE);
+ assertWithMessage(
+ "The packages shouldn't match for app target and direct target")
+ .that(call.getDirectTargetAlsoRanked()).isEqualTo(-1);
+ }
+
+ @Test
+ public void testWorkTab_displayedWhenWorkProfileUserAvailable() {
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+
+ onView(withId(android.R.id.tabs)).check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testWorkTab_hiddenWhenWorkProfileUserNotAvailable() {
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+
+ onView(withId(android.R.id.tabs)).check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ public void testWorkTab_eachTabUsesExpectedAdapter() {
+ int personalProfileTargets = 3;
+ int otherProfileTargets = 1;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(
+ personalProfileTargets + otherProfileTargets, /* userID */ 10);
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(
+ workProfileTargets);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+
+ assertThat(activity.getCurrentUserHandle().getIdentifier(), is(0));
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10));
+ assertThat(activity.getPersonalListAdapter().getCount(), is(personalProfileTargets));
+ assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets));
+ }
+
+ @Test
+ public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+
+ assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets));
+ }
+
+ @Test @Ignore
+ public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+
+ onView(first(allOf(
+ withText(workResolvedComponentInfos.get(0)
+ .getResolveInfoAt(0).activityInfo.applicationInfo.name),
+ isDisplayed())))
+ .perform(click());
+ waitForIdle();
+ assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0)));
+ }
+
+ @Test
+ public void testWorkTab_crossProfileIntentsDisabled_personalToWork_emptyStateShown() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets);
+ ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false;
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.contentPanel))
+ .perform(swipeUp());
+
+ onView(withText(R.string.resolver_cross_profile_blocked))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testWorkTab_workProfileDisabled_emptyStateShown() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets);
+ ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true;
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.contentPanel))
+ .perform(swipeUp());
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+
+ onView(withText(R.string.resolver_turn_on_work_apps))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testWorkTab_noWorkAppsAvailable_emptyStateShown() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(0);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.contentPanel))
+ .perform(swipeUp());
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+
+ onView(withText(R.string.resolver_no_work_apps_available))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_SCROLLABLE_PREVIEW)
+ public void testWorkTab_previewIsScrollable() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(300);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+
+ Uri uri = createTestContentProviderUri("image/png", null);
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createWideBitmap());
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Scrollable preview test"));
+ waitForIdle();
+
+ onView(withId(com.android.intentresolver.R.id.scrollable_image_preview))
+ .check(matches(isDisplayed()));
+
+ onView(withId(com.android.internal.R.id.contentPanel)).perform(swipeUp());
+ waitForIdle();
+
+ onView(withId(com.android.intentresolver.R.id.chooser_headline_row_container))
+ .check(matches(isCompletelyDisplayed()));
+ onView(withId(com.android.intentresolver.R.id.headline))
+ .check(matches(isDisplayed()));
+ onView(withId(com.android.intentresolver.R.id.scrollable_image_preview))
+ .check(matches(not(isDisplayed())));
+ }
+
+ @Ignore // b/220067877
+ @Test
+ public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(0);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true;
+ ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false;
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.contentPanel))
+ .perform(swipeUp());
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+
+ onView(withText(R.string.resolver_cross_profile_blocked))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(0);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true;
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.contentPanel))
+ .perform(swipeUp());
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+
+ onView(withText(R.string.resolver_no_work_apps_available))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test @Ignore("b/222124533")
+ public void testAppTargetLogging() throws InterruptedException {
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // TODO(b/222124533): other test cases use a timeout to make sure that the UI is fully
+ // populated; without one, this test flakes. Ideally we should address the need for a
+ // timeout everywhere instead of introducing one to fix this particular test.
+
+ assertThat(activity.getAdapter().getCount(), is(2));
+ onView(withId(com.android.internal.R.id.profile_button)).check(doesNotExist());
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0);
+ onView(withText(toChoose.activityInfo.name))
+ .perform(click());
+ waitForIdle();
+
+ // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
+ }
+
+ @Test
+ public void testDirectTargetLogging() {
+ Intent sendIntent = createSendTextIntent();
+ // We need app targets for direct targets to get displayed
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ // create test shortcut loader factory, remember loaders and their callbacks
+ SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+ new SparseArray<>();
+ ChooserActivityOverrideData.getInstance().shortcutLoaderFactory =
+ (userHandle, callback) -> {
+ Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>> pair =
+ new Pair<>(mock(ShortcutLoader.class), callback);
+ shortcutLoaders.put(userHandle.getIdentifier(), pair);
+ return pair.first;
+ };
+
+ // Start activity
+ ChooserWrapperActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // verify that ShortcutLoader was queried
+ ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+ ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+ verify(shortcutLoaders.get(0).first, times(1))
+ .updateAppTargets(appTargets.capture());
+
+ // send shortcuts
+ assertThat(
+ "Wrong number of app targets",
+ appTargets.getValue().length,
+ is(resolvedComponentInfos.size()));
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(1,
+ resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+ ShortcutLoader.Result result = new ShortcutLoader.Result(
+ // TODO: test another value as well
+ false,
+ appTargets.getValue(),
+ new ShortcutLoader.ShortcutResultInfo[] {
+ new ShortcutLoader.ShortcutResultInfo(
+ appTargets.getValue()[0],
+ serviceTargets
+ )
+ },
+ new HashMap<>(),
+ new HashMap<>()
+ );
+ activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+ waitForIdle();
+
+ assertThat("Chooser should have 3 targets (2 apps, 1 direct)",
+ activity.getAdapter().getCount(), is(3));
+ assertThat("Chooser should have exactly one selectable direct target",
+ activity.getAdapter().getSelectableServiceTargetCount(), is(1));
+ assertThat(
+ "The resolver info must match the resolver info used to create the target",
+ activity.getAdapter().getItem(0).getResolveInfo(),
+ is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
+
+ // Click on the direct target
+ String name = serviceTargets.get(0).getTitle().toString();
+ onView(withText(name))
+ .perform(click());
+ waitForIdle();
+
+ FakeEventLog eventLog = getEventLog(activity);
+ assertThat(eventLog.getShareTargetSelected()).hasSize(1);
+ FakeEventLog.ShareTargetSelected call = eventLog.getShareTargetSelected().get(0);
+ assertThat(call.getTargetType()).isEqualTo(EventLog.SELECTION_TYPE_SERVICE);
+ }
+
+ @Test
+ public void testDirectTargetPinningDialog() {
+ Intent sendIntent = createSendTextIntent();
+ // We need app targets for direct targets to get displayed
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ // create test shortcut loader factory, remember loaders and their callbacks
+ SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+ new SparseArray<>();
+ ChooserActivityOverrideData.getInstance().shortcutLoaderFactory =
+ (userHandle, callback) -> {
+ Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>> pair =
+ new Pair<>(mock(ShortcutLoader.class), callback);
+ shortcutLoaders.put(userHandle.getIdentifier(), pair);
+ return pair.first;
+ };
+
+ // Start activity
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // verify that ShortcutLoader was queried
+ ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+ ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+ verify(shortcutLoaders.get(0).first, times(1))
+ .updateAppTargets(appTargets.capture());
+
+ // send shortcuts
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(
+ 1,
+ resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+ ShortcutLoader.Result result = new ShortcutLoader.Result(
+ // TODO: test another value as well
+ false,
+ appTargets.getValue(),
+ new ShortcutLoader.ShortcutResultInfo[] {
+ new ShortcutLoader.ShortcutResultInfo(
+ appTargets.getValue()[0],
+ serviceTargets
+ )
+ },
+ new HashMap<>(),
+ new HashMap<>()
+ );
+ activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+ waitForIdle();
+
+ // Long-click on the direct target
+ String name = serviceTargets.get(0).getTitle().toString();
+ onView(withText(name)).perform(longClick());
+ waitForIdle();
+
+ onView(withId(R.id.chooser_dialog_content)).check(matches(isDisplayed()));
+ }
+
+ @Test @Ignore
+ public void testEmptyDirectRowLogging() throws InterruptedException {
+ Intent sendIntent = createSendTextIntent();
+ // We need app targets for direct targets to get displayed
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ // Start activity
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+
+ // Thread.sleep shouldn't be a thing in an integration test but it's
+ // necessary here because of the way the code is structured
+ Thread.sleep(3000);
+
+ assertThat("Chooser should have 2 app targets",
+ activity.getAdapter().getCount(), is(2));
+ assertThat("Chooser should have no direct targets",
+ activity.getAdapter().getSelectableServiceTargetCount(), is(0));
+
+ // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
+ }
+
+ @Ignore // b/220067877
+ @Test
+ public void testCopyTextToClipboardLogging() throws Exception {
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withId(com.android.internal.R.id.chooser_copy_button)).check(matches(isDisplayed()));
+ onView(withId(com.android.internal.R.id.chooser_copy_button)).perform(click());
+
+ // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
+ }
+
+ @Test @Ignore("b/222124533")
+ public void testSwitchProfileLogging() throws InterruptedException {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+ onView(withText(R.string.resolver_personal_tab)).perform(click());
+ waitForIdle();
+
+ // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
+ }
+
+ @Test
+ public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_doesNotAutoLaunch() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets);
+ ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false;
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test"));
+ waitForIdle();
+
+ assertNull(chosen[0]);
+ }
+
+ @Test
+ public void testOneInitialIntent_noAutolaunch() {
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(1);
+ setupResolverControllers(personalResolvedComponentInfos);
+ Intent chooserIntent = createChooserIntent(createSendTextIntent(),
+ new Intent[] {new Intent("action.fake")});
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartInternalCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+ ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class);
+ ResolveInfo ri = createFakeResolveInfo();
+ when(
+ ChooserActivityOverrideData
+ .getInstance().packageManager
+ .resolveActivity(any(Intent.class), any()))
+ .thenReturn(ri);
+ waitForIdle();
+
+ IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent);
+ waitForIdle();
+
+ assertNull(chosen[0]);
+ assertThat(activity
+ .getPersonalListAdapter().getCallerTargetCount(), is(1));
+ }
+
+ @Test
+ public void testWorkTab_withInitialIntents_workTabDoesNotIncludePersonalInitialIntents() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ int workProfileTargets = 1;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent[] initialIntents = {
+ new Intent("action.fake1"),
+ new Intent("action.fake2")
+ };
+ Intent chooserIntent = createChooserIntent(createSendTextIntent(), initialIntents);
+ ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class);
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .packageManager
+ .resolveActivity(any(Intent.class), any()))
+ .thenReturn(createFakeResolveInfo());
+ waitForIdle();
+
+ IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent);
+ waitForIdle();
+
+ assertThat(activity.getPersonalListAdapter().getCallerTargetCount(), is(2));
+ assertThat(activity.getWorkListAdapter().getCallerTargetCount(), is(0));
+ }
+
+ @Test
+ public void testWorkTab_xProfileIntentsDisabled_personalToWork_nonSendIntent_emptyStateShown() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets);
+ ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false;
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent[] initialIntents = {
+ new Intent("action.fake1"),
+ new Intent("action.fake2")
+ };
+ Intent chooserIntent = createChooserIntent(new Intent(), initialIntents);
+ ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class);
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .packageManager
+ .resolveActivity(any(Intent.class), any()))
+ .thenReturn(createFakeResolveInfo());
+
+ mActivityRule.launchActivity(chooserIntent);
+ waitForIdle();
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.contentPanel))
+ .perform(swipeUp());
+
+ onView(withText(R.string.resolver_cross_profile_blocked))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testWorkTab_noWorkAppsAvailable_nonSendIntent_emptyStateShown() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(0);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent[] initialIntents = {
+ new Intent("action.fake1"),
+ new Intent("action.fake2")
+ };
+ Intent chooserIntent = createChooserIntent(new Intent(), initialIntents);
+ ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class);
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .packageManager
+ .resolveActivity(any(Intent.class), any()))
+ .thenReturn(createFakeResolveInfo());
+
+ mActivityRule.launchActivity(chooserIntent);
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.contentPanel))
+ .perform(swipeUp());
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+
+ onView(withText(R.string.resolver_no_work_apps_available))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testDeduplicateCallerTargetRankedTarget() {
+ // Create 4 ranked app targets.
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(4);
+ setupResolverControllers(personalResolvedComponentInfos);
+ // Create caller target which is duplicate with one of app targets
+ Intent chooserIntent = createChooserIntent(createSendTextIntent(),
+ new Intent[] {new Intent("action.fake")});
+ ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class);
+ ResolveInfo ri = ResolverDataProvider.createResolveInfo(0,
+ UserHandle.USER_CURRENT, PERSONAL_USER_HANDLE);
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .packageManager
+ .resolveActivity(any(Intent.class), any()))
+ .thenReturn(ri);
+ waitForIdle();
+
+ IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent);
+ waitForIdle();
+
+ // Total 4 targets (1 caller target, 3 ranked targets)
+ assertThat(activity.getAdapter().getCount(), is(4));
+ assertThat(activity.getAdapter().getCallerTargetCount(), is(1));
+ assertThat(activity.getAdapter().getRankedTargetCount(), is(3));
+ }
+
+ @Test
+ public void test_query_shortcut_loader_for_the_selected_tab() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ ShortcutLoader personalProfileShortcutLoader = mock(ShortcutLoader.class);
+ ShortcutLoader workProfileShortcutLoader = mock(ShortcutLoader.class);
+ final SparseArray<ShortcutLoader> shortcutLoaders = new SparseArray<>();
+ shortcutLoaders.put(0, personalProfileShortcutLoader);
+ shortcutLoaders.put(10, workProfileShortcutLoader);
+ ChooserActivityOverrideData.getInstance().shortcutLoaderFactory =
+ (userHandle, callback) -> shortcutLoaders.get(userHandle.getIdentifier(), null);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.contentPanel))
+ .perform(swipeUp());
+ waitForIdle();
+
+ verify(personalProfileShortcutLoader, times(1)).updateAppTargets(any());
+
+ onView(withText(R.string.resolver_work_tab)).perform(click());
+ waitForIdle();
+
+ verify(workProfileShortcutLoader, times(1)).updateAppTargets(any());
+ }
+
+ @Test
+ public void testClonedProfilePresent_personalAdapterIsSetWithPersonalProfile() {
+ // enable cloneProfile
+ markOtherProfileAvailability(/* workAvailable= */ false, /* cloneAvailable= */ true);
+ List<ResolvedComponentInfo> resolvedComponentInfos =
+ createResolvedComponentsWithCloneProfileForTest(
+ 3,
+ PERSONAL_USER_HANDLE,
+ CLONE_PROFILE_USER_HANDLE);
+ setupResolverControllers(resolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+
+ final IChooserWrapper activity = (IChooserWrapper) mActivityRule
+ .launchActivity(Intent.createChooser(sendIntent, "personalProfileTest"));
+ waitForIdle();
+
+ assertThat(activity.getPersonalListAdapter().getUserHandle(), is(PERSONAL_USER_HANDLE));
+ assertThat(activity.getAdapter().getCount(), is(3));
+ }
+
+ @Test
+ public void testClonedProfilePresent_personalTabUsesExpectedAdapter() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ true);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(
+ 4);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "multi tab test"));
+ waitForIdle();
+
+ assertThat(activity.getCurrentUserHandle(), is(PERSONAL_USER_HANDLE));
+ }
+
+ private Intent createChooserIntent(Intent intent, Intent[] initialIntents) {
+ Intent chooserIntent = new Intent();
+ chooserIntent.setAction(Intent.ACTION_CHOOSER);
+ chooserIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending");
+ chooserIntent.putExtra(Intent.EXTRA_TITLE, "some title");
+ chooserIntent.putExtra(Intent.EXTRA_INTENT, intent);
+ chooserIntent.setType("text/plain");
+ if (initialIntents != null) {
+ chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, initialIntents);
+ }
+ return chooserIntent;
+ }
+
+ /* This is a "test of a test" to make sure that our inherited test class
+ * is successfully configured to operate on the unbundled-equivalent
+ * ChooserWrapperActivity.
+ *
+ * TODO: remove after unbundling is complete.
+ */
+ @Test
+ public void testWrapperActivityHasExpectedConcreteType() {
+ final ChooserActivity activity = mActivityRule.launchActivity(
+ Intent.createChooser(new Intent("ACTION_FOO"), "foo"));
+ waitForIdle();
+ assertThat(activity).isInstanceOf(ChooserWrapperActivity.class);
+ }
+
+ private ResolveInfo createFakeResolveInfo() {
+ ResolveInfo ri = new ResolveInfo();
+ ri.activityInfo = new ActivityInfo();
+ ri.activityInfo.name = "FakeActivityName";
+ ri.activityInfo.packageName = "fake.package.name";
+ ri.activityInfo.applicationInfo = new ApplicationInfo();
+ ri.activityInfo.applicationInfo.packageName = "fake.package.name";
+ ri.userHandle = UserHandle.CURRENT;
+ return ri;
+ }
+
+ private Intent createSendTextIntent() {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending");
+ sendIntent.setType("text/plain");
+ return sendIntent;
+ }
+
+ private Intent createSendImageIntent(Uri imageThumbnail) {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_STREAM, imageThumbnail);
+ sendIntent.setType("image/png");
+ if (imageThumbnail != null) {
+ ClipData.Item clipItem = new ClipData.Item(imageThumbnail);
+ sendIntent.setClipData(new ClipData("Clip Label", new String[]{"image/png"}, clipItem));
+ }
+
+ return sendIntent;
+ }
+
+ private Uri createTestContentProviderUri(
+ @Nullable String mimeType, @Nullable String streamType) {
+ return createTestContentProviderUri(mimeType, streamType, 0);
+ }
+
+ private Uri createTestContentProviderUri(
+ @Nullable String mimeType, @Nullable String streamType, long streamTypeTimeout) {
+ String packageName =
+ InstrumentationRegistry.getInstrumentation().getContext().getPackageName();
+ Uri.Builder builder = Uri.parse("content://" + packageName + "/image.png")
+ .buildUpon();
+ if (mimeType != null) {
+ builder.appendQueryParameter(TestContentProvider.PARAM_MIME_TYPE, mimeType);
+ }
+ if (streamType != null) {
+ builder.appendQueryParameter(TestContentProvider.PARAM_STREAM_TYPE, streamType);
+ }
+ if (streamTypeTimeout > 0) {
+ builder.appendQueryParameter(
+ TestContentProvider.PARAM_STREAM_TYPE_TIMEOUT,
+ Long.toString(streamTypeTimeout));
+ }
+ return builder.build();
+ }
+
+ private Intent createSendTextIntentWithPreview(String title, Uri imageThumbnail) {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending");
+ sendIntent.putExtra(Intent.EXTRA_TITLE, title);
+ if (imageThumbnail != null) {
+ ClipData.Item clipItem = new ClipData.Item(imageThumbnail);
+ sendIntent.setClipData(new ClipData("Clip Label", new String[]{"image/png"}, clipItem));
+ }
+
+ return sendIntent;
+ }
+
+ private Intent createSendUriIntentWithPreview(ArrayList<Uri> uris) {
+ Intent sendIntent = new Intent();
+
+ if (uris.size() > 1) {
+ sendIntent.setAction(Intent.ACTION_SEND_MULTIPLE);
+ sendIntent.putExtra(Intent.EXTRA_STREAM, uris);
+ } else {
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
+ }
+
+ return sendIntent;
+ }
+
+ private Intent createViewTextIntent() {
+ Intent viewIntent = new Intent();
+ viewIntent.setAction(Intent.ACTION_VIEW);
+ viewIntent.putExtra(Intent.EXTRA_TEXT, "testing intent viewing");
+ return viewIntent;
+ }
+
+ private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < numberOfResults; i++) {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, PERSONAL_USER_HANDLE));
+ }
+ return infoList;
+ }
+
+ private List<ResolvedComponentInfo> createResolvedComponentsWithCloneProfileForTest(
+ int numberOfResults,
+ UserHandle resolvedForPersonalUser,
+ UserHandle resolvedForClonedUser) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < 1; i++) {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfo(i,
+ resolvedForPersonalUser));
+ }
+ for (int i = 1; i < numberOfResults; i++) {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfo(i,
+ resolvedForClonedUser));
+ }
+ return infoList;
+ }
+
+ private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile(
+ int numberOfResults) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < numberOfResults; i++) {
+ if (i == 0) {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i,
+ PERSONAL_USER_HANDLE));
+ } else {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfo(i,
+ PERSONAL_USER_HANDLE));
+ }
+ }
+ return infoList;
+ }
+
+ private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile(
+ int numberOfResults, int userId) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < numberOfResults; i++) {
+ if (i == 0) {
+ infoList.add(
+ ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId,
+ PERSONAL_USER_HANDLE));
+ } else {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfo(i,
+ PERSONAL_USER_HANDLE));
+ }
+ }
+ return infoList;
+ }
+
+ private List<ResolvedComponentInfo> createResolvedComponentsForTestWithUserId(
+ int numberOfResults, int userId) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < numberOfResults; i++) {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId,
+ PERSONAL_USER_HANDLE));
+ }
+ return infoList;
+ }
+
+ private List<ChooserTarget> createDirectShareTargets(int numberOfResults, String packageName) {
+ Icon icon = Icon.createWithBitmap(createBitmap());
+ String testTitle = "testTitle";
+ List<ChooserTarget> targets = new ArrayList<>();
+ for (int i = 0; i < numberOfResults; i++) {
+ ComponentName componentName;
+ if (packageName.isEmpty()) {
+ componentName = ResolverDataProvider.createComponentName(i);
+ } else {
+ componentName = new ComponentName(packageName, packageName + ".class");
+ }
+ ChooserTarget tempTarget = new ChooserTarget(
+ testTitle + i,
+ icon,
+ (float) (1 - ((i + 1) / 10.0)),
+ componentName,
+ null);
+ targets.add(tempTarget);
+ }
+ return targets;
+ }
+
+ private void waitForIdle() {
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
+
+ private Bitmap createBitmap() {
+ return createBitmap(200, 200);
+ }
+
+ private Bitmap createWideBitmap() {
+ return createWideBitmap(Color.RED);
+ }
+
+ private Bitmap createWideBitmap(int bgColor) {
+ WindowManager windowManager = InstrumentationRegistry.getInstrumentation()
+ .getTargetContext()
+ .getSystemService(WindowManager.class);
+ int width = 3000;
+ if (windowManager != null) {
+ Rect bounds = windowManager.getMaximumWindowMetrics().getBounds();
+ width = bounds.width() + 200;
+ }
+ return createBitmap(width, 100, bgColor);
+ }
+
+ private Bitmap createBitmap(int width, int height) {
+ return createBitmap(width, height, Color.RED);
+ }
+
+ private Bitmap createBitmap(int width, int height, int bgColor) {
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+ Paint paint = new Paint();
+ paint.setColor(bgColor);
+ paint.setStyle(Paint.Style.FILL);
+ canvas.drawPaint(paint);
+
+ paint.setColor(Color.WHITE);
+ paint.setAntiAlias(true);
+ paint.setTextSize(14.f);
+ paint.setTextAlign(Paint.Align.CENTER);
+ canvas.drawText("Hi!", (width / 2.f), (height / 2.f), paint);
+
+ return bitmap;
+ }
+
+ private List<ShareShortcutInfo> createShortcuts(Context context) {
+ Intent testIntent = new Intent("TestIntent");
+
+ List<ShareShortcutInfo> shortcuts = new ArrayList<>();
+ shortcuts.add(new ShareShortcutInfo(
+ new ShortcutInfo.Builder(context, "shortcut1")
+ .setIntent(testIntent).setShortLabel("label1").setRank(3).build(), // 0 2
+ new ComponentName("package1", "class1")));
+ shortcuts.add(new ShareShortcutInfo(
+ new ShortcutInfo.Builder(context, "shortcut2")
+ .setIntent(testIntent).setShortLabel("label2").setRank(7).build(), // 1 3
+ new ComponentName("package2", "class2")));
+ shortcuts.add(new ShareShortcutInfo(
+ new ShortcutInfo.Builder(context, "shortcut3")
+ .setIntent(testIntent).setShortLabel("label3").setRank(1).build(), // 2 0
+ new ComponentName("package3", "class3")));
+ shortcuts.add(new ShareShortcutInfo(
+ new ShortcutInfo.Builder(context, "shortcut4")
+ .setIntent(testIntent).setShortLabel("label4").setRank(3).build(), // 3 2
+ new ComponentName("package4", "class4")));
+
+ return shortcuts;
+ }
+
+ private void markOtherProfileAvailability(boolean workAvailable, boolean cloneAvailable) {
+ AnnotatedUserHandles.Builder handles = AnnotatedUserHandles.newBuilder();
+ handles
+ .setUserIdOfCallingApp(1234) // Must be non-negative.
+ .setUserHandleSharesheetLaunchedAs(PERSONAL_USER_HANDLE)
+ .setPersonalProfileUserHandle(PERSONAL_USER_HANDLE);
+ if (workAvailable) {
+ handles.setWorkProfileUserHandle(WORK_PROFILE_USER_HANDLE);
+ }
+ if (cloneAvailable) {
+ handles.setCloneProfileUserHandle(CLONE_PROFILE_USER_HANDLE);
+ }
+ ChooserWrapperActivity.sOverrides.annotatedUserHandles = handles.build();
+ }
+
+ private void setupResolverControllers(
+ List<ResolvedComponentInfo> personalResolvedComponentInfos) {
+ setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>());
+ }
+
+ private void setupResolverControllers(
+ List<ResolvedComponentInfo> personalResolvedComponentInfos,
+ List<ResolvedComponentInfo> workResolvedComponentInfos) {
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntentAsUser(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.SYSTEM)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .workResolverListController
+ .getResolversForIntentAsUser(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.SYSTEM)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .workResolverListController
+ .getResolversForIntentAsUser(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.of(10))))
+ .thenReturn(new ArrayList<>(workResolvedComponentInfos));
+ }
+
+ private static GridRecyclerSpanCountMatcher withGridColumnCount(int columnCount) {
+ return new GridRecyclerSpanCountMatcher(Matchers.is(columnCount));
+ }
+
+ private static class GridRecyclerSpanCountMatcher extends
+ BoundedDiagnosingMatcher<View, RecyclerView> {
+
+ private final Matcher<Integer> mIntegerMatcher;
+
+ private GridRecyclerSpanCountMatcher(Matcher<Integer> integerMatcher) {
+ super(RecyclerView.class);
+ this.mIntegerMatcher = integerMatcher;
+ }
+
+ @Override
+ protected void describeMoreTo(Description description) {
+ description.appendText("RecyclerView grid layout span count to match: ");
+ this.mIntegerMatcher.describeTo(description);
+ }
+
+ @Override
+ protected boolean matchesSafely(RecyclerView view, Description mismatchDescription) {
+ int spanCount = ((GridLayoutManager) view.getLayoutManager()).getSpanCount();
+ if (this.mIntegerMatcher.matches(spanCount)) {
+ return true;
+ } else {
+ mismatchDescription.appendText("RecyclerView grid layout span count was ")
+ .appendValue(spanCount);
+ return false;
+ }
+ }
+ }
+
+ private void givenAppTargets(int appCount) {
+ List<ResolvedComponentInfo> resolvedComponentInfos =
+ createResolvedComponentsForTest(appCount);
+ setupResolverControllers(resolvedComponentInfos);
+ }
+
+ private void updateMaxTargetsPerRowResource(int targetsPerRow) {
+ Resources resources = Mockito.spy(
+ InstrumentationRegistry.getInstrumentation().getContext().getResources());
+ ChooserActivityOverrideData.getInstance().resources = resources;
+ doReturn(targetsPerRow).when(resources).getInteger(
+ R.integer.config_chooser_max_targets_per_row);
+ }
+
+ private SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>>
+ createShortcutLoaderFactory() {
+ SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+ new SparseArray<>();
+ ChooserActivityOverrideData.getInstance().shortcutLoaderFactory =
+ (userHandle, callback) -> {
+ Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>> pair =
+ new Pair<>(mock(ShortcutLoader.class), callback);
+ shortcutLoaders.put(userHandle.getIdentifier(), pair);
+ return pair.first;
+ };
+ return shortcutLoaders;
+ }
+
+ private static ImageLoader createImageLoader(Uri uri, Bitmap bitmap) {
+ return new TestPreviewImageLoader(Collections.singletonMap(uri, bitmap));
+ }
+}
diff --git a/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityWorkProfileTest.java b/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityWorkProfileTest.java
new file mode 100644
index 0000000..e4ec177
--- /dev/null
+++ b/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityWorkProfileTest.java
@@ -0,0 +1,481 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2;
+
+import static android.testing.PollingCheck.waitFor;
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.action.ViewActions.swipeUp;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.isSelected;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+import static com.android.intentresolver.v2.ChooserWrapperActivity.sOverrides;
+import static com.android.intentresolver.v2.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.NO_BLOCKER;
+import static com.android.intentresolver.v2.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_ACCESS_BLOCKER;
+import static com.android.intentresolver.v2.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_SHARE_BLOCKER;
+import static com.android.intentresolver.v2.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_ACCESS_BLOCKER;
+import static com.android.intentresolver.v2.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_SHARE_BLOCKER;
+import static com.android.intentresolver.v2.UnbundledChooserActivityWorkProfileTest.TestCase.Tab.PERSONAL;
+import static com.android.intentresolver.v2.UnbundledChooserActivityWorkProfileTest.TestCase.Tab.WORK;
+import static org.hamcrest.CoreMatchers.not;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.companion.DeviceFilter;
+import android.content.Intent;
+import android.os.UserHandle;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.espresso.NoMatchingViewException;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.intentresolver.AnnotatedUserHandles;
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolvedComponentInfo;
+import com.android.intentresolver.ResolverDataProvider;
+import com.android.intentresolver.v2.UnbundledChooserActivityWorkProfileTest.TestCase.Tab;
+
+import junit.framework.AssertionFailedError;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import dagger.hilt.android.testing.HiltAndroidRule;
+import dagger.hilt.android.testing.HiltAndroidTest;
+
+@DeviceFilter.MediumType
+@RunWith(Parameterized.class)
+@HiltAndroidTest
+public class UnbundledChooserActivityWorkProfileTest {
+
+ private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry
+ .getInstrumentation().getTargetContext().getUser();
+ private static final UserHandle WORK_USER_HANDLE = UserHandle.of(10);
+
+ @Rule(order = 0)
+ public HiltAndroidRule mHiltAndroidRule = new HiltAndroidRule(this);
+
+ @Rule(order = 1)
+ public ActivityTestRule<ChooserWrapperActivity> mActivityRule =
+ new ActivityTestRule<>(ChooserWrapperActivity.class, false,
+ false);
+ private final TestCase mTestCase;
+
+ public UnbundledChooserActivityWorkProfileTest(TestCase testCase) {
+ mTestCase = testCase;
+ }
+
+ @Before
+ public void cleanOverrideData() {
+ // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the
+ // permissions we require (which we'll read from the manifest at runtime).
+ InstrumentationRegistry
+ .getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity();
+
+ sOverrides.reset();
+ }
+
+ @Test
+ public void testBlocker() {
+ setUpPersonalAndWorkComponentInfos();
+ sOverrides.hasCrossProfileIntents = mTestCase.hasCrossProfileIntents();
+
+ launchActivity(mTestCase.getIsSendAction());
+ switchToTab(mTestCase.getTab());
+
+ switch (mTestCase.getExpectedBlocker()) {
+ case NO_BLOCKER:
+ assertNoBlockerDisplayed();
+ break;
+ case PERSONAL_PROFILE_SHARE_BLOCKER:
+ assertCantSharePersonalAppsBlockerDisplayed();
+ break;
+ case WORK_PROFILE_SHARE_BLOCKER:
+ assertCantShareWorkAppsBlockerDisplayed();
+ break;
+ case PERSONAL_PROFILE_ACCESS_BLOCKER:
+ assertCantAccessPersonalAppsBlockerDisplayed();
+ break;
+ case WORK_PROFILE_ACCESS_BLOCKER:
+ assertCantAccessWorkAppsBlockerDisplayed();
+ break;
+ }
+ }
+
+ @Parameterized.Parameters(name = "{0}")
+ public static Collection tests() {
+ return Arrays.asList(
+ new TestCase(
+ /* isSendAction= */ true,
+ /* hasCrossProfileIntents= */ true,
+ /* myUserHandle= */ WORK_USER_HANDLE,
+ /* tab= */ WORK,
+ /* expectedBlocker= */ NO_BLOCKER
+ ),
+ new TestCase(
+ /* isSendAction= */ true,
+ /* hasCrossProfileIntents= */ false,
+ /* myUserHandle= */ WORK_USER_HANDLE,
+ /* tab= */ WORK,
+ /* expectedBlocker= */ NO_BLOCKER
+ ),
+ new TestCase(
+ /* isSendAction= */ true,
+ /* hasCrossProfileIntents= */ true,
+ /* myUserHandle= */ PERSONAL_USER_HANDLE,
+ /* tab= */ WORK,
+ /* expectedBlocker= */ NO_BLOCKER
+ ),
+ new TestCase(
+ /* isSendAction= */ true,
+ /* hasCrossProfileIntents= */ false,
+ /* myUserHandle= */ PERSONAL_USER_HANDLE,
+ /* tab= */ WORK,
+ /* expectedBlocker= */ WORK_PROFILE_SHARE_BLOCKER
+ ),
+ new TestCase(
+ /* isSendAction= */ true,
+ /* hasCrossProfileIntents= */ true,
+ /* myUserHandle= */ WORK_USER_HANDLE,
+ /* tab= */ PERSONAL,
+ /* expectedBlocker= */ NO_BLOCKER
+ ),
+ new TestCase(
+ /* isSendAction= */ true,
+ /* hasCrossProfileIntents= */ false,
+ /* myUserHandle= */ WORK_USER_HANDLE,
+ /* tab= */ PERSONAL,
+ /* expectedBlocker= */ PERSONAL_PROFILE_SHARE_BLOCKER
+ ),
+ new TestCase(
+ /* isSendAction= */ true,
+ /* hasCrossProfileIntents= */ true,
+ /* myUserHandle= */ PERSONAL_USER_HANDLE,
+ /* tab= */ PERSONAL,
+ /* expectedBlocker= */ NO_BLOCKER
+ ),
+ new TestCase(
+ /* isSendAction= */ true,
+ /* hasCrossProfileIntents= */ false,
+ /* myUserHandle= */ PERSONAL_USER_HANDLE,
+ /* tab= */ PERSONAL,
+ /* expectedBlocker= */ NO_BLOCKER
+ ),
+ new TestCase(
+ /* isSendAction= */ false,
+ /* hasCrossProfileIntents= */ true,
+ /* myUserHandle= */ WORK_USER_HANDLE,
+ /* tab= */ WORK,
+ /* expectedBlocker= */ NO_BLOCKER
+ ),
+ new TestCase(
+ /* isSendAction= */ false,
+ /* hasCrossProfileIntents= */ false,
+ /* myUserHandle= */ WORK_USER_HANDLE,
+ /* tab= */ WORK,
+ /* expectedBlocker= */ NO_BLOCKER
+ ),
+ new TestCase(
+ /* isSendAction= */ false,
+ /* hasCrossProfileIntents= */ true,
+ /* myUserHandle= */ PERSONAL_USER_HANDLE,
+ /* tab= */ WORK,
+ /* expectedBlocker= */ NO_BLOCKER
+ ),
+ new TestCase(
+ /* isSendAction= */ false,
+ /* hasCrossProfileIntents= */ false,
+ /* myUserHandle= */ PERSONAL_USER_HANDLE,
+ /* tab= */ WORK,
+ /* expectedBlocker= */ WORK_PROFILE_ACCESS_BLOCKER
+ ),
+ new TestCase(
+ /* isSendAction= */ false,
+ /* hasCrossProfileIntents= */ true,
+ /* myUserHandle= */ WORK_USER_HANDLE,
+ /* tab= */ PERSONAL,
+ /* expectedBlocker= */ NO_BLOCKER
+ ),
+ new TestCase(
+ /* isSendAction= */ false,
+ /* hasCrossProfileIntents= */ false,
+ /* myUserHandle= */ WORK_USER_HANDLE,
+ /* tab= */ PERSONAL,
+ /* expectedBlocker= */ PERSONAL_PROFILE_ACCESS_BLOCKER
+ ),
+ new TestCase(
+ /* isSendAction= */ false,
+ /* hasCrossProfileIntents= */ true,
+ /* myUserHandle= */ PERSONAL_USER_HANDLE,
+ /* tab= */ PERSONAL,
+ /* expectedBlocker= */ NO_BLOCKER
+ ),
+ new TestCase(
+ /* isSendAction= */ false,
+ /* hasCrossProfileIntents= */ false,
+ /* myUserHandle= */ PERSONAL_USER_HANDLE,
+ /* tab= */ PERSONAL,
+ /* expectedBlocker= */ NO_BLOCKER
+ )
+ );
+ }
+
+ private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile(
+ int numberOfResults, int userId, UserHandle resolvedForUser) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < numberOfResults; i++) {
+ infoList.add(
+ ResolverDataProvider
+ .createResolvedComponentInfoWithOtherId(i, userId, resolvedForUser));
+ }
+ return infoList;
+ }
+
+ private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults,
+ UserHandle resolvedForUser) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < numberOfResults; i++) {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfo(i, resolvedForUser));
+ }
+ return infoList;
+ }
+
+ private void setUpPersonalAndWorkComponentInfos() {
+ ChooserWrapperActivity.sOverrides.annotatedUserHandles = AnnotatedUserHandles.newBuilder()
+ .setUserIdOfCallingApp(1234) // Must be non-negative.
+ .setUserHandleSharesheetLaunchedAs(mTestCase.getMyUserHandle())
+ .setPersonalProfileUserHandle(PERSONAL_USER_HANDLE)
+ .setWorkProfileUserHandle(WORK_USER_HANDLE)
+ .build();
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3,
+ /* userId */ WORK_USER_HANDLE.getIdentifier(), PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets, WORK_USER_HANDLE);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ }
+
+ private void setupResolverControllers(
+ List<ResolvedComponentInfo> personalResolvedComponentInfos,
+ List<ResolvedComponentInfo> workResolvedComponentInfos) {
+ when(sOverrides.resolverListController.getResolversForIntentAsUser(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.SYSTEM)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ when(sOverrides.workResolverListController.getResolversForIntentAsUser(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.SYSTEM)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ when(sOverrides.workResolverListController.getResolversForIntentAsUser(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(WORK_USER_HANDLE)))
+ .thenReturn(new ArrayList<>(workResolvedComponentInfos));
+ }
+
+ private void waitForIdle() {
+ InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+ }
+
+ private void assertCantAccessWorkAppsBlockerDisplayed() {
+ onView(withText(R.string.resolver_cross_profile_blocked))
+ .check(matches(isDisplayed()));
+ onView(withText(R.string.resolver_cant_access_work_apps_explanation))
+ .check(matches(isDisplayed()));
+ }
+
+ private void assertCantAccessPersonalAppsBlockerDisplayed() {
+ onView(withText(R.string.resolver_cross_profile_blocked))
+ .check(matches(isDisplayed()));
+ onView(withText(R.string.resolver_cant_access_personal_apps_explanation))
+ .check(matches(isDisplayed()));
+ }
+
+ private void assertCantShareWorkAppsBlockerDisplayed() {
+ onView(withText(R.string.resolver_cross_profile_blocked))
+ .check(matches(isDisplayed()));
+ onView(withText(R.string.resolver_cant_share_with_work_apps_explanation))
+ .check(matches(isDisplayed()));
+ }
+
+ private void assertCantSharePersonalAppsBlockerDisplayed() {
+ onView(withText(R.string.resolver_cross_profile_blocked))
+ .check(matches(isDisplayed()));
+ onView(withText(R.string.resolver_cant_share_with_personal_apps_explanation))
+ .check(matches(isDisplayed()));
+ }
+
+ private void assertNoBlockerDisplayed() {
+ try {
+ onView(withText(R.string.resolver_cross_profile_blocked))
+ .check(matches(not(isDisplayed())));
+ } catch (NoMatchingViewException ignored) {
+ }
+ }
+
+ private void switchToTab(Tab tab) {
+ final int stringId = tab == Tab.WORK ? R.string.resolver_work_tab
+ : R.string.resolver_personal_tab;
+
+ waitFor(() -> {
+ onView(withText(stringId)).perform(click());
+ waitForIdle();
+
+ try {
+ onView(withText(stringId)).check(matches(isSelected()));
+ return true;
+ } catch (AssertionFailedError e) {
+ return false;
+ }
+ });
+
+ onView(withId(com.android.internal.R.id.contentPanel))
+ .perform(swipeUp());
+ waitForIdle();
+ }
+
+ private Intent createTextIntent(boolean isSendAction) {
+ Intent sendIntent = new Intent();
+ if (isSendAction) {
+ sendIntent.setAction(Intent.ACTION_SEND);
+ }
+ sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending");
+ sendIntent.setType("text/plain");
+ return sendIntent;
+ }
+
+ private void launchActivity(boolean isSendAction) {
+ Intent sendIntent = createTextIntent(isSendAction);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test"));
+ waitForIdle();
+ }
+
+ public static class TestCase {
+ private final boolean mIsSendAction;
+ private final boolean mHasCrossProfileIntents;
+ private final UserHandle mMyUserHandle;
+ private final Tab mTab;
+ private final ExpectedBlocker mExpectedBlocker;
+
+ public enum ExpectedBlocker {
+ NO_BLOCKER,
+ PERSONAL_PROFILE_SHARE_BLOCKER,
+ WORK_PROFILE_SHARE_BLOCKER,
+ PERSONAL_PROFILE_ACCESS_BLOCKER,
+ WORK_PROFILE_ACCESS_BLOCKER
+ }
+
+ public enum Tab {
+ WORK,
+ PERSONAL
+ }
+
+ public TestCase(boolean isSendAction, boolean hasCrossProfileIntents,
+ UserHandle myUserHandle, Tab tab, ExpectedBlocker expectedBlocker) {
+ mIsSendAction = isSendAction;
+ mHasCrossProfileIntents = hasCrossProfileIntents;
+ mMyUserHandle = myUserHandle;
+ mTab = tab;
+ mExpectedBlocker = expectedBlocker;
+ }
+
+ public boolean getIsSendAction() {
+ return mIsSendAction;
+ }
+
+ public boolean hasCrossProfileIntents() {
+ return mHasCrossProfileIntents;
+ }
+
+ public UserHandle getMyUserHandle() {
+ return mMyUserHandle;
+ }
+
+ public Tab getTab() {
+ return mTab;
+ }
+
+ public ExpectedBlocker getExpectedBlocker() {
+ return mExpectedBlocker;
+ }
+
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder("test");
+
+ if (mTab == WORK) {
+ result.append("WorkTab_");
+ } else {
+ result.append("PersonalTab_");
+ }
+
+ if (mIsSendAction) {
+ result.append("sendAction_");
+ } else {
+ result.append("notSendAction_");
+ }
+
+ if (mHasCrossProfileIntents) {
+ result.append("hasCrossProfileIntents_");
+ } else {
+ result.append("doesNotHaveCrossProfileIntents_");
+ }
+
+ if (mMyUserHandle.equals(PERSONAL_USER_HANDLE)) {
+ result.append("myUserIsPersonal_");
+ } else {
+ result.append("myUserIsWork_");
+ }
+
+ if (mExpectedBlocker == ExpectedBlocker.NO_BLOCKER) {
+ result.append("thenNoBlocker");
+ } else if (mExpectedBlocker == PERSONAL_PROFILE_ACCESS_BLOCKER) {
+ result.append("thenAccessBlockerOnPersonalProfile");
+ } else if (mExpectedBlocker == PERSONAL_PROFILE_SHARE_BLOCKER) {
+ result.append("thenShareBlockerOnPersonalProfile");
+ } else if (mExpectedBlocker == WORK_PROFILE_ACCESS_BLOCKER) {
+ result.append("thenAccessBlockerOnWorkProfile");
+ } else if (mExpectedBlocker == WORK_PROFILE_SHARE_BLOCKER) {
+ result.append("thenShareBlockerOnWorkProfile");
+ }
+
+ return result.toString();
+ }
+ }
+}
diff --git a/tests/integration/Android.bp b/tests/integration/Android.bp
new file mode 100644
index 0000000..f17df16
--- /dev/null
+++ b/tests/integration/Android.bp
@@ -0,0 +1,44 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "IntentResolver-tests-integration",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ "framework",
+ ],
+ resource_dirs: ["res"],
+ test_config: "AndroidTest.xml",
+ static_libs: [
+ "androidx.test.runner",
+ "IntentResolver-core",
+ "IntentResolver-tests-shared",
+ "junit",
+ "truth",
+ "truth-java8-extension",
+ ],
+ test_suites: ["general-tests"]
+}
diff --git a/tests/integration/AndroidManifest.xml b/tests/integration/AndroidManifest.xml
new file mode 100644
index 0000000..1a7b035
--- /dev/null
+++ b/tests/integration/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.intentresolver.tests.integration" >
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.intentresolver.tests.integration">
+ </instrumentation>
+</manifest>
diff --git a/tests/integration/AndroidTest.xml b/tests/integration/AndroidTest.xml
new file mode 100644
index 0000000..4a2eee9
--- /dev/null
+++ b/tests/integration/AndroidTest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<configuration description="Run IntentResolver Tests.">
+ <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+ <option name="cleanup-apks" value="true" />
+ <option name="test-file-name" value="IntentResolver-tests-integration.apk" />
+ </target_preparer>
+
+ <target_preparer class="com.android.tradefed.targetprep.DeviceSetup">
+ <option name="screen-always-on" value="on" />
+ </target_preparer>
+
+ <target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
+ <option name="run-command" value="input keyevent KEYCODE_WAKEUP" />
+ <option name="run-command" value="wm dismiss-keyguard" />
+ </target_preparer>
+
+ <target_preparer class="com.android.tradefed.targetprep.RootTargetPreparer"/>
+
+ <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+ <option name="package" value="com.android.intentresolver.tests.integration" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+ <option name="hidden-api-checks" value="false"/>
+ </test>
+</configuration>
diff --git a/tests/integration/res/values/strings.xml b/tests/integration/res/values/strings.xml
new file mode 100644
index 0000000..3115a7a
--- /dev/null
+++ b/tests/integration/res/values/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+</resources>
diff --git a/java/src-release/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt b/tests/integration/src/com/android/intentresolver/v2/data/repository/PlaceholderTest.kt
index 6bf7579..b66a190 100644
--- a/java/src-release/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt
+++ b/tests/integration/src/com/android/intentresolver/v2/data/repository/PlaceholderTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 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.
@@ -14,11 +14,14 @@
* limitations under the License.
*/
-package com.android.intentresolver.flags
+package com.android.intentresolver
-import android.content.Context
+import org.junit.Test
-class FeatureFlagRepositoryFactory {
- fun create(context: Context): FeatureFlagRepository =
- ReleaseFeatureFlagRepository(DeviceConfigProxy())
+class PlaceholderTest {
+
+ /** Allows this test target to function while tests are being developed. */
+ @Test
+ fun placeHolder() {
+ }
}
diff --git a/tests/shared/Android.bp b/tests/shared/Android.bp
new file mode 100644
index 0000000..55188ee
--- /dev/null
+++ b/tests/shared/Android.bp
@@ -0,0 +1,37 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "IntentResolver-tests-shared",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ libs: [
+ "android.test.mock",
+ "framework",
+ ],
+ static_libs: [
+ "hamcrest",
+ "IntentResolver-core",
+ "mockito-target-minus-junit4",
+ "truth"
+ ],
+}
diff --git a/java/tests/src/com/android/intentresolver/MatcherUtils.java b/tests/shared/src/com/android/intentresolver/MatcherUtils.java
index 6168968..97cc698 100644
--- a/java/tests/src/com/android/intentresolver/MatcherUtils.java
+++ b/tests/shared/src/com/android/intentresolver/MatcherUtils.java
@@ -29,7 +29,7 @@ public class MatcherUtils {
/**
* Returns a {@link Matcher} which only matches the first occurrence of a set criteria.
*/
- static <T> Matcher<T> first(final Matcher<T> matcher) {
+ public static <T> Matcher<T> first(final Matcher<T> matcher) {
return new BaseMatcher<T>() {
boolean isFirstMatch = true;
diff --git a/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt b/tests/shared/src/com/android/intentresolver/MockitoKotlinHelpers.kt
index aaa7a28..db9fbd9 100644
--- a/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt
+++ b/tests/shared/src/com/android/intentresolver/MockitoKotlinHelpers.kt
@@ -23,12 +23,14 @@ package com.android.intentresolver
* causes Kotlin to skip the null checks.
* Cloned from frameworks/base/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
*/
-
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatcher
import org.mockito.ArgumentMatchers
+import org.mockito.MockSettings
import org.mockito.Mockito
+import org.mockito.stubbing.Answer
import org.mockito.stubbing.OngoingStubbing
+import org.mockito.stubbing.Stubber
/**
* Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when
@@ -83,8 +85,10 @@ inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> =
*
* @param apply builder function to simplify stub configuration by improving type inference.
*/
-inline fun <reified T : Any> mock(apply: T.() -> Unit = {}): T = Mockito.mock(T::class.java)
- .apply(apply)
+inline fun <reified T : Any> mock(
+ mockSettings: MockSettings = Mockito.withSettings(),
+ apply: T.() -> Unit = {}
+): T = Mockito.mock(T::class.java, mockSettings).apply(apply)
/**
* Helper function for stubbing methods without the need to use backticks.
@@ -94,6 +98,11 @@ inline fun <reified T : Any> mock(apply: T.() -> Unit = {}): T = Mockito.mock(T:
fun <T> whenever(methodCall: T): OngoingStubbing<T> = Mockito.`when`(methodCall)
/**
+ * Helper function for stubbing methods without the need to use backticks.
+ */
+fun <T> Stubber.whenever(mock: T): T = `when`(mock)
+
+/**
* A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when
* kotlin tests are mocking kotlin objects and the methods take non-null parameters:
*
@@ -144,6 +153,25 @@ inline fun <reified T : Any> withArgCaptor(block: KotlinArgumentCaptor<T>.() ->
* val capturedList = captureMany<Foo> { verify(...).someMethod(capture()) }
*/
inline fun <reified T : Any> captureMany(block: KotlinArgumentCaptor<T>.() -> Unit): List<T> =
- kotlinArgumentCaptor<T>().apply{ block() }.allValues
+ kotlinArgumentCaptor<T>().apply { block() }.allValues
inline fun <reified T> anyOrNull() = ArgumentMatchers.argThat(ArgumentMatcher<T?> { true })
+
+/**
+ * Intended as a default Answer for a mock to prevent dependence on defaults.
+ *
+ * Use as:
+ * ```
+ * val context = mock<Context>(withSettings()
+ * .defaultAnswer(THROWS_EXCEPTION))
+ * ```
+ *
+ * To avoid triggering the exception during stubbing, must ONLY use one of the doXXX() methods, such
+ * as:
+ * * [doAnswer][Mockito.doAnswer]
+ * * [doCallRealMethod][Mockito.doCallRealMethod]
+ * * [doNothing][Mockito.doNothing]
+ * * [doReturn][Mockito.doReturn]
+ * * [doThrow][Mockito.doThrow]
+ */
+val THROWS_EXCEPTION = Answer { error("Unstubbed behavior was accessed.") }
diff --git a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java b/tests/shared/src/com/android/intentresolver/ResolverDataProvider.java
index 1f8d9be..db10994 100644
--- a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
+++ b/tests/shared/src/com/android/intentresolver/ResolverDataProvider.java
@@ -29,6 +29,8 @@ import android.test.mock.MockContext;
import android.test.mock.MockPackageManager;
import android.test.mock.MockResources;
+import androidx.annotation.NonNull;
+
/**
* Utility class used by resolver tests to create mock data
*/
@@ -43,7 +45,7 @@ public class ResolverDataProvider {
createResolveInfo(i, UserHandle.USER_CURRENT));
}
- static ResolvedComponentInfo createResolvedComponentInfo(int i,
+ public static ResolvedComponentInfo createResolvedComponentInfo(int i,
UserHandle resolvedForUser) {
return new ResolvedComponentInfo(
createComponentName(i),
@@ -59,7 +61,7 @@ public class ResolverDataProvider {
createResolveInfo(componentName, UserHandle.USER_CURRENT));
}
- static ResolvedComponentInfo createResolvedComponentInfo(
+ public static ResolvedComponentInfo createResolvedComponentInfo(
ComponentName componentName, Intent intent, UserHandle resolvedForUser) {
return new ResolvedComponentInfo(
componentName,
@@ -74,8 +76,8 @@ public class ResolverDataProvider {
createResolveInfo(i, USER_SOMEONE_ELSE));
}
- static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i,
- UserHandle resolvedForUser) {
+ public static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i,
+ UserHandle resolvedForUser) {
return new ResolvedComponentInfo(
createComponentName(i),
createResolverIntent(i),
@@ -89,7 +91,7 @@ public class ResolverDataProvider {
createResolveInfo(i, userId));
}
- static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i,
+ public static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i,
int userId, UserHandle resolvedForUser) {
return new ResolvedComponentInfo(
createComponentName(i),
@@ -195,28 +197,31 @@ public class ResolverDataProvider {
@Override
public Resources getResources() {
return new MockResources() {
+ @NonNull
@Override
public String getString(int id) throws NotFoundException {
if (id == 1) return appLabel;
if (id == 2) return activityLabel;
if (id == 3) return resolveInfoLabel;
- return null;
+ throw new NotFoundException();
}
};
}
};
ApplicationInfo appInfo = new ApplicationInfo() {
+ @NonNull
@Override
- public CharSequence loadLabel(PackageManager pm) {
+ public CharSequence loadLabel(@NonNull PackageManager pm) {
return appLabel;
}
};
appInfo.labelRes = 1;
ActivityInfo activityInfo = new ActivityInfo() {
+ @NonNull
@Override
- public CharSequence loadLabel(PackageManager pm) {
+ public CharSequence loadLabel(@NonNull PackageManager pm) {
return activityLabel;
}
};
@@ -224,8 +229,9 @@ public class ResolverDataProvider {
activityInfo.applicationInfo = appInfo;
ResolveInfo resolveInfo = new ResolveInfo() {
+ @NonNull
@Override
- public CharSequence loadLabel(PackageManager pm) {
+ public CharSequence loadLabel(@NonNull PackageManager pm) {
return resolveInfoLabel;
}
};
diff --git a/java/tests/src/com/android/intentresolver/TestContentPreviewViewModel.kt b/tests/shared/src/com/android/intentresolver/TestContentPreviewViewModel.kt
index d239f61..888fc16 100644
--- a/java/tests/src/com/android/intentresolver/TestContentPreviewViewModel.kt
+++ b/tests/shared/src/com/android/intentresolver/TestContentPreviewViewModel.kt
@@ -16,6 +16,7 @@
package com.android.intentresolver
+import android.content.Intent
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
@@ -29,8 +30,8 @@ class TestContentPreviewViewModel(
private val imageLoader: ImageLoader? = null,
) : BasePreviewViewModel() {
override fun createOrReuseProvider(
- chooserRequest: ChooserRequestParameters
- ): PreviewDataProvider = viewModel.createOrReuseProvider(chooserRequest)
+ targetIntent: Intent
+ ): PreviewDataProvider = viewModel.createOrReuseProvider(targetIntent)
override fun createOrReuseImageLoader(): ImageLoader =
imageLoader ?: viewModel.createOrReuseImageLoader()
diff --git a/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt b/tests/shared/src/com/android/intentresolver/TestPreviewImageLoader.kt
index bf87ed8..f0203bb 100644
--- a/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt
+++ b/tests/shared/src/com/android/intentresolver/TestPreviewImageLoader.kt
@@ -18,12 +18,12 @@ package com.android.intentresolver
import android.graphics.Bitmap
import android.net.Uri
-import androidx.lifecycle.Lifecycle
import com.android.intentresolver.contentpreview.ImageLoader
import java.util.function.Consumer
+import kotlinx.coroutines.CoroutineScope
-internal class TestPreviewImageLoader(private val bitmaps: Map<Uri, Bitmap>) : ImageLoader {
- override fun loadImage(callerLifecycle: Lifecycle, uri: Uri, callback: Consumer<Bitmap?>) {
+class TestPreviewImageLoader(private val bitmaps: Map<Uri, Bitmap>) : ImageLoader {
+ override fun loadImage(callerScope: CoroutineScope, uri: Uri, callback: Consumer<Bitmap?>) {
callback.accept(bitmaps[uri])
}
diff --git a/tests/shared/src/com/android/intentresolver/logging/FakeEventLog.kt b/tests/shared/src/com/android/intentresolver/logging/FakeEventLog.kt
new file mode 100644
index 0000000..9ed47db
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/logging/FakeEventLog.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.logging
+
+import android.net.Uri
+import android.util.HashedStringCache
+import android.util.Log
+import com.android.internal.logging.InstanceId
+import javax.inject.Inject
+
+private const val TAG = "EventLog"
+private const val LOG = true
+
+/** A fake EventLog. */
+class FakeEventLog @Inject constructor(private val instanceId: InstanceId) : EventLog {
+
+ var chooserActivityShown: ChooserActivityShown? = null
+ var actionSelected: ActionSelected? = null
+ var customActionSelected: CustomActionSelected? = null
+ var actionShareWithPreview: ActionShareWithPreview? = null
+ val shareTargetSelected: MutableList<ShareTargetSelected> = mutableListOf()
+
+ private fun log(message: () -> Any?) {
+ if (LOG) {
+ Log.d(TAG, "[%04x] ".format(instanceId.id) + message())
+ }
+ }
+
+ override fun logChooserActivityShown(
+ isWorkProfile: Boolean,
+ targetMimeType: String?,
+ systemCost: Long
+ ) {
+ chooserActivityShown = ChooserActivityShown(isWorkProfile, targetMimeType, systemCost)
+ log { chooserActivityShown }
+ }
+
+ override fun logShareStarted(
+ packageName: String?,
+ mimeType: String?,
+ appProvidedDirect: Int,
+ appProvidedApp: Int,
+ isWorkprofile: Boolean,
+ previewType: Int,
+ intent: String?,
+ customActionCount: Int,
+ modifyShareActionProvided: Boolean
+ ) {
+ log {
+ ShareStarted(
+ packageName,
+ mimeType,
+ appProvidedDirect,
+ appProvidedApp,
+ isWorkprofile,
+ previewType,
+ intent,
+ customActionCount,
+ modifyShareActionProvided
+ )
+ }
+ }
+
+ override fun logCustomActionSelected(positionPicked: Int) {
+ customActionSelected = CustomActionSelected(positionPicked)
+ log { "logCustomActionSelected(positionPicked=$positionPicked)" }
+ }
+
+ override fun logShareTargetSelected(
+ targetType: Int,
+ packageName: String?,
+ positionPicked: Int,
+ directTargetAlsoRanked: Int,
+ numCallerProvided: Int,
+ directTargetHashed: HashedStringCache.HashResult?,
+ isPinned: Boolean,
+ successfullySelected: Boolean,
+ selectionCost: Long
+ ) {
+ shareTargetSelected.add(
+ ShareTargetSelected(
+ targetType,
+ packageName,
+ positionPicked,
+ directTargetAlsoRanked,
+ numCallerProvided,
+ directTargetHashed,
+ isPinned,
+ successfullySelected,
+ selectionCost
+ )
+ )
+ log { shareTargetSelected.last() }
+ shareTargetSelected.limitSize(10)
+ }
+
+ private fun MutableList<*>.limitSize(n: Int) {
+ while (size > n) {
+ removeFirst()
+ }
+ }
+
+ override fun logDirectShareTargetReceived(category: Int, latency: Int) {
+ log { "logDirectShareTargetReceived(category=$category, latency=$latency)" }
+ }
+
+ override fun logActionShareWithPreview(previewType: Int) {
+ actionShareWithPreview = ActionShareWithPreview(previewType)
+ log { actionShareWithPreview }
+ }
+
+ override fun logActionSelected(targetType: Int) {
+ actionSelected = ActionSelected(targetType)
+ log { actionSelected }
+ }
+
+ override fun logContentPreviewWarning(uri: Uri?) {
+ log { "logContentPreviewWarning(uri=$uri)" }
+ }
+
+ override fun logSharesheetTriggered() {
+ log { "logSharesheetTriggered()" }
+ }
+
+ override fun logSharesheetAppLoadComplete() {
+ log { "logSharesheetAppLoadComplete()" }
+ }
+
+ override fun logSharesheetDirectLoadComplete() {
+ log { "logSharesheetAppLoadComplete()" }
+ }
+
+ override fun logSharesheetDirectLoadTimeout() {
+ log { "logSharesheetDirectLoadTimeout()" }
+ }
+
+ override fun logSharesheetProfileChanged() {
+ log { "logSharesheetProfileChanged()" }
+ }
+
+ override fun logSharesheetExpansionChanged(isCollapsed: Boolean) {
+ log { "logSharesheetExpansionChanged(isCollapsed=$isCollapsed)" }
+ }
+
+ override fun logSharesheetAppShareRankingTimeout() {
+ log { "logSharesheetAppShareRankingTimeout()" }
+ }
+
+ override fun logSharesheetEmptyDirectShareRow() {
+ log { "logSharesheetEmptyDirectShareRow()" }
+ }
+
+ data class ActionSelected(val targetType: Int)
+ data class CustomActionSelected(val positionPicked: Int)
+ data class ActionShareWithPreview(val previewType: Int)
+ data class ChooserActivityShown(
+ val isWorkProfile: Boolean,
+ val targetMimeType: String?,
+ val systemCost: Long
+ )
+ data class ShareStarted(
+ val packageName: String?,
+ val mimeType: String?,
+ val appProvidedDirect: Int,
+ val appProvidedApp: Int,
+ val isWorkprofile: Boolean,
+ val previewType: Int,
+ val intent: String?,
+ val customActionCount: Int,
+ val modifyShareActionProvided: Boolean
+ )
+ data class ShareTargetSelected(
+ val targetType: Int,
+ val packageName: String?,
+ val positionPicked: Int,
+ val directTargetAlsoRanked: Int,
+ val numCallerProvided: Int,
+ val directTargetHashed: HashedStringCache.HashResult?,
+ val pinned: Boolean,
+ val successfullySelected: Boolean,
+ val selectionCost: Long
+ )
+}
diff --git a/tests/shared/src/com/android/intentresolver/logging/FakeFrameworkStatsLogger.kt b/tests/shared/src/com/android/intentresolver/logging/FakeFrameworkStatsLogger.kt
new file mode 100644
index 0000000..dcf8d23
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/logging/FakeFrameworkStatsLogger.kt
@@ -0,0 +1,95 @@
+package com.android.intentresolver.logging
+/*
+ * Copyright (C) 2023 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.
+ */
+
+import com.android.internal.util.FrameworkStatsLog
+
+internal data class ShareSheetStarted(
+ val frameworkEventId: Int = FrameworkStatsLog.SHARESHEET_STARTED,
+ val appEventId: Int,
+ val packageName: String?,
+ val instanceId: Int,
+ val mimeType: String?,
+ val numAppProvidedDirectTargets: Int,
+ val numAppProvidedAppTargets: Int,
+ val isWorkProfile: Boolean,
+ val previewType: Int,
+ val intentType: Int,
+ val numCustomActions: Int,
+ val modifyShareActionProvided: Boolean
+)
+
+internal data class RankingSelected(
+ val frameworkEventId: Int = FrameworkStatsLog.RANKING_SELECTED,
+ val appEventId: Int,
+ val packageName: String?,
+ val instanceId: Int,
+ val positionPicked: Int,
+ val isPinned: Boolean
+)
+
+internal class FakeFrameworkStatsLogger : FrameworkStatsLogger {
+ var shareSheetStarted: ShareSheetStarted? = null
+ var rankingSelected: RankingSelected? = null
+ override fun write(
+ frameworkEventId: Int,
+ appEventId: Int,
+ packageName: String?,
+ instanceId: Int,
+ mimeType: String?,
+ numAppProvidedDirectTargets: Int,
+ numAppProvidedAppTargets: Int,
+ isWorkProfile: Boolean,
+ previewType: Int,
+ intentType: Int,
+ numCustomActions: Int,
+ modifyShareActionProvided: Boolean
+ ) {
+ shareSheetStarted =
+ ShareSheetStarted(
+ frameworkEventId,
+ appEventId,
+ packageName,
+ instanceId,
+ mimeType,
+ numAppProvidedDirectTargets,
+ numAppProvidedAppTargets,
+ isWorkProfile,
+ previewType,
+ intentType,
+ numCustomActions,
+ modifyShareActionProvided
+ )
+ }
+ override fun write(
+ frameworkEventId: Int,
+ appEventId: Int,
+ packageName: String?,
+ instanceId: Int,
+ positionPicked: Int,
+ isPinned: Boolean
+ ) {
+ rankingSelected =
+ RankingSelected(
+ frameworkEventId,
+ appEventId,
+ packageName,
+ instanceId,
+ positionPicked,
+ isPinned
+ )
+ }
+}
diff --git a/tests/shared/src/com/android/intentresolver/v2/platform/FakeSecureSettings.kt b/tests/shared/src/com/android/intentresolver/v2/platform/FakeSecureSettings.kt
new file mode 100644
index 0000000..4e27962
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/v2/platform/FakeSecureSettings.kt
@@ -0,0 +1,44 @@
+package com.android.intentresolver.v2.platform
+
+/**
+ * Creates a SecureSettings instance with predefined values:
+ *
+ * val settings = fakeSecureSettings {
+ * putString("stringValue", "example")
+ * putInt("intValue", 42)
+ * }
+ */
+fun fakeSecureSettings(block: FakeSecureSettings.Builder.() -> Unit): SecureSettings {
+ return FakeSecureSettings.Builder().apply(block).build()
+}
+
+/** An in memory implementation of [SecureSettings]. */
+class FakeSecureSettings private constructor(private val map: Map<String, String>) :
+ SecureSettings {
+
+ override fun getString(name: String): String? = map[name]
+ override fun getInt(name: String): Int? = getString(name)?.toIntOrNull()
+ override fun getLong(name: String): Long? = getString(name)?.toLongOrNull()
+ override fun getFloat(name: String): Float? = getString(name)?.toFloatOrNull()
+
+ class Builder {
+ private val map = mutableMapOf<String, String>()
+
+ fun putString(name: String, value: String) {
+ map[name] = value
+ }
+ fun putInt(name: String, value: Int) {
+ map[name] = value.toString()
+ }
+ fun putLong(name: String, value: Long) {
+ map[name] = value.toString()
+ }
+ fun putFloat(name: String, value: Float) {
+ map[name] = value.toString()
+ }
+
+ fun build(): SecureSettings {
+ return FakeSecureSettings(map.toMap())
+ }
+ }
+}
diff --git a/tests/shared/src/com/android/intentresolver/v2/platform/FakeUserManager.kt b/tests/shared/src/com/android/intentresolver/v2/platform/FakeUserManager.kt
new file mode 100644
index 0000000..370e5a0
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/v2/platform/FakeUserManager.kt
@@ -0,0 +1,239 @@
+package com.android.intentresolver.v2.platform
+
+import android.content.Context
+import android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE
+import android.content.Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE
+import android.content.Intent.ACTION_PROFILE_ADDED
+import android.content.Intent.ACTION_PROFILE_AVAILABLE
+import android.content.Intent.ACTION_PROFILE_REMOVED
+import android.content.Intent.ACTION_PROFILE_UNAVAILABLE
+import android.content.pm.UserInfo
+import android.content.pm.UserInfo.FLAG_FULL
+import android.content.pm.UserInfo.FLAG_INITIALIZED
+import android.content.pm.UserInfo.FLAG_PROFILE
+import android.content.pm.UserInfo.NO_PROFILE_GROUP_ID
+import android.os.IUserManager
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.annotation.NonNull
+import com.android.intentresolver.THROWS_EXCEPTION
+import com.android.intentresolver.mock
+import com.android.intentresolver.v2.data.repository.UserRepositoryImpl.UserEvent
+import com.android.intentresolver.v2.platform.FakeUserManager.State
+import com.android.intentresolver.whenever
+import kotlin.random.Random
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.consumeAsFlow
+import org.mockito.Mockito.RETURNS_SELF
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.withSettings
+
+/**
+ * A stand-in for [UserManager] to support testing of data layer components which depend on it.
+ *
+ * This fake targets system applications which need to interact with any or all of the current
+ * user's associated profiles (as reported by [getEnabledProfiles]). Support for manipulating
+ * non-profile (full) secondary users (switching active foreground user, adding or removing users)
+ * is not included.
+ *
+ * Upon creation [FakeUserManager] contains a single primary (full) user with a randomized ID. This
+ * is available from [FakeUserManager.state] using [primaryUserHandle][State.primaryUserHandle] or
+ * [getPrimaryUser][State.getPrimaryUser].
+ *
+ * To make state changes, use functions available from [FakeUserManager.state]:
+ * * [createProfile][State.createProfile]
+ * * [removeProfile][State.removeProfile]
+ * * [setQuietMode][State.setQuietMode]
+ *
+ * Any functionality not explicitly overridden here is guaranteed to throw an exception when
+ * accessed (access to the real system service is prevented).
+ */
+class FakeUserManager(val state: State = State()) :
+ UserManager(/* context = */ mockContext(), /* service = */ mockService()) {
+
+ enum class ProfileType {
+ WORK,
+ CLONE,
+ PRIVATE
+ }
+
+ override fun getProfileParent(userHandle: UserHandle): UserHandle? {
+ return state.getUserOrNull(userHandle)?.let { user ->
+ if (user.isProfile) {
+ state.getUserOrNull(UserHandle.of(user.profileGroupId))?.userHandle
+ } else {
+ null
+ }
+ }
+ }
+
+ override fun getUserInfo(userId: Int): UserInfo? {
+ return state.getUserOrNull(UserHandle.of(userId))
+ }
+
+ @Suppress("OVERRIDE_DEPRECATION")
+ override fun getEnabledProfiles(userId: Int): List<UserInfo> {
+ val user = state.users.single { it.id == userId }
+ return state.users.filter { other ->
+ user.id == other.id || user.profileGroupId == other.profileGroupId
+ }
+ }
+
+ override fun requestQuietModeEnabled(
+ enableQuietMode: Boolean,
+ @NonNull userHandle: UserHandle
+ ): Boolean {
+ state.setQuietMode(userHandle, enableQuietMode)
+ return true
+ }
+
+ override fun isQuietModeEnabled(userHandle: UserHandle): Boolean {
+ return state.getUser(userHandle).isQuietModeEnabled
+ }
+
+ override fun toString(): String {
+ return "FakeUserManager(state=$state)"
+ }
+
+ class State {
+ private val eventChannel = Channel<UserEvent>()
+ private val userInfoMap: MutableMap<UserHandle, UserInfo> = mutableMapOf()
+
+ /** The id of the primary/full/system user, which is automatically created. */
+ val primaryUserHandle: UserHandle
+
+ /**
+ * Retrieves the primary user. The value returned changes, but the values are immutable.
+ *
+ * Do not cache this value in tests, between operations.
+ */
+ fun getPrimaryUser(): UserInfo = getUser(primaryUserHandle)
+
+ private var nextUserId: Int = 100 + Random.nextInt(0, 900)
+
+ /**
+ * A flow of [UserEvent] which emulates those normally generated from system broadcasts.
+ *
+ * Events are produced by calls to [createPrimaryUser], [createProfile], [removeProfile].
+ */
+ val userEvents: Flow<UserEvent>
+
+ val users: List<UserInfo>
+ get() = userInfoMap.values.toList()
+
+ val userHandles: List<UserHandle>
+ get() = userInfoMap.keys.toList()
+
+ init {
+ primaryUserHandle = createPrimaryUser(allocateNextId())
+ userEvents = eventChannel.consumeAsFlow()
+ }
+
+ private fun allocateNextId() = nextUserId++
+
+ private fun createPrimaryUser(id: Int): UserHandle {
+ val userInfo =
+ UserInfo(id, "", "", FLAG_INITIALIZED or FLAG_FULL, USER_TYPE_FULL_SYSTEM)
+ userInfoMap[userInfo.userHandle] = userInfo
+ return userInfo.userHandle
+ }
+
+ fun getUserOrNull(handle: UserHandle): UserInfo? = userInfoMap[handle]
+
+ fun getUser(handle: UserHandle): UserInfo =
+ requireNotNull(getUserOrNull(handle)) {
+ "Expected userInfoMap to contain an entry for $handle"
+ }
+
+ fun setQuietMode(user: UserHandle, quietMode: Boolean) {
+ userInfoMap[user]?.also {
+ it.flags =
+ if (quietMode) {
+ it.flags or UserInfo.FLAG_QUIET_MODE
+ } else {
+ it.flags and UserInfo.FLAG_QUIET_MODE.inv()
+ }
+ val actions = mutableListOf<String>()
+ if (quietMode) {
+ actions += ACTION_PROFILE_UNAVAILABLE
+ if (it.isManagedProfile) {
+ actions += ACTION_MANAGED_PROFILE_UNAVAILABLE
+ }
+ } else {
+ actions += ACTION_PROFILE_AVAILABLE
+ if (it.isManagedProfile) {
+ actions += ACTION_MANAGED_PROFILE_AVAILABLE
+ }
+ }
+ actions.forEach { action ->
+ eventChannel.trySend(UserEvent(action, user, quietMode))
+ }
+ }
+ }
+
+ fun createProfile(type: ProfileType, parent: UserHandle = primaryUserHandle): UserHandle {
+ val parentUser = getUser(parent)
+ require(!parentUser.isProfile) { "Parent user cannot be a profile" }
+
+ // Ensure the parent user has a valid profileGroupId
+ if (parentUser.profileGroupId == NO_PROFILE_GROUP_ID) {
+ parentUser.profileGroupId = parentUser.id
+ }
+ val id = allocateNextId()
+ val userInfo =
+ UserInfo(id, "", "", FLAG_INITIALIZED or FLAG_PROFILE, type.toUserType()).apply {
+ profileGroupId = parentUser.profileGroupId
+ }
+ userInfoMap[userInfo.userHandle] = userInfo
+ eventChannel.trySend(UserEvent(ACTION_PROFILE_ADDED, userInfo.userHandle))
+ return userInfo.userHandle
+ }
+
+ fun removeProfile(handle: UserHandle): Boolean {
+ return userInfoMap[handle]?.let { user ->
+ require(user.isProfile) { "Only profiles can be removed" }
+ userInfoMap.remove(user.userHandle)
+ eventChannel.trySend(UserEvent(ACTION_PROFILE_REMOVED, user.userHandle))
+ return true
+ }
+ ?: false
+ }
+
+ override fun toString() = buildString {
+ append("State(nextUserId=$nextUserId, userInfoMap=[")
+ userInfoMap.entries.forEach {
+ append("UserHandle[${it.key.identifier}] = ${it.value.debugString},")
+ }
+ append("])")
+ }
+ }
+}
+
+/** A safe mock of [Context] which throws on any unstubbed method call. */
+private fun mockContext(user: UserHandle = UserHandle.SYSTEM): Context {
+ return mock<Context>(withSettings().defaultAnswer(THROWS_EXCEPTION)) {
+ doAnswer(RETURNS_SELF).whenever(this).applicationContext
+ doReturn(user).whenever(this).user
+ doReturn(user.identifier).whenever(this).userId
+ }
+}
+
+private fun FakeUserManager.ProfileType.toUserType(): String {
+ return when (this) {
+ FakeUserManager.ProfileType.WORK -> UserManager.USER_TYPE_PROFILE_MANAGED
+ FakeUserManager.ProfileType.CLONE -> UserManager.USER_TYPE_PROFILE_CLONE
+ FakeUserManager.ProfileType.PRIVATE -> UserManager.USER_TYPE_PROFILE_PRIVATE
+ }
+}
+
+/** A safe mock of [IUserManager] which throws on any unstubbed method call. */
+fun mockService(): IUserManager {
+ return mock<IUserManager>(withSettings().defaultAnswer(THROWS_EXCEPTION))
+}
+
+val UserInfo.debugString: String
+ get() =
+ "UserInfo(id=$id, profileGroupId=$profileGroupId, name=$name, " +
+ "type=$userType, flags=${UserInfo.flagsToString(flags)})"
diff --git a/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt b/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt
new file mode 100644
index 0000000..1ff0ce8
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt
@@ -0,0 +1,22 @@
+package com.android.intentresolver.v2.validation
+
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.IterableSubject
+import com.google.common.truth.Subject
+import com.google.common.truth.Truth.assertAbout
+
+class ValidationResultSubject(metadata: FailureMetadata, private val actual: ValidationResult<*>?) :
+ Subject(metadata, actual) {
+
+ fun isSuccess() = check("isSuccess()").that(actual?.isSuccess()).isTrue()
+ fun isFailure() = check("isSuccess()").that(actual?.isSuccess()).isFalse()
+
+ fun value(): Subject = check("value").that(actual?.value)
+
+ fun findings(): IterableSubject = check("findings").that(actual?.findings)
+
+ companion object {
+ fun assertThat(input: ValidationResult<*>): ValidationResultSubject =
+ assertAbout(::ValidationResultSubject).that(input)
+ }
+}
diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp
new file mode 100644
index 0000000..a07af1a
--- /dev/null
+++ b/tests/unit/Android.bp
@@ -0,0 +1,62 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+android_test {
+ name: "IntentResolver-tests-unit",
+ manifest: "AndroidManifest.xml",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+
+ libs: [
+ "android.test.runner",
+ "android.test.base",
+ "android.test.mock",
+ "framework",
+ "framework-res",
+ ],
+
+ resource_dirs: ["res"],
+ test_config: "AndroidTest.xml",
+ static_libs: [
+ "androidx.test.core",
+ "androidx.test.ext.junit",
+ "androidx.test.ext.truth",
+ "androidx.test.espresso.contrib",
+ "androidx.test.espresso.core",
+ "androidx.test.rules",
+ "androidx.test.runner",
+ "androidx.lifecycle_lifecycle-common-java8",
+ "androidx.lifecycle_lifecycle-extensions",
+ "androidx.lifecycle_lifecycle-runtime-testing",
+ "IntentResolver-core",
+ "IntentResolver-tests-shared",
+ "junit",
+ "kotlinx_coroutines_test",
+ "mockito-target-minus-junit4",
+ "testables", // TestableContext/TestableResources
+ "truth",
+ "truth-java8-extension",
+ "flag-junit",
+ "platform-test-annotations",
+ ],
+ test_suites: ["general-tests"],
+}
diff --git a/tests/unit/AndroidManifest.xml b/tests/unit/AndroidManifest.xml
new file mode 100644
index 0000000..80bc784
--- /dev/null
+++ b/tests/unit/AndroidManifest.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2021 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.intentresolver.tests.unit">
+
+ <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
+ android:targetPackage="com.android.intentresolver.tests.unit">
+ </instrumentation>
+</manifest>
diff --git a/java/tests/AndroidTest.xml b/tests/unit/AndroidTest.xml
index d1d77c1..2815c93 100644
--- a/java/tests/AndroidTest.xml
+++ b/tests/unit/AndroidTest.xml
@@ -15,14 +15,12 @@
-->
<configuration description="Run IntentResolver Tests.">
<target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
- <option name="test-file-name" value="IntentResolverUnitTests.apk" />
+ <option name="test-file-name" value="IntentResolver-tests-unit.apk" />
</target_preparer>
- <option name="test-suite-tag" value="apct" />
- <option name="test-tag" value="IntentResolverUnitTests" />
<test class="com.android.tradefed.testtype.AndroidJUnitTest" >
- <option name="package" value="com.android.intentresolver.tests" />
- <option name="runner" value="android.testing.TestableInstrumentation" />
+ <option name="package" value="com.android.intentresolver.tests.unit" />
+ <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
<option name="hidden-api-checks" value="false"/>
</test>
</configuration>
diff --git a/tests/unit/res/values/strings.xml b/tests/unit/res/values/strings.xml
new file mode 100644
index 0000000..3115a7a
--- /dev/null
+++ b/tests/unit/res/values/strings.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2023 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 xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+</resources>
diff --git a/java/tests/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt b/tests/unit/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt
index cd2fbc7..cd2fbc7 100644
--- a/java/tests/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt
+++ b/tests/unit/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt
diff --git a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt b/tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt
index af6e5f1..55a94eb 100644
--- a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt
+++ b/tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt
@@ -20,6 +20,7 @@ import android.app.Activity
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
+import android.content.Context.RECEIVER_EXPORTED
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Resources
@@ -35,6 +36,7 @@ import java.util.concurrent.TimeUnit
import java.util.function.Consumer
import org.junit.After
import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -67,7 +69,7 @@ class ChooserActionFactoryTest {
@Before
fun setup() {
- context.registerReceiver(testReceiver, IntentFilter(testAction))
+ context.registerReceiver(testReceiver, IntentFilter(testAction), RECEIVER_EXPORTED)
}
@After
@@ -90,7 +92,7 @@ class ChooserActionFactoryTest {
Mockito.verify(logger).logCustomActionSelected(eq(0))
assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn)
// Verify the pending intent has been called
- countdown.await(500, TimeUnit.MILLISECONDS)
+ assertTrue("Timed out waiting for broadcast", countdown.await(2500, TimeUnit.MILLISECONDS))
}
@Test
@@ -107,11 +109,10 @@ class ChooserActionFactoryTest {
val action = factory.modifyShareAction ?: error("Modify share action should not be null")
action.onClicked.run()
- Mockito.verify(logger)
- .logActionSelected(eq(EventLog.SELECTION_TYPE_MODIFY_SHARE))
+ Mockito.verify(logger).logActionSelected(eq(EventLog.SELECTION_TYPE_MODIFY_SHARE))
assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn)
// Verify the pending intent has been called
- countdown.await(500, TimeUnit.MILLISECONDS)
+ assertTrue("Timed out waiting for broadcast", countdown.await(2500, TimeUnit.MILLISECONDS))
}
@Test
@@ -187,7 +188,8 @@ class ChooserActionFactoryTest {
}
private fun createFactory(includeModifyShare: Boolean = false): ChooserActionFactory {
- val testPendingIntent = PendingIntent.getActivity(context, 0, Intent(testAction), 0)
+ val testPendingIntent =
+ PendingIntent.getBroadcast(context, 0, Intent(testAction), PendingIntent.FLAG_IMMUTABLE)
val targetIntent = Intent()
val action =
ChooserAction.Builder(
diff --git a/java/tests/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt b/tests/unit/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt
index 9a5dabd..9a5dabd 100644
--- a/java/tests/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt
+++ b/tests/unit/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt
diff --git a/tests/unit/src/com/android/intentresolver/ChooserListAdapterDataTest.kt b/tests/unit/src/com/android/intentresolver/ChooserListAdapterDataTest.kt
new file mode 100644
index 0000000..98c5e00
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/ChooserListAdapterDataTest.kt
@@ -0,0 +1,177 @@
+package com.android.intentresolver
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.ComponentInfoFlags
+import android.os.UserHandle
+import android.os.UserManager
+import android.view.LayoutInflater
+import com.android.intentresolver.ResolverDataProvider.createActivityInfo
+import com.android.intentresolver.ResolverDataProvider.createResolvedComponentInfo
+import com.android.intentresolver.icons.TargetDataLoader
+import com.android.intentresolver.logging.FakeEventLog
+import com.android.intentresolver.util.TestExecutor
+import com.android.internal.logging.InstanceId
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.Mockito
+
+class ChooserListAdapterDataTest {
+ private val layoutInflater = mock<LayoutInflater>()
+ private val packageManager = mock<PackageManager>()
+ private val userManager = mock<UserManager> { whenever(isManagedProfile).thenReturn(false) }
+ private val resources =
+ mock<android.content.res.Resources> {
+ whenever(getInteger(R.integer.config_maxShortcutTargetsPerApp)).thenReturn(2)
+ }
+ private val context =
+ mock<Context> {
+ whenever(getSystemService(Context.LAYOUT_INFLATER_SERVICE)).thenReturn(layoutInflater)
+ whenever(getSystemService(Context.USER_SERVICE)).thenReturn(userManager)
+ whenever(packageManager).thenReturn(this@ChooserListAdapterDataTest.packageManager)
+ whenever(resources).thenReturn(this@ChooserListAdapterDataTest.resources)
+ }
+ private val targetIntent = Intent(Intent.ACTION_SEND)
+ private val payloadIntents = listOf(targetIntent)
+ private val resolverListController =
+ mock<ResolverListController> {
+ whenever(filterIneligibleActivities(any(), Mockito.anyBoolean())).thenReturn(null)
+ whenever(filterLowPriority(any(), Mockito.anyBoolean())).thenReturn(null)
+ }
+ private val resolverListCommunicator = FakeResolverListCommunicator()
+ private val userHandle = UserHandle.of(UserHandle.USER_CURRENT)
+ private val targetDataLoader = mock<TargetDataLoader>()
+ private val backgroundExecutor = TestExecutor()
+ private val immediateExecutor = TestExecutor(immediate = true)
+ private val referrerFillInIntent =
+ Intent().putExtra(Intent.EXTRA_REFERRER, "org.referrer.package")
+
+ @Test
+ fun test_twoTargetsWithNonOverlappingInitialIntent_threeTargetsInResolverAdapter() {
+ val resolvedTargets =
+ listOf(
+ createResolvedComponentInfo(1),
+ createResolvedComponentInfo(2),
+ )
+ val targetIntent = Intent(Intent.ACTION_SEND)
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ val initialActivityInfo = createActivityInfo(3)
+ val initialIntents =
+ arrayOf(
+ Intent(Intent.ACTION_SEND).apply { component = initialActivityInfo.componentName }
+ )
+ whenever(
+ packageManager.getActivityInfo(
+ eq(initialActivityInfo.componentName),
+ any<ComponentInfoFlags>()
+ )
+ )
+ .thenReturn(initialActivityInfo)
+ val testSubject =
+ ChooserListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ false,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ referrerFillInIntent,
+ resolverListCommunicator,
+ packageManager,
+ FakeEventLog(InstanceId.fakeInstanceId(1)),
+ /*maxRankedTargets=*/ 2,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ null,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = true
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isFalse()
+ assertThat(testSubject.displayResolveInfoCount).isEqualTo(0)
+ assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(1)
+
+ backgroundExecutor.runUntilIdle()
+
+ // we don't reset placeholder count (legacy logic, likely an oversight?)
+ assertThat(testSubject.displayResolveInfoCount).isEqualTo(resolvedTargets.size)
+ }
+
+ @Test
+ fun test_twoTargetsWithOverlappingInitialIntent_oneTargetsInResolverAdapter() {
+ val resolvedTargets =
+ listOf(
+ createResolvedComponentInfo(1),
+ createResolvedComponentInfo(2),
+ )
+ val targetIntent = Intent(Intent.ACTION_SEND)
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ val activityInfo = resolvedTargets[1].getResolveInfoAt(0).activityInfo
+ val initialIntents =
+ arrayOf(Intent(Intent.ACTION_SEND).apply { component = activityInfo.componentName })
+ whenever(
+ packageManager.getActivityInfo(
+ eq(activityInfo.componentName),
+ any<ComponentInfoFlags>()
+ )
+ )
+ .thenReturn(activityInfo)
+ val testSubject =
+ ChooserListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ false,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ referrerFillInIntent,
+ resolverListCommunicator,
+ packageManager,
+ FakeEventLog(InstanceId.fakeInstanceId(1)),
+ /*maxRankedTargets=*/ 2,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ null,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = true
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isFalse()
+ assertThat(testSubject.displayResolveInfoCount).isEqualTo(0)
+ assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(1)
+
+ backgroundExecutor.runUntilIdle()
+
+ // we don't reset placeholder count (legacy logic, likely an oversight?)
+ assertThat(testSubject.displayResolveInfoCount).isEqualTo(resolvedTargets.size - 1)
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt b/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt
index c8cb4b9..cb04394 100644
--- a/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt
+++ b/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt
@@ -20,6 +20,7 @@ import android.content.ComponentName
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.PackageManager.ResolveInfoFlags
+import android.content.pm.ShortcutInfo
import android.os.UserHandle
import android.view.View
import android.widget.FrameLayout
@@ -31,8 +32,9 @@ import com.android.intentresolver.chooser.DisplayResolveInfo
import com.android.intentresolver.chooser.SelectableTargetInfo
import com.android.intentresolver.chooser.TargetInfo
import com.android.intentresolver.icons.TargetDataLoader
-import com.android.intentresolver.logging.EventLog
+import com.android.intentresolver.logging.EventLogImpl
import com.android.internal.R
+import com.google.common.truth.Truth.assertThat
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -50,8 +52,11 @@ class ChooserListAdapterTest {
}
private val context = InstrumentationRegistry.getInstrumentation().context
private val resolverListController = mock<ResolverListController>()
- private val mEventLog = mock<EventLog>()
+ private val appLabel = "App"
+ private val targetLabel = "Target"
+ private val mEventLog = mock<EventLogImpl>()
private val mTargetDataLoader = mock<TargetDataLoader>()
+ private val mPackageChangeCallback = mock<ChooserListAdapter.PackageChangeCallback>()
private val testSubject by lazy {
ChooserListAdapter(
@@ -63,13 +68,14 @@ class ChooserListAdapterTest {
resolverListController,
userHandle,
Intent(),
+ Intent(),
mock(),
packageManager,
mEventLog,
- mock(),
0,
null,
- mTargetDataLoader
+ mTargetDataLoader,
+ mPackageChangeCallback
)
}
@@ -119,8 +125,7 @@ class ChooserListAdapterTest {
ResolverDataProvider.createResolveInfo(2, 0, userHandle),
null,
"extended info",
- Intent(),
- /* resolveInfoPresentationGetter= */ null
+ Intent()
)
testSubject.onBindView(view, targetInfo, 0)
@@ -132,29 +137,82 @@ class ChooserListAdapterTest {
verify(mTargetDataLoader, times(1)).loadAppTargetIcon(any(), any(), any())
}
- private fun createSelectableTargetInfo(): TargetInfo =
- SelectableTargetInfo.newSelectableTargetInfo(
- /* sourceInfo = */ DisplayResolveInfo.newDisplayResolveInfo(
- Intent(),
- ResolverDataProvider.createResolveInfo(2, 0, userHandle),
- "label",
- "extended info",
- Intent(),
- /* resolveInfoPresentationGetter= */ null
- ),
+ @Test
+ fun onBindView_contentDescription() {
+ val view = createView()
+ val viewHolder = ResolverListAdapter.ViewHolder(view)
+ view.tag = viewHolder
+ val targetInfo = createSelectableTargetInfo()
+ testSubject.onBindView(view, targetInfo, 0)
+
+ assertThat(view.contentDescription).isEqualTo("$targetLabel $appLabel")
+ }
+
+ @Test
+ fun onBindView_contentDescriptionPinned() {
+ val view = createView()
+ val viewHolder = ResolverListAdapter.ViewHolder(view)
+ view.tag = viewHolder
+ val targetInfo = createSelectableTargetInfo(true)
+ testSubject.onBindView(view, targetInfo, 0)
+
+ assertThat(view.contentDescription).isEqualTo("$targetLabel $appLabel. Pinned")
+ }
+
+ @Test
+ fun onBindView_displayInfoContentDescriptionPinned() {
+ val view = createView()
+ val viewHolder = ResolverListAdapter.ViewHolder(view)
+ view.tag = viewHolder
+ val targetInfo = createDisplayResolveInfo(isPinned = true)
+ testSubject.onBindView(view, targetInfo, 0)
+
+ assertThat(view.contentDescription).isEqualTo("$appLabel. Pinned")
+ }
+
+ @Test
+ fun handlePackagesChanged_invokesCallback() {
+ testSubject.handlePackagesChanged()
+ verify(mPackageChangeCallback, times(1)).beforeHandlingPackagesChanged()
+ }
+
+ private fun createSelectableTargetInfo(isPinned: Boolean = false): TargetInfo {
+ val shortcutInfo =
+ createShortcutInfo("id-1", ComponentName("pkg", "Class"), 1).apply {
+ if (isPinned) {
+ addFlags(ShortcutInfo.FLAG_PINNED)
+ }
+ }
+ return SelectableTargetInfo.newSelectableTargetInfo(
+ /* sourceInfo = */ createDisplayResolveInfo(isPinned),
/* backupResolveInfo = */ mock(),
/* resolvedIntent = */ Intent(),
/* chooserTarget = */ createChooserTarget(
- "Target",
+ targetLabel,
0.5f,
ComponentName("pkg", "Class"),
"id-1"
),
/* modifiedScore = */ 1f,
- /* shortcutInfo = */ createShortcutInfo("id-1", ComponentName("pkg", "Class"), 1),
+ shortcutInfo,
/* appTarget */ null,
/* referrerFillInIntent = */ Intent()
)
+ }
+
+ private fun createDisplayResolveInfo(isPinned: Boolean = false): DisplayResolveInfo =
+ DisplayResolveInfo.newDisplayResolveInfo(
+ Intent(),
+ ResolverDataProvider.createResolveInfo(2, 0, userHandle),
+ appLabel,
+ "extended info",
+ Intent(),
+ )
+ .apply {
+ if (isPinned) {
+ setPinned(true)
+ }
+ }
private fun createView(): View {
val view = FrameLayout(context)
diff --git a/java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.kt b/tests/unit/src/com/android/intentresolver/ChooserRefinementManagerTest.kt
index 61ac0c2..61ac0c2 100644
--- a/java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.kt
+++ b/tests/unit/src/com/android/intentresolver/ChooserRefinementManagerTest.kt
diff --git a/java/tests/src/com/android/intentresolver/ChooserRequestParametersTest.kt b/tests/unit/src/com/android/intentresolver/ChooserRequestParametersTest.kt
index 331d1c2..90f6cf9 100644
--- a/java/tests/src/com/android/intentresolver/ChooserRequestParametersTest.kt
+++ b/tests/unit/src/com/android/intentresolver/ChooserRequestParametersTest.kt
@@ -29,7 +29,6 @@ import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class ChooserRequestParametersTest {
- val flags = TestFeatureFlagRepository(mapOf())
@Test
fun testChooserActions() {
@@ -41,7 +40,7 @@ class ChooserRequestParametersTest {
putExtra(Intent.EXTRA_INTENT, intent)
putExtra(Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, actions)
}
- val request = ChooserRequestParameters(chooserIntent, "", Uri.EMPTY, flags)
+ val request = ChooserRequestParameters(chooserIntent, "", Uri.EMPTY)
assertThat(request.chooserActions).containsExactlyElementsIn(actions).inOrder()
}
@@ -50,7 +49,7 @@ class ChooserRequestParametersTest {
val intent = Intent(Intent.ACTION_SEND)
val chooserIntent =
Intent(Intent.ACTION_CHOOSER).apply { putExtra(Intent.EXTRA_INTENT, intent) }
- val request = ChooserRequestParameters(chooserIntent, "", Uri.EMPTY, flags)
+ val request = ChooserRequestParameters(chooserIntent, "", Uri.EMPTY)
assertThat(request.chooserActions).isEmpty()
}
@@ -64,7 +63,7 @@ class ChooserRequestParametersTest {
putExtra(Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, chooserActions)
}
- val request = ChooserRequestParameters(chooserIntent, "", Uri.EMPTY, flags)
+ val request = ChooserRequestParameters(chooserIntent, "", Uri.EMPTY)
val expectedActions = chooserActions.sliceArray(0 until 5)
assertThat(request.chooserActions).containsExactlyElementsIn(expectedActions).inOrder()
diff --git a/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt b/tests/unit/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt
index c7d2000..c7d2000 100644
--- a/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt
+++ b/tests/unit/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt
diff --git a/tests/unit/src/com/android/intentresolver/FakeResolverListCommunicator.kt b/tests/unit/src/com/android/intentresolver/FakeResolverListCommunicator.kt
new file mode 100644
index 0000000..5e9cd98
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/FakeResolverListCommunicator.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 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.intentresolver
+
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import java.util.concurrent.atomic.AtomicInteger
+
+class FakeResolverListCommunicator(private val layoutWithDefaults: Boolean = true) :
+ ResolverListAdapter.ResolverListCommunicator {
+ private val sendVoiceCounter = AtomicInteger()
+ private val updateProfileViewButtonCounter = AtomicInteger()
+
+ val sendVoiceCommandCount
+ get() = sendVoiceCounter.get()
+ val updateProfileViewButtonCount
+ get() = updateProfileViewButtonCounter.get()
+
+ override fun getReplacementIntent(activityInfo: ActivityInfo?, defIntent: Intent): Intent {
+ return defIntent
+ }
+
+ override fun onPostListReady(
+ listAdapter: ResolverListAdapter?,
+ updateUi: Boolean,
+ rebuildCompleted: Boolean,
+ ) = Unit
+
+ override fun sendVoiceChoicesIfNeeded() {
+ sendVoiceCounter.incrementAndGet()
+ }
+
+ override fun updateProfileViewButton() {
+ updateProfileViewButtonCounter.incrementAndGet()
+ }
+
+ override fun useLayoutWithDefault(): Boolean = layoutWithDefaults
+
+ override fun shouldGetActivityMetadata(): Boolean = true
+
+ override fun onHandlePackagesChanged(listAdapter: ResolverListAdapter?) {}
+}
diff --git a/tests/unit/src/com/android/intentresolver/MultiProfilePagerAdapterTest.kt b/tests/unit/src/com/android/intentresolver/MultiProfilePagerAdapterTest.kt
new file mode 100644
index 0000000..ed06f7d
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/MultiProfilePagerAdapterTest.kt
@@ -0,0 +1,277 @@
+/*
+ * Copyright (C) 2023 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.intentresolver
+
+import android.os.UserHandle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ListView
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.intentresolver.MultiProfilePagerAdapter.PROFILE_PERSONAL
+import com.android.intentresolver.MultiProfilePagerAdapter.PROFILE_WORK
+import com.android.intentresolver.emptystate.EmptyStateProvider
+import com.google.common.collect.ImmutableList
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import java.util.function.Supplier
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class MultiProfilePagerAdapterTest {
+ private val PERSONAL_USER_HANDLE = UserHandle.of(10)
+ private val WORK_USER_HANDLE = UserHandle.of(20)
+
+ private val context = InstrumentationRegistry.getInstrumentation().getContext()
+ private val inflater = Supplier {
+ LayoutInflater.from(context).inflate(R.layout.resolver_list_per_profile, null, false)
+ as ViewGroup
+ }
+
+ @Test
+ fun testSinglePageProfileAdapter() {
+ val personalListAdapter =
+ mock<ResolverListAdapter> { whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE) }
+ val pagerAdapter =
+ MultiProfilePagerAdapter(
+ { listAdapter: ResolverListAdapter -> listAdapter },
+ { listView: ListView, bindAdapter: ResolverListAdapter ->
+ listView.setAdapter(bindAdapter)
+ },
+ ImmutableList.of(personalListAdapter),
+ object : EmptyStateProvider {},
+ { false },
+ PROFILE_PERSONAL,
+ null,
+ null,
+ inflater,
+ { Optional.empty() }
+ )
+ assertThat(pagerAdapter.count).isEqualTo(1)
+ assertThat(pagerAdapter.currentPage).isEqualTo(PROFILE_PERSONAL)
+ assertThat(pagerAdapter.currentUserHandle).isEqualTo(PERSONAL_USER_HANDLE)
+ assertThat(pagerAdapter.getAdapterForIndex(0)).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.activeListAdapter).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.inactiveListAdapter).isNull()
+ assertThat(pagerAdapter.personalListAdapter).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.workListAdapter).isNull()
+ assertThat(pagerAdapter.itemCount).isEqualTo(1)
+ // TODO: consider covering some of the package-private methods (and making them public?).
+ // TODO: consider exercising responsibilities as an implementation of a ViewPager adapter.
+ }
+
+ @Test
+ fun testTwoProfilePagerAdapter() {
+ val personalListAdapter =
+ mock<ResolverListAdapter> { whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE) }
+ val workListAdapter =
+ mock<ResolverListAdapter> { whenever(getUserHandle()).thenReturn(WORK_USER_HANDLE) }
+ val pagerAdapter =
+ MultiProfilePagerAdapter(
+ { listAdapter: ResolverListAdapter -> listAdapter },
+ { listView: ListView, bindAdapter: ResolverListAdapter ->
+ listView.setAdapter(bindAdapter)
+ },
+ ImmutableList.of(personalListAdapter, workListAdapter),
+ object : EmptyStateProvider {},
+ { false },
+ PROFILE_PERSONAL,
+ WORK_USER_HANDLE, // TODO: why does this test pass even if this is null?
+ null,
+ inflater,
+ { Optional.empty() }
+ )
+ assertThat(pagerAdapter.count).isEqualTo(2)
+ assertThat(pagerAdapter.currentPage).isEqualTo(PROFILE_PERSONAL)
+ assertThat(pagerAdapter.currentUserHandle).isEqualTo(PERSONAL_USER_HANDLE)
+ assertThat(pagerAdapter.getAdapterForIndex(0)).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.getAdapterForIndex(1)).isSameInstanceAs(workListAdapter)
+ assertThat(pagerAdapter.activeListAdapter).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.inactiveListAdapter).isSameInstanceAs(workListAdapter)
+ assertThat(pagerAdapter.personalListAdapter).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.workListAdapter).isSameInstanceAs(workListAdapter)
+ assertThat(pagerAdapter.itemCount).isEqualTo(2)
+ // TODO: consider covering some of the package-private methods (and making them public?).
+ // TODO: consider exercising responsibilities as an implementation of a ViewPager adapter;
+ // especially matching profiles to ListViews?
+ // TODO: test ProfileSelectedListener (and getters for "current" state) as the selected
+ // page changes. Currently there's no API to change the selected page directly; that's
+ // only possible through manipulation of the bound ViewPager.
+ }
+
+ @Test
+ fun testTwoProfilePagerAdapter_workIsDefault() {
+ val personalListAdapter =
+ mock<ResolverListAdapter> { whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE) }
+ val workListAdapter =
+ mock<ResolverListAdapter> { whenever(getUserHandle()).thenReturn(WORK_USER_HANDLE) }
+ val pagerAdapter =
+ MultiProfilePagerAdapter(
+ { listAdapter: ResolverListAdapter -> listAdapter },
+ { listView: ListView, bindAdapter: ResolverListAdapter ->
+ listView.setAdapter(bindAdapter)
+ },
+ ImmutableList.of(personalListAdapter, workListAdapter),
+ object : EmptyStateProvider {},
+ { false },
+ PROFILE_WORK, // <-- This test specifically requests we start on work profile.
+ WORK_USER_HANDLE, // TODO: why does this test pass even if this is null?
+ null,
+ inflater,
+ { Optional.empty() }
+ )
+ assertThat(pagerAdapter.count).isEqualTo(2)
+ assertThat(pagerAdapter.currentPage).isEqualTo(PROFILE_WORK)
+ assertThat(pagerAdapter.currentUserHandle).isEqualTo(WORK_USER_HANDLE)
+ assertThat(pagerAdapter.getAdapterForIndex(0)).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.getAdapterForIndex(1)).isSameInstanceAs(workListAdapter)
+ assertThat(pagerAdapter.activeListAdapter).isSameInstanceAs(workListAdapter)
+ assertThat(pagerAdapter.inactiveListAdapter).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.personalListAdapter).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.workListAdapter).isSameInstanceAs(workListAdapter)
+ assertThat(pagerAdapter.itemCount).isEqualTo(2)
+ // TODO: consider covering some of the package-private methods (and making them public?).
+ // TODO: test ProfileSelectedListener (and getters for "current" state) as the selected
+ // page changes. Currently there's no API to change the selected page directly; that's
+ // only possible through manipulation of the bound ViewPager.
+ }
+
+ @Test
+ fun testBottomPaddingDelegate_default() {
+ val container =
+ mock<View> {
+ whenever(getPaddingLeft()).thenReturn(1)
+ whenever(getPaddingTop()).thenReturn(2)
+ whenever(getPaddingRight()).thenReturn(3)
+ whenever(getPaddingBottom()).thenReturn(4)
+ }
+ val pagerAdapter =
+ MultiProfilePagerAdapter(
+ { listAdapter: ResolverListAdapter -> listAdapter },
+ { listView: ListView, bindAdapter: ResolverListAdapter ->
+ listView.setAdapter(bindAdapter)
+ },
+ ImmutableList.of(),
+ object : EmptyStateProvider {},
+ { false },
+ PROFILE_PERSONAL,
+ null,
+ null,
+ inflater,
+ { Optional.empty() }
+ )
+ pagerAdapter.setupContainerPadding(container)
+ verify(container, never()).setPadding(any(), any(), any(), any())
+ }
+
+ @Test
+ fun testBottomPaddingDelegate_override() {
+ val container =
+ mock<View> {
+ whenever(getPaddingLeft()).thenReturn(1)
+ whenever(getPaddingTop()).thenReturn(2)
+ whenever(getPaddingRight()).thenReturn(3)
+ whenever(getPaddingBottom()).thenReturn(4)
+ }
+ val pagerAdapter =
+ MultiProfilePagerAdapter(
+ { listAdapter: ResolverListAdapter -> listAdapter },
+ { listView: ListView, bindAdapter: ResolverListAdapter ->
+ listView.setAdapter(bindAdapter)
+ },
+ ImmutableList.of(),
+ object : EmptyStateProvider {},
+ { false },
+ PROFILE_PERSONAL,
+ null,
+ null,
+ inflater,
+ { Optional.of(42) }
+ )
+ pagerAdapter.setupContainerPadding(container)
+ verify(container).setPadding(1, 2, 3, 42)
+ }
+
+ @Test
+ fun testPresumedQuietModeEmptyStateForWorkProfile_whenQuiet() {
+ // TODO: this is "presumed" because the conditions to determine whether we "should" show an
+ // empty state aren't enforced to align with the conditions when we actually *would* -- I
+ // believe `shouldShowEmptyStateScreen` should be implemented in terms of the provider?
+ val personalListAdapter =
+ mock<ResolverListAdapter> {
+ whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE)
+ whenever(getUnfilteredCount()).thenReturn(1)
+ }
+ val workListAdapter =
+ mock<ResolverListAdapter> {
+ whenever(getUserHandle()).thenReturn(WORK_USER_HANDLE)
+ whenever(getUnfilteredCount()).thenReturn(1)
+ }
+ val pagerAdapter =
+ MultiProfilePagerAdapter(
+ { listAdapter: ResolverListAdapter -> listAdapter },
+ { listView: ListView, bindAdapter: ResolverListAdapter ->
+ listView.setAdapter(bindAdapter)
+ },
+ ImmutableList.of(personalListAdapter, workListAdapter),
+ object : EmptyStateProvider {},
+ { true }, // <-- Work mode is quiet.
+ PROFILE_WORK,
+ WORK_USER_HANDLE,
+ null,
+ inflater,
+ { Optional.empty() }
+ )
+ assertThat(pagerAdapter.shouldShowEmptyStateScreen(workListAdapter)).isTrue()
+ assertThat(pagerAdapter.shouldShowEmptyStateScreen(personalListAdapter)).isFalse()
+ }
+
+ @Test
+ fun testPresumedQuietModeEmptyStateForWorkProfile_notWhenNotQuiet() {
+ // TODO: this is "presumed" because the conditions to determine whether we "should" show an
+ // empty state aren't enforced to align with the conditions when we actually *would* -- I
+ // believe `shouldShowEmptyStateScreen` should be implemented in terms of the provider?
+ val personalListAdapter =
+ mock<ResolverListAdapter> {
+ whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE)
+ whenever(getUnfilteredCount()).thenReturn(1)
+ }
+ val workListAdapter =
+ mock<ResolverListAdapter> {
+ whenever(getUserHandle()).thenReturn(WORK_USER_HANDLE)
+ whenever(getUnfilteredCount()).thenReturn(1)
+ }
+ val pagerAdapter =
+ MultiProfilePagerAdapter(
+ { listAdapter: ResolverListAdapter -> listAdapter },
+ { listView: ListView, bindAdapter: ResolverListAdapter ->
+ listView.setAdapter(bindAdapter)
+ },
+ ImmutableList.of(personalListAdapter, workListAdapter),
+ object : EmptyStateProvider {},
+ { false }, // <-- Work mode is not quiet.
+ PROFILE_WORK,
+ WORK_USER_HANDLE,
+ null,
+ inflater,
+ { Optional.empty() }
+ )
+ assertThat(pagerAdapter.shouldShowEmptyStateScreen(workListAdapter)).isFalse()
+ assertThat(pagerAdapter.shouldShowEmptyStateScreen(personalListAdapter)).isFalse()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/ResolverListAdapterTest.kt b/tests/unit/src/com/android/intentresolver/ResolverListAdapterTest.kt
new file mode 100644
index 0000000..61b9fd9
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/ResolverListAdapterTest.kt
@@ -0,0 +1,1048 @@
+/*
+ * Copyright (C) 2023 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.intentresolver
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.UserHandle
+import android.os.UserManager
+import android.view.LayoutInflater
+import com.android.intentresolver.ResolverDataProvider.createActivityInfo
+import com.android.intentresolver.ResolverListAdapter.ResolverListCommunicator
+import com.android.intentresolver.icons.TargetDataLoader
+import com.android.intentresolver.util.TestExecutor
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.Mockito.anyBoolean
+import org.mockito.Mockito.inOrder
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+private const val PKG_NAME = "org.pkg.app"
+private const val PKG_NAME_TWO = "org.pkg.two.app"
+private const val PKG_NAME_THREE = "org.pkg.three.app"
+private const val CLASS_NAME = "org.pkg.app.TheClass"
+
+class ResolverListAdapterTest {
+ private val layoutInflater = mock<LayoutInflater>()
+ private val packageManager = mock<PackageManager>()
+ private val userManager = mock<UserManager> { whenever(isManagedProfile).thenReturn(false) }
+ private val context =
+ mock<Context> {
+ whenever(getSystemService(Context.LAYOUT_INFLATER_SERVICE)).thenReturn(layoutInflater)
+ whenever(getSystemService(Context.USER_SERVICE)).thenReturn(userManager)
+ whenever(packageManager).thenReturn(this@ResolverListAdapterTest.packageManager)
+ }
+ private val targetIntent = Intent(Intent.ACTION_SEND)
+ private val payloadIntents = listOf(targetIntent)
+ private val resolverListController =
+ mock<ResolverListController> {
+ whenever(filterIneligibleActivities(any(), anyBoolean())).thenReturn(null)
+ whenever(filterLowPriority(any(), anyBoolean())).thenReturn(null)
+ }
+ private val resolverListCommunicator = FakeResolverListCommunicator()
+ private val userHandle = UserHandle.of(UserHandle.USER_CURRENT)
+ private val targetDataLoader = mock<TargetDataLoader>()
+ private val backgroundExecutor = TestExecutor()
+ private val immediateExecutor = TestExecutor(immediate = true)
+
+ @Test
+ fun test_oneTargetNoLastChosen_oneTargetInAdapter() {
+ val resolvedTargets = createResolvedComponents(ComponentName(PKG_NAME, CLASS_NAME))
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = true
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isTrue()
+ assertThat(testSubject.count).isEqualTo(resolvedTargets.size)
+ assertThat(testSubject.placeholderCount).isEqualTo(0)
+ assertThat(testSubject.hasFilteredItem()).isFalse()
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isTrue()
+ assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(0)
+ assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(0)
+ assertThat(resolverListCommunicator.sendVoiceCommandCount).isEqualTo(1)
+ }
+
+ @Test
+ fun test_oneTargetThatWasLastChosen_NoTargetsInAdapter() {
+ val resolvedTargets = createResolvedComponents(ComponentName(PKG_NAME, CLASS_NAME))
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ whenever(resolverListController.lastChosen)
+ .thenReturn(resolvedTargets[0].getResolveInfoAt(0))
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = true
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isTrue()
+ assertThat(testSubject.count).isEqualTo(0)
+ assertThat(testSubject.placeholderCount).isEqualTo(0)
+ assertThat(testSubject.hasFilteredItem()).isTrue()
+ assertThat(testSubject.filteredItem).isNotNull()
+ assertThat(testSubject.filteredPosition).isEqualTo(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isTrue()
+ assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(0)
+ }
+
+ @Test
+ fun test_oneTargetLastChosenNotInTheList_oneTargetInAdapter() {
+ val resolvedTargets = createResolvedComponents(ComponentName(PKG_NAME, CLASS_NAME))
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ whenever(resolverListController.lastChosen)
+ .thenReturn(createResolveInfo(PKG_NAME_TWO, CLASS_NAME))
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = true
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isTrue()
+ assertThat(testSubject.count).isEqualTo(resolvedTargets.size)
+ assertThat(testSubject.placeholderCount).isEqualTo(0)
+ assertThat(testSubject.hasFilteredItem()).isTrue()
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isTrue()
+ assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(0)
+ }
+
+ @Test
+ fun test_oneTargetThatWasLastChosenFilteringDisabled_oneTargetInAdapter() {
+ val resolvedTargets = createResolvedComponents(ComponentName(PKG_NAME, CLASS_NAME))
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ whenever(resolverListController.lastChosen)
+ .thenReturn(resolvedTargets[0].getResolveInfoAt(0))
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ false,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = true
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isTrue()
+ assertThat(testSubject.count).isEqualTo(resolvedTargets.size)
+ // we don't reset placeholder count
+ assertThat(testSubject.placeholderCount).isEqualTo(0)
+ assertThat(testSubject.hasFilteredItem()).isFalse()
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isTrue()
+ }
+
+ @Test
+ fun test_twoTargetsNoLastChosenUseLayoutWithDefaults_twoTargetsInAdapter() {
+ testTwoTargets(hasLastChosen = false, useLayoutWithDefaults = true)
+ }
+
+ @Test
+ fun test_twoTargetsNoLastChosenDontUseLayoutWithDefaults_twoTargetsInAdapter() {
+ testTwoTargets(hasLastChosen = false, useLayoutWithDefaults = false)
+ }
+
+ @Test
+ fun test_twoTargetsLastChosenUseLayoutWithDefaults_oneTargetInAdapter() {
+ testTwoTargets(hasLastChosen = true, useLayoutWithDefaults = true)
+ }
+
+ @Test
+ fun test_twoTargetsLastChosenDontUseLayoutWithDefaults_oneTargetInAdapter() {
+ testTwoTargets(hasLastChosen = true, useLayoutWithDefaults = false)
+ }
+
+ private fun testTwoTargets(hasLastChosen: Boolean, useLayoutWithDefaults: Boolean) {
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ if (hasLastChosen) {
+ whenever(resolverListController.lastChosen)
+ .thenReturn(resolvedTargets[0].getResolveInfoAt(0))
+ }
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ val resolverListCommunicator = FakeResolverListCommunicator(useLayoutWithDefaults)
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = true
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isFalse()
+ val placeholderCount = resolvedTargets.size - (if (useLayoutWithDefaults) 1 else 0)
+ assertThat(testSubject.count).isEqualTo(placeholderCount)
+ assertThat(testSubject.placeholderCount).isEqualTo(placeholderCount)
+ assertThat(testSubject.hasFilteredItem()).isEqualTo(hasLastChosen)
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isFalse()
+ assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(1)
+ assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(0)
+ assertThat(resolverListCommunicator.sendVoiceCommandCount).isEqualTo(0)
+
+ backgroundExecutor.runUntilIdle()
+
+ // we don't reset placeholder count (legacy logic, likely an oversight?)
+ assertThat(testSubject.placeholderCount).isEqualTo(placeholderCount)
+ assertThat(testSubject.hasFilteredItem()).isEqualTo(hasLastChosen)
+ if (hasLastChosen) {
+ assertThat(testSubject.count).isEqualTo(resolvedTargets.size - 1)
+ assertThat(testSubject.filteredItem).isNotNull()
+ assertThat(testSubject.filteredPosition).isEqualTo(0)
+ } else {
+ assertThat(testSubject.count).isEqualTo(resolvedTargets.size)
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ }
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isTrue()
+ assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(1)
+ assertThat(resolverListCommunicator.sendVoiceCommandCount).isEqualTo(1)
+ assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(0)
+ }
+
+ @Test
+ fun test_twoTargetsLastChosenNotInTheList_twoTargetsInAdapter() {
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ whenever(resolverListController.lastChosen)
+ .thenReturn(createResolveInfo(PKG_NAME, CLASS_NAME + "2"))
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = false
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isFalse()
+ val placeholderCount = resolvedTargets.size - 1
+ assertThat(testSubject.count).isEqualTo(placeholderCount)
+ assertThat(testSubject.placeholderCount).isEqualTo(placeholderCount)
+ assertThat(testSubject.hasFilteredItem()).isTrue()
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isFalse()
+ assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(1)
+ assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(0)
+
+ backgroundExecutor.runUntilIdle()
+
+ // we don't reset placeholder count (legacy logic, likely an oversight?)
+ assertThat(testSubject.placeholderCount).isEqualTo(placeholderCount)
+ assertThat(testSubject.hasFilteredItem()).isTrue()
+ assertThat(testSubject.count).isEqualTo(resolvedTargets.size)
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isTrue()
+ assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(0)
+ assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(0)
+ }
+
+ @Test
+ fun test_twoTargetsWithOtherProfileAndLastChosen_oneTargetInAdapter() {
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ resolvedTargets[1].getResolveInfoAt(0).targetUserId = 10
+ whenever(resolvedTargets[1].getResolveInfoAt(0).loadLabel(any())).thenReturn("Label")
+ whenever(resolverListController.lastChosen)
+ .thenReturn(resolvedTargets[0].getResolveInfoAt(0))
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = true
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isTrue()
+ assertThat(testSubject.count).isEqualTo(1)
+ assertThat(testSubject.placeholderCount).isEqualTo(0)
+ assertThat(testSubject.otherProfile).isNotNull()
+ assertThat(testSubject.hasFilteredItem()).isFalse()
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isTrue()
+ assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(0)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ @Test
+ fun test_resultsSorted_appearInSortedOrderInAdapter() {
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ whenever(resolverListController.sort(any())).thenAnswer { invocation ->
+ val components = invocation.arguments[0] as MutableList<ResolvedComponentInfo>
+ components[0] = components[1].also { components[1] = components[0] }
+ null
+ }
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = true
+
+ testSubject.rebuildList(doPostProcessing)
+
+ backgroundExecutor.runUntilIdle()
+
+ // we don't reset placeholder count (legacy logic, likely an oversight?)
+ assertThat(testSubject.count).isEqualTo(resolvedTargets.size)
+ assertThat(resolvedTargets[0].getResolveInfoAt(0).activityInfo.packageName)
+ .isEqualTo(PKG_NAME_TWO)
+ assertThat(resolvedTargets[1].getResolveInfoAt(0).activityInfo.packageName)
+ .isEqualTo(PKG_NAME)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ @Test
+ fun test_ineligibleActivityFilteredOut_filteredComponentNotPresentInAdapter() {
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ whenever(resolverListController.filterIneligibleActivities(any(), anyBoolean()))
+ .thenAnswer { invocation ->
+ val components = invocation.arguments[0] as MutableList<ResolvedComponentInfo>
+ val original = ArrayList(components)
+ components.removeAt(1)
+ original
+ }
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = true
+
+ testSubject.rebuildList(doPostProcessing)
+
+ backgroundExecutor.runUntilIdle()
+
+ // we don't reset placeholder count (legacy logic, likely an oversight?)
+ assertThat(testSubject.count).isEqualTo(1)
+ assertThat(testSubject.getItem(0)?.resolveInfo)
+ .isEqualTo(resolvedTargets[0].getResolveInfoAt(0))
+ assertThat(testSubject.unfilteredResolveList).hasSize(2)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ @Test
+ fun test_baseResolveList_excludedFromIneligibleActivityFiltering() {
+ val rList = listOf(createResolveInfo(PKG_NAME, CLASS_NAME))
+ whenever(resolverListController.addResolveListDedupe(any(), eq(targetIntent), eq(rList)))
+ .thenAnswer { invocation ->
+ val result = invocation.arguments[0] as MutableList<ResolvedComponentInfo>
+ result.addAll(
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ )
+ null
+ }
+ whenever(resolverListController.filterIneligibleActivities(any(), anyBoolean()))
+ .thenAnswer { invocation ->
+ val components = invocation.arguments[0] as MutableList<ResolvedComponentInfo>
+ val original = ArrayList(components)
+ components.clear()
+ original
+ }
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ rList,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = true
+
+ testSubject.rebuildList(doPostProcessing)
+
+ backgroundExecutor.runUntilIdle()
+
+ // we don't reset placeholder count (legacy logic, likely an oversight?)
+ assertThat(testSubject.count).isEqualTo(2)
+ assertThat(testSubject.unfilteredResolveList).hasSize(2)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ @Test
+ fun test_lowPriorityComponentFilteredOut_filteredComponentNotPresentInAdapter() {
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ whenever(resolverListController.filterLowPriority(any(), anyBoolean())).thenAnswer {
+ invocation ->
+ val components = invocation.arguments[0] as MutableList<ResolvedComponentInfo>
+ val original = ArrayList(components)
+ components.removeAt(1)
+ original
+ }
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = true
+
+ testSubject.rebuildList(doPostProcessing)
+
+ backgroundExecutor.runUntilIdle()
+
+ // we don't reset placeholder count (legacy logic, likely an oversight?)
+ assertThat(testSubject.count).isEqualTo(1)
+ assertThat(testSubject.getItem(0)?.resolveInfo)
+ .isEqualTo(resolvedTargets[0].getResolveInfoAt(0))
+ assertThat(testSubject.unfilteredResolveList).hasSize(2)
+ }
+
+ @Test
+ fun test_twoTargetsWithNonOverlappingInitialIntent_threeTargetsInAdapter() {
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ val initialComponent = ComponentName(PKG_NAME_THREE, CLASS_NAME)
+ val initialIntents =
+ arrayOf(Intent(Intent.ACTION_SEND).apply { component = initialComponent })
+ whenever(packageManager.getActivityInfo(eq(initialComponent), eq(0)))
+ .thenReturn(createActivityInfo(initialComponent))
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = true
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isFalse()
+ val placeholderCount = resolvedTargets.size - 1
+ assertThat(testSubject.count).isEqualTo(placeholderCount)
+ assertThat(testSubject.placeholderCount).isEqualTo(placeholderCount)
+ assertThat(testSubject.hasFilteredItem()).isFalse()
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isFalse()
+ assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(1)
+ assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(0)
+ assertThat(resolverListCommunicator.sendVoiceCommandCount).isEqualTo(0)
+
+ backgroundExecutor.runUntilIdle()
+
+ // we don't reset placeholder count (legacy logic, likely an oversight?)
+ assertThat(testSubject.placeholderCount).isEqualTo(placeholderCount)
+ assertThat(testSubject.hasFilteredItem()).isFalse()
+ assertThat(testSubject.count).isEqualTo(resolvedTargets.size + initialIntents.size)
+ assertThat(testSubject.getItem(0)?.targetIntent?.component)
+ .isEqualTo(initialIntents[0].component)
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isTrue()
+ assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(1)
+ assertThat(resolverListCommunicator.sendVoiceCommandCount).isEqualTo(1)
+ assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(0)
+ }
+
+ @Test
+ fun test_twoTargetsWithOverlappingInitialIntent_twoTargetsInAdapter() {
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ )
+ .thenReturn(resolvedTargets)
+ val initialComponent = ComponentName(PKG_NAME_TWO, CLASS_NAME)
+ val initialIntents =
+ arrayOf(Intent(Intent.ACTION_SEND).apply { component = initialComponent })
+ whenever(packageManager.getActivityInfo(eq(initialComponent), eq(0)))
+ .thenReturn(createActivityInfo(initialComponent))
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = true
+
+ val isLoaded = testSubject.rebuildList(doPostProcessing)
+
+ assertThat(isLoaded).isFalse()
+ val placeholderCount = resolvedTargets.size - 1
+ assertThat(testSubject.count).isEqualTo(placeholderCount)
+ assertThat(testSubject.placeholderCount).isEqualTo(placeholderCount)
+ assertThat(testSubject.hasFilteredItem()).isFalse()
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isFalse()
+ assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(1)
+ assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(0)
+ assertThat(resolverListCommunicator.sendVoiceCommandCount).isEqualTo(0)
+
+ backgroundExecutor.runUntilIdle()
+
+ // we don't reset placeholder count (legacy logic, likely an oversight?)
+ assertThat(testSubject.placeholderCount).isEqualTo(placeholderCount)
+ assertThat(testSubject.hasFilteredItem()).isFalse()
+ assertThat(testSubject.count).isEqualTo(resolvedTargets.size)
+ assertThat(testSubject.getItem(0)?.targetIntent?.component)
+ .isEqualTo(initialIntents[0].component)
+ assertThat(testSubject.filteredItem).isNull()
+ assertThat(testSubject.filteredPosition).isLessThan(0)
+ assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertThat(testSubject.isTabLoaded).isTrue()
+ assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(1)
+ assertThat(resolverListCommunicator.sendVoiceCommandCount).isEqualTo(1)
+ assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(0)
+ }
+
+ @Test
+ fun testPostListReadyAtEndOfRebuild_synchronous() {
+ val communicator = mock<ResolverListCommunicator> {}
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ communicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = false
+
+ testSubject.rebuildList(doPostProcessing)
+
+ verify(communicator).onPostListReady(testSubject, doPostProcessing, true)
+ }
+
+ @Test
+ fun testPostListReadyAtEndOfRebuild_stages() {
+ // We need at least two targets to trigger asynchronous sorting/"staged" progress callbacks.
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ // TODO: there's a lot of boilerplate required for this test even to trigger the expected
+ // conditions; if the configuration is incorrect, the test may accidentally pass for the
+ // wrong reasons. Separating responsibilities to other components will help minimize the
+ // *amount* of boilerplate, but we should also consider setting up test defaults that work
+ // according to our usual expectations so that we don't overlook false-negative results.
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ )
+ )
+ .thenReturn(resolvedTargets)
+ val communicator =
+ mock<ResolverListCommunicator> {
+ whenever(getReplacementIntent(any(), any())).thenAnswer { invocation ->
+ invocation.arguments[1]
+ }
+ }
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ communicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ immediateExecutor,
+ )
+ val doPostProcessing = false
+
+ testSubject.rebuildList(doPostProcessing)
+
+ backgroundExecutor.runUntilIdle()
+
+ val inOrder = inOrder(communicator)
+ inOrder.verify(communicator).onPostListReady(testSubject, doPostProcessing, false)
+ inOrder.verify(communicator).onPostListReady(testSubject, doPostProcessing, true)
+ }
+
+ @Test
+ fun testPostListReadyAtEndOfRebuild_queued() {
+ val queuedCallbacksExecutor = TestExecutor()
+
+ // We need at least two targets to trigger asynchronous sorting/"staged" progress callbacks.
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ // TODO: there's a lot of boilerplate required for this test even to trigger the expected
+ // conditions; if the configuration is incorrect, the test may accidentally pass for the
+ // wrong reasons. Separating responsibilities to other components will help minimize the
+ // *amount* of boilerplate, but we should also consider setting up test defaults that work
+ // according to our usual expectations so that we don't overlook false-negative results.
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ )
+ )
+ .thenReturn(resolvedTargets)
+ val communicator =
+ mock<ResolverListCommunicator> {
+ whenever(getReplacementIntent(any(), any())).thenAnswer { invocation ->
+ invocation.arguments[1]
+ }
+ }
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ communicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ queuedCallbacksExecutor
+ )
+ val doPostProcessing = false
+ testSubject.rebuildList(doPostProcessing)
+
+ // Finish all the background work (enqueueing both the "partial" and "complete" progress
+ // callbacks) before dequeueing either callback.
+ backgroundExecutor.runUntilIdle()
+ queuedCallbacksExecutor.runUntilIdle()
+
+ // TODO: we may not necessarily care to assert that there's a "partial progress" callback in
+ // this case, since there won't be a chance to reflect the "partial" state in the UI before
+ // the "completion" is queued (and if we depend on seeing an intermediate state, that could
+ // be a bad sign for our handling in the "synchronous" case?). But we should probably at
+ // least assert that the "partial" callback never arrives *after* the completion?
+ val inOrder = inOrder(communicator)
+ inOrder.verify(communicator).onPostListReady(testSubject, doPostProcessing, false)
+ inOrder.verify(communicator).onPostListReady(testSubject, doPostProcessing, true)
+ }
+
+ @Test
+ fun testPostListReadyAtEndOfRebuild_skippedIfStillQueuedOnDestroy() {
+ val queuedCallbacksExecutor = TestExecutor()
+
+ // We need at least two targets to trigger asynchronous sorting/"staged" progress callbacks.
+ val resolvedTargets =
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ // TODO: there's a lot of boilerplate required for this test even to trigger the expected
+ // conditions; if the configuration is incorrect, the test may accidentally pass for the
+ // wrong reasons. Separating responsibilities to other components will help minimize the
+ // *amount* of boilerplate, but we should also consider setting up test defaults that work
+ // according to our usual expectations so that we don't overlook false-negative results.
+ whenever(
+ resolverListController.getResolversForIntentAsUser(
+ any(),
+ any(),
+ any(),
+ any(),
+ any(),
+ )
+ )
+ .thenReturn(resolvedTargets)
+ val communicator =
+ mock<ResolverListCommunicator> {
+ whenever(getReplacementIntent(any(), any())).thenAnswer { invocation ->
+ invocation.arguments[1]
+ }
+ }
+ val testSubject =
+ ResolverListAdapter(
+ context,
+ payloadIntents,
+ /*initialIntents=*/ null,
+ /*rList=*/ null,
+ /*filterLastUsed=*/ true,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ communicator,
+ /*initialIntentsUserSpace=*/ userHandle,
+ targetDataLoader,
+ backgroundExecutor,
+ queuedCallbacksExecutor
+ )
+ val doPostProcessing = false
+ testSubject.rebuildList(doPostProcessing)
+
+ // Finish all the background work (enqueueing both the "partial" and "complete" progress
+ // callbacks) before dequeueing either callback.
+ backgroundExecutor.runUntilIdle()
+
+ // Notify that our activity is being destroyed while the callbacks are still queued.
+ testSubject.onDestroy()
+
+ queuedCallbacksExecutor.runUntilIdle()
+
+ verify(communicator, never()).onPostListReady(eq(testSubject), eq(doPostProcessing), any())
+ }
+
+ private fun createResolvedComponents(
+ vararg components: ComponentName
+ ): List<ResolvedComponentInfo> {
+ val result = ArrayList<ResolvedComponentInfo>(components.size)
+ for (component in components) {
+ val resolvedComponentInfo =
+ ResolvedComponentInfo(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ targetIntent,
+ createResolveInfo(component.packageName, component.className)
+ )
+ result.add(resolvedComponentInfo)
+ }
+ return result
+ }
+
+ private fun createResolveInfo(packageName: String, className: String): ResolveInfo =
+ mock<ResolveInfo> {
+ activityInfo = createActivityInfo(ComponentName(packageName, className))
+ targetUserId = this@ResolverListAdapterTest.userHandle.identifier
+ userHandle = this@ResolverListAdapterTest.userHandle
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt b/tests/unit/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt
index 9ddeed8..2346d98 100644
--- a/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt
+++ b/tests/unit/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt
@@ -19,7 +19,6 @@ package com.android.intentresolver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import android.content.pm.ResolveInfo
import android.content.pm.ShortcutInfo
import android.os.UserHandle
import android.service.chooser.ChooserTarget
@@ -60,16 +59,16 @@ class ShortcutSelectionLogicTest {
ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE),
"label",
"extended info",
- Intent(),
- /* resolveInfoPresentationGetter= */ null)
+ Intent()
+ )
private val otherBaseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo(
Intent(),
ResolverDataProvider.createResolveInfo(4, 0, PERSONAL_USER_HANDLE),
"label 2",
"extended info 2",
- Intent(),
- /* resolveInfoPresentationGetter= */ null)
+ Intent()
+ )
private operator fun Map<String, Array<ChooserTarget>>.get(pkg: String, idx: Int) =
this[pkg]?.get(idx) ?: error("missing package $pkg")
diff --git a/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt b/tests/unit/src/com/android/intentresolver/TargetPresentationGetterTest.kt
index e62672a..e62672a 100644
--- a/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt
+++ b/tests/unit/src/com/android/intentresolver/TargetPresentationGetterTest.kt
diff --git a/java/tests/src/com/android/intentresolver/TestHelpers.kt b/tests/unit/src/com/android/intentresolver/TestHelpers.kt
index 5b583fe..5b583fe 100644
--- a/java/tests/src/com/android/intentresolver/TestHelpers.kt
+++ b/tests/unit/src/com/android/intentresolver/TestHelpers.kt
diff --git a/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt b/tests/unit/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt
index f3ca76a..6712bf3 100644
--- a/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt
+++ b/tests/unit/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt
@@ -21,7 +21,6 @@ import android.app.prediction.AppTarget
import android.app.prediction.AppTargetId
import android.content.ComponentName
import android.content.Intent
-import android.content.pm.ResolveInfo
import android.os.Bundle
import android.os.UserHandle
import com.android.intentresolver.createShortcutInfo
@@ -52,15 +51,15 @@ class ImmutableTargetInfoTest {
ResolverDataProvider.createResolveInfo(2, 0, PERSONAL_USER_HANDLE),
"display1 label",
"display1 extended info",
- Intent("display1_resolved"),
- /* resolveInfoPresentationGetter= */ null)
+ Intent("display1_resolved")
+ )
private val displayTarget2 = DisplayResolveInfo.newDisplayResolveInfo(
Intent("display2"),
ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE),
"display2 label",
"display2 extended info",
- Intent("display2_resolved"),
- /* resolveInfoPresentationGetter= */ null)
+ Intent("display2_resolved")
+ )
private val directShareShortcutInfo = createShortcutInfo(
"shortcutid", ResolverDataProvider.createComponentName(4), 4)
private val directShareAppTarget = AppTarget(
@@ -73,8 +72,8 @@ class ImmutableTargetInfoTest {
ResolverDataProvider.createResolveInfo(5, 0, PERSONAL_USER_HANDLE),
"displayresolve label",
"displayresolve extended info",
- Intent("display_resolved"),
- /* resolveInfoPresentationGetter= */ null)
+ Intent("display_resolved")
+ )
private val hashProvider: ImmutableTargetInfo.TargetHashProvider = mock()
@Test
diff --git a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt b/tests/unit/src/com/android/intentresolver/chooser/TargetInfoTest.kt
index 78e0c3e..a7574c1 100644
--- a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt
+++ b/tests/unit/src/com/android/intentresolver/chooser/TargetInfoTest.kt
@@ -87,8 +87,8 @@ class TargetInfoTest {
ResolverDataProvider.createResolveInfo(1, 0, PERSONAL_USER_HANDLE),
"label",
"extended info",
- resolvedIntent,
- /* resolveInfoPresentationGetter= */ null)
+ resolvedIntent
+ )
val chooserTarget = createChooserTarget(
"title", 0.3f, ResolverDataProvider.createComponentName(2), "test_shortcut_id")
val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(3), 3)
@@ -161,8 +161,8 @@ class TargetInfoTest {
ResolverDataProvider.createResolveInfo(1, 0),
"label",
"extended info",
- resolvedIntent,
- /* resolveInfoPresentationGetter= */ null)
+ resolvedIntent
+ )
val chooserTarget = createChooserTarget(
"title", 0.3f, ResolverDataProvider.createComponentName(2), "test_shortcut_id")
val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(3), 3)
@@ -200,8 +200,8 @@ class TargetInfoTest {
resolveInfo,
"label",
"extended info",
- intent,
- /* resolveInfoPresentationGetter= */ null)
+ intent
+ )
assertThat(targetInfo.isDisplayResolveInfo()).isTrue()
assertThat(targetInfo.isMultiDisplayResolveInfo()).isFalse()
assertThat(targetInfo.isChooserTargetInfo()).isFalse()
@@ -223,8 +223,8 @@ class TargetInfoTest {
ResolverDataProvider.createResolveInfo(3, 0),
"label",
"extended info",
- originalIntent,
- /* resolveInfoPresentationGetter= */ null)
+ originalIntent
+ )
originalInfo.addAlternateSourceIntent(mismatchedAlternate)
originalInfo.addAlternateSourceIntent(targetAlternate)
originalInfo.addAlternateSourceIntent(extraMatch)
@@ -257,8 +257,8 @@ class TargetInfoTest {
ResolverDataProvider.createResolveInfo(3, 0),
"label",
"extended info",
- originalIntent,
- /* resolveInfoPresentationGetter= */ null)
+ originalIntent
+ )
originalInfo.addAlternateSourceIntent(mismatchedAlternate)
val refinement = Intent("PROPOSED_REFINEMENT")
@@ -277,15 +277,15 @@ class TargetInfoTest {
resolveInfo,
"label 1",
"extended info 1",
- intent,
- /* resolveInfoPresentationGetter= */ null)
+ intent
+ )
val secondTargetInfo = DisplayResolveInfo.newDisplayResolveInfo(
intent,
resolveInfo,
"label 2",
"extended info 2",
- intent,
- /* resolveInfoPresentationGetter= */ null)
+ intent
+ )
val multiTargetInfo = MultiDisplayResolveInfo.newMultiDisplayResolveInfo(
listOf(firstTargetInfo, secondTargetInfo))
@@ -328,24 +328,23 @@ class TargetInfoTest {
resolveInfo,
"Send Image",
"Sends only images",
- sendImage,
- /* resolveInfoPresentationGetter= */ null)
+ sendImage
+ )
val textOnlyTarget = DisplayResolveInfo.newDisplayResolveInfo(
sendUri,
resolveInfo,
"Send Text",
"Sends only text",
- sendUri,
- /* resolveInfoPresentationGetter= */ null)
+ sendUri
+ )
val imageOrTextTarget = DisplayResolveInfo.newDisplayResolveInfo(
sendImage,
resolveInfo,
"Send Image or Text",
"Sends images or text",
- sendImage,
- /* resolveInfoPresentationGetter= */ null
+ sendImage
).apply {
addAlternateSourceIntent(sendUri)
}
@@ -377,8 +376,7 @@ class TargetInfoTest {
ResolverDataProvider.createResolveInfo(1, 0),
"Target One",
"Target One",
- sendImage,
- /* resolveInfoPresentationGetter= */ null
+ sendImage
)
)
val targetTwo = mock<DisplayResolveInfo> {
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt
index dab1a95..083ef18 100644
--- a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt
@@ -17,39 +17,29 @@
package com.android.intentresolver.contentpreview
import android.content.Intent
-import android.graphics.Bitmap
import android.net.Uri
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.testing.TestLifecycleOwner
import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory
+import com.android.intentresolver.TestPreviewImageLoader
import com.android.intentresolver.mock
import com.android.intentresolver.whenever
import com.android.intentresolver.widget.ActionRow
import com.android.intentresolver.widget.ImagePreviewView
import com.google.common.truth.Truth.assertThat
import java.util.function.Consumer
+import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
import org.junit.Test
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
class ChooserContentPreviewUiTest {
- private val lifecycleOwner = TestLifecycleOwner()
+ private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher())
private val previewData = mock<PreviewDataProvider>()
private val headlineGenerator = mock<HeadlineGenerator>()
- private val imageLoader =
- object : ImageLoader {
- override fun loadImage(
- callerLifecycle: Lifecycle,
- uri: Uri,
- callback: Consumer<Bitmap?>,
- ) {
- callback.accept(null)
- }
- override fun prePopulate(uris: List<Uri>) = Unit
- override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = null
- }
+ private val imageLoader = TestPreviewImageLoader(emptyMap())
private val actionFactory =
object : ActionFactory {
override fun getCopyButtonRunnable(): Runnable? = null
@@ -65,7 +55,7 @@ class ChooserContentPreviewUiTest {
whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_TEXT)
val testSubject =
ChooserContentPreviewUi(
- lifecycleOwner.lifecycle,
+ testScope,
previewData,
Intent(Intent.ACTION_VIEW),
imageLoader,
@@ -84,7 +74,7 @@ class ChooserContentPreviewUiTest {
whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_FILE)
val testSubject =
ChooserContentPreviewUi(
- lifecycleOwner.lifecycle,
+ testScope,
previewData,
Intent(Intent.ACTION_SEND),
imageLoader,
@@ -108,7 +98,7 @@ class ChooserContentPreviewUiTest {
whenever(previewData.imagePreviewFileInfoFlow).thenReturn(MutableSharedFlow())
val testSubject =
ChooserContentPreviewUi(
- lifecycleOwner.lifecycle,
+ testScope,
previewData,
Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_TEXT, "Shared text") },
imageLoader,
@@ -132,7 +122,7 @@ class ChooserContentPreviewUiTest {
whenever(previewData.imagePreviewFileInfoFlow).thenReturn(MutableSharedFlow())
val testSubject =
ChooserContentPreviewUi(
- lifecycleOwner.lifecycle,
+ testScope,
previewData,
Intent(Intent.ACTION_SEND),
imageLoader,
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt
index 6db53a9..6db53a9 100644
--- a/java/tests/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/ContentPreviewUiTest.kt
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/FileContentPreviewUiTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/FileContentPreviewUiTest.kt
new file mode 100644
index 0000000..d2d952a
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/FileContentPreviewUiTest.kt
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.contentpreview
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.intentresolver.R
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import com.android.intentresolver.widget.ActionRow
+import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class FileContentPreviewUiTest {
+ private val fileCount = 2
+ private val text = "Sharing 2 files"
+ private val actionFactory =
+ object : ChooserContentPreviewUi.ActionFactory {
+ override fun getEditButtonRunnable(): Runnable? = null
+ override fun getCopyButtonRunnable(): Runnable? = null
+ override fun createCustomActions(): List<ActionRow.Action> = emptyList()
+ override fun getModifyShareAction(): ActionRow.Action? = null
+ override fun getExcludeSharedTextAction(): Consumer<Boolean> = Consumer<Boolean> {}
+ }
+ private val headlineGenerator =
+ mock<HeadlineGenerator> { whenever(getFilesHeadline(fileCount)).thenReturn(text) }
+
+ private val context
+ get() = InstrumentationRegistry.getInstrumentation().context
+
+ private val testSubject =
+ FileContentPreviewUi(
+ fileCount,
+ actionFactory,
+ headlineGenerator,
+ )
+
+ @Test
+ fun test_display_titleIsDisplayed() {
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup
+
+ val previewView =
+ testSubject.display(
+ context.resources,
+ layoutInflater,
+ gridLayout,
+ /*headlineViewParent=*/ null
+ )
+
+ assertThat(previewView).isNotNull()
+ val headlineView = previewView?.findViewById<TextView>(R.id.headline)
+ assertThat(headlineView).isNotNull()
+ assertThat(headlineView?.text).isEqualTo(text)
+ }
+
+ @Test
+ fun test_displayWithExternalHeaderView() {
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout =
+ layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false)
+ as ViewGroup
+ val externalHeaderView =
+ gridLayout.requireViewById<View>(R.id.chooser_headline_row_container)
+
+ assertThat(externalHeaderView.findViewById<View>(R.id.headline)).isNull()
+
+ val previewView =
+ testSubject.display(context.resources, layoutInflater, gridLayout, externalHeaderView)
+
+ assertThat(previewView).isNotNull()
+ assertThat(previewView.findViewById<View>(R.id.headline)).isNull()
+
+ val headlineView = externalHeaderView.findViewById<TextView>(R.id.headline)
+ assertThat(headlineView).isNotNull()
+ assertThat(headlineView?.text).isEqualTo(text)
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt
new file mode 100644
index 0000000..7cc0b4b
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.contentpreview
+
+import android.net.Uri
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.intentresolver.R
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import com.android.intentresolver.widget.ActionRow
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.function.Consumer
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+private const val HEADLINE_IMAGES = "Image Headline"
+private const val HEADLINE_VIDEOS = "Video Headline"
+private const val HEADLINE_FILES = "Files Headline"
+private const val SHARED_TEXT = "Some text to share"
+
+@RunWith(AndroidJUnit4::class)
+class FilesPlusTextContentPreviewUiTest {
+ private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher())
+ private val actionFactory =
+ object : ChooserContentPreviewUi.ActionFactory {
+ override fun getEditButtonRunnable(): Runnable? = null
+ override fun getCopyButtonRunnable(): Runnable? = null
+ override fun createCustomActions(): List<ActionRow.Action> = emptyList()
+ override fun getModifyShareAction(): ActionRow.Action? = null
+ override fun getExcludeSharedTextAction(): Consumer<Boolean> = Consumer<Boolean> {}
+ }
+ private val imageLoader = mock<ImageLoader>()
+ private val headlineGenerator =
+ mock<HeadlineGenerator> {
+ whenever(getImagesHeadline(anyInt())).thenReturn(HEADLINE_IMAGES)
+ whenever(getVideosHeadline(anyInt())).thenReturn(HEADLINE_VIDEOS)
+ whenever(getFilesHeadline(anyInt())).thenReturn(HEADLINE_FILES)
+ }
+
+ private val context
+ get() = getInstrumentation().context
+
+ @Test
+ fun test_displayImagesPlusTextWithoutUriMetadata_showImagesHeadline() {
+ val sharedFileCount = 2
+ val previewView = testLoadingHeadline("image/*", sharedFileCount)
+
+ verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_IMAGES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayImagesPlusTextWithoutUriMetadataExternalHeader_showImagesHeadline() {
+ val sharedFileCount = 2
+ val (previewView, headerParent) = testLoadingExternalHeadline("image/*", sharedFileCount)
+
+ verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(headerParent, HEADLINE_IMAGES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayVideosPlusTextWithoutUriMetadata_showVideosHeadline() {
+ val sharedFileCount = 2
+ val previewView = testLoadingHeadline("video/*", sharedFileCount)
+
+ verify(headlineGenerator, times(1)).getVideosHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_VIDEOS)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayVideosPlusTextWithoutUriMetadataExternalHeader_showVideosHeadline() {
+ val sharedFileCount = 2
+ val (previewView, headerParent) = testLoadingExternalHeadline("video/*", sharedFileCount)
+
+ verify(headlineGenerator, times(1)).getVideosHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(headerParent, HEADLINE_VIDEOS)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayDocsPlusTextWithoutUriMetadata_showFilesHeadline() {
+ val sharedFileCount = 2
+ val previewView = testLoadingHeadline("application/pdf", sharedFileCount)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_FILES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayDocsPlusTextWithoutUriMetadataExternalHeader_showFilesHeadline() {
+ val sharedFileCount = 2
+ val (previewView, headerParent) =
+ testLoadingExternalHeadline("application/pdf", sharedFileCount)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(headerParent, HEADLINE_FILES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayMixedContentPlusTextWithoutUriMetadata_showFilesHeadline() {
+ val sharedFileCount = 2
+ val previewView = testLoadingHeadline("*/*", sharedFileCount)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_FILES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayMixedContentPlusTextWithoutUriMetadataExternalHeader_showFilesHeadline() {
+ val sharedFileCount = 2
+ val (previewView, headerParent) = testLoadingExternalHeadline("*/*", sharedFileCount)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(headerParent, HEADLINE_FILES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayImagesPlusTextWithUriMetadataSet_showImagesHeadline() {
+ val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "image/jpeg")
+ val sharedFileCount = loadedFileMetadata.size
+ val previewView = testLoadingHeadline("image/*", sharedFileCount, loadedFileMetadata)
+
+ verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_IMAGES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayImagesPlusTextWithUriMetadataSetExternalHeader_showImagesHeadline() {
+ val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "image/jpeg")
+ val sharedFileCount = loadedFileMetadata.size
+ val (previewView, headerParent) =
+ testLoadingExternalHeadline("image/*", sharedFileCount, loadedFileMetadata)
+
+ verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(headerParent, HEADLINE_IMAGES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayVideosPlusTextWithUriMetadataSet_showVideosHeadline() {
+ val loadedFileMetadata = createFileInfosWithMimeTypes("video/mp4", "video/mp4")
+ val sharedFileCount = loadedFileMetadata.size
+ val previewView = testLoadingHeadline("video/*", sharedFileCount, loadedFileMetadata)
+
+ verify(headlineGenerator, times(1)).getVideosHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_VIDEOS)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayVideosPlusTextWithUriMetadataSetExternalHeader_showVideosHeadline() {
+ val loadedFileMetadata = createFileInfosWithMimeTypes("video/mp4", "video/mp4")
+ val sharedFileCount = loadedFileMetadata.size
+ val (previewView, headerParent) =
+ testLoadingExternalHeadline("video/*", sharedFileCount, loadedFileMetadata)
+
+ verify(headlineGenerator, times(1)).getVideosHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(headerParent, HEADLINE_VIDEOS)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayImagesAndVideosPlusTextWithUriMetadataSet_showFilesHeadline() {
+ val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "video/mp4")
+ val sharedFileCount = loadedFileMetadata.size
+ val previewView = testLoadingHeadline("*/*", sharedFileCount, loadedFileMetadata)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_FILES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayImagesAndVideosPlusTextWithUriMetadataSetExternalHeader_showFilesHeadline() {
+ val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "video/mp4")
+ val sharedFileCount = loadedFileMetadata.size
+ val (previewView, headerParent) =
+ testLoadingExternalHeadline("*/*", sharedFileCount, loadedFileMetadata)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(headerParent, HEADLINE_FILES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayDocsPlusTextWithUriMetadataSet_showFilesHeadline() {
+ val loadedFileMetadata = createFileInfosWithMimeTypes("application/pdf", "application/pdf")
+ val sharedFileCount = loadedFileMetadata.size
+ val previewView =
+ testLoadingHeadline("application/pdf", sharedFileCount, loadedFileMetadata)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_FILES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayDocsPlusTextWithUriMetadataSetExternalHeader_showFilesHeadline() {
+ val loadedFileMetadata = createFileInfosWithMimeTypes("application/pdf", "application/pdf")
+ val sharedFileCount = loadedFileMetadata.size
+ val (previewView, headerParent) =
+ testLoadingExternalHeadline("application/pdf", sharedFileCount, loadedFileMetadata)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(headerParent, HEADLINE_FILES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_uriMetadataIsMoreSpecificThanIntentMimeType_headlineGetsUpdated() {
+ val sharedFileCount = 2
+ val testSubject =
+ FilesPlusTextContentPreviewUi(
+ testScope,
+ /*isSingleImage=*/ false,
+ sharedFileCount,
+ SHARED_TEXT,
+ /*intentMimeType=*/ "*/*",
+ actionFactory,
+ imageLoader,
+ DefaultMimeTypeClassifier,
+ headlineGenerator
+ )
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup
+
+ val previewView =
+ testSubject.display(context.resources, LayoutInflater.from(context), gridLayout, null)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verify(headlineGenerator, never()).getImagesHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_FILES)
+
+ testSubject.updatePreviewMetadata(createFileInfosWithMimeTypes("image/png", "image/jpg"))
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_IMAGES)
+ }
+
+ @Test
+ fun test_uriMetadataIsMoreSpecificThanIntentMimeTypeExternalHeader_headlineGetsUpdated() {
+ val sharedFileCount = 2
+ val testSubject =
+ FilesPlusTextContentPreviewUi(
+ testScope,
+ /*isSingleImage=*/ false,
+ sharedFileCount,
+ SHARED_TEXT,
+ /*intentMimeType=*/ "*/*",
+ actionFactory,
+ imageLoader,
+ DefaultMimeTypeClassifier,
+ headlineGenerator
+ )
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout =
+ layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false)
+ as ViewGroup
+ val externalHeaderView =
+ gridLayout.requireViewById<View>(R.id.chooser_headline_row_container)
+
+ assertWithMessage("External headline should not be inflated by default")
+ .that(externalHeaderView.findViewById<View>(R.id.headline))
+ .isNull()
+
+ val previewView =
+ testSubject.display(
+ context.resources,
+ LayoutInflater.from(context),
+ gridLayout,
+ externalHeaderView
+ )
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verify(headlineGenerator, never()).getImagesHeadline(sharedFileCount)
+ verifyInternalHeadlineAbsence(previewView)
+ verifyPreviewHeadline(externalHeaderView, HEADLINE_FILES)
+
+ testSubject.updatePreviewMetadata(createFileInfosWithMimeTypes("image/png", "image/jpg"))
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
+ verifyPreviewHeadline(externalHeaderView, HEADLINE_IMAGES)
+ }
+
+ private fun testLoadingHeadline(
+ intentMimeType: String,
+ sharedFileCount: Int,
+ loadedFileMetadata: List<FileInfo>? = null,
+ ): ViewGroup? {
+ val testSubject =
+ FilesPlusTextContentPreviewUi(
+ testScope,
+ /*isSingleImage=*/ false,
+ sharedFileCount,
+ SHARED_TEXT,
+ intentMimeType,
+ actionFactory,
+ imageLoader,
+ DefaultMimeTypeClassifier,
+ headlineGenerator
+ )
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup
+
+ loadedFileMetadata?.let(testSubject::updatePreviewMetadata)
+ return testSubject.display(
+ context.resources,
+ LayoutInflater.from(context),
+ gridLayout,
+ /*headlineViewParent=*/ null
+ )
+ }
+
+ private fun testLoadingExternalHeadline(
+ intentMimeType: String,
+ sharedFileCount: Int,
+ loadedFileMetadata: List<FileInfo>? = null,
+ ): Pair<ViewGroup?, View> {
+ val testSubject =
+ FilesPlusTextContentPreviewUi(
+ testScope,
+ /*isSingleImage=*/ false,
+ sharedFileCount,
+ SHARED_TEXT,
+ intentMimeType,
+ actionFactory,
+ imageLoader,
+ DefaultMimeTypeClassifier,
+ headlineGenerator
+ )
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout =
+ layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false)
+ as ViewGroup
+ val externalHeaderView =
+ gridLayout.requireViewById<View>(R.id.chooser_headline_row_container)
+
+ assertWithMessage("External headline should not be inflated by default")
+ .that(externalHeaderView.findViewById<View>(R.id.headline))
+ .isNull()
+
+ loadedFileMetadata?.let(testSubject::updatePreviewMetadata)
+ return testSubject.display(
+ context.resources,
+ LayoutInflater.from(context),
+ gridLayout,
+ externalHeaderView
+ ) to externalHeaderView
+ }
+
+ private fun createFileInfosWithMimeTypes(vararg mimeTypes: String): List<FileInfo> {
+ val uri = Uri.parse("content://pkg.app/file")
+ return mimeTypes.map { mimeType -> FileInfo.Builder(uri).withMimeType(mimeType).build() }
+ }
+
+ private fun verifyPreviewHeadline(headerViewParent: View?, expectedText: String) {
+ assertThat(headerViewParent).isNotNull()
+ val headlineView = headerViewParent?.findViewById<TextView>(R.id.headline)
+ assertThat(headlineView).isNotNull()
+ assertThat(headlineView?.text).isEqualTo(expectedText)
+ }
+
+ private fun verifySharedText(previewView: ViewGroup?) {
+ assertThat(previewView).isNotNull()
+ val textContentView = previewView?.findViewById<TextView>(R.id.content_preview_text)
+ assertThat(textContentView).isNotNull()
+ assertThat(textContentView?.text).isEqualTo(SHARED_TEXT)
+ }
+
+ private fun verifyInternalHeadlineAbsence(previewView: ViewGroup?) {
+ assertWithMessage("Preview parent should not be null").that(previewView).isNotNull()
+ assertWithMessage(
+ "Preview headline should not be inflated when an external headline is used"
+ )
+ .that(previewView?.findViewById<View>(R.id.headline))
+ .isNull()
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt
index a65280e..a65280e 100644
--- a/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt
index b5fd1fa..8997870 100644
--- a/java/tests/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt
@@ -55,6 +55,7 @@ import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import kotlinx.coroutines.yield
import org.junit.After
+import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito.never
@@ -301,7 +302,7 @@ class ImagePreviewImageLoaderTest {
val latch = CountDownLatch(1)
synchronized(pendingThumbnailCalls) { pendingThumbnailCalls.offer(latch) }
thumbnailCallsCdl.countDown()
- latch.await()
+ assertTrue("Timeout waiting thumbnail calls", latch.await(1, SECONDS))
bitmap
}
}
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt
index 6599baa..4a8c139 100644
--- a/java/tests/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt
@@ -192,8 +192,9 @@ class PreviewDataProviderTest {
val uri = Uri.parse("content://org.pkg.app/test.pdf")
val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
whenever(contentResolver.getType(uri)).thenReturn("application/pdf")
- whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null))
- .thenReturn(MatrixCursor(columns).apply { addRow(values) })
+ val cursor = MatrixCursor(columns).apply { addRow(values) }
+ whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null)).thenReturn(cursor)
+
val testSubject =
PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
@@ -202,6 +203,23 @@ class PreviewDataProviderTest {
assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri)
assertThat(testSubject.firstFileInfo?.previewUri).isNotNull()
verify(contentResolver, times(1)).getType(any())
+ assertThat(cursor.isClosed).isTrue()
+ }
+
+ @Test
+ fun test_emptyQueryResult_cursorGetsClosed() {
+ val uri = Uri.parse("content://org.pkg.app/test.pdf")
+ val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
+ whenever(contentResolver.getType(uri)).thenReturn("application/pdf")
+ val cursor = MatrixCursor(emptyArray())
+ whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null)).thenReturn(cursor)
+
+ val testSubject =
+ PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+
+ assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
+ verify(contentResolver, times(1)).query(uri, METADATA_COLUMNS, null, null)
+ assertThat(cursor.isClosed).isTrue()
}
@Test
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/TextContentPreviewUiTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/TextContentPreviewUiTest.kt
new file mode 100644
index 0000000..3536240
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/TextContentPreviewUiTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.contentpreview
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.intentresolver.R
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import com.android.intentresolver.widget.ActionRow
+import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TextContentPreviewUiTest {
+ private val text = "Shared Text"
+ private val title = "Preview Title"
+ private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher())
+ private val actionFactory =
+ object : ChooserContentPreviewUi.ActionFactory {
+ override fun getEditButtonRunnable(): Runnable? = null
+ override fun getCopyButtonRunnable(): Runnable? = null
+ override fun createCustomActions(): List<ActionRow.Action> = emptyList()
+ override fun getModifyShareAction(): ActionRow.Action? = null
+ override fun getExcludeSharedTextAction(): Consumer<Boolean> = Consumer<Boolean> {}
+ }
+ private val imageLoader = mock<ImageLoader>()
+ private val headlineGenerator =
+ mock<HeadlineGenerator> { whenever(getTextHeadline(text)).thenReturn(text) }
+
+ private val context
+ get() = InstrumentationRegistry.getInstrumentation().context
+
+ private val testSubject =
+ TextContentPreviewUi(
+ testScope,
+ text,
+ title,
+ /*previewThumbnail=*/ null,
+ actionFactory,
+ imageLoader,
+ headlineGenerator,
+ )
+
+ @Test
+ fun test_display_headlineIsDisplayed() {
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup
+
+ val previewView =
+ testSubject.display(
+ context.resources,
+ layoutInflater,
+ gridLayout,
+ /*headlineViewParent=*/ null
+ )
+
+ assertThat(previewView).isNotNull()
+ val headlineView = previewView?.findViewById<TextView>(R.id.headline)
+ assertThat(headlineView).isNotNull()
+ assertThat(headlineView?.text).isEqualTo(text)
+ }
+
+ @Test
+ fun test_displayWithExternalHeaderView_externalHeaderIsDisplayed() {
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout =
+ layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false)
+ as ViewGroup
+ val externalHeaderView =
+ gridLayout.requireViewById<View>(R.id.chooser_headline_row_container)
+
+ assertThat(externalHeaderView.findViewById<View>(R.id.headline)).isNull()
+
+ val previewView =
+ testSubject.display(context.resources, layoutInflater, gridLayout, externalHeaderView)
+
+ assertThat(previewView).isNotNull()
+ assertThat(previewView.findViewById<View>(R.id.headline)).isNull()
+
+ val headlineView = externalHeaderView.findViewById<TextView>(R.id.headline)
+ assertThat(headlineView).isNotNull()
+ assertThat(headlineView?.text).isEqualTo(text)
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt
new file mode 100644
index 0000000..7e07e0c
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt
@@ -0,0 +1,348 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.contentpreview
+
+import android.net.Uri
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.intentresolver.R
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback
+import com.google.common.truth.Truth
+import com.google.common.truth.Truth.assertWithMessage
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.takeWhile
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+private const val IMAGE_HEADLINE = "Image Headline"
+private const val VIDEO_HEADLINE = "Video Headline"
+private const val FILES_HEADLINE = "Files Headline"
+
+@RunWith(AndroidJUnit4::class)
+class UnifiedContentPreviewUiTest {
+ private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher())
+ private val actionFactory =
+ mock<ChooserContentPreviewUi.ActionFactory> {
+ whenever(createCustomActions()).thenReturn(emptyList())
+ }
+ private val imageLoader = mock<ImageLoader>()
+ private val headlineGenerator =
+ mock<HeadlineGenerator> {
+ whenever(getImagesHeadline(anyInt())).thenReturn(IMAGE_HEADLINE)
+ whenever(getVideosHeadline(anyInt())).thenReturn(VIDEO_HEADLINE)
+ whenever(getFilesHeadline(anyInt())).thenReturn(FILES_HEADLINE)
+ }
+
+ private val context
+ get() = getInstrumentation().context
+
+ @Test
+ fun test_displayImagesWithoutUriMetadata_showImagesHeadline() {
+ testLoadingHeadline("image/*", files = null) { previewView ->
+ verify(headlineGenerator, times(1)).getImagesHeadline(2)
+ verifyPreviewHeadline(previewView, IMAGE_HEADLINE)
+ }
+ }
+
+ @Test
+ fun test_displayImagesWithoutUriMetadataExternalHeader_showImagesHeadline() {
+ testLoadingExternalHeadline("image/*", files = null) { externalHeaderView ->
+ verify(headlineGenerator, times(1)).getImagesHeadline(2)
+ verifyPreviewHeadline(externalHeaderView, IMAGE_HEADLINE)
+ }
+ }
+
+ @Test
+ fun test_displayVideosWithoutUriMetadata_showImagesHeadline() {
+ testLoadingHeadline("video/*", files = null) { previewView ->
+ verify(headlineGenerator, times(1)).getVideosHeadline(2)
+ verifyPreviewHeadline(previewView, VIDEO_HEADLINE)
+ }
+ }
+
+ @Test
+ fun test_displayVideosWithoutUriMetadataExternalHeader_showImagesHeadline() {
+ testLoadingExternalHeadline("video/*", files = null) { externalHeaderView ->
+ verify(headlineGenerator, times(1)).getVideosHeadline(2)
+ verifyPreviewHeadline(externalHeaderView, VIDEO_HEADLINE)
+ }
+ }
+
+ @Test
+ fun test_displayDocumentsWithoutUriMetadata_showImagesHeadline() {
+ testLoadingHeadline("application/pdf", files = null) { previewView ->
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ verifyPreviewHeadline(previewView, FILES_HEADLINE)
+ }
+ }
+
+ @Test
+ fun test_displayDocumentsWithoutUriMetadataExternalHeader_showImagesHeadline() {
+ testLoadingExternalHeadline("application/pdf", files = null) { externalHeaderView ->
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ verifyPreviewHeadline(externalHeaderView, FILES_HEADLINE)
+ }
+ }
+
+ @Test
+ fun test_displayMixedContentWithoutUriMetadata_showImagesHeadline() {
+ testLoadingHeadline("*/*", files = null) { previewView ->
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ verifyPreviewHeadline(previewView, FILES_HEADLINE)
+ }
+ }
+
+ @Test
+ fun test_displayMixedContentWithoutUriMetadataExternalHeader_showImagesHeadline() {
+ testLoadingExternalHeadline("*/*", files = null) { externalHeader ->
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ verifyPreviewHeadline(externalHeader, FILES_HEADLINE)
+ }
+ }
+
+ @Test
+ fun test_displayImagesWithUriMetadataSet_showImagesHeadline() {
+ val uri = Uri.parse("content://pkg.app/image.png")
+ val files =
+ listOf(
+ FileInfo.Builder(uri).withMimeType("image/png").build(),
+ FileInfo.Builder(uri).withMimeType("image/jpeg").build(),
+ )
+ testLoadingHeadline("image/*", files) { preivewView ->
+ verify(headlineGenerator, times(1)).getImagesHeadline(2)
+ verifyPreviewHeadline(preivewView, IMAGE_HEADLINE)
+ }
+ }
+
+ @Test
+ fun test_displayImagesWithUriMetadataSetExternalHeader_showImagesHeadline() {
+ val uri = Uri.parse("content://pkg.app/image.png")
+ val files =
+ listOf(
+ FileInfo.Builder(uri).withMimeType("image/png").build(),
+ FileInfo.Builder(uri).withMimeType("image/jpeg").build(),
+ )
+ testLoadingExternalHeadline("image/*", files) { externalHeader ->
+ verify(headlineGenerator, times(1)).getImagesHeadline(2)
+ verifyPreviewHeadline(externalHeader, IMAGE_HEADLINE)
+ }
+ }
+
+ @Test
+ fun test_displayVideosWithUriMetadataSet_showImagesHeadline() {
+ val uri = Uri.parse("content://pkg.app/image.png")
+ val files =
+ listOf(
+ FileInfo.Builder(uri).withMimeType("video/mp4").build(),
+ FileInfo.Builder(uri).withMimeType("video/mp4").build(),
+ )
+ testLoadingHeadline("video/*", files) { previewView ->
+ verify(headlineGenerator, times(1)).getVideosHeadline(2)
+ verifyPreviewHeadline(previewView, VIDEO_HEADLINE)
+ }
+ }
+
+ @Test
+ fun test_displayVideosWithUriMetadataSetExternalHeader_showImagesHeadline() {
+ val uri = Uri.parse("content://pkg.app/image.png")
+ val files =
+ listOf(
+ FileInfo.Builder(uri).withMimeType("video/mp4").build(),
+ FileInfo.Builder(uri).withMimeType("video/mp4").build(),
+ )
+ testLoadingExternalHeadline("video/*", files) { externalHeader ->
+ verify(headlineGenerator, times(1)).getVideosHeadline(2)
+ verifyPreviewHeadline(externalHeader, VIDEO_HEADLINE)
+ }
+ }
+
+ @Test
+ fun test_displayImagesAndVideosWithUriMetadataSet_showImagesHeadline() {
+ val uri = Uri.parse("content://pkg.app/image.png")
+ val files =
+ listOf(
+ FileInfo.Builder(uri).withMimeType("image/png").build(),
+ FileInfo.Builder(uri).withMimeType("video/mp4").build(),
+ )
+ testLoadingHeadline("*/*", files) { previewView ->
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ verifyPreviewHeadline(previewView, FILES_HEADLINE)
+ }
+ }
+
+ @Test
+ fun test_displayImagesAndVideosWithUriMetadataSetExternalHeader_showImagesHeadline() {
+ val uri = Uri.parse("content://pkg.app/image.png")
+ val files =
+ listOf(
+ FileInfo.Builder(uri).withMimeType("image/png").build(),
+ FileInfo.Builder(uri).withMimeType("video/mp4").build(),
+ )
+ testLoadingExternalHeadline("*/*", files) { externalHeader ->
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ verifyPreviewHeadline(externalHeader, FILES_HEADLINE)
+ }
+ }
+
+ @Test
+ fun test_displayDocumentsWithUriMetadataSet_showImagesHeadline() {
+ val uri = Uri.parse("content://pkg.app/image.png")
+ val files =
+ listOf(
+ FileInfo.Builder(uri).withMimeType("application/pdf").build(),
+ FileInfo.Builder(uri).withMimeType("application/pdf").build(),
+ )
+ testLoadingHeadline("application/pdf", files) { previewView ->
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ verifyPreviewHeadline(previewView, FILES_HEADLINE)
+ }
+ }
+
+ @Test
+ fun test_displayDocumentsWithUriMetadataSetExternalHeader_showImagesHeadline() {
+ val uri = Uri.parse("content://pkg.app/image.png")
+ val files =
+ listOf(
+ FileInfo.Builder(uri).withMimeType("application/pdf").build(),
+ FileInfo.Builder(uri).withMimeType("application/pdf").build(),
+ )
+ testLoadingExternalHeadline("application/pdf", files) { externalHeader ->
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ verifyPreviewHeadline(externalHeader, FILES_HEADLINE)
+ }
+ }
+
+ private fun testLoadingHeadline(
+ intentMimeType: String,
+ files: List<FileInfo>?,
+ verificationBlock: (ViewGroup?) -> Unit,
+ ) {
+ testScope.runTest {
+ val endMarker = FileInfo.Builder(Uri.EMPTY).build()
+ val emptySourceFlow = MutableSharedFlow<FileInfo>(replay = 1)
+ val testSubject =
+ UnifiedContentPreviewUi(
+ testScope,
+ /*isSingleImage=*/ false,
+ intentMimeType,
+ actionFactory,
+ imageLoader,
+ DefaultMimeTypeClassifier,
+ object : TransitionElementStatusCallback {
+ override fun onTransitionElementReady(name: String) = Unit
+ override fun onAllTransitionElementsReady() = Unit
+ },
+ files?.let { it.asFlow() } ?: emptySourceFlow.takeWhile { it !== endMarker },
+ /*itemCount=*/ 2,
+ headlineGenerator
+ )
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup
+
+ val previewView =
+ testSubject.display(
+ context.resources,
+ LayoutInflater.from(context),
+ gridLayout,
+ /*headlineViewParent=*/ null
+ )
+ emptySourceFlow.tryEmit(endMarker)
+
+ verificationBlock(previewView)
+ }
+ }
+
+ private fun testLoadingExternalHeadline(
+ intentMimeType: String,
+ files: List<FileInfo>?,
+ verificationBlock: (View?) -> Unit,
+ ) {
+ testScope.runTest {
+ val endMarker = FileInfo.Builder(Uri.EMPTY).build()
+ val emptySourceFlow = MutableSharedFlow<FileInfo>(replay = 1)
+ val testSubject =
+ UnifiedContentPreviewUi(
+ testScope,
+ /*isSingleImage=*/ false,
+ intentMimeType,
+ actionFactory,
+ imageLoader,
+ DefaultMimeTypeClassifier,
+ object : TransitionElementStatusCallback {
+ override fun onTransitionElementReady(name: String) = Unit
+ override fun onAllTransitionElementsReady() = Unit
+ },
+ files?.let { it.asFlow() } ?: emptySourceFlow.takeWhile { it !== endMarker },
+ /*itemCount=*/ 2,
+ headlineGenerator
+ )
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout =
+ layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false)
+ as ViewGroup
+ val externalHeaderView =
+ gridLayout.requireViewById<View>(R.id.chooser_headline_row_container)
+
+ assertWithMessage("External headline should not be inflated by default")
+ .that(externalHeaderView.findViewById<View>(R.id.headline))
+ .isNull()
+
+ val previewView =
+ testSubject.display(
+ context.resources,
+ LayoutInflater.from(context),
+ gridLayout,
+ externalHeaderView,
+ )
+
+ emptySourceFlow.tryEmit(endMarker)
+
+ verifyInternalHeadlineAbsence(previewView)
+ verificationBlock(externalHeaderView)
+ }
+ }
+
+ private fun verifyPreviewHeadline(headerViewParent: View?, expectedText: String) {
+ Truth.assertThat(headerViewParent).isNotNull()
+ val headlineView = headerViewParent?.findViewById<TextView>(R.id.headline)
+ Truth.assertThat(headlineView).isNotNull()
+ Truth.assertThat(headlineView?.text).isEqualTo(expectedText)
+ }
+
+ private fun verifyInternalHeadlineAbsence(previewView: ViewGroup?) {
+ assertWithMessage("Preview parent should not be null").that(previewView).isNotNull()
+ assertWithMessage(
+ "Preview headline should not be inflated when an external headline is used"
+ )
+ .that(previewView?.findViewById<View>(R.id.headline))
+ .isNull()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/emptystate/CompositeEmptyStateProviderTest.kt b/tests/unit/src/com/android/intentresolver/emptystate/CompositeEmptyStateProviderTest.kt
new file mode 100644
index 0000000..4c05dfb
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/emptystate/CompositeEmptyStateProviderTest.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.emptystate
+
+import com.android.intentresolver.ResolverListAdapter
+import com.android.intentresolver.mock
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class CompositeEmptyStateProviderTest {
+ val listAdapter = mock<ResolverListAdapter>()
+
+ val emptyState1 = object : EmptyState {}
+ val emptyState2 = object : EmptyState {}
+
+ val positiveEmptyStateProvider1 =
+ object : EmptyStateProvider {
+ override fun getEmptyState(listAdapter: ResolverListAdapter) = emptyState1
+ }
+ val positiveEmptyStateProvider2 =
+ object : EmptyStateProvider {
+ override fun getEmptyState(listAdapter: ResolverListAdapter) = emptyState2
+ }
+ val nullEmptyStateProvider =
+ object : EmptyStateProvider {
+ override fun getEmptyState(listAdapter: ResolverListAdapter) = null
+ }
+
+ @Test
+ fun testComposedProvider_returnsFirstEmptyStateInOrder() {
+ val provider =
+ CompositeEmptyStateProvider(
+ nullEmptyStateProvider,
+ positiveEmptyStateProvider1,
+ positiveEmptyStateProvider2
+ )
+ assertThat(provider.getEmptyState(listAdapter)).isSameInstanceAs(emptyState1)
+ }
+
+ @Test
+ fun testComposedProvider_allProvidersReturnNull_composedResultIsNull() {
+ val provider = CompositeEmptyStateProvider(nullEmptyStateProvider)
+ assertThat(provider.getEmptyState(listAdapter)).isNull()
+ }
+
+ @Test
+ fun testComposedProvider_noEmptyStateIfNoDelegateProviders() {
+ val provider = CompositeEmptyStateProvider()
+ assertThat(provider.getEmptyState(listAdapter)).isNull()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/emptystate/CrossProfileIntentsCheckerTest.kt b/tests/unit/src/com/android/intentresolver/emptystate/CrossProfileIntentsCheckerTest.kt
new file mode 100644
index 0000000..2bcddf5
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/emptystate/CrossProfileIntentsCheckerTest.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.emptystate
+
+import android.content.ContentResolver
+import android.content.Intent
+import android.content.pm.IPackageManager
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.nullable
+
+class CrossProfileIntentsCheckerTest {
+ private val PERSONAL_USER_ID = 10
+ private val WORK_USER_ID = 20
+
+ private val contentResolver = mock<ContentResolver>()
+
+ @Test
+ fun testChecker_hasCrossProfileIntents() {
+ val packageManager =
+ mock<IPackageManager> {
+ whenever(
+ canForwardTo(
+ any(Intent::class.java),
+ nullable(String::class.java),
+ eq(PERSONAL_USER_ID),
+ eq(WORK_USER_ID)
+ )
+ )
+ .thenReturn(true)
+ }
+ val checker = CrossProfileIntentsChecker(contentResolver, packageManager)
+ val intents = listOf(Intent())
+ assertThat(checker.hasCrossProfileIntents(intents, PERSONAL_USER_ID, WORK_USER_ID)).isTrue()
+ }
+
+ @Test
+ fun testChecker_noCrossProfileIntents() {
+ val packageManager =
+ mock<IPackageManager> {
+ whenever(
+ canForwardTo(
+ any(Intent::class.java),
+ nullable(String::class.java),
+ anyInt(),
+ anyInt()
+ )
+ )
+ .thenReturn(false)
+ }
+ val checker = CrossProfileIntentsChecker(contentResolver, packageManager)
+ val intents = listOf(Intent())
+ assertThat(checker.hasCrossProfileIntents(intents, PERSONAL_USER_ID, WORK_USER_ID))
+ .isFalse()
+ }
+
+ @Test
+ fun testChecker_noIntents() {
+ val packageManager = mock<IPackageManager>()
+ val checker = CrossProfileIntentsChecker(contentResolver, packageManager)
+ val intents = listOf<Intent>()
+ assertThat(checker.hasCrossProfileIntents(intents, PERSONAL_USER_ID, WORK_USER_ID))
+ .isFalse()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/emptystate/EmptyStateUiHelperTest.kt b/tests/unit/src/com/android/intentresolver/emptystate/EmptyStateUiHelperTest.kt
new file mode 100644
index 0000000..bc5545d
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/emptystate/EmptyStateUiHelperTest.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.emptystate
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+
+class EmptyStateUiHelperTest {
+ private val context = InstrumentationRegistry.getInstrumentation().getContext()
+
+ lateinit var rootContainer: ViewGroup
+ lateinit var emptyStateTitleView: View
+ lateinit var emptyStateSubtitleView: View
+ lateinit var emptyStateButtonView: View
+ lateinit var emptyStateProgressView: View
+ lateinit var emptyStateDefaultTextView: View
+ lateinit var emptyStateContainerView: View
+ lateinit var emptyStateRootView: View
+ lateinit var emptyStateUiHelper: EmptyStateUiHelper
+
+ @Before
+ fun setup() {
+ rootContainer = FrameLayout(context)
+ LayoutInflater.from(context)
+ .inflate(
+ com.android.intentresolver.R.layout.resolver_list_per_profile,
+ rootContainer,
+ true
+ )
+ emptyStateRootView =
+ rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state)
+ emptyStateTitleView =
+ rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state_title)
+ emptyStateSubtitleView = rootContainer.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_subtitle)
+ emptyStateButtonView = rootContainer.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_button)
+ emptyStateProgressView = rootContainer.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_progress)
+ emptyStateDefaultTextView =
+ rootContainer.requireViewById(com.android.internal.R.id.empty)
+ emptyStateContainerView = rootContainer.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_container)
+ emptyStateUiHelper = EmptyStateUiHelper(rootContainer)
+ }
+
+ @Test
+ fun testResetViewVisibilities() {
+ // First set each view's visibility to differ from the expected "reset" state so we can then
+ // assert that they're all reset afterward.
+ // TODO: for historic reasons "reset" doesn't cover `emptyStateContainerView`; should it?
+ emptyStateRootView.visibility = View.GONE
+ emptyStateTitleView.visibility = View.GONE
+ emptyStateSubtitleView.visibility = View.GONE
+ emptyStateButtonView.visibility = View.VISIBLE
+ emptyStateProgressView.visibility = View.VISIBLE
+ emptyStateDefaultTextView.visibility = View.VISIBLE
+
+ emptyStateUiHelper.resetViewVisibilities()
+
+ assertThat(emptyStateRootView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateTitleView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateSubtitleView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateButtonView.visibility).isEqualTo(View.INVISIBLE)
+ assertThat(emptyStateProgressView.visibility).isEqualTo(View.GONE)
+ assertThat(emptyStateDefaultTextView.visibility).isEqualTo(View.GONE)
+ }
+
+ @Test
+ fun testShowSpinner() {
+ emptyStateTitleView.visibility = View.VISIBLE
+ emptyStateButtonView.visibility = View.VISIBLE
+ emptyStateProgressView.visibility = View.GONE
+ emptyStateDefaultTextView.visibility = View.VISIBLE
+
+ emptyStateUiHelper.showSpinner()
+
+ // TODO: should this cover any other views? Subtitle?
+ assertThat(emptyStateTitleView.visibility).isEqualTo(View.INVISIBLE)
+ assertThat(emptyStateButtonView.visibility).isEqualTo(View.INVISIBLE)
+ assertThat(emptyStateProgressView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateDefaultTextView.visibility).isEqualTo(View.GONE)
+ }
+
+ @Test
+ fun testHide() {
+ emptyStateRootView.visibility = View.VISIBLE
+
+ emptyStateUiHelper.hide()
+
+ assertThat(emptyStateRootView.visibility).isEqualTo(View.GONE)
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/logging/EventLogTest.java b/tests/unit/src/com/android/intentresolver/logging/EventLogImplTest.java
index 1745277..d75ea99 100644
--- a/java/tests/src/com/android/intentresolver/logging/EventLogTest.java
+++ b/tests/unit/src/com/android/intentresolver/logging/EventLogImplTest.java
@@ -32,12 +32,12 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.content.Intent;
import android.metrics.LogMaker;
-import com.android.intentresolver.logging.EventLog.FrameworkStatsLogger;
-import com.android.intentresolver.logging.EventLog.SharesheetStandardEvent;
-import com.android.intentresolver.logging.EventLog.SharesheetStartedEvent;
-import com.android.intentresolver.logging.EventLog.SharesheetTargetSelectedEvent;
+import com.android.intentresolver.logging.EventLogImpl.SharesheetStandardEvent;
+import com.android.intentresolver.logging.EventLogImpl.SharesheetStartedEvent;
+import com.android.intentresolver.logging.EventLogImpl.SharesheetTargetSelectedEvent;
import com.android.intentresolver.contentpreview.ContentPreviewType;
import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.InstanceIdSequence;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.UiEventLogger.UiEventEnum;
@@ -53,17 +53,19 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
-public final class EventLogTest {
+public final class EventLogImplTest {
@Mock private UiEventLogger mUiEventLog;
@Mock private FrameworkStatsLogger mFrameworkLog;
@Mock private MetricsLogger mMetricsLogger;
- private EventLog mChooserLogger;
+ private EventLogImpl mChooserLogger;
+
+ private final InstanceIdSequence mSequence = EventLogImpl.newIdSequence();
@Before
public void setUp() {
- //Mockito.reset(mUiEventLog, mFrameworkLog, mMetricsLogger);
- mChooserLogger = new EventLog(mUiEventLog, mFrameworkLog, mMetricsLogger);
+ mChooserLogger = new EventLogImpl(mUiEventLog, mFrameworkLog, mMetricsLogger,
+ mSequence.newInstanceId());
}
@After
@@ -151,7 +153,7 @@ public final class EventLogTest {
@Test
public void testLogShareTargetSelected() {
- final int targetType = EventLog.SELECTION_TYPE_SERVICE;
+ final int targetType = EventLogImpl.SELECTION_TYPE_SERVICE;
final String packageName = "com.test.foo";
final int positionPicked = 123;
final int directTargetAlsoRanked = -1;
@@ -189,7 +191,7 @@ public final class EventLogTest {
@Test
public void testLogActionSelected() {
- mChooserLogger.logActionSelected(EventLog.SELECTION_TYPE_COPY);
+ mChooserLogger.logActionSelected(EventLogImpl.SELECTION_TYPE_COPY);
verify(mFrameworkLog).write(
eq(FrameworkStatsLog.RANKING_SELECTED),
@@ -320,10 +322,11 @@ public final class EventLogTest {
@Test
public void testDifferentLoggerInstancesUseDifferentInstanceIds() {
ArgumentCaptor<Integer> idIntCaptor = ArgumentCaptor.forClass(Integer.class);
- EventLog chooserLogger2 =
- new EventLog(mUiEventLog, mFrameworkLog, mMetricsLogger);
+ EventLogImpl chooserLogger2 =
+ new EventLogImpl(mUiEventLog, mFrameworkLog, mMetricsLogger,
+ mSequence.newInstanceId());
- final int targetType = EventLog.SELECTION_TYPE_COPY;
+ final int targetType = EventLogImpl.SELECTION_TYPE_COPY;
final String packageName = "com.test.foo";
final int positionPicked = 123;
final int directTargetAlsoRanked = -1;
@@ -370,7 +373,7 @@ public final class EventLogTest {
ArgumentCaptor<Integer> idIntCaptor = ArgumentCaptor.forClass(Integer.class);
ArgumentCaptor<InstanceId> idObjectCaptor = ArgumentCaptor.forClass(InstanceId.class);
- final int targetType = EventLog.SELECTION_TYPE_COPY;
+ final int targetType = EventLogImpl.SELECTION_TYPE_COPY;
final String packageName = "com.test.foo";
final int positionPicked = 123;
final int directTargetAlsoRanked = -1;
@@ -403,20 +406,20 @@ public final class EventLogTest {
@Test
public void testTargetSelectionCategories() {
- assertThat(EventLog.getTargetSelectionCategory(
- EventLog.SELECTION_TYPE_SERVICE))
+ assertThat(EventLogImpl.getTargetSelectionCategory(
+ EventLogImpl.SELECTION_TYPE_SERVICE))
.isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET);
- assertThat(EventLog.getTargetSelectionCategory(
- EventLog.SELECTION_TYPE_APP))
+ assertThat(EventLogImpl.getTargetSelectionCategory(
+ EventLogImpl.SELECTION_TYPE_APP))
.isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET);
- assertThat(EventLog.getTargetSelectionCategory(
- EventLog.SELECTION_TYPE_STANDARD))
+ assertThat(EventLogImpl.getTargetSelectionCategory(
+ EventLogImpl.SELECTION_TYPE_STANDARD))
.isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET);
- assertThat(EventLog.getTargetSelectionCategory(
- EventLog.SELECTION_TYPE_COPY)).isEqualTo(0);
- assertThat(EventLog.getTargetSelectionCategory(
- EventLog.SELECTION_TYPE_NEARBY)).isEqualTo(0);
- assertThat(EventLog.getTargetSelectionCategory(
- EventLog.SELECTION_TYPE_EDIT)).isEqualTo(0);
+ assertThat(EventLogImpl.getTargetSelectionCategory(
+ EventLogImpl.SELECTION_TYPE_COPY)).isEqualTo(0);
+ assertThat(EventLogImpl.getTargetSelectionCategory(
+ EventLogImpl.SELECTION_TYPE_NEARBY)).isEqualTo(0);
+ assertThat(EventLogImpl.getTargetSelectionCategory(
+ EventLogImpl.SELECTION_TYPE_EDIT)).isEqualTo(0);
}
}
diff --git a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java b/tests/unit/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java
index 5f0ead7..2140a67 100644
--- a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java
+++ b/tests/unit/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java
@@ -118,14 +118,14 @@ public class AbstractResolverComparatorTest {
Lists.newArrayList(context.getUser()), promoteToFirst) {
@Override
- int compare(ResolveInfo lhs, ResolveInfo rhs) {
+ public int compare(ResolveInfo lhs, ResolveInfo rhs) {
// Used for testing pinning, so we should never get here --- the overrides
// should determine the result instead.
return 1;
}
@Override
- void doCompute(List<ResolvedComponentInfo> targets) {}
+ public void doCompute(List<ResolvedComponentInfo> targets) {}
@Override
public float getScore(TargetInfo targetInfo) {
@@ -133,7 +133,7 @@ public class AbstractResolverComparatorTest {
}
@Override
- void handleResultMessage(Message message) {}
+ public void handleResultMessage(Message message) {}
};
return testComparator;
}
diff --git a/tests/unit/src/com/android/intentresolver/shortcuts/ScopedAppTargetListCallbackTest.kt b/tests/unit/src/com/android/intentresolver/shortcuts/ScopedAppTargetListCallbackTest.kt
new file mode 100644
index 0000000..c81e88a
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/shortcuts/ScopedAppTargetListCallbackTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.shortcuts
+
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class ScopedAppTargetListCallbackTest {
+
+ @Test
+ fun test_consumerInvocations_onlyInvokedWhileScopeIsActive() {
+ val scope = TestScope(UnconfinedTestDispatcher())
+ var counter = 0
+ val testSubject = ScopedAppTargetListCallback(scope) { counter++ }.toConsumer()
+
+ testSubject.accept(ArrayList())
+
+ assertThat(counter).isEqualTo(1)
+
+ scope.cancel()
+ testSubject.accept(ArrayList())
+
+ assertThat(counter).isEqualTo(1)
+ }
+
+ @Test
+ fun test_appPredictorCallbackInvocations_onlyInvokedWhileScopeIsActive() {
+ val scope = TestScope(UnconfinedTestDispatcher())
+ var counter = 0
+ val testSubject = ScopedAppTargetListCallback(scope) { counter++ }.toAppPredictorCallback()
+
+ testSubject.onTargetsAvailable(ArrayList())
+
+ assertThat(counter).isEqualTo(1)
+
+ scope.cancel()
+ testSubject.onTargetsAvailable(ArrayList())
+
+ assertThat(counter).isEqualTo(1)
+ }
+
+ @Test
+ fun test_createdWithClosedScope_noCallbackInvocations() {
+ val scope = TestScope(UnconfinedTestDispatcher()).apply { cancel() }
+ var counter = 0
+ val testSubject = ScopedAppTargetListCallback(scope) { counter++ }.toConsumer()
+
+ testSubject.accept(ArrayList())
+
+ assertThat(counter).isEqualTo(0)
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/tests/unit/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
new file mode 100644
index 0000000..43d0df7
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
@@ -0,0 +1,490 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.shortcuts
+
+import android.app.prediction.AppPredictor
+import android.content.ComponentName
+import android.content.Context
+import android.content.IntentFilter
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.ApplicationInfoFlags
+import android.content.pm.ShortcutManager
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.intentresolver.any
+import com.android.intentresolver.argumentCaptor
+import com.android.intentresolver.capture
+import com.android.intentresolver.chooser.DisplayResolveInfo
+import com.android.intentresolver.createAppTarget
+import com.android.intentresolver.createShareShortcutInfo
+import com.android.intentresolver.createShortcutInfo
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import java.util.function.Consumer
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.atLeastOnce
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+class ShortcutLoaderTest {
+ private val appInfo =
+ ApplicationInfo().apply {
+ enabled = true
+ flags = 0
+ }
+ private val pm =
+ mock<PackageManager> {
+ whenever(getApplicationInfo(any(), any<ApplicationInfoFlags>())).thenReturn(appInfo)
+ }
+ private val userManager =
+ mock<UserManager> {
+ whenever(isUserRunning(any<UserHandle>())).thenReturn(true)
+ whenever(isUserUnlocked(any<UserHandle>())).thenReturn(true)
+ whenever(isQuietModeEnabled(any<UserHandle>())).thenReturn(false)
+ }
+ private val context =
+ mock<Context> {
+ whenever(packageManager).thenReturn(pm)
+ whenever(createContextAsUser(any(), anyInt())).thenReturn(this)
+ whenever(getSystemService(Context.USER_SERVICE)).thenReturn(userManager)
+ }
+ private val scheduler = TestCoroutineScheduler()
+ private val dispatcher = UnconfinedTestDispatcher(scheduler)
+ private val scope = TestScope(dispatcher)
+ private val intentFilter = mock<IntentFilter>()
+ private val appPredictor = mock<ShortcutLoader.AppPredictorProxy>()
+ private val callback = mock<Consumer<ShortcutLoader.Result>>()
+ private val componentName = ComponentName("pkg", "Class")
+ private val appTarget =
+ mock<DisplayResolveInfo> { whenever(resolvedComponentName).thenReturn(componentName) }
+ private val appTargets = arrayOf(appTarget)
+ private val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1)
+
+ @Test
+ fun test_loadShortcutsWithAppPredictor_resultIntegrity() =
+ scope.runTest {
+ val testSubject =
+ ShortcutLoader(
+ context,
+ backgroundScope,
+ appPredictor,
+ UserHandle.of(0),
+ true,
+ intentFilter,
+ dispatcher,
+ callback
+ )
+
+ testSubject.updateAppTargets(appTargets)
+
+ val matchingAppTarget = createAppTarget(matchingShortcutInfo)
+ val shortcuts =
+ listOf(
+ matchingAppTarget,
+ // an AppTarget that does not belong to any resolved application; should be
+ // ignored
+ createAppTarget(
+ createShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+ )
+ )
+ val appPredictorCallbackCaptor = argumentCaptor<AppPredictor.Callback>()
+ verify(appPredictor, atLeastOnce())
+ .registerPredictionUpdates(any(), capture(appPredictorCallbackCaptor))
+ appPredictorCallbackCaptor.value.onTargetsAvailable(shortcuts)
+
+ val resultCaptor = argumentCaptor<ShortcutLoader.Result>()
+ verify(callback, times(1)).accept(capture(resultCaptor))
+
+ val result = resultCaptor.value
+ assertTrue("An app predictor result is expected", result.isFromAppPredictor)
+ assertArrayEquals(
+ "Wrong input app targets in the result",
+ appTargets,
+ result.appTargets
+ )
+ assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size)
+ assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget)
+ for (shortcut in result.shortcutsByApp[0].shortcuts) {
+ assertEquals(
+ "Wrong AppTarget in the cache",
+ matchingAppTarget,
+ result.directShareAppTargetCache[shortcut]
+ )
+ assertEquals(
+ "Wrong ShortcutInfo in the cache",
+ matchingShortcutInfo,
+ result.directShareShortcutInfoCache[shortcut]
+ )
+ }
+ }
+
+ @Test
+ fun test_loadShortcutsWithShortcutManager_resultIntegrity() =
+ scope.runTest {
+ val shortcutManagerResult =
+ listOf(
+ ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
+ // mismatching shortcut
+ createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+ )
+ val shortcutManager =
+ mock<ShortcutManager> {
+ whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
+ }
+ whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
+ val testSubject =
+ ShortcutLoader(
+ context,
+ backgroundScope,
+ null,
+ UserHandle.of(0),
+ true,
+ intentFilter,
+ dispatcher,
+ callback
+ )
+
+ testSubject.updateAppTargets(appTargets)
+
+ val resultCaptor = argumentCaptor<ShortcutLoader.Result>()
+ verify(callback, times(1)).accept(capture(resultCaptor))
+
+ val result = resultCaptor.value
+ assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor)
+ assertArrayEquals(
+ "Wrong input app targets in the result",
+ appTargets,
+ result.appTargets
+ )
+ assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size)
+ assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget)
+ for (shortcut in result.shortcutsByApp[0].shortcuts) {
+ assertTrue(
+ "AppTargets are not expected the cache of a ShortcutManager result",
+ result.directShareAppTargetCache.isEmpty()
+ )
+ assertEquals(
+ "Wrong ShortcutInfo in the cache",
+ matchingShortcutInfo,
+ result.directShareShortcutInfoCache[shortcut]
+ )
+ }
+ }
+
+ @Test
+ fun test_appPredictorReturnsEmptyList_fallbackToShortcutManager() =
+ scope.runTest {
+ val shortcutManagerResult =
+ listOf(
+ ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
+ // mismatching shortcut
+ createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+ )
+ val shortcutManager =
+ mock<ShortcutManager> {
+ whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
+ }
+ whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
+ val testSubject =
+ ShortcutLoader(
+ context,
+ backgroundScope,
+ appPredictor,
+ UserHandle.of(0),
+ true,
+ intentFilter,
+ dispatcher,
+ callback
+ )
+
+ testSubject.updateAppTargets(appTargets)
+
+ verify(appPredictor, times(1)).requestPredictionUpdate()
+ val appPredictorCallbackCaptor = argumentCaptor<AppPredictor.Callback>()
+ verify(appPredictor, times(1))
+ .registerPredictionUpdates(any(), capture(appPredictorCallbackCaptor))
+ appPredictorCallbackCaptor.value.onTargetsAvailable(emptyList())
+
+ val resultCaptor = argumentCaptor<ShortcutLoader.Result>()
+ verify(callback, times(1)).accept(capture(resultCaptor))
+
+ val result = resultCaptor.value
+ assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor)
+ assertArrayEquals(
+ "Wrong input app targets in the result",
+ appTargets,
+ result.appTargets
+ )
+ assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size)
+ assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget)
+ for (shortcut in result.shortcutsByApp[0].shortcuts) {
+ assertTrue(
+ "AppTargets are not expected the cache of a ShortcutManager result",
+ result.directShareAppTargetCache.isEmpty()
+ )
+ assertEquals(
+ "Wrong ShortcutInfo in the cache",
+ matchingShortcutInfo,
+ result.directShareShortcutInfoCache[shortcut]
+ )
+ }
+ }
+
+ @Test
+ fun test_appPredictor_requestPredictionUpdateFailure_fallbackToShortcutManager() =
+ scope.runTest {
+ val shortcutManagerResult =
+ listOf(
+ ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
+ // mismatching shortcut
+ createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+ )
+ val shortcutManager =
+ mock<ShortcutManager> {
+ whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
+ }
+ whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
+ whenever(appPredictor.requestPredictionUpdate())
+ .thenThrow(IllegalStateException("Test exception"))
+ val testSubject =
+ ShortcutLoader(
+ context,
+ backgroundScope,
+ appPredictor,
+ UserHandle.of(0),
+ true,
+ intentFilter,
+ dispatcher,
+ callback
+ )
+
+ testSubject.updateAppTargets(appTargets)
+
+ verify(appPredictor, times(1)).requestPredictionUpdate()
+
+ val resultCaptor = argumentCaptor<ShortcutLoader.Result>()
+ verify(callback, times(1)).accept(capture(resultCaptor))
+
+ val result = resultCaptor.value
+ assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor)
+ assertArrayEquals(
+ "Wrong input app targets in the result",
+ appTargets,
+ result.appTargets
+ )
+ assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size)
+ assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget)
+ for (shortcut in result.shortcutsByApp[0].shortcuts) {
+ assertTrue(
+ "AppTargets are not expected the cache of a ShortcutManager result",
+ result.directShareAppTargetCache.isEmpty()
+ )
+ assertEquals(
+ "Wrong ShortcutInfo in the cache",
+ matchingShortcutInfo,
+ result.directShareShortcutInfoCache[shortcut]
+ )
+ }
+ }
+
+ @Test
+ fun test_ShortcutLoader_shortcutsRequestedIndependentlyFromAppTargets() =
+ scope.runTest {
+ ShortcutLoader(
+ context,
+ backgroundScope,
+ appPredictor,
+ UserHandle.of(0),
+ true,
+ intentFilter,
+ dispatcher,
+ callback
+ )
+
+ verify(appPredictor, times(1)).requestPredictionUpdate()
+ verify(callback, never()).accept(any())
+ }
+
+ @Test
+ fun test_ShortcutLoader_noResultsWithoutAppTargets() =
+ scope.runTest {
+ val shortcutManagerResult =
+ listOf(
+ ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
+ // mismatching shortcut
+ createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+ )
+ val shortcutManager =
+ mock<ShortcutManager> {
+ whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
+ }
+ whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
+ val testSubject =
+ ShortcutLoader(
+ context,
+ backgroundScope,
+ null,
+ UserHandle.of(0),
+ true,
+ intentFilter,
+ dispatcher,
+ callback
+ )
+
+ verify(shortcutManager, times(1)).getShareTargets(any())
+ verify(callback, never()).accept(any())
+
+ testSubject.reset()
+
+ verify(shortcutManager, times(2)).getShareTargets(any())
+ verify(callback, never()).accept(any())
+
+ testSubject.updateAppTargets(appTargets)
+
+ verify(shortcutManager, times(2)).getShareTargets(any())
+ verify(callback, times(1)).accept(any())
+ }
+
+ @Test
+ fun test_OnScopeCancellation_unsubscribeFromAppPredictor() {
+ scope.runTest {
+ ShortcutLoader(
+ context,
+ backgroundScope,
+ appPredictor,
+ UserHandle.of(0),
+ true,
+ intentFilter,
+ dispatcher,
+ callback
+ )
+
+ verify(appPredictor, never()).unregisterPredictionUpdates(any())
+ }
+
+ verify(appPredictor, times(1)).unregisterPredictionUpdates(any())
+ }
+
+ @Test
+ fun test_workProfileNotRunning_doNotCallServices() {
+ testDisabledWorkProfileDoNotCallSystem(isUserRunning = false)
+ }
+
+ @Test
+ fun test_workProfileLocked_doNotCallServices() {
+ testDisabledWorkProfileDoNotCallSystem(isUserUnlocked = false)
+ }
+
+ @Test
+ fun test_workProfileQuiteModeEnabled_doNotCallServices() {
+ testDisabledWorkProfileDoNotCallSystem(isQuietModeEnabled = true)
+ }
+
+ @Test
+ fun test_mainProfileNotRunning_callServicesAnyway() {
+ testAlwaysCallSystemForMainProfile(isUserRunning = false)
+ }
+
+ @Test
+ fun test_mainProfileLocked_callServicesAnyway() {
+ testAlwaysCallSystemForMainProfile(isUserUnlocked = false)
+ }
+
+ @Test
+ fun test_mainProfileQuiteModeEnabled_callServicesAnyway() {
+ testAlwaysCallSystemForMainProfile(isQuietModeEnabled = true)
+ }
+
+ private fun testDisabledWorkProfileDoNotCallSystem(
+ isUserRunning: Boolean = true,
+ isUserUnlocked: Boolean = true,
+ isQuietModeEnabled: Boolean = false
+ ) =
+ scope.runTest {
+ val userHandle = UserHandle.of(10)
+ with(userManager) {
+ whenever(isUserRunning(userHandle)).thenReturn(isUserRunning)
+ whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked)
+ whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled)
+ }
+ whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager)
+ val appPredictor = mock<ShortcutLoader.AppPredictorProxy>()
+ val callback = mock<Consumer<ShortcutLoader.Result>>()
+ val testSubject =
+ ShortcutLoader(
+ context,
+ backgroundScope,
+ appPredictor,
+ userHandle,
+ false,
+ intentFilter,
+ dispatcher,
+ callback
+ )
+
+ testSubject.updateAppTargets(arrayOf<DisplayResolveInfo>(mock()))
+
+ verify(appPredictor, never()).requestPredictionUpdate()
+ }
+
+ private fun testAlwaysCallSystemForMainProfile(
+ isUserRunning: Boolean = true,
+ isUserUnlocked: Boolean = true,
+ isQuietModeEnabled: Boolean = false
+ ) =
+ scope.runTest {
+ val userHandle = UserHandle.of(10)
+ with(userManager) {
+ whenever(isUserRunning(userHandle)).thenReturn(isUserRunning)
+ whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked)
+ whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled)
+ }
+ whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager)
+ val appPredictor = mock<ShortcutLoader.AppPredictorProxy>()
+ val callback = mock<Consumer<ShortcutLoader.Result>>()
+ val testSubject =
+ ShortcutLoader(
+ context,
+ backgroundScope,
+ appPredictor,
+ userHandle,
+ true,
+ intentFilter,
+ dispatcher,
+ callback
+ )
+
+ testSubject.updateAppTargets(arrayOf<DisplayResolveInfo>(mock()))
+
+ verify(appPredictor, times(1)).requestPredictionUpdate()
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt b/tests/unit/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt
index e0de005..e0de005 100644
--- a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt
+++ b/tests/unit/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt
diff --git a/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt b/tests/unit/src/com/android/intentresolver/util/TestExecutor.kt
index b904771..214b970 100644
--- a/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt
+++ b/tests/unit/src/com/android/intentresolver/util/TestExecutor.kt
@@ -14,18 +14,27 @@
* limitations under the License.
*/
-package com.android.intentresolver
+package com.android.intentresolver.util
-import com.android.intentresolver.flags.FeatureFlagRepository
-import com.android.systemui.flags.BooleanFlag
-import com.android.systemui.flags.ReleasedFlag
-import com.android.systemui.flags.UnreleasedFlag
+import java.util.concurrent.Executor
-class TestFeatureFlagRepository(
- private val overrides: Map<BooleanFlag, Boolean>
-) : FeatureFlagRepository {
- override fun isEnabled(flag: UnreleasedFlag): Boolean = getValue(flag)
- override fun isEnabled(flag: ReleasedFlag): Boolean = getValue(flag)
+class TestExecutor(private val immediate: Boolean = false) : Executor {
+ private var pendingCommands = ArrayDeque<Runnable>()
- private fun getValue(flag: BooleanFlag) = overrides.getOrDefault(flag, flag.default)
+ val pendingCommandCount: Int
+ get() = pendingCommands.size
+
+ override fun execute(command: Runnable) {
+ if (immediate) {
+ command.run()
+ } else {
+ pendingCommands.add(command)
+ }
+ }
+
+ fun runUntilIdle() {
+ while (pendingCommands.isNotEmpty()) {
+ pendingCommands.removeFirst().run()
+ }
+ }
}
diff --git a/java/tests/src/com/android/intentresolver/util/UriFiltersTest.kt b/tests/unit/src/com/android/intentresolver/util/UriFiltersTest.kt
index 1821806..1821806 100644
--- a/java/tests/src/com/android/intentresolver/util/UriFiltersTest.kt
+++ b/tests/unit/src/com/android/intentresolver/util/UriFiltersTest.kt
diff --git a/tests/unit/src/com/android/intentresolver/v2/ChooserActionFactoryTest.kt b/tests/unit/src/com/android/intentresolver/v2/ChooserActionFactoryTest.kt
new file mode 100644
index 0000000..b3486bb
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/ChooserActionFactoryTest.kt
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2
+
+import android.app.Activity
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Context.RECEIVER_EXPORTED
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.res.Resources
+import android.graphics.drawable.Icon
+import android.service.chooser.ChooserAction
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.intentresolver.ChooserRequestParameters
+import com.android.intentresolver.logging.EventLog
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import com.google.common.collect.ImmutableList
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.function.Consumer
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mockito
+
+@RunWith(AndroidJUnit4::class)
+class ChooserActionFactoryTest {
+ private val context = InstrumentationRegistry.getInstrumentation().context
+
+ private val logger = mock<EventLog>()
+ private val actionLabel = "Action label"
+ private val modifyShareLabel = "Modify share"
+ private val testAction = "com.android.intentresolver.testaction"
+ private val countdown = CountDownLatch(1)
+ private val testReceiver: BroadcastReceiver =
+ object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ // Just doing at most a single countdown per test.
+ countdown.countDown()
+ }
+ }
+ private val resultConsumer =
+ object : Consumer<Int> {
+ var latestReturn = Integer.MIN_VALUE
+
+ override fun accept(resultCode: Int) {
+ latestReturn = resultCode
+ }
+ }
+
+ @Before
+ fun setup() {
+ context.registerReceiver(testReceiver, IntentFilter(testAction), RECEIVER_EXPORTED)
+ }
+
+ @After
+ fun teardown() {
+ context.unregisterReceiver(testReceiver)
+ }
+
+ @Test
+ fun testCreateCustomActions() {
+ val factory = createFactory()
+
+ val customActions = factory.createCustomActions()
+
+ assertThat(customActions.size).isEqualTo(1)
+ assertThat(customActions[0].label).isEqualTo(actionLabel)
+
+ // click it
+ customActions[0].onClicked.run()
+
+ Mockito.verify(logger).logCustomActionSelected(eq(0))
+ assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn)
+ // Verify the pending intent has been called
+ assertTrue("Timed out waiting for broadcast", countdown.await(2500, TimeUnit.MILLISECONDS))
+ }
+
+ @Test
+ fun testNoModifyShareAction() {
+ val factory = createFactory(includeModifyShare = false)
+
+ assertThat(factory.modifyShareAction).isNull()
+ }
+
+ @Test
+ fun testModifyShareAction() {
+ val factory = createFactory(includeModifyShare = true)
+
+ val action = factory.modifyShareAction ?: error("Modify share action should not be null")
+ action.onClicked.run()
+
+ Mockito.verify(logger).logActionSelected(eq(EventLog.SELECTION_TYPE_MODIFY_SHARE))
+ assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn)
+ // Verify the pending intent has been called
+ assertTrue("Timed out waiting for broadcast", countdown.await(2500, TimeUnit.MILLISECONDS))
+ }
+
+ @Test
+ fun nonSendAction_noCopyRunnable() {
+ val targetIntent =
+ Intent(Intent.ACTION_SEND_MULTIPLE).apply {
+ putExtra(Intent.EXTRA_TEXT, "Text to show")
+ }
+
+ val chooserRequest =
+ mock<ChooserRequestParameters> {
+ whenever(this.targetIntent).thenReturn(targetIntent)
+ whenever(chooserActions).thenReturn(ImmutableList.of())
+ }
+ val testSubject =
+ ChooserActionFactory(
+ context,
+ chooserRequest.targetIntent,
+ chooserRequest.referrerPackageName,
+ chooserRequest.chooserActions,
+ chooserRequest.modifyShareAction,
+ Optional.empty(),
+ logger,
+ {},
+ { null },
+ mock(),
+ {},
+ )
+ assertThat(testSubject.copyButtonRunnable).isNull()
+ }
+
+ @Test
+ fun sendActionNoText_noCopyRunnable() {
+ val targetIntent = Intent(Intent.ACTION_SEND)
+
+ val chooserRequest =
+ mock<ChooserRequestParameters> {
+ whenever(this.targetIntent).thenReturn(targetIntent)
+ whenever(chooserActions).thenReturn(ImmutableList.of())
+ }
+ val testSubject =
+ ChooserActionFactory(
+ context,
+ chooserRequest.targetIntent,
+ chooserRequest.referrerPackageName,
+ chooserRequest.chooserActions,
+ chooserRequest.modifyShareAction,
+ Optional.empty(),
+ logger,
+ {},
+ { null },
+ mock(),
+ {},
+ )
+ assertThat(testSubject.copyButtonRunnable).isNull()
+ }
+
+ @Test
+ fun sendActionWithText_nonNullCopyRunnable() {
+ val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_TEXT, "Text") }
+
+ val chooserRequest =
+ mock<ChooserRequestParameters> {
+ whenever(this.targetIntent).thenReturn(targetIntent)
+ whenever(chooserActions).thenReturn(ImmutableList.of())
+ }
+ val testSubject =
+ ChooserActionFactory(
+ context,
+ chooserRequest.targetIntent,
+ chooserRequest.referrerPackageName,
+ chooserRequest.chooserActions,
+ chooserRequest.modifyShareAction,
+ Optional.empty(),
+ logger,
+ {},
+ { null },
+ mock(),
+ {},
+ )
+ assertThat(testSubject.copyButtonRunnable).isNotNull()
+ }
+
+ private fun createFactory(includeModifyShare: Boolean = false): ChooserActionFactory {
+ val testPendingIntent =
+ PendingIntent.getBroadcast(context, 0, Intent(testAction), PendingIntent.FLAG_IMMUTABLE)
+ val targetIntent = Intent()
+ val action =
+ ChooserAction.Builder(
+ Icon.createWithResource("", Resources.ID_NULL),
+ actionLabel,
+ testPendingIntent
+ )
+ .build()
+ val chooserRequest = mock<ChooserRequestParameters>()
+ whenever(chooserRequest.targetIntent).thenReturn(targetIntent)
+ whenever(chooserRequest.chooserActions).thenReturn(ImmutableList.of(action))
+
+ if (includeModifyShare) {
+ val modifyShare =
+ ChooserAction.Builder(
+ Icon.createWithResource("", Resources.ID_NULL),
+ modifyShareLabel,
+ testPendingIntent
+ )
+ .build()
+ whenever(chooserRequest.modifyShareAction).thenReturn(modifyShare)
+ }
+
+ return ChooserActionFactory(
+ context,
+ chooserRequest.targetIntent,
+ chooserRequest.referrerPackageName,
+ chooserRequest.chooserActions,
+ chooserRequest.modifyShareAction,
+ Optional.empty(),
+ logger,
+ {},
+ { null },
+ mock(),
+ resultConsumer
+ )
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/MultiProfilePagerAdapterTest.kt b/tests/unit/src/com/android/intentresolver/v2/MultiProfilePagerAdapterTest.kt
new file mode 100644
index 0000000..f5dc093
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/MultiProfilePagerAdapterTest.kt
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2
+
+import android.os.UserHandle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ListView
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.intentresolver.MultiProfilePagerAdapter.PROFILE_PERSONAL
+import com.android.intentresolver.MultiProfilePagerAdapter.PROFILE_WORK
+import com.android.intentresolver.R
+import com.android.intentresolver.ResolverListAdapter
+import com.android.intentresolver.emptystate.EmptyStateProvider
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import com.google.common.collect.ImmutableList
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import java.util.function.Supplier
+import org.junit.Test
+
+class MultiProfilePagerAdapterTest {
+ private val PERSONAL_USER_HANDLE = UserHandle.of(10)
+ private val WORK_USER_HANDLE = UserHandle.of(20)
+
+ private val context = InstrumentationRegistry.getInstrumentation().getContext()
+ private val inflater = Supplier {
+ LayoutInflater.from(context).inflate(R.layout.resolver_list_per_profile, null, false)
+ as ViewGroup
+ }
+
+ @Test
+ fun testSinglePageProfileAdapter() {
+ val personalListAdapter =
+ mock<ResolverListAdapter> { whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE) }
+ val pagerAdapter =
+ MultiProfilePagerAdapter(
+ { listAdapter: ResolverListAdapter -> listAdapter },
+ { listView: ListView, bindAdapter: ResolverListAdapter ->
+ listView.setAdapter(bindAdapter)
+ },
+ ImmutableList.of(personalListAdapter),
+ object : EmptyStateProvider {},
+ { false },
+ PROFILE_PERSONAL,
+ null,
+ null,
+ inflater,
+ { Optional.empty() }
+ )
+ assertThat(pagerAdapter.count).isEqualTo(1)
+ assertThat(pagerAdapter.currentPage).isEqualTo(PROFILE_PERSONAL)
+ assertThat(pagerAdapter.currentUserHandle).isEqualTo(PERSONAL_USER_HANDLE)
+ assertThat(pagerAdapter.getAdapterForIndex(0)).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.activeListAdapter).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.inactiveListAdapter).isNull()
+ assertThat(pagerAdapter.personalListAdapter).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.workListAdapter).isNull()
+ assertThat(pagerAdapter.itemCount).isEqualTo(1)
+ // TODO: consider covering some of the package-private methods (and making them public?).
+ // TODO: consider exercising responsibilities as an implementation of a ViewPager adapter.
+ }
+
+ @Test
+ fun testTwoProfilePagerAdapter() {
+ val personalListAdapter =
+ mock<ResolverListAdapter> { whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE) }
+ val workListAdapter =
+ mock<ResolverListAdapter> { whenever(getUserHandle()).thenReturn(WORK_USER_HANDLE) }
+ val pagerAdapter =
+ MultiProfilePagerAdapter(
+ { listAdapter: ResolverListAdapter -> listAdapter },
+ { listView: ListView, bindAdapter: ResolverListAdapter ->
+ listView.setAdapter(bindAdapter)
+ },
+ ImmutableList.of(personalListAdapter, workListAdapter),
+ object : EmptyStateProvider {},
+ { false },
+ PROFILE_PERSONAL,
+ WORK_USER_HANDLE, // TODO: why does this test pass even if this is null?
+ null,
+ inflater,
+ { Optional.empty() }
+ )
+ assertThat(pagerAdapter.count).isEqualTo(2)
+ assertThat(pagerAdapter.currentPage).isEqualTo(PROFILE_PERSONAL)
+ assertThat(pagerAdapter.currentUserHandle).isEqualTo(PERSONAL_USER_HANDLE)
+ assertThat(pagerAdapter.getAdapterForIndex(0)).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.getAdapterForIndex(1)).isSameInstanceAs(workListAdapter)
+ assertThat(pagerAdapter.activeListAdapter).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.inactiveListAdapter).isSameInstanceAs(workListAdapter)
+ assertThat(pagerAdapter.personalListAdapter).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.workListAdapter).isSameInstanceAs(workListAdapter)
+ assertThat(pagerAdapter.itemCount).isEqualTo(2)
+ // TODO: consider covering some of the package-private methods (and making them public?).
+ // TODO: consider exercising responsibilities as an implementation of a ViewPager adapter;
+ // especially matching profiles to ListViews?
+ // TODO: test ProfileSelectedListener (and getters for "current" state) as the selected
+ // page changes. Currently there's no API to change the selected page directly; that's
+ // only possible through manipulation of the bound ViewPager.
+ }
+
+ @Test
+ fun testTwoProfilePagerAdapter_workIsDefault() {
+ val personalListAdapter =
+ mock<ResolverListAdapter> { whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE) }
+ val workListAdapter =
+ mock<ResolverListAdapter> { whenever(getUserHandle()).thenReturn(WORK_USER_HANDLE) }
+ val pagerAdapter =
+ MultiProfilePagerAdapter(
+ { listAdapter: ResolverListAdapter -> listAdapter },
+ { listView: ListView, bindAdapter: ResolverListAdapter ->
+ listView.setAdapter(bindAdapter)
+ },
+ ImmutableList.of(personalListAdapter, workListAdapter),
+ object : EmptyStateProvider {},
+ { false },
+ PROFILE_WORK, // <-- This test specifically requests we start on work profile.
+ WORK_USER_HANDLE, // TODO: why does this test pass even if this is null?
+ null,
+ inflater,
+ { Optional.empty() }
+ )
+ assertThat(pagerAdapter.count).isEqualTo(2)
+ assertThat(pagerAdapter.currentPage).isEqualTo(PROFILE_WORK)
+ assertThat(pagerAdapter.currentUserHandle).isEqualTo(WORK_USER_HANDLE)
+ assertThat(pagerAdapter.getAdapterForIndex(0)).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.getAdapterForIndex(1)).isSameInstanceAs(workListAdapter)
+ assertThat(pagerAdapter.activeListAdapter).isSameInstanceAs(workListAdapter)
+ assertThat(pagerAdapter.inactiveListAdapter).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.personalListAdapter).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.workListAdapter).isSameInstanceAs(workListAdapter)
+ assertThat(pagerAdapter.itemCount).isEqualTo(2)
+ // TODO: consider covering some of the package-private methods (and making them public?).
+ // TODO: test ProfileSelectedListener (and getters for "current" state) as the selected
+ // page changes. Currently there's no API to change the selected page directly; that's
+ // only possible through manipulation of the bound ViewPager.
+ }
+
+ @Test
+ fun testBottomPaddingDelegate_default() {
+ val personalListAdapter =
+ mock<ResolverListAdapter> { whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE) }
+ val pagerAdapter =
+ MultiProfilePagerAdapter(
+ { listAdapter: ResolverListAdapter -> listAdapter },
+ { listView: ListView, bindAdapter: ResolverListAdapter ->
+ listView.setAdapter(bindAdapter)
+ },
+ ImmutableList.of(personalListAdapter),
+ object : EmptyStateProvider {},
+ { false },
+ PROFILE_PERSONAL,
+ null,
+ null,
+ inflater,
+ { Optional.empty() }
+ )
+ val container =
+ pagerAdapter
+ .getActiveEmptyStateView()
+ .requireViewById<View>(com.android.internal.R.id.resolver_empty_state_container)
+ container.setPadding(1, 2, 3, 4)
+ pagerAdapter.setupContainerPadding()
+ assertThat(container.paddingLeft).isEqualTo(1)
+ assertThat(container.paddingTop).isEqualTo(2)
+ assertThat(container.paddingRight).isEqualTo(3)
+ assertThat(container.paddingBottom).isEqualTo(4)
+ }
+
+ @Test
+ fun testBottomPaddingDelegate_override() {
+ val personalListAdapter =
+ mock<ResolverListAdapter> { whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE) }
+ val pagerAdapter =
+ MultiProfilePagerAdapter(
+ { listAdapter: ResolverListAdapter -> listAdapter },
+ { listView: ListView, bindAdapter: ResolverListAdapter ->
+ listView.setAdapter(bindAdapter)
+ },
+ ImmutableList.of(personalListAdapter),
+ object : EmptyStateProvider {},
+ { false },
+ PROFILE_PERSONAL,
+ null,
+ null,
+ inflater,
+ { Optional.of(42) }
+ )
+ val container =
+ pagerAdapter
+ .getActiveEmptyStateView()
+ .requireViewById<View>(com.android.internal.R.id.resolver_empty_state_container)
+ container.setPadding(1, 2, 3, 4)
+ pagerAdapter.setupContainerPadding()
+ assertThat(container.paddingLeft).isEqualTo(1)
+ assertThat(container.paddingTop).isEqualTo(2)
+ assertThat(container.paddingRight).isEqualTo(3)
+ assertThat(container.paddingBottom).isEqualTo(42)
+ }
+
+ @Test
+ fun testPresumedQuietModeEmptyStateForWorkProfile_whenQuiet() {
+ // TODO: this is "presumed" because the conditions to determine whether we "should" show an
+ // empty state aren't enforced to align with the conditions when we actually *would* -- I
+ // believe `shouldShowEmptyStateScreen` should be implemented in terms of the provider?
+ val personalListAdapter =
+ mock<ResolverListAdapter> {
+ whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE)
+ whenever(getUnfilteredCount()).thenReturn(1)
+ }
+ val workListAdapter =
+ mock<ResolverListAdapter> {
+ whenever(getUserHandle()).thenReturn(WORK_USER_HANDLE)
+ whenever(getUnfilteredCount()).thenReturn(1)
+ }
+ val pagerAdapter =
+ MultiProfilePagerAdapter(
+ { listAdapter: ResolverListAdapter -> listAdapter },
+ { listView: ListView, bindAdapter: ResolverListAdapter ->
+ listView.setAdapter(bindAdapter)
+ },
+ ImmutableList.of(personalListAdapter, workListAdapter),
+ object : EmptyStateProvider {},
+ { true }, // <-- Work mode is quiet.
+ PROFILE_WORK,
+ WORK_USER_HANDLE,
+ null,
+ inflater,
+ { Optional.empty() }
+ )
+ assertThat(pagerAdapter.shouldShowEmptyStateScreen(workListAdapter)).isTrue()
+ assertThat(pagerAdapter.shouldShowEmptyStateScreen(personalListAdapter)).isFalse()
+ }
+
+ @Test
+ fun testPresumedQuietModeEmptyStateForWorkProfile_notWhenNotQuiet() {
+ // TODO: this is "presumed" because the conditions to determine whether we "should" show an
+ // empty state aren't enforced to align with the conditions when we actually *would* -- I
+ // believe `shouldShowEmptyStateScreen` should be implemented in terms of the provider?
+ val personalListAdapter =
+ mock<ResolverListAdapter> {
+ whenever(getUserHandle()).thenReturn(PERSONAL_USER_HANDLE)
+ whenever(getUnfilteredCount()).thenReturn(1)
+ }
+ val workListAdapter =
+ mock<ResolverListAdapter> {
+ whenever(getUserHandle()).thenReturn(WORK_USER_HANDLE)
+ whenever(getUnfilteredCount()).thenReturn(1)
+ }
+ val pagerAdapter =
+ MultiProfilePagerAdapter(
+ { listAdapter: ResolverListAdapter -> listAdapter },
+ { listView: ListView, bindAdapter: ResolverListAdapter ->
+ listView.setAdapter(bindAdapter)
+ },
+ ImmutableList.of(personalListAdapter, workListAdapter),
+ object : EmptyStateProvider {},
+ { false }, // <-- Work mode is not quiet.
+ PROFILE_WORK,
+ WORK_USER_HANDLE,
+ null,
+ inflater,
+ { Optional.empty() }
+ )
+ assertThat(pagerAdapter.shouldShowEmptyStateScreen(workListAdapter)).isFalse()
+ assertThat(pagerAdapter.shouldShowEmptyStateScreen(personalListAdapter)).isFalse()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/coroutines/Flow.kt b/tests/unit/src/com/android/intentresolver/v2/coroutines/Flow.kt
new file mode 100644
index 0000000..a5677d9
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/coroutines/Flow.kt
@@ -0,0 +1,89 @@
+@file:Suppress("OPT_IN_USAGE")
+
+package com.android.intentresolver.v2.coroutines
+
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+
+/**
+ * Collect [flow] in a new [Job] and return a getter for the last collected value.
+ *
+ * ```
+ * fun myTest() = runTest {
+ * // ...
+ * val actual by collectLastValue(underTest.flow)
+ * assertThat(actual).isEqualTo(expected)
+ * }
+ * ```
+ */
+fun <T> TestScope.collectLastValue(
+ flow: Flow<T>,
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+): FlowValue<T?> {
+ val values by
+ collectValues(
+ flow = flow,
+ context = context,
+ start = start,
+ )
+ return FlowValueImpl { values.lastOrNull() }
+}
+
+/**
+ * Collect [flow] in a new [Job] and return a getter for the collection of values collected.
+ *
+ * ```
+ * fun myTest() = runTest {
+ * // ...
+ * val values by collectValues(underTest.flow)
+ * assertThat(values).isEqualTo(listOf(expected1, expected2, ...))
+ * }
+ * ```
+ */
+fun <T> TestScope.collectValues(
+ flow: Flow<T>,
+ context: CoroutineContext = EmptyCoroutineContext,
+ start: CoroutineStart = CoroutineStart.DEFAULT,
+): FlowValue<List<T>> {
+ val values = mutableListOf<T>()
+ backgroundScope.launch(context, start) { flow.collect(values::add) }
+ return FlowValueImpl {
+ runCurrent()
+ values.toList()
+ }
+}
+
+/** @see collectLastValue */
+interface FlowValue<T> : ReadOnlyProperty<Any?, T> {
+ operator fun invoke(): T
+}
+
+private class FlowValueImpl<T>(private val block: () -> T) : FlowValue<T> {
+ override operator fun invoke(): T = block()
+ override fun getValue(thisRef: Any?, property: KProperty<*>): T = invoke()
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt b/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt
new file mode 100644
index 0000000..4f514db
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt
@@ -0,0 +1,222 @@
+package com.android.intentresolver.v2.data.repository
+
+import android.content.Intent
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.os.UserHandle.SYSTEM
+import android.os.UserHandle.USER_SYSTEM
+import android.os.UserManager
+import com.android.intentresolver.mock
+import com.android.intentresolver.v2.coroutines.collectLastValue
+import com.android.intentresolver.v2.data.model.User
+import com.android.intentresolver.v2.data.model.User.Role
+import com.android.intentresolver.v2.platform.FakeUserManager
+import com.android.intentresolver.v2.platform.FakeUserManager.ProfileType
+import com.android.intentresolver.whenever
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.Mockito.doReturn
+
+internal class UserRepositoryImplTest {
+ private val userManager = FakeUserManager()
+ private val userState = userManager.state
+
+ @Test
+ fun initialization() = runTest {
+ val repo = createUserRepository(userManager)
+ val users by collectLastValue(repo.users)
+
+ assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull()
+ assertThat(users)
+ .containsExactly(
+ userState.primaryUserHandle,
+ User(userState.primaryUserHandle.identifier, Role.PERSONAL)
+ )
+ }
+
+ @Test
+ fun createProfile() = runTest {
+ val repo = createUserRepository(userManager)
+ val users by collectLastValue(repo.users)
+
+ assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull()
+ assertThat(users!!.values.filter { it.role.type == User.Type.PROFILE }).isEmpty()
+
+ val profile = userState.createProfile(ProfileType.WORK)
+ assertThat(users).containsEntry(profile, User(profile.identifier, Role.WORK))
+ }
+
+ @Test
+ fun removeProfile() = runTest {
+ val repo = createUserRepository(userManager)
+ val users by collectLastValue(repo.users)
+
+ assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull()
+ val work = userState.createProfile(ProfileType.WORK)
+ assertThat(users).containsEntry(work, User(work.identifier, Role.WORK))
+
+ userState.removeProfile(work)
+ assertThat(users).doesNotContainEntry(work, User(work.identifier, Role.WORK))
+ }
+
+ @Test
+ fun isAvailable() = runTest {
+ val repo = createUserRepository(userManager)
+ val work = userState.createProfile(ProfileType.WORK)
+
+ val available by collectLastValue(repo.isAvailable(work))
+ assertThat(available).isTrue()
+
+ userState.setQuietMode(work, true)
+ assertThat(available).isFalse()
+
+ userState.setQuietMode(work, false)
+ assertThat(available).isTrue()
+ }
+
+ @Test
+ fun requestState() = runTest {
+ val repo = createUserRepository(userManager)
+ val work = userState.createProfile(ProfileType.WORK)
+
+ val available by collectLastValue(repo.isAvailable(work))
+ assertThat(available).isTrue()
+
+ repo.requestState(work, false)
+ assertThat(available).isFalse()
+
+ repo.requestState(work, true)
+ assertThat(available).isTrue()
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun requestState_invalidForFullUser() = runTest {
+ val repo = createUserRepository(userManager)
+ val primaryUser = User(userState.primaryUserHandle.identifier, Role.PERSONAL)
+ repo.requestState(primaryUser, available = false)
+ }
+
+ /**
+ * This and all the 'recovers_from_*' tests below all configure a static event flow instead of
+ * using [FakeUserManager]. These tests verify that a invalid broadcast causes the flow to
+ * reinitialize with the user profile group.
+ */
+ @Test
+ fun recovers_from_invalid_profile_added_event() = runTest {
+ val userManager =
+ mockUserManager(validUser = USER_SYSTEM, invalidUser = UserHandle.USER_NULL)
+ val events =
+ flowOf(
+ UserRepositoryImpl.UserEvent(
+ Intent.ACTION_PROFILE_ADDED,
+ UserHandle.of(UserHandle.USER_NULL)
+ )
+ )
+ val repo =
+ UserRepositoryImpl(
+ profileParent = SYSTEM,
+ userManager = userManager,
+ userEvents = events,
+ scope = backgroundScope,
+ backgroundDispatcher = Dispatchers.Unconfined
+ )
+ val users by collectLastValue(repo.users)
+
+ assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull()
+ assertThat(users).containsExactly(SYSTEM, User(USER_SYSTEM, Role.PERSONAL))
+ }
+
+ @Test
+ fun recovers_from_invalid_profile_removed_event() = runTest {
+ val userManager =
+ mockUserManager(validUser = USER_SYSTEM, invalidUser = UserHandle.USER_NULL)
+ val events =
+ flowOf(
+ UserRepositoryImpl.UserEvent(
+ Intent.ACTION_PROFILE_REMOVED,
+ UserHandle.of(UserHandle.USER_NULL)
+ )
+ )
+ val repo =
+ UserRepositoryImpl(
+ profileParent = SYSTEM,
+ userManager = userManager,
+ userEvents = events,
+ scope = backgroundScope,
+ backgroundDispatcher = Dispatchers.Unconfined
+ )
+ val users by collectLastValue(repo.users)
+
+ assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull()
+ assertThat(users).containsExactly(SYSTEM, User(USER_SYSTEM, Role.PERSONAL))
+ }
+
+ @Test
+ fun recovers_from_invalid_profile_available_event() = runTest {
+ val userManager =
+ mockUserManager(validUser = USER_SYSTEM, invalidUser = UserHandle.USER_NULL)
+ val events =
+ flowOf(
+ UserRepositoryImpl.UserEvent(
+ Intent.ACTION_PROFILE_AVAILABLE,
+ UserHandle.of(UserHandle.USER_NULL)
+ )
+ )
+ val repo =
+ UserRepositoryImpl(SYSTEM, userManager, events, backgroundScope, Dispatchers.Unconfined)
+ val users by collectLastValue(repo.users)
+
+ assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull()
+ assertThat(users).containsExactly(SYSTEM, User(USER_SYSTEM, Role.PERSONAL))
+ }
+
+ @Test
+ fun recovers_from_unknown_event() = runTest {
+ val userManager =
+ mockUserManager(validUser = USER_SYSTEM, invalidUser = UserHandle.USER_NULL)
+ val events =
+ flowOf(
+ UserRepositoryImpl.UserEvent("UNKNOWN_EVENT", UserHandle.of(UserHandle.USER_NULL))
+ )
+ val repo =
+ UserRepositoryImpl(
+ profileParent = SYSTEM,
+ userManager = userManager,
+ userEvents = events,
+ scope = backgroundScope,
+ backgroundDispatcher = Dispatchers.Unconfined
+ )
+ val users by collectLastValue(repo.users)
+
+ assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull()
+ assertThat(users).containsExactly(SYSTEM, User(USER_SYSTEM, Role.PERSONAL))
+ }
+}
+
+@Suppress("SameParameterValue", "DEPRECATION")
+private fun mockUserManager(validUser: Int, invalidUser: Int) =
+ mock<UserManager> {
+ val info = UserInfo(validUser, "", "", UserInfo.FLAG_FULL)
+ doReturn(listOf(info)).whenever(this).getEnabledProfiles(Mockito.anyInt())
+
+ doReturn(info).whenever(this).getUserInfo(Mockito.eq(validUser))
+
+ doReturn(listOf<UserInfo>()).whenever(this).getEnabledProfiles(Mockito.eq(invalidUser))
+
+ doReturn(null).whenever(this).getUserInfo(Mockito.eq(invalidUser))
+ }
+
+private fun TestScope.createUserRepository(userManager: FakeUserManager) =
+ UserRepositoryImpl(
+ profileParent = userManager.state.primaryUserHandle,
+ userManager = userManager,
+ userEvents = userManager.state.userEvents,
+ scope = backgroundScope,
+ backgroundDispatcher = Dispatchers.Unconfined
+ )
diff --git a/tests/unit/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelperTest.kt b/tests/unit/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelperTest.kt
new file mode 100644
index 0000000..696dd1f
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelperTest.kt
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.emptystate
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.TextView
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.intentresolver.any
+import com.android.intentresolver.emptystate.EmptyState
+import com.android.intentresolver.mock
+import com.google.common.truth.Truth.assertThat
+import java.util.Optional
+import java.util.function.Supplier
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+
+class EmptyStateUiHelperTest {
+ private val context = InstrumentationRegistry.getInstrumentation().getContext()
+
+ var shouldOverrideContainerPadding = false
+ val containerPaddingSupplier =
+ Supplier<Optional<Int>> {
+ Optional.ofNullable(if (shouldOverrideContainerPadding) 42 else null)
+ }
+
+ lateinit var rootContainer: ViewGroup
+ lateinit var mainListView: View // Visible when no empty state is showing.
+ lateinit var emptyStateTitleView: TextView
+ lateinit var emptyStateSubtitleView: TextView
+ lateinit var emptyStateButtonView: View
+ lateinit var emptyStateProgressView: View
+ lateinit var emptyStateDefaultTextView: View
+ lateinit var emptyStateContainerView: View
+ lateinit var emptyStateRootView: View
+ lateinit var emptyStateUiHelper: EmptyStateUiHelper
+
+ @Before
+ fun setup() {
+ rootContainer = FrameLayout(context)
+ LayoutInflater.from(context)
+ .inflate(
+ com.android.intentresolver.R.layout.resolver_list_per_profile,
+ rootContainer,
+ true
+ )
+ mainListView = rootContainer.requireViewById(com.android.internal.R.id.resolver_list)
+ emptyStateRootView =
+ rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state)
+ emptyStateTitleView =
+ rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state_title)
+ emptyStateSubtitleView =
+ rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state_subtitle)
+ emptyStateButtonView =
+ rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state_button)
+ emptyStateProgressView =
+ rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state_progress)
+ emptyStateDefaultTextView = rootContainer.requireViewById(com.android.internal.R.id.empty)
+ emptyStateContainerView =
+ rootContainer.requireViewById(com.android.internal.R.id.resolver_empty_state_container)
+ emptyStateUiHelper =
+ EmptyStateUiHelper(
+ rootContainer,
+ com.android.internal.R.id.resolver_list,
+ containerPaddingSupplier
+ )
+ }
+
+ @Test
+ fun testResetViewVisibilities() {
+ // First set each view's visibility to differ from the expected "reset" state so we can then
+ // assert that they're all reset afterward.
+ // TODO: for historic reasons "reset" doesn't cover `emptyStateContainerView`; should it?
+ emptyStateRootView.visibility = View.GONE
+ emptyStateTitleView.visibility = View.GONE
+ emptyStateSubtitleView.visibility = View.GONE
+ emptyStateButtonView.visibility = View.VISIBLE
+ emptyStateProgressView.visibility = View.VISIBLE
+ emptyStateDefaultTextView.visibility = View.VISIBLE
+
+ emptyStateUiHelper.resetViewVisibilities()
+
+ assertThat(emptyStateRootView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateTitleView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateSubtitleView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateButtonView.visibility).isEqualTo(View.INVISIBLE)
+ assertThat(emptyStateProgressView.visibility).isEqualTo(View.GONE)
+ assertThat(emptyStateDefaultTextView.visibility).isEqualTo(View.GONE)
+ }
+
+ @Test
+ fun testShowSpinner() {
+ emptyStateTitleView.visibility = View.VISIBLE
+ emptyStateButtonView.visibility = View.VISIBLE
+ emptyStateProgressView.visibility = View.GONE
+ emptyStateDefaultTextView.visibility = View.VISIBLE
+
+ emptyStateUiHelper.showSpinner()
+
+ // TODO: should this cover any other views? Subtitle?
+ assertThat(emptyStateTitleView.visibility).isEqualTo(View.INVISIBLE)
+ assertThat(emptyStateButtonView.visibility).isEqualTo(View.INVISIBLE)
+ assertThat(emptyStateProgressView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateDefaultTextView.visibility).isEqualTo(View.GONE)
+ }
+
+ @Test
+ fun testHide() {
+ emptyStateRootView.visibility = View.VISIBLE
+ mainListView.visibility = View.GONE
+
+ emptyStateUiHelper.hide()
+
+ assertThat(emptyStateRootView.visibility).isEqualTo(View.GONE)
+ assertThat(mainListView.visibility).isEqualTo(View.VISIBLE)
+ }
+
+ @Test
+ fun testBottomPaddingDelegate_default() {
+ shouldOverrideContainerPadding = false
+ emptyStateContainerView.setPadding(1, 2, 3, 4)
+
+ emptyStateUiHelper.setupContainerPadding()
+
+ assertThat(emptyStateContainerView.paddingLeft).isEqualTo(1)
+ assertThat(emptyStateContainerView.paddingTop).isEqualTo(2)
+ assertThat(emptyStateContainerView.paddingRight).isEqualTo(3)
+ assertThat(emptyStateContainerView.paddingBottom).isEqualTo(4)
+ }
+
+ @Test
+ fun testBottomPaddingDelegate_override() {
+ shouldOverrideContainerPadding = true // Set bottom padding to 42.
+ emptyStateContainerView.setPadding(1, 2, 3, 4)
+
+ emptyStateUiHelper.setupContainerPadding()
+
+ assertThat(emptyStateContainerView.paddingLeft).isEqualTo(1)
+ assertThat(emptyStateContainerView.paddingTop).isEqualTo(2)
+ assertThat(emptyStateContainerView.paddingRight).isEqualTo(3)
+ assertThat(emptyStateContainerView.paddingBottom).isEqualTo(42)
+ }
+
+ @Test
+ fun testShowEmptyState_noOnClickHandler() {
+ mainListView.visibility = View.VISIBLE
+
+ // Note: an `EmptyState.ClickListener` isn't invoked directly by the UI helper; it has to be
+ // built into the "on-click handler" that's injected to implement the button-press. We won't
+ // display the button without a click "handler," even if it *does* have a `ClickListener`.
+ val clickListener = mock<EmptyState.ClickListener>()
+
+ val emptyState =
+ object : EmptyState {
+ override fun getTitle() = "Test title"
+ override fun getSubtitle() = "Test subtitle"
+
+ override fun getButtonClickListener() = clickListener
+ }
+ emptyStateUiHelper.showEmptyState(emptyState, null)
+
+ assertThat(mainListView.visibility).isEqualTo(View.GONE)
+ assertThat(emptyStateRootView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateTitleView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateSubtitleView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateButtonView.visibility).isEqualTo(View.GONE)
+ assertThat(emptyStateProgressView.visibility).isEqualTo(View.GONE)
+ assertThat(emptyStateDefaultTextView.visibility).isEqualTo(View.GONE)
+
+ assertThat(emptyStateTitleView.text).isEqualTo("Test title")
+ assertThat(emptyStateSubtitleView.text).isEqualTo("Test subtitle")
+
+ verify(clickListener, never()).onClick(any())
+ }
+
+ @Test
+ fun testShowEmptyState_withOnClickHandlerAndClickListener() {
+ mainListView.visibility = View.VISIBLE
+
+ val clickListener = mock<EmptyState.ClickListener>()
+ val onClickHandler = mock<View.OnClickListener>()
+
+ val emptyState =
+ object : EmptyState {
+ override fun getTitle() = "Test title"
+ override fun getSubtitle() = "Test subtitle"
+
+ override fun getButtonClickListener() = clickListener
+ }
+ emptyStateUiHelper.showEmptyState(emptyState, onClickHandler)
+
+ assertThat(mainListView.visibility).isEqualTo(View.GONE)
+ assertThat(emptyStateRootView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateTitleView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateSubtitleView.visibility).isEqualTo(View.VISIBLE)
+ assertThat(emptyStateButtonView.visibility).isEqualTo(View.VISIBLE) // Now shown.
+ assertThat(emptyStateProgressView.visibility).isEqualTo(View.GONE)
+ assertThat(emptyStateDefaultTextView.visibility).isEqualTo(View.GONE)
+
+ assertThat(emptyStateTitleView.text).isEqualTo("Test title")
+ assertThat(emptyStateSubtitleView.text).isEqualTo("Test subtitle")
+
+ emptyStateButtonView.performClick()
+
+ verify(onClickHandler).onClick(emptyStateButtonView)
+ // The test didn't explicitly configure its `OnClickListener` to relay the click event on
+ // to the `EmptyState.ClickListener`, so it still won't have fired here.
+ verify(clickListener, never()).onClick(any())
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/listcontroller/ChooserRequestFilteredComponentsTest.kt b/tests/unit/src/com/android/intentresolver/v2/listcontroller/ChooserRequestFilteredComponentsTest.kt
new file mode 100644
index 0000000..59494be
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/listcontroller/ChooserRequestFilteredComponentsTest.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import com.android.intentresolver.ChooserRequestParameters
+import com.android.intentresolver.whenever
+import com.google.common.collect.ImmutableList
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+class ChooserRequestFilteredComponentsTest {
+
+ @Mock lateinit var mockChooserRequestParameters: ChooserRequestParameters
+
+ private lateinit var chooserRequestFilteredComponents: ChooserRequestFilteredComponents
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ chooserRequestFilteredComponents =
+ ChooserRequestFilteredComponents(mockChooserRequestParameters)
+ }
+
+ @Test
+ fun isComponentFiltered_returnsRequestParametersFilteredState() {
+ // Arrange
+ whenever(mockChooserRequestParameters.filteredComponentNames)
+ .thenReturn(
+ ImmutableList.of(ComponentName("FilteredPackage", "FilteredClass")),
+ )
+ val testComponent = ComponentName("TestPackage", "TestClass")
+ val filteredComponent = ComponentName("FilteredPackage", "FilteredClass")
+
+ // Act
+ val result = chooserRequestFilteredComponents.isComponentFiltered(testComponent)
+ val filteredResult = chooserRequestFilteredComponents.isComponentFiltered(filteredComponent)
+
+ // Assert
+ assertThat(result).isFalse()
+ assertThat(filteredResult).isTrue()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/listcontroller/FakeResolverComparator.kt b/tests/unit/src/com/android/intentresolver/v2/listcontroller/FakeResolverComparator.kt
new file mode 100644
index 0000000..ce40567
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/listcontroller/FakeResolverComparator.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ResolveInfo
+import android.content.res.Configuration
+import android.content.res.Resources
+import android.os.Message
+import android.os.UserHandle
+import com.android.intentresolver.ResolvedComponentInfo
+import com.android.intentresolver.chooser.TargetInfo
+import com.android.intentresolver.model.AbstractResolverComparator
+import com.android.intentresolver.whenever
+import java.util.Locale
+import org.mockito.Mockito
+
+class FakeResolverComparator(
+ context: Context =
+ Mockito.mock(Context::class.java).also {
+ val mockResources = Mockito.mock(Resources::class.java)
+ whenever(it.resources).thenReturn(mockResources)
+ whenever(mockResources.configuration)
+ .thenReturn(Configuration().apply { setLocale(Locale.US) })
+ },
+ targetIntent: Intent = Intent("TestAction"),
+ resolvedActivityUserSpaceList: List<UserHandle> = emptyList(),
+ promoteToFirst: ComponentName? = null,
+) :
+ AbstractResolverComparator(
+ context,
+ targetIntent,
+ resolvedActivityUserSpaceList,
+ promoteToFirst,
+ ) {
+ var lastUpdateModel: TargetInfo? = null
+ private set
+ var lastUpdateChooserCounts: Triple<String, UserHandle, String>? = null
+ private set
+ var destroyCalled = false
+ private set
+
+ override fun compare(lhs: ResolveInfo?, rhs: ResolveInfo?): Int =
+ lhs!!.activityInfo.packageName.compareTo(rhs!!.activityInfo.packageName)
+
+ override fun doCompute(targets: MutableList<ResolvedComponentInfo>?) {}
+
+ override fun getScore(targetInfo: TargetInfo?): Float = 1.23f
+
+ override fun handleResultMessage(message: Message?) {}
+
+ override fun updateModel(targetInfo: TargetInfo?) {
+ lastUpdateModel = targetInfo
+ }
+
+ override fun updateChooserCounts(
+ packageName: String,
+ user: UserHandle,
+ action: String,
+ ) {
+ lastUpdateChooserCounts = Triple(packageName, user, action)
+ }
+
+ override fun destroy() {
+ destroyCalled = true
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/listcontroller/FilterableComponentsTest.kt b/tests/unit/src/com/android/intentresolver/v2/listcontroller/FilterableComponentsTest.kt
new file mode 100644
index 0000000..396505e
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/listcontroller/FilterableComponentsTest.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import com.android.intentresolver.ChooserRequestParameters
+import com.android.intentresolver.whenever
+import com.google.common.collect.ImmutableList
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+class FilterableComponentsTest {
+
+ @Mock lateinit var mockChooserRequestParameters: ChooserRequestParameters
+
+ private val unfilteredComponent = ComponentName("TestPackage", "TestClass")
+ private val filteredComponent = ComponentName("FilteredPackage", "FilteredClass")
+ private val noComponentFiltering = NoComponentFiltering()
+
+ private lateinit var chooserRequestFilteredComponents: ChooserRequestFilteredComponents
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ chooserRequestFilteredComponents =
+ ChooserRequestFilteredComponents(mockChooserRequestParameters)
+ }
+
+ @Test
+ fun isComponentFiltered_noComponentFiltering_neverFilters() {
+ // Arrange
+
+ // Act
+ val unfilteredResult = noComponentFiltering.isComponentFiltered(unfilteredComponent)
+ val filteredResult = noComponentFiltering.isComponentFiltered(filteredComponent)
+
+ // Assert
+ assertThat(unfilteredResult).isFalse()
+ assertThat(filteredResult).isFalse()
+ }
+
+ @Test
+ fun isComponentFiltered_chooserRequestFilteredComponents_filtersAccordingToChooserRequest() {
+ // Arrange
+ whenever(mockChooserRequestParameters.filteredComponentNames)
+ .thenReturn(
+ ImmutableList.of(filteredComponent),
+ )
+
+ // Act
+ val unfilteredResult =
+ chooserRequestFilteredComponents.isComponentFiltered(unfilteredComponent)
+ val filteredResult = chooserRequestFilteredComponents.isComponentFiltered(filteredComponent)
+
+ // Assert
+ assertThat(unfilteredResult).isFalse()
+ assertThat(filteredResult).isTrue()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/listcontroller/IntentResolverTest.kt b/tests/unit/src/com/android/intentresolver/v2/listcontroller/IntentResolverTest.kt
new file mode 100644
index 0000000..09f6d37
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/listcontroller/IntentResolverTest.kt
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.net.Uri
+import android.os.UserHandle
+import com.android.intentresolver.any
+import com.android.intentresolver.eq
+import com.android.intentresolver.kotlinArgumentCaptor
+import com.android.intentresolver.whenever
+import com.google.common.truth.Truth.assertThat
+import java.lang.IndexOutOfBoundsException
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+class IntentResolverTest {
+
+ @Mock lateinit var mockPackageManager: PackageManager
+
+ private lateinit var intentResolver: IntentResolver
+
+ private val fakePinnableComponents =
+ object : PinnableComponents {
+ override fun isComponentPinned(name: ComponentName): Boolean {
+ return name.packageName == "PinnedPackage"
+ }
+ }
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ intentResolver =
+ IntentResolverImpl(mockPackageManager, ResolveListDeduperImpl(fakePinnableComponents))
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_noIntents_returnsEmptyList() {
+ // Arrange
+ val testIntents = emptyList<Intent>()
+
+ // Act
+ val result =
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_noResolveInfo_returnsEmptyList() {
+ // Arrange
+ val testIntents = listOf(Intent("TestAction"))
+ val testResolveInfos = emptyList<ResolveInfo>()
+ whenever(mockPackageManager.queryIntentActivitiesAsUser(any(), anyInt(), any<UserHandle>()))
+ .thenReturn(testResolveInfos)
+
+ // Act
+ val result =
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_returnsAllResolveComponentInfo() {
+ // Arrange
+ val testIntent1 = Intent("TestAction1")
+ val testIntent2 = Intent("TestAction2")
+ val testIntents = listOf(testIntent1, testIntent2)
+ val testResolveInfos1 =
+ listOf(
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage1"
+ activityInfo.name = "TestClass1"
+ },
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage2"
+ activityInfo.name = "TestClass2"
+ },
+ )
+ val testResolveInfos2 =
+ listOf(
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage3"
+ activityInfo.name = "TestClass3"
+ },
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage4"
+ activityInfo.name = "TestClass4"
+ },
+ )
+ whenever(
+ mockPackageManager.queryIntentActivitiesAsUser(
+ eq(testIntent1),
+ anyInt(),
+ any<UserHandle>(),
+ )
+ )
+ .thenReturn(testResolveInfos1)
+ whenever(
+ mockPackageManager.queryIntentActivitiesAsUser(
+ eq(testIntent2),
+ anyInt(),
+ any<UserHandle>(),
+ )
+ )
+ .thenReturn(testResolveInfos2)
+
+ // Act
+ val result =
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ result.forEachIndexed { index, it ->
+ val postfix = index + 1
+ assertThat(it.name.packageName).isEqualTo("TestPackage$postfix")
+ assertThat(it.name.className).isEqualTo("TestClass$postfix")
+ assertThrows(IndexOutOfBoundsException::class.java) { it.getIntentAt(1) }
+ }
+ assertThat(result.map { it.getIntentAt(0) })
+ .containsExactly(
+ testIntent1,
+ testIntent1,
+ testIntent2,
+ testIntent2,
+ )
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_resolveInfoWithoutUserHandle_isSkipped() {
+ // Arrange
+ val testIntent = Intent("TestAction")
+ val testIntents = listOf(testIntent)
+ val testResolveInfos =
+ listOf(
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage"
+ activityInfo.name = "TestClass"
+ },
+ )
+ whenever(
+ mockPackageManager.queryIntentActivitiesAsUser(
+ any(),
+ anyInt(),
+ any<UserHandle>(),
+ )
+ )
+ .thenReturn(testResolveInfos)
+
+ // Act
+ val result =
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_duplicateComponents_areCombined() {
+ // Arrange
+ val testIntent1 = Intent("TestAction1")
+ val testIntent2 = Intent("TestAction2")
+ val testIntents = listOf(testIntent1, testIntent2)
+ val testResolveInfos1 =
+ listOf(
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "DuplicatePackage"
+ activityInfo.name = "DuplicateClass"
+ },
+ )
+ val testResolveInfos2 =
+ listOf(
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "DuplicatePackage"
+ activityInfo.name = "DuplicateClass"
+ },
+ )
+ whenever(
+ mockPackageManager.queryIntentActivitiesAsUser(
+ eq(testIntent1),
+ anyInt(),
+ any<UserHandle>(),
+ )
+ )
+ .thenReturn(testResolveInfos1)
+ whenever(
+ mockPackageManager.queryIntentActivitiesAsUser(
+ eq(testIntent2),
+ anyInt(),
+ any<UserHandle>(),
+ )
+ )
+ .thenReturn(testResolveInfos2)
+
+ // Act
+ val result =
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ assertThat(result).hasSize(1)
+ with(result.first()) {
+ assertThat(name.packageName).isEqualTo("DuplicatePackage")
+ assertThat(name.className).isEqualTo("DuplicateClass")
+ assertThat(getIntentAt(0)).isEqualTo(testIntent1)
+ assertThat(getIntentAt(1)).isEqualTo(testIntent2)
+ assertThrows(IndexOutOfBoundsException::class.java) { getIntentAt(2) }
+ }
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_pinnedComponentsArePinned() {
+ // Arrange
+ val testIntent1 = Intent("TestAction1")
+ val testIntent2 = Intent("TestAction2")
+ val testIntents = listOf(testIntent1, testIntent2)
+ val testResolveInfos1 =
+ listOf(
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "UnpinnedPackage"
+ activityInfo.name = "UnpinnedClass"
+ },
+ )
+ val testResolveInfos2 =
+ listOf(
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "PinnedPackage"
+ activityInfo.name = "PinnedClass"
+ },
+ )
+ whenever(
+ mockPackageManager.queryIntentActivitiesAsUser(
+ eq(testIntent1),
+ anyInt(),
+ any<UserHandle>(),
+ )
+ )
+ .thenReturn(testResolveInfos1)
+ whenever(
+ mockPackageManager.queryIntentActivitiesAsUser(
+ eq(testIntent2),
+ anyInt(),
+ any<UserHandle>(),
+ )
+ )
+ .thenReturn(testResolveInfos2)
+
+ // Act
+ val result =
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ assertThat(result.map { it.isPinned }).containsExactly(false, true)
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_whenNoExtraBehavior_usesBaseFlags() {
+ // Arrange
+ val baseFlags =
+ PackageManager.MATCH_DIRECT_BOOT_AWARE or
+ PackageManager.MATCH_DIRECT_BOOT_UNAWARE or
+ PackageManager.MATCH_CLONE_PROFILE
+ val testIntent = Intent()
+ val testIntents = listOf(testIntent)
+
+ // Act
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ val flags = kotlinArgumentCaptor<Int>()
+ verify(mockPackageManager)
+ .queryIntentActivitiesAsUser(
+ any(),
+ flags.capture(),
+ any<UserHandle>(),
+ )
+ assertThat(flags.value).isEqualTo(baseFlags)
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_whenShouldGetResolvedFilter_usesGetResolvedFilterFlag() {
+ // Arrange
+ val testIntent = Intent()
+ val testIntents = listOf(testIntent)
+
+ // Act
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = true,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ val flags = kotlinArgumentCaptor<Int>()
+ verify(mockPackageManager)
+ .queryIntentActivitiesAsUser(
+ any(),
+ flags.capture(),
+ any<UserHandle>(),
+ )
+ assertThat(flags.value and PackageManager.GET_RESOLVED_FILTER)
+ .isEqualTo(PackageManager.GET_RESOLVED_FILTER)
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_whenShouldGetActivityMetadata_usesGetMetaDataFlag() {
+ // Arrange
+ val testIntent = Intent()
+ val testIntents = listOf(testIntent)
+
+ // Act
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = true,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ val flags = kotlinArgumentCaptor<Int>()
+ verify(mockPackageManager)
+ .queryIntentActivitiesAsUser(
+ any(),
+ flags.capture(),
+ any<UserHandle>(),
+ )
+ assertThat(flags.value and PackageManager.GET_META_DATA)
+ .isEqualTo(PackageManager.GET_META_DATA)
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_whenShouldGetOnlyDefaultActivities_usesMatchDefaultOnlyFlag() {
+ // Arrange
+ val testIntent = Intent()
+ val testIntents = listOf(testIntent)
+
+ // Act
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = true,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ val flags = kotlinArgumentCaptor<Int>()
+ verify(mockPackageManager)
+ .queryIntentActivitiesAsUser(
+ any(),
+ flags.capture(),
+ any<UserHandle>(),
+ )
+ assertThat(flags.value and PackageManager.MATCH_DEFAULT_ONLY)
+ .isEqualTo(PackageManager.MATCH_DEFAULT_ONLY)
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_whenWebIntent_usesMatchInstantFlag() {
+ // Arrange
+ val testIntent = Intent(Intent.ACTION_VIEW, Uri.fromParts(IntentFilter.SCHEME_HTTP, "", ""))
+ val testIntents = listOf(testIntent)
+
+ // Act
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ val flags = kotlinArgumentCaptor<Int>()
+ verify(mockPackageManager)
+ .queryIntentActivitiesAsUser(
+ any(),
+ flags.capture(),
+ any<UserHandle>(),
+ )
+ assertThat(flags.value and PackageManager.MATCH_INSTANT)
+ .isEqualTo(PackageManager.MATCH_INSTANT)
+ }
+
+ @Test
+ fun getResolversForIntentAsUser_whenActivityMatchExternalFlag_usesMatchInstantFlag() {
+ // Arrange
+ val testIntent = Intent().addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL)
+ val testIntents = listOf(testIntent)
+
+ // Act
+ intentResolver.getResolversForIntentAsUser(
+ shouldGetResolvedFilter = false,
+ shouldGetActivityMetadata = false,
+ shouldGetOnlyDefaultActivities = false,
+ intents = testIntents,
+ userHandle = UserHandle(456),
+ )
+
+ // Assert
+ val flags = kotlinArgumentCaptor<Int>()
+ verify(mockPackageManager)
+ .queryIntentActivitiesAsUser(
+ any(),
+ flags.capture(),
+ any<UserHandle>(),
+ )
+ assertThat(flags.value and PackageManager.MATCH_INSTANT)
+ .isEqualTo(PackageManager.MATCH_INSTANT)
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/listcontroller/LastChosenManagerTest.kt b/tests/unit/src/com/android/intentresolver/v2/listcontroller/LastChosenManagerTest.kt
new file mode 100644
index 0000000..ce5e52b
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/listcontroller/LastChosenManagerTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.ContentResolver
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.IPackageManager
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import com.android.intentresolver.any
+import com.android.intentresolver.eq
+import com.android.intentresolver.nullable
+import com.android.intentresolver.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito.isNull
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class LastChosenManagerTest {
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+ private val testTargetIntent = Intent("TestAction")
+
+ @Mock lateinit var mockContentResolver: ContentResolver
+ @Mock lateinit var mockIPackageManager: IPackageManager
+
+ private lateinit var lastChosenManager: LastChosenManager
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ lastChosenManager =
+ PackageManagerLastChosenManager(mockContentResolver, testDispatcher, testTargetIntent) {
+ mockIPackageManager
+ }
+ }
+
+ @Test
+ fun getLastChosen_returnsLastChosenActivity() =
+ testScope.runTest {
+ // Arrange
+ val testResolveInfo = ResolveInfo()
+ whenever(mockIPackageManager.getLastChosenActivity(any(), nullable(), any()))
+ .thenReturn(testResolveInfo)
+
+ // Act
+ val lastChosen = lastChosenManager.getLastChosen()
+ runCurrent()
+
+ // Assert
+ verify(mockIPackageManager)
+ .getLastChosenActivity(
+ eq(testTargetIntent),
+ isNull(),
+ eq(PackageManager.MATCH_DEFAULT_ONLY),
+ )
+ assertThat(lastChosen).isSameInstanceAs(testResolveInfo)
+ }
+
+ @Test
+ fun setLastChosen_setsLastChosenActivity() =
+ testScope.runTest {
+ // Arrange
+ val testComponent = ComponentName("TestPackage", "TestClass")
+ val testIntent = Intent().apply { component = testComponent }
+ val testIntentFilter = IntentFilter()
+ val testMatch = 456
+
+ // Act
+ lastChosenManager.setLastChosen(testIntent, testIntentFilter, testMatch)
+ runCurrent()
+
+ // Assert
+ verify(mockIPackageManager)
+ .setLastChosenActivity(
+ eq(testIntent),
+ isNull(),
+ eq(PackageManager.MATCH_DEFAULT_ONLY),
+ eq(testIntentFilter),
+ eq(testMatch),
+ eq(testComponent),
+ )
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/listcontroller/PinnableComponentsTest.kt b/tests/unit/src/com/android/intentresolver/v2/listcontroller/PinnableComponentsTest.kt
new file mode 100644
index 0000000..112342a
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/listcontroller/PinnableComponentsTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.SharedPreferences
+import com.android.intentresolver.any
+import com.android.intentresolver.eq
+import com.android.intentresolver.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.MockitoAnnotations
+
+class PinnableComponentsTest {
+
+ @Mock lateinit var mockSharedPreferences: SharedPreferences
+
+ private val unpinnedComponent = ComponentName("TestPackage", "TestClass")
+ private val pinnedComponent = ComponentName("PinnedPackage", "PinnedClass")
+ private val noComponentPinning = NoComponentPinning()
+
+ private lateinit var sharedPreferencesPinnedComponents: PinnableComponents
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ sharedPreferencesPinnedComponents = SharedPreferencesPinnedComponents(mockSharedPreferences)
+ }
+
+ @Test
+ fun isComponentPinned_noComponentPinning_neverPins() {
+ // Arrange
+
+ // Act
+ val unpinnedResult = noComponentPinning.isComponentPinned(unpinnedComponent)
+ val pinnedResult = noComponentPinning.isComponentPinned(pinnedComponent)
+
+ // Assert
+ assertThat(unpinnedResult).isFalse()
+ assertThat(pinnedResult).isFalse()
+ }
+
+ @Test
+ fun isComponentFiltered_chooserRequestFilteredComponents_filtersAccordingToChooserRequest() {
+ // Arrange
+ whenever(mockSharedPreferences.getBoolean(eq(pinnedComponent.flattenToString()), any()))
+ .thenReturn(true)
+
+ // Act
+ val unpinnedResult = sharedPreferencesPinnedComponents.isComponentPinned(unpinnedComponent)
+ val pinnedResult = sharedPreferencesPinnedComponents.isComponentPinned(pinnedComponent)
+
+ // Assert
+ assertThat(unpinnedResult).isFalse()
+ assertThat(pinnedResult).isTrue()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduperTest.kt b/tests/unit/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduperTest.kt
new file mode 100644
index 0000000..26f0199
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduperTest.kt
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ResolveInfo
+import android.os.UserHandle
+import com.android.intentresolver.ResolvedComponentInfo
+import com.google.common.truth.Truth.assertThat
+import java.lang.IndexOutOfBoundsException
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+
+class ResolveListDeduperTest {
+
+ private lateinit var resolveListDeduper: ResolveListDeduper
+
+ @Before
+ fun setup() {
+ resolveListDeduper = ResolveListDeduperImpl(NoComponentPinning())
+ }
+
+ @Test
+ fun addResolveListDedupe_addsDifferentComponents() {
+ // Arrange
+ val testIntent = Intent()
+ val testResolveInfo1 =
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage1"
+ activityInfo.name = "TestClass1"
+ }
+ val testResolveInfo2 =
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage2"
+ activityInfo.name = "TestClass2"
+ }
+ val testResolvedComponentInfo1 =
+ ResolvedComponentInfo(
+ ComponentName("TestPackage1", "TestClass1"),
+ testIntent,
+ testResolveInfo1,
+ )
+ .apply { isPinned = false }
+ val listUnderTest = mutableListOf(testResolvedComponentInfo1)
+ val listToAdd = listOf(testResolveInfo2)
+
+ // Act
+ resolveListDeduper.addToResolveListWithDedupe(
+ into = listUnderTest,
+ intent = testIntent,
+ from = listToAdd,
+ )
+
+ // Assert
+ listUnderTest.forEachIndexed { index, it ->
+ val postfix = index + 1
+ assertThat(it.name.packageName).isEqualTo("TestPackage$postfix")
+ assertThat(it.name.className).isEqualTo("TestClass$postfix")
+ assertThat(it.getIntentAt(0)).isEqualTo(testIntent)
+ assertThrows(IndexOutOfBoundsException::class.java) { it.getIntentAt(1) }
+ }
+ }
+
+ @Test
+ fun addResolveListDedupe_combinesDuplicateComponents() {
+ // Arrange
+ val testIntent = Intent()
+ val testResolveInfo1 =
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "DuplicatePackage"
+ activityInfo.name = "DuplicateClass"
+ }
+ val testResolveInfo2 =
+ ResolveInfo().apply {
+ userHandle = UserHandle(456)
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "DuplicatePackage"
+ activityInfo.name = "DuplicateClass"
+ }
+ val testResolvedComponentInfo1 =
+ ResolvedComponentInfo(
+ ComponentName("DuplicatePackage", "DuplicateClass"),
+ testIntent,
+ testResolveInfo1,
+ )
+ .apply { isPinned = false }
+ val listUnderTest = mutableListOf(testResolvedComponentInfo1)
+ val listToAdd = listOf(testResolveInfo2)
+
+ // Act
+ resolveListDeduper.addToResolveListWithDedupe(
+ into = listUnderTest,
+ intent = testIntent,
+ from = listToAdd,
+ )
+
+ // Assert
+ assertThat(listUnderTest).containsExactly(testResolvedComponentInfo1)
+ assertThat(testResolvedComponentInfo1.getResolveInfoAt(0)).isEqualTo(testResolveInfo1)
+ assertThat(testResolvedComponentInfo1.getResolveInfoAt(1)).isEqualTo(testResolveInfo2)
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFilteringTest.kt b/tests/unit/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFilteringTest.kt
new file mode 100644
index 0000000..9786b80
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFilteringTest.kt
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import com.android.intentresolver.ResolvedComponentInfo
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertThrows
+import org.junit.Before
+import org.junit.Test
+
+class ResolvedComponentFilteringTest {
+
+ private lateinit var resolvedComponentFiltering: ResolvedComponentFiltering
+
+ private val fakeFilterableComponents =
+ object : FilterableComponents {
+ override fun isComponentFiltered(name: ComponentName): Boolean {
+ return name.packageName == "FilteredPackage"
+ }
+ }
+
+ private val fakePermissionChecker =
+ object : PermissionChecker {
+ override suspend fun checkComponentPermission(
+ permission: String,
+ uid: Int,
+ owningUid: Int,
+ exported: Boolean
+ ): Int {
+ return if (permission == "MissingPermission") {
+ PackageManager.PERMISSION_DENIED
+ } else {
+ PackageManager.PERMISSION_GRANTED
+ }
+ }
+ }
+
+ @Before
+ fun setup() {
+ resolvedComponentFiltering =
+ ResolvedComponentFilteringImpl(
+ launchedFromUid = 123,
+ filterableComponents = fakeFilterableComponents,
+ permissionChecker = fakePermissionChecker,
+ )
+ }
+
+ @Test
+ fun filterIneligibleActivities_returnsListWithoutFilteredComponents() = runTest {
+ // Arrange
+ val testIntent = Intent("TestAction")
+ val testResolveInfo =
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage"
+ activityInfo.name = "TestClass"
+ activityInfo.permission = "TestPermission"
+ activityInfo.applicationInfo = ApplicationInfo()
+ activityInfo.applicationInfo.uid = 456
+ activityInfo.exported = false
+ }
+ val filteredResolveInfo =
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "FilteredPackage"
+ activityInfo.name = "FilteredClass"
+ activityInfo.permission = "TestPermission"
+ activityInfo.applicationInfo = ApplicationInfo()
+ activityInfo.applicationInfo.uid = 456
+ activityInfo.exported = false
+ }
+ val missingPermissionResolveInfo =
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "NoPermissionPackage"
+ activityInfo.name = "NoPermissionClass"
+ activityInfo.permission = "MissingPermission"
+ activityInfo.applicationInfo = ApplicationInfo()
+ activityInfo.applicationInfo.uid = 456
+ activityInfo.exported = false
+ }
+ val testInput =
+ listOf(
+ ResolvedComponentInfo(
+ ComponentName("TestPackage", "TestClass"),
+ testIntent,
+ testResolveInfo,
+ ),
+ ResolvedComponentInfo(
+ ComponentName("FilteredPackage", "FilteredClass"),
+ testIntent,
+ filteredResolveInfo,
+ ),
+ ResolvedComponentInfo(
+ ComponentName("NoPermissionPackage", "NoPermissionClass"),
+ testIntent,
+ missingPermissionResolveInfo,
+ )
+ )
+
+ // Act
+ val result = resolvedComponentFiltering.filterIneligibleActivities(testInput)
+
+ // Assert
+ assertThat(result).hasSize(1)
+ with(result.first()) {
+ assertThat(name.packageName).isEqualTo("TestPackage")
+ assertThat(name.className).isEqualTo("TestClass")
+ assertThat(getIntentAt(0)).isEqualTo(testIntent)
+ assertThrows(IndexOutOfBoundsException::class.java) { getIntentAt(1) }
+ assertThat(getResolveInfoAt(0)).isEqualTo(testResolveInfo)
+ assertThrows(IndexOutOfBoundsException::class.java) { getResolveInfoAt(1) }
+ }
+ }
+
+ @Test
+ fun filterLowPriority_filtersAfterFirstDifferentPriority() {
+ // Arrange
+ val testIntent = Intent("TestAction")
+ val testResolveInfo =
+ ResolveInfo().apply {
+ priority = 1
+ isDefault = true
+ }
+ val equalResolveInfo =
+ ResolveInfo().apply {
+ priority = 1
+ isDefault = true
+ }
+ val diffResolveInfo =
+ ResolveInfo().apply {
+ priority = 2
+ isDefault = true
+ }
+ val testInput =
+ listOf(
+ ResolvedComponentInfo(
+ ComponentName("TestPackage", "TestClass"),
+ testIntent,
+ testResolveInfo,
+ ),
+ ResolvedComponentInfo(
+ ComponentName("EqualPackage", "EqualClass"),
+ testIntent,
+ equalResolveInfo,
+ ),
+ ResolvedComponentInfo(
+ ComponentName("DiffPackage", "DiffClass"),
+ testIntent,
+ diffResolveInfo,
+ ),
+ )
+
+ // Act
+ val result = resolvedComponentFiltering.filterLowPriority(testInput)
+
+ // Assert
+ assertThat(result).hasSize(2)
+ with(result.first()) {
+ assertThat(name.packageName).isEqualTo("TestPackage")
+ assertThat(name.className).isEqualTo("TestClass")
+ assertThat(getIntentAt(0)).isEqualTo(testIntent)
+ assertThrows(IndexOutOfBoundsException::class.java) { getIntentAt(1) }
+ assertThat(getResolveInfoAt(0)).isEqualTo(testResolveInfo)
+ assertThrows(IndexOutOfBoundsException::class.java) { getResolveInfoAt(1) }
+ }
+ with(result[1]) {
+ assertThat(name.packageName).isEqualTo("EqualPackage")
+ assertThat(name.className).isEqualTo("EqualClass")
+ assertThat(getIntentAt(0)).isEqualTo(testIntent)
+ assertThrows(IndexOutOfBoundsException::class.java) { getIntentAt(1) }
+ assertThat(getResolveInfoAt(0)).isEqualTo(equalResolveInfo)
+ assertThrows(IndexOutOfBoundsException::class.java) { getResolveInfoAt(1) }
+ }
+ }
+
+ @Test
+ fun filterLowPriority_filtersAfterFirstDifferentDefault() {
+ // Arrange
+ val testIntent = Intent("TestAction")
+ val testResolveInfo =
+ ResolveInfo().apply {
+ priority = 1
+ isDefault = true
+ }
+ val equalResolveInfo =
+ ResolveInfo().apply {
+ priority = 1
+ isDefault = true
+ }
+ val diffResolveInfo =
+ ResolveInfo().apply {
+ priority = 1
+ isDefault = false
+ }
+ val testInput =
+ listOf(
+ ResolvedComponentInfo(
+ ComponentName("TestPackage", "TestClass"),
+ testIntent,
+ testResolveInfo,
+ ),
+ ResolvedComponentInfo(
+ ComponentName("EqualPackage", "EqualClass"),
+ testIntent,
+ equalResolveInfo,
+ ),
+ ResolvedComponentInfo(
+ ComponentName("DiffPackage", "DiffClass"),
+ testIntent,
+ diffResolveInfo,
+ ),
+ )
+
+ // Act
+ val result = resolvedComponentFiltering.filterLowPriority(testInput)
+
+ // Assert
+ assertThat(result).hasSize(2)
+ with(result.first()) {
+ assertThat(name.packageName).isEqualTo("TestPackage")
+ assertThat(name.className).isEqualTo("TestClass")
+ assertThat(getIntentAt(0)).isEqualTo(testIntent)
+ assertThrows(IndexOutOfBoundsException::class.java) { getIntentAt(1) }
+ assertThat(getResolveInfoAt(0)).isEqualTo(testResolveInfo)
+ assertThrows(IndexOutOfBoundsException::class.java) { getResolveInfoAt(1) }
+ }
+ with(result[1]) {
+ assertThat(name.packageName).isEqualTo("EqualPackage")
+ assertThat(name.className).isEqualTo("EqualClass")
+ assertThat(getIntentAt(0)).isEqualTo(testIntent)
+ assertThrows(IndexOutOfBoundsException::class.java) { getIntentAt(1) }
+ assertThat(getResolveInfoAt(0)).isEqualTo(equalResolveInfo)
+ assertThrows(IndexOutOfBoundsException::class.java) { getResolveInfoAt(1) }
+ }
+ }
+
+ @Test
+ fun filterLowPriority_whenNoDifference_returnsOriginal() {
+ // Arrange
+ val testIntent = Intent("TestAction")
+ val testResolveInfo =
+ ResolveInfo().apply {
+ priority = 1
+ isDefault = true
+ }
+ val equalResolveInfo =
+ ResolveInfo().apply {
+ priority = 1
+ isDefault = true
+ }
+ val testInput =
+ listOf(
+ ResolvedComponentInfo(
+ ComponentName("TestPackage", "TestClass"),
+ testIntent,
+ testResolveInfo,
+ ),
+ ResolvedComponentInfo(
+ ComponentName("EqualPackage", "EqualClass"),
+ testIntent,
+ equalResolveInfo,
+ ),
+ )
+
+ // Act
+ val result = resolvedComponentFiltering.filterLowPriority(testInput)
+
+ // Assert
+ assertThat(result).hasSize(2)
+ with(result.first()) {
+ assertThat(name.packageName).isEqualTo("TestPackage")
+ assertThat(name.className).isEqualTo("TestClass")
+ assertThat(getIntentAt(0)).isEqualTo(testIntent)
+ assertThrows(IndexOutOfBoundsException::class.java) { getIntentAt(1) }
+ assertThat(getResolveInfoAt(0)).isEqualTo(testResolveInfo)
+ assertThrows(IndexOutOfBoundsException::class.java) { getResolveInfoAt(1) }
+ }
+ with(result[1]) {
+ assertThat(name.packageName).isEqualTo("EqualPackage")
+ assertThat(name.className).isEqualTo("EqualClass")
+ assertThat(getIntentAt(0)).isEqualTo(testIntent)
+ assertThrows(IndexOutOfBoundsException::class.java) { getIntentAt(1) }
+ assertThat(getResolveInfoAt(0)).isEqualTo(equalResolveInfo)
+ assertThrows(IndexOutOfBoundsException::class.java) { getResolveInfoAt(1) }
+ }
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSortingTest.kt b/tests/unit/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSortingTest.kt
new file mode 100644
index 0000000..39b328e
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSortingTest.kt
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ApplicationInfo
+import android.content.pm.ResolveInfo
+import android.os.UserHandle
+import com.android.intentresolver.ResolvedComponentInfo
+import com.android.intentresolver.chooser.DisplayResolveInfo
+import com.android.intentresolver.chooser.TargetInfo
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.async
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.mockito.Mockito
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class ResolvedComponentSortingTest {
+
+ private val testDispatcher = UnconfinedTestDispatcher()
+ private val testScope = TestScope(testDispatcher)
+
+ private val fakeResolverComparator = FakeResolverComparator()
+
+ private val resolvedComponentSorting =
+ ResolvedComponentSortingImpl(testDispatcher, fakeResolverComparator)
+
+ @Test
+ fun sorted_onNullList_returnsNull() =
+ testScope.runTest {
+ // Arrange
+ val testInput: List<ResolvedComponentInfo>? = null
+
+ // Act
+ val result = resolvedComponentSorting.sorted(testInput)
+ runCurrent()
+
+ // Assert
+ assertThat(result).isNull()
+ }
+
+ @Test
+ fun sorted_onEmptyList_returnsEmptyList() =
+ testScope.runTest {
+ // Arrange
+ val testInput = emptyList<ResolvedComponentInfo>()
+
+ // Act
+ val result = resolvedComponentSorting.sorted(testInput)
+ runCurrent()
+
+ // Assert
+ assertThat(result).isEmpty()
+ }
+
+ @Test
+ fun sorted_returnsListSortedByGivenComparator() =
+ testScope.runTest {
+ // Arrange
+ val testIntent = Intent("TestAction")
+ val testInput =
+ listOf(
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage3"
+ activityInfo.name = "TestClass3"
+ },
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage1"
+ activityInfo.name = "TestClass1"
+ },
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo()
+ activityInfo.packageName = "TestPackage2"
+ activityInfo.name = "TestClass2"
+ },
+ )
+ .map {
+ it.targetUserId = UserHandle.USER_CURRENT
+ ResolvedComponentInfo(
+ ComponentName(it.activityInfo.packageName, it.activityInfo.name),
+ testIntent,
+ it,
+ )
+ }
+
+ // Act
+ val result = async { resolvedComponentSorting.sorted(testInput) }
+ runCurrent()
+
+ // Assert
+ assertThat(result.await()?.map { it.name.packageName })
+ .containsExactly("TestPackage1", "TestPackage2", "TestPackage3")
+ .inOrder()
+ }
+
+ @Test
+ fun getScore_displayResolveInfo_returnsTheScoreAccordingToTheResolverComparator() {
+ // Arrange
+ val testTarget =
+ DisplayResolveInfo.newDisplayResolveInfo(
+ Intent(),
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo()
+ activityInfo.name = "TestClass"
+ activityInfo.applicationInfo = ApplicationInfo()
+ activityInfo.applicationInfo.packageName = "TestPackage"
+ },
+ Intent(),
+ )
+
+ // Act
+ val result = resolvedComponentSorting.getScore(testTarget)
+
+ // Assert
+ assertThat(result).isEqualTo(1.23f)
+ }
+
+ @Test
+ fun getScore_targetInfo_returnsTheScoreAccordingToTheResolverComparator() {
+ // Arrange
+ val mockTargetInfo = Mockito.mock(TargetInfo::class.java)
+
+ // Act
+ val result = resolvedComponentSorting.getScore(mockTargetInfo)
+
+ // Assert
+ assertThat(result).isEqualTo(1.23f)
+ }
+
+ @Test
+ fun updateModel_updatesResolverComparatorModel() =
+ testScope.runTest {
+ // Arrange
+ val mockTargetInfo = Mockito.mock(TargetInfo::class.java)
+ assertThat(fakeResolverComparator.lastUpdateModel).isNull()
+
+ // Act
+ resolvedComponentSorting.updateModel(mockTargetInfo)
+ runCurrent()
+
+ // Assert
+ assertThat(fakeResolverComparator.lastUpdateModel).isSameInstanceAs(mockTargetInfo)
+ }
+
+ @Test
+ fun updateChooserCounts_updatesResolverComparaterChooserCounts() =
+ testScope.runTest {
+ // Arrange
+ val testPackageName = "TestPackage"
+ val testUser = UserHandle(456)
+ val testAction = "TestAction"
+ assertThat(fakeResolverComparator.lastUpdateChooserCounts).isNull()
+
+ // Act
+ resolvedComponentSorting.updateChooserCounts(testPackageName, testUser, testAction)
+ runCurrent()
+
+ // Assert
+ assertThat(fakeResolverComparator.lastUpdateChooserCounts)
+ .isEqualTo(Triple(testPackageName, testUser, testAction))
+ }
+
+ @Test
+ fun destroy_destroysResolverComparator() {
+ // Arrange
+ assertThat(fakeResolverComparator.destroyCalled).isFalse()
+
+ // Act
+ resolvedComponentSorting.destroy()
+
+ // Assert
+ assertThat(fakeResolverComparator.destroyCalled).isTrue()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/listcontroller/SharedPreferencesPinnedComponentsTest.kt b/tests/unit/src/com/android/intentresolver/v2/listcontroller/SharedPreferencesPinnedComponentsTest.kt
new file mode 100644
index 0000000..9d6394f
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/listcontroller/SharedPreferencesPinnedComponentsTest.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 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.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.SharedPreferences
+import com.android.intentresolver.any
+import com.android.intentresolver.eq
+import com.android.intentresolver.whenever
+import com.google.common.truth.Truth
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.MockitoAnnotations
+
+class SharedPreferencesPinnedComponentsTest {
+
+ @Mock lateinit var mockSharedPreferences: SharedPreferences
+
+ private lateinit var sharedPreferencesPinnedComponents: SharedPreferencesPinnedComponents
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ sharedPreferencesPinnedComponents = SharedPreferencesPinnedComponents(mockSharedPreferences)
+ }
+
+ @Test
+ fun isComponentPinned_returnsSavedPinnedState() {
+ // Arrange
+ val testComponent = ComponentName("TestPackage", "TestClass")
+ val pinnedComponent = ComponentName("PinnedPackage", "PinnedClass")
+ whenever(mockSharedPreferences.getBoolean(eq(pinnedComponent.flattenToString()), any()))
+ .thenReturn(true)
+
+ // Act
+ val result = sharedPreferencesPinnedComponents.isComponentPinned(testComponent)
+ val pinnedResult = sharedPreferencesPinnedComponents.isComponentPinned(pinnedComponent)
+
+ // Assert
+ Mockito.verify(mockSharedPreferences).getBoolean(eq(testComponent.flattenToString()), any())
+ Mockito.verify(mockSharedPreferences)
+ .getBoolean(eq(pinnedComponent.flattenToString()), any())
+ Truth.assertThat(result).isFalse()
+ Truth.assertThat(pinnedResult).isTrue()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/platform/FakeSecureSettingsTest.kt b/tests/unit/src/com/android/intentresolver/v2/platform/FakeSecureSettingsTest.kt
new file mode 100644
index 0000000..04c7093
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/platform/FakeSecureSettingsTest.kt
@@ -0,0 +1,61 @@
+package com.android.intentresolver.v2.platform
+
+import com.google.common.truth.Truth.assertThat
+
+class FakeSecureSettingsTest {
+
+ private val secureSettings = fakeSecureSettings {
+ putInt(intKey, intVal)
+ putString(stringKey, stringVal)
+ putFloat(floatKey, floatVal)
+ putLong(longKey, longVal)
+ }
+
+ fun testExpectedValues_returned() {
+ assertThat(secureSettings.getInt(intKey)).isEqualTo(intVal)
+ assertThat(secureSettings.getString(stringKey)).isEqualTo(stringVal)
+ assertThat(secureSettings.getFloat(floatKey)).isEqualTo(floatVal)
+ assertThat(secureSettings.getLong(longKey)).isEqualTo(longVal)
+ }
+
+ fun testUndefinedValues_returnNull() {
+ assertThat(secureSettings.getInt("unknown")).isNull()
+ assertThat(secureSettings.getString("unknown")).isNull()
+ assertThat(secureSettings.getFloat("unknown")).isNull()
+ assertThat(secureSettings.getLong("unknown")).isNull()
+ }
+
+ /**
+ * FakeSecureSettings models the real secure settings by storing values in String form. The
+ * value is returned if/when it can be parsed from the string value, otherwise null.
+ */
+ fun testMismatchedTypes() {
+ assertThat(secureSettings.getString(intKey)).isEqualTo(intVal.toString())
+ assertThat(secureSettings.getString(floatKey)).isEqualTo(floatVal.toString())
+ assertThat(secureSettings.getString(longKey)).isEqualTo(longVal.toString())
+
+ assertThat(secureSettings.getInt(stringKey)).isNull()
+ assertThat(secureSettings.getLong(stringKey)).isNull()
+ assertThat(secureSettings.getFloat(stringKey)).isNull()
+
+ assertThat(secureSettings.getInt(longKey)).isNull()
+ assertThat(secureSettings.getFloat(longKey)).isNull() // TODO: verify Long.MAX > Float.MAX ?
+
+ assertThat(secureSettings.getLong(floatKey)).isNull() // TODO: or is Float.MAX > Long.MAX?
+ assertThat(secureSettings.getInt(floatKey)).isNull()
+ }
+
+ companion object Data {
+ const val intKey = "int"
+ const val intVal = Int.MAX_VALUE
+
+ const val stringKey = "string"
+ const val stringVal = "String"
+
+ const val floatKey = "float"
+ const val floatVal = Float.MAX_VALUE
+
+ const val longKey = "long"
+ const val longVal = Long.MAX_VALUE
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/platform/FakeUserManagerTest.kt b/tests/unit/src/com/android/intentresolver/v2/platform/FakeUserManagerTest.kt
new file mode 100644
index 0000000..a223919
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/platform/FakeUserManagerTest.kt
@@ -0,0 +1,128 @@
+package com.android.intentresolver.v2.platform
+
+import android.content.pm.UserInfo
+import android.content.pm.UserInfo.NO_PROFILE_GROUP_ID
+import android.os.UserHandle
+import android.os.UserManager
+import com.android.intentresolver.v2.platform.FakeUserManager.ProfileType
+import com.google.common.truth.Correspondence
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class FakeUserManagerTest {
+ private val userManager = FakeUserManager()
+ private val state = userManager.state
+
+ @Test
+ fun initialState() {
+ val personal = userManager.getEnabledProfiles(state.primaryUserHandle.identifier).single()
+
+ assertThat(personal.id).isEqualTo(state.primaryUserHandle.identifier)
+ assertThat(personal.userType).isEqualTo(UserManager.USER_TYPE_FULL_SYSTEM)
+ assertThat(personal.flags and UserInfo.FLAG_FULL).isEqualTo(UserInfo.FLAG_FULL)
+ }
+
+ @Test
+ fun getProfileParent() {
+ val workHandle = state.createProfile(ProfileType.WORK)
+
+ assertThat(userManager.getProfileParent(state.primaryUserHandle)).isNull()
+ assertThat(userManager.getProfileParent(workHandle)).isEqualTo(state.primaryUserHandle)
+ assertThat(userManager.getProfileParent(UserHandle.of(-1))).isNull()
+ }
+
+ @Test
+ fun getUserInfo() {
+ val personalUser =
+ requireNotNull(userManager.getUserInfo(state.primaryUserHandle.identifier)) {
+ "Expected getUserInfo to return non-null"
+ }
+ assertTrue(userInfoAreEqual.apply(personalUser, state.getPrimaryUser()))
+
+ val workHandle = state.createProfile(ProfileType.WORK)
+
+ val workUser =
+ requireNotNull(userManager.getUserInfo(workHandle.identifier)) {
+ "Expected getUserInfo to return non-null"
+ }
+ assertTrue(
+ userInfoAreEqual.apply(workUser, userManager.getUserInfo(workHandle.identifier)!!)
+ )
+ }
+
+ @Test
+ fun getEnabledProfiles_usingParentId() {
+ val personal = state.primaryUserHandle
+ val work = state.createProfile(ProfileType.WORK)
+ val private = state.createProfile(ProfileType.PRIVATE)
+
+ val enabledProfiles = userManager.getEnabledProfiles(personal.identifier)
+
+ assertWithMessage("enabledProfiles: List<UserInfo>")
+ .that(enabledProfiles)
+ .comparingElementsUsing(userInfoEquality)
+ .displayingDiffsPairedBy { it.id }
+ .containsExactly(state.getPrimaryUser(), state.getUser(work), state.getUser(private))
+ }
+
+ @Test
+ fun getEnabledProfiles_usingProfileId() {
+ val clone = state.createProfile(ProfileType.CLONE)
+
+ val enabledProfiles = userManager.getEnabledProfiles(clone.identifier)
+
+ assertWithMessage("getEnabledProfiles(clone.identifier)")
+ .that(enabledProfiles)
+ .comparingElementsUsing(userInfoEquality)
+ .displayingDiffsPairedBy { it.id }
+ .containsExactly(state.getPrimaryUser(), state.getUser(clone))
+ }
+
+ @Test
+ fun getUserOrNull() {
+ val personal = state.getPrimaryUser()
+
+ assertThat(state.getUserOrNull(personal.userHandle)).isEqualTo(personal)
+ assertThat(state.getUserOrNull(UserHandle.of(personal.id - 1))).isNull()
+ }
+
+ @Test
+ fun createProfile() {
+ // Order dependent: profile creation modifies the primary user
+ val workHandle = state.createProfile(ProfileType.WORK)
+
+ val primaryUser = state.getPrimaryUser()
+ val workUser = state.getUser(workHandle)
+
+ assertThat(primaryUser.profileGroupId).isNotEqualTo(NO_PROFILE_GROUP_ID)
+ assertThat(workUser.profileGroupId).isEqualTo(primaryUser.profileGroupId)
+ }
+
+ @Test
+ fun removeProfile() {
+ val personal = state.getPrimaryUser()
+ val work = state.createProfile(ProfileType.WORK)
+ val private = state.createProfile(ProfileType.PRIVATE)
+
+ state.removeProfile(private)
+ assertThat(state.userHandles).containsExactly(personal.userHandle, work)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun removeProfile_primaryNotAllowed() {
+ state.removeProfile(state.primaryUserHandle)
+ }
+}
+
+private val userInfoAreEqual =
+ Correspondence.BinaryPredicate<UserInfo, UserInfo> { actual, expected ->
+ actual.id == expected.id &&
+ actual.profileGroupId == expected.profileGroupId &&
+ actual.userType == expected.userType &&
+ actual.flags == expected.flags
+ }
+
+val userInfoEquality: Correspondence<UserInfo, UserInfo> =
+ Correspondence.from(userInfoAreEqual, "==")
diff --git a/tests/unit/src/com/android/intentresolver/v2/platform/NearbyShareModuleTest.kt b/tests/unit/src/com/android/intentresolver/v2/platform/NearbyShareModuleTest.kt
new file mode 100644
index 0000000..fd5c8b3
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/platform/NearbyShareModuleTest.kt
@@ -0,0 +1,83 @@
+package com.android.intentresolver.v2.platform
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.res.Configuration
+import android.provider.Settings
+import android.testing.TestableResources
+
+import androidx.test.platform.app.InstrumentationRegistry
+
+import com.android.intentresolver.R
+
+import com.google.common.truth.Truth8.assertThat
+
+import org.junit.Before
+import org.junit.Test
+
+class NearbyShareModuleTest {
+
+ lateinit var context: Context
+
+ /** Create Resources with overridden values. */
+ private fun Context.fakeResources(
+ config: Configuration? = null,
+ block: TestableResources.() -> Unit
+ ) =
+ TestableResources(resources)
+ .apply { config?.let { overrideConfiguration(it) } }
+ .apply(block)
+ .resources
+
+ @Before
+ fun setup() {
+ val instr = InstrumentationRegistry.getInstrumentation()
+ context = instr.context
+ }
+
+ @Test
+ fun valueIsAbsent_whenUnset() {
+ val secureSettings = fakeSecureSettings {}
+ val resources =
+ context.fakeResources { addOverride(R.string.config_defaultNearbySharingComponent, "") }
+
+ val componentName = NearbyShareModule.nearbyShareComponent(resources, secureSettings)
+ assertThat(componentName).isEmpty()
+ }
+
+ @Test
+ fun defaultValue_readFromResources() {
+ val secureSettings = fakeSecureSettings {}
+ val resources =
+ context.fakeResources {
+ addOverride(
+ R.string.config_defaultNearbySharingComponent,
+ "com.example/.ComponentName"
+ )
+ }
+
+ val nearbyShareComponent = NearbyShareModule.nearbyShareComponent(resources, secureSettings)
+
+ assertThat(nearbyShareComponent).hasValue(
+ ComponentName.unflattenFromString("com.example/.ComponentName"))
+ }
+
+ @Test
+ fun secureSettings_overridesDefault() {
+ val secureSettings = fakeSecureSettings {
+ putString(Settings.Secure.NEARBY_SHARING_COMPONENT, "com.example/.BComponent")
+ }
+ val resources =
+ context.fakeResources {
+ addOverride(
+ R.string.config_defaultNearbySharingComponent,
+ "com.example/.AComponent"
+ )
+ }
+
+ val nearbyShareComponent = NearbyShareModule.nearbyShareComponent(resources, secureSettings)
+
+ assertThat(nearbyShareComponent).hasValue(
+ ComponentName.unflattenFromString("com.example/.BComponent"))
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/validation/ValidationTest.kt b/tests/unit/src/com/android/intentresolver/v2/validation/ValidationTest.kt
new file mode 100644
index 0000000..43fb448
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/validation/ValidationTest.kt
@@ -0,0 +1,99 @@
+package com.android.intentresolver.v2.validation
+
+import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat
+import com.android.intentresolver.v2.validation.types.value
+import com.google.common.truth.Truth.assertThat
+import org.junit.Assert.fail
+import org.junit.Test
+
+class ValidationTest {
+
+ /** Test required values. */
+ @Test
+ fun required_valuePresent() {
+ val result: ValidationResult<String> =
+ validateFrom({ 1 }) {
+ val required: Int = required(value<Int>("key"))
+ "return value: $required"
+ }
+ assertThat(result).value().isEqualTo("return value: 1")
+ assertThat(result).findings().isEmpty()
+ }
+
+ /** Test reporting of absent required values. */
+ @Test
+ fun required_valueAbsent() {
+ val result: ValidationResult<String> =
+ validateFrom({ null }) {
+ required(value<Int>("key"))
+ fail("'required' should have thrown an exception")
+ "return value"
+ }
+ assertThat(result).isFailure()
+ assertThat(result).findings().containsExactly(
+ RequiredValueMissing("key", Int::class))
+ }
+
+ /** Test optional values are ignored when absent. */
+ @Test
+ fun optional_valuePresent() {
+ val result: ValidationResult<String> =
+ validateFrom({ 1 }) {
+ val optional: Int? = optional(value<Int>("key"))
+ "return value: $optional"
+ }
+ assertThat(result).value().isEqualTo("return value: 1")
+ assertThat(result).findings().isEmpty()
+ }
+
+ /** Test optional values are ignored when absent. */
+ @Test
+ fun optional_valueAbsent() {
+ val result: ValidationResult<String?> =
+ validateFrom({ null }) {
+ val optional: String? = optional(value<String>("key"))
+ "return value: $optional"
+ }
+ assertThat(result).isSuccess()
+ assertThat(result).findings().isEmpty()
+ }
+
+ /** Test reporting of ignored values. */
+ @Test
+ fun ignored_valuePresent() {
+ val result: ValidationResult<String> =
+ validateFrom(mapOf("key" to 1)::get) {
+ ignored(value<Int>("key"), "no longer supported")
+ "result value"
+ }
+ assertThat(result).value().isEqualTo("result value")
+ assertThat(result)
+ .findings()
+ .containsExactly(IgnoredValue("key", "no longer supported"))
+ }
+
+ /** Test reporting of ignored values. */
+ @Test
+ fun ignored_valueAbsent() {
+ val result: ValidationResult<String> =
+ validateFrom({ null }) {
+ ignored(value<Int>("key"), "ignored when option foo is set")
+ "result value"
+ }
+ assertThat(result).value().isEqualTo("result value")
+ assertThat(result).findings().isEmpty()
+ }
+
+ /** Test handling of exceptions in the validation function. */
+ @Test
+ fun thrown_exception() {
+ val result: ValidationResult<String> =
+ validateFrom({ null }) {
+ error("something")
+ }
+ assertThat(result).isFailure()
+ val findingTypes = result.findings.map { it::class }
+ assertThat(findingTypes.first()).isEqualTo(UncaughtException::class)
+ }
+
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/validation/types/IntentOrUriTest.kt b/tests/unit/src/com/android/intentresolver/v2/validation/types/IntentOrUriTest.kt
new file mode 100644
index 0000000..ad23048
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/validation/types/IntentOrUriTest.kt
@@ -0,0 +1,107 @@
+package com.android.intentresolver.v2.validation.types
+
+import android.content.Intent
+import android.content.Intent.URI_INTENT_SCHEME
+import android.net.Uri
+import androidx.core.net.toUri
+import androidx.test.ext.truth.content.IntentSubject.assertThat
+import com.android.intentresolver.v2.validation.Importance.CRITICAL
+import com.android.intentresolver.v2.validation.Importance.WARNING
+import com.android.intentresolver.v2.validation.RequiredValueMissing
+import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat
+import com.android.intentresolver.v2.validation.ValueIsWrongType
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class IntentOrUriTest {
+
+ /** Test for validation success when the value is an Intent. */
+ @Test
+ fun intent() {
+ val keyValidator = IntentOrUri("key")
+ val values = mapOf("key" to Intent("GO"))
+
+ val result = keyValidator.validate(values::get, CRITICAL)
+ assertThat(result).findings().isEmpty()
+ assertThat(result.value).hasAction("GO")
+ }
+
+ /** Test for validation success when the value is a Uri. */
+ @Test
+ fun uri() {
+ val keyValidator = IntentOrUri("key")
+ val values = mapOf("key" to Intent("GO").toUri(URI_INTENT_SCHEME).toUri())
+
+ val result = keyValidator.validate(values::get, CRITICAL)
+ assertThat(result).findings().isEmpty()
+ assertThat(result.value).hasAction("GO")
+ }
+
+ /** Test the failure result when the value is missing. */
+ @Test
+ fun missing() {
+ val keyValidator = IntentOrUri("key")
+
+ val result = keyValidator.validate({ null }, CRITICAL)
+
+ assertThat(result).value().isNull()
+ assertThat(result).findings().containsExactly(RequiredValueMissing("key", Intent::class))
+ }
+
+ /** Check validation passes when value is null and importance is [WARNING] (optional). */
+ @Test
+ fun optional() {
+ val keyValidator = ParceledArray("key", Intent::class)
+
+ val result = keyValidator.validate(source = { null }, WARNING)
+
+ assertThat(result).findings().isEmpty()
+ assertThat(result.value).isNull()
+ }
+
+ /**
+ * Test for failure result when the value is neither Intent nor Uri, with importance CRITICAL.
+ */
+ @Test
+ fun wrongType_required() {
+ val keyValidator = IntentOrUri("key")
+ val values = mapOf("key" to 1)
+
+ val result = keyValidator.validate(values::get, CRITICAL)
+
+ assertThat(result).value().isNull()
+ assertThat(result)
+ .findings()
+ .containsExactly(
+ ValueIsWrongType(
+ "key",
+ importance = CRITICAL,
+ actualType = Int::class,
+ allowedTypes = listOf(Intent::class, Uri::class)
+ )
+ )
+ }
+
+ /**
+ * Test for warnings when the value is neither Intent nor Uri, with importance WARNING.
+ */
+ @Test
+ fun wrongType_optional() {
+ val keyValidator = IntentOrUri("key")
+ val values = mapOf("key" to 1)
+
+ val result = keyValidator.validate(values::get, WARNING)
+
+ assertThat(result).value().isNull()
+ assertThat(result)
+ .findings()
+ .containsExactly(
+ ValueIsWrongType(
+ "key",
+ importance = WARNING,
+ actualType = Int::class,
+ allowedTypes = listOf(Intent::class, Uri::class)
+ )
+ )
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/validation/types/ParceledArrayTest.kt b/tests/unit/src/com/android/intentresolver/v2/validation/types/ParceledArrayTest.kt
new file mode 100644
index 0000000..d4dca01
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/validation/types/ParceledArrayTest.kt
@@ -0,0 +1,93 @@
+package com.android.intentresolver.v2.validation.types
+
+import android.content.Intent
+import android.graphics.Point
+import com.android.intentresolver.v2.validation.Importance.CRITICAL
+import com.android.intentresolver.v2.validation.Importance.WARNING
+import com.android.intentresolver.v2.validation.RequiredValueMissing
+import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat
+import com.android.intentresolver.v2.validation.ValueIsWrongType
+import com.android.intentresolver.v2.validation.WrongElementType
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class ParceledArrayTest {
+
+ /** Check that a array is handled correctly when valid. */
+ @Test
+ fun valid() {
+ val keyValidator = ParceledArray("key", elementType = String::class)
+ val values = mapOf("key" to arrayOf("String"))
+
+ val result = keyValidator.validate(values::get, CRITICAL)
+
+ assertThat(result).findings().isEmpty()
+ assertThat(result.value).containsExactly("String")
+ }
+
+ /** Check correct failure result when an array has the wrong element type. */
+ @Test
+ fun wrongElementType() {
+ val keyValidator = ParceledArray("key", elementType = Intent::class)
+ val values = mapOf("key" to arrayOf(Point()))
+
+ val result = keyValidator.validate(values::get, CRITICAL)
+
+ assertThat(result).value().isNull()
+ assertThat(result)
+ .findings()
+ .containsExactly(
+ // TODO: report with a new class `WrongElementType` to improve clarity
+ WrongElementType(
+ "key",
+ importance = CRITICAL,
+ container = Array::class,
+ actualType = Point::class,
+ expectedType = Intent::class
+ )
+ )
+ }
+
+ /** Check correct failure result when an array value is missing. */
+ @Test
+ fun missing() {
+ val keyValidator = ParceledArray("key", Intent::class)
+
+ val result = keyValidator.validate(source = { null }, CRITICAL)
+
+ assertThat(result).value().isNull()
+ assertThat(result).findings().containsExactly(RequiredValueMissing("key", Intent::class))
+ }
+
+ /** Check validation passes when value is null and importance is [WARNING] (optional). */
+ @Test
+ fun optional() {
+ val keyValidator = ParceledArray("key", Intent::class)
+
+ val result = keyValidator.validate(source = { null }, WARNING)
+
+ assertThat(result).findings().isEmpty()
+ assertThat(result.value).isNull()
+ }
+
+ /** Check correct failure result when the array value itself is the wrong type. */
+ @Test
+ fun wrongType() {
+ val keyValidator = ParceledArray("key", Intent::class)
+ val values = mapOf("key" to 1)
+
+ val result = keyValidator.validate(values::get, CRITICAL)
+
+ assertThat(result).value().isNull()
+ assertThat(result)
+ .findings()
+ .containsExactly(
+ ValueIsWrongType(
+ "key",
+ importance = CRITICAL,
+ actualType = Int::class,
+ allowedTypes = listOf(Intent::class)
+ )
+ )
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/validation/types/SimpleValueTest.kt b/tests/unit/src/com/android/intentresolver/v2/validation/types/SimpleValueTest.kt
new file mode 100644
index 0000000..13bb4b3
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/v2/validation/types/SimpleValueTest.kt
@@ -0,0 +1,52 @@
+package com.android.intentresolver.v2.validation.types
+
+import com.android.intentresolver.v2.validation.Importance.CRITICAL
+import com.android.intentresolver.v2.validation.RequiredValueMissing
+import com.android.intentresolver.v2.validation.ValidationResultSubject.Companion.assertThat
+import com.android.intentresolver.v2.validation.ValueIsWrongType
+import org.junit.Test
+
+class SimpleValueTest {
+
+ /** Test for validation success when the value is present and the correct type. */
+ @Test
+ fun present() {
+ val keyValidator = SimpleValue("key", expected = Double::class)
+ val values = mapOf("key" to Math.PI)
+
+ val result = keyValidator.validate(values::get, CRITICAL)
+ assertThat(result).findings().isEmpty()
+ assertThat(result).value().isEqualTo(Math.PI)
+ }
+
+ /** Test for validation success when the value is present and the correct type. */
+ @Test
+ fun wrongType() {
+ val keyValidator = SimpleValue("key", expected = Double::class)
+ val values = mapOf("key" to "Apple Pie")
+
+ val result = keyValidator.validate(values::get, CRITICAL)
+ assertThat(result).value().isNull()
+ assertThat(result)
+ .findings()
+ .containsExactly(
+ ValueIsWrongType(
+ "key",
+ importance = CRITICAL,
+ actualType = String::class,
+ allowedTypes = listOf(Double::class)
+ )
+ )
+ }
+
+ /** Test the failure result when the value is missing. */
+ @Test
+ fun missing() {
+ val keyValidator = SimpleValue("key", expected = Double::class)
+
+ val result = keyValidator.validate(source = { null }, CRITICAL)
+
+ assertThat(result).value().isNull()
+ assertThat(result).findings().containsExactly(RequiredValueMissing("key", Double::class))
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt b/tests/unit/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt
index 4f4223c..4f4223c 100644
--- a/java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt
+++ b/tests/unit/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt