summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp15
-rw-r--r--AndroidManifest-app.xml52
-rw-r--r--AndroidManifest-lib.xml2
-rw-r--r--TEST_MAPPING6
-rw-r--r--aconfig/FeatureFlags.aconfig46
-rw-r--r--java/res/drawable/checkbox.xml10
-rw-r--r--java/res/drawable/ic_play_circle_filled_24px.xml3
-rw-r--r--java/res/drawable/resolver_profile_tab_bg.xml2
-rw-r--r--java/res/layout/chooser_action_view.xml1
-rw-r--r--java/res/layout/chooser_grid.xml97
-rw-r--r--java/res/layout/chooser_grid_item.xml70
-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_text.xml9
-rw-r--r--java/res/layout/chooser_headline_row.xml23
-rw-r--r--java/res/layout/chooser_list_per_profile.xml34
-rw-r--r--java/res/layout/chooser_list_per_profile_wrap.xml1
-rw-r--r--java/res/values-af/strings.xml4
-rw-r--r--java/res/values-am/strings.xml5
-rw-r--r--java/res/values-ar/strings.xml8
-rw-r--r--java/res/values-as/strings.xml5
-rw-r--r--java/res/values-az/strings.xml4
-rw-r--r--java/res/values-b+sr+Latn/strings.xml7
-rw-r--r--java/res/values-be/strings.xml5
-rw-r--r--java/res/values-bg/strings.xml4
-rw-r--r--java/res/values-bn/strings.xml5
-rw-r--r--java/res/values-bs/strings.xml5
-rw-r--r--java/res/values-ca/strings.xml7
-rw-r--r--java/res/values-cs/strings.xml5
-rw-r--r--java/res/values-da/strings.xml5
-rw-r--r--java/res/values-de/strings.xml5
-rw-r--r--java/res/values-el/strings.xml5
-rw-r--r--java/res/values-en-rAU/strings.xml4
-rw-r--r--java/res/values-en-rCA/strings.xml4
-rw-r--r--java/res/values-en-rGB/strings.xml4
-rw-r--r--java/res/values-en-rIN/strings.xml4
-rw-r--r--java/res/values-en-rXC/strings.xml4
-rw-r--r--java/res/values-es-rUS/strings.xml5
-rw-r--r--java/res/values-es/strings.xml9
-rw-r--r--java/res/values-et/strings.xml4
-rw-r--r--java/res/values-eu/strings.xml5
-rw-r--r--java/res/values-fa/strings.xml7
-rw-r--r--java/res/values-fi/strings.xml5
-rw-r--r--java/res/values-fr-rCA/strings.xml4
-rw-r--r--java/res/values-fr/strings.xml7
-rw-r--r--java/res/values-gl/strings.xml5
-rw-r--r--java/res/values-gu/strings.xml4
-rw-r--r--java/res/values-h480dp/dimens.xml2
-rw-r--r--java/res/values-h480dp/integers.xml2
-rw-r--r--java/res/values-hi/strings.xml4
-rw-r--r--java/res/values-hr/strings.xml7
-rw-r--r--java/res/values-hu/strings.xml4
-rw-r--r--java/res/values-hy/strings.xml5
-rw-r--r--java/res/values-in/strings.xml6
-rw-r--r--java/res/values-is/strings.xml5
-rw-r--r--java/res/values-it/strings.xml5
-rw-r--r--java/res/values-iw/strings.xml7
-rw-r--r--java/res/values-ja/strings.xml6
-rw-r--r--java/res/values-ka/strings.xml4
-rw-r--r--java/res/values-kk/strings.xml5
-rw-r--r--java/res/values-km/strings.xml4
-rw-r--r--java/res/values-kn/strings.xml4
-rw-r--r--java/res/values-ko/strings.xml5
-rw-r--r--java/res/values-ky/strings.xml5
-rw-r--r--java/res/values-lo/strings.xml4
-rw-r--r--java/res/values-lt/strings.xml4
-rw-r--r--java/res/values-lv/strings.xml5
-rw-r--r--java/res/values-mk/strings.xml5
-rw-r--r--java/res/values-ml/strings.xml4
-rw-r--r--java/res/values-mn/strings.xml5
-rw-r--r--java/res/values-mr/strings.xml4
-rw-r--r--java/res/values-ms/strings.xml4
-rw-r--r--java/res/values-my/strings.xml5
-rw-r--r--java/res/values-nb/strings.xml7
-rw-r--r--java/res/values-ne/strings.xml4
-rw-r--r--java/res/values-nl/strings.xml6
-rw-r--r--java/res/values-or/strings.xml5
-rw-r--r--java/res/values-pa/strings.xml5
-rw-r--r--java/res/values-pl/strings.xml6
-rw-r--r--java/res/values-pt-rBR/strings.xml7
-rw-r--r--java/res/values-pt-rPT/strings.xml5
-rw-r--r--java/res/values-pt/strings.xml7
-rw-r--r--java/res/values-ro/strings.xml5
-rw-r--r--java/res/values-ru/strings.xml5
-rw-r--r--java/res/values-si/strings.xml4
-rw-r--r--java/res/values-sk/strings.xml8
-rw-r--r--java/res/values-sl/strings.xml4
-rw-r--r--java/res/values-sq/strings.xml5
-rw-r--r--java/res/values-sr/strings.xml7
-rw-r--r--java/res/values-sv/strings.xml5
-rw-r--r--java/res/values-sw/strings.xml9
-rw-r--r--java/res/values-ta/strings.xml5
-rw-r--r--java/res/values-te/strings.xml8
-rw-r--r--java/res/values-th/strings.xml5
-rw-r--r--java/res/values-tl/strings.xml9
-rw-r--r--java/res/values-tr/strings.xml5
-rw-r--r--java/res/values-uk/strings.xml5
-rw-r--r--java/res/values-ur/strings.xml4
-rw-r--r--java/res/values-uz/strings.xml5
-rw-r--r--java/res/values-vi/strings.xml5
-rw-r--r--java/res/values-zh-rCN/strings.xml5
-rw-r--r--java/res/values-zh-rHK/strings.xml5
-rw-r--r--java/res/values-zh-rTW/strings.xml5
-rw-r--r--java/res/values-zu/strings.xml4
-rw-r--r--java/res/values/dimens.xml3
-rw-r--r--java/res/values/integers.xml2
-rw-r--r--java/res/values/strings.xml14
-rw-r--r--java/res/values/styles.xml2
-rw-r--r--java/src/com/android/intentresolver/AnnotatedUserHandles.java217
-rw-r--r--java/src/com/android/intentresolver/ChooserActionFactory.java121
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java1927
-rw-r--r--java/src/com/android/intentresolver/ChooserHelper.kt190
-rw-r--r--java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java85
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java103
-rw-r--r--java/src/com/android/intentresolver/ChooserListController.java65
-rw-r--r--java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java214
-rw-r--r--java/src/com/android/intentresolver/ChooserRefinementManager.java118
-rw-r--r--java/src/com/android/intentresolver/ChooserRequestParameters.java26
-rw-r--r--java/src/com/android/intentresolver/ChooserSelector.kt (renamed from java/src/com/android/intentresolver/v2/ChooserSelector.kt)16
-rw-r--r--java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java2
-rw-r--r--java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java2
-rw-r--r--java/src/com/android/intentresolver/ContentTypeHint.kt25
-rw-r--r--java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt35
-rw-r--r--java/src/com/android/intentresolver/IntentForwarderActivity.java9
-rw-r--r--java/src/com/android/intentresolver/IntentForwarding.kt111
-rw-r--r--java/src/com/android/intentresolver/ItemRevealAnimationTracker.kt4
-rw-r--r--java/src/com/android/intentresolver/JavaFlowHelper.kt30
-rw-r--r--java/src/com/android/intentresolver/MultiProfilePagerAdapter.java583
-rw-r--r--java/src/com/android/intentresolver/PackagesChangedListener.kt22
-rw-r--r--java/src/com/android/intentresolver/ProfileAvailability.kt103
-rw-r--r--java/src/com/android/intentresolver/ProfileHelper.kt97
-rw-r--r--java/src/com/android/intentresolver/ResolverActivity.java1603
-rw-r--r--java/src/com/android/intentresolver/ResolverHelper.kt129
-rw-r--r--java/src/com/android/intentresolver/ResolverListAdapter.java57
-rw-r--r--java/src/com/android/intentresolver/ResolverViewPager.java8
-rw-r--r--java/src/com/android/intentresolver/SecureSettings.kt4
-rw-r--r--java/src/com/android/intentresolver/ShortcutSelectionLogic.java17
-rw-r--r--java/src/com/android/intentresolver/SimpleIconFactory.java1
-rw-r--r--java/src/com/android/intentresolver/StartsSelectedItem.kt21
-rw-r--r--java/src/com/android/intentresolver/annotation/JavaInterop.kt28
-rw-r--r--java/src/com/android/intentresolver/chooser/DisplayResolveInfoAzInfoComparator.java44
-rw-r--r--java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java15
-rw-r--r--java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt15
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java84
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java4
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java42
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java20
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java22
-rw-r--r--java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt6
-rw-r--r--java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt22
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ImageLoaderModule.kt44
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt34
-rw-r--r--java/src/com/android/intentresolver/contentpreview/IsHttpUri.kt11
-rw-r--r--java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt2
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt82
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt85
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt106
-rw-r--r--java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java28
-rw-r--r--java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java25
-rw-r--r--java/src/com/android/intentresolver/contentpreview/UriMetadataHelpers.kt116
-rw-r--r--java/src/com/android/intentresolver/contentpreview/UriMetadataReader.kt87
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/CustomActionModel.kt29
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/ActivityResultRepository.kt28
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/CursorPreviewsRepository.kt32
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PendingSelectionCallbackRepository.kt32
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt28
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/CursorResolver.kt24
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/PayloadToggleCursorResolver.kt69
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/CustomActionPendingIntentSender.kt64
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/InitialCustomActionsModule.kt55
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/PendingIntentSender.kt24
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifier.kt92
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ChooserRequestInteractor.kt41
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractor.kt294
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractor.kt66
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt65
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ProcessTargetIntentUpdatesInteractor.kt42
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt42
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractor.kt40
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt54
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SetCursorPreviewsInteractor.kt59
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractor.kt63
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt37
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ActionModel.kt31
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/LoadDirection.kt23
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/LoadedWindow.kt102
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ShareouselUpdate.kt34
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ValueUpdate.kt37
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallback.kt173
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewModel.kt29
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewsModel.kt35
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ComposeIconComposable.kt61
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt102
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt210
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ActionChipViewModel.kt29
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt39
-rw-r--r--java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt146
-rw-r--r--java/src/com/android/intentresolver/data/BroadcastSubscriber.kt73
-rw-r--r--java/src/com/android/intentresolver/data/model/ChooserRequest.kt195
-rw-r--r--java/src/com/android/intentresolver/data/repository/ChooserRequestRepository.kt39
-rw-r--r--java/src/com/android/intentresolver/data/repository/DevicePolicyResources.kt143
-rw-r--r--java/src/com/android/intentresolver/data/repository/UserInfoExt.kt45
-rw-r--r--java/src/com/android/intentresolver/data/repository/UserRepository.kt329
-rw-r--r--java/src/com/android/intentresolver/data/repository/UserRepositoryModule.kt53
-rw-r--r--java/src/com/android/intentresolver/data/repository/UserScopedService.kt67
-rw-r--r--java/src/com/android/intentresolver/domain/interactor/UserInteractor.kt92
-rw-r--r--java/src/com/android/intentresolver/emptystate/DevicePolicyBlockerEmptyState.java85
-rw-r--r--java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java117
-rw-r--r--java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyState.java55
-rw-r--r--java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java132
-rw-r--r--java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java130
-rw-r--r--java/src/com/android/intentresolver/emptystate/WorkProfileOffEmptyState.java57
-rw-r--r--java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java78
-rw-r--r--java/src/com/android/intentresolver/ext/CreationExtrasExt.kt34
-rw-r--r--java/src/com/android/intentresolver/ext/IntentExt.kt45
-rw-r--r--java/src/com/android/intentresolver/ext/ParcelExt.kt27
-rw-r--r--java/src/com/android/intentresolver/grid/ChooserGridAdapter.java104
-rw-r--r--java/src/com/android/intentresolver/icon/ComposeIcon.kt88
-rw-r--r--java/src/com/android/intentresolver/icons/TargetDataLoaderModule.kt (renamed from java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt)4
-rw-r--r--java/src/com/android/intentresolver/inject/ActivityModelModule.kt127
-rw-r--r--java/src/com/android/intentresolver/inject/ConcurrencyModule.kt29
-rw-r--r--java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt32
-rw-r--r--java/src/com/android/intentresolver/inject/FrameworkModule.kt76
-rw-r--r--java/src/com/android/intentresolver/inject/Qualifiers.kt7
-rw-r--r--java/src/com/android/intentresolver/inject/SingletonModule.kt16
-rw-r--r--java/src/com/android/intentresolver/inject/SystemServices.kt136
-rw-r--r--java/src/com/android/intentresolver/inject/ViewModelCoroutineScopeModule.kt42
-rw-r--r--java/src/com/android/intentresolver/logging/EventLogImpl.java4
-rw-r--r--java/src/com/android/intentresolver/logging/EventLogModule.kt8
-rw-r--r--java/src/com/android/intentresolver/model/AbstractResolverComparator.java35
-rw-r--r--java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java32
-rw-r--r--java/src/com/android/intentresolver/model/ResolveInfoAzInfoComparator.java44
-rw-r--r--java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java43
-rw-r--r--java/src/com/android/intentresolver/platform/AppPredictionModule.kt42
-rw-r--r--java/src/com/android/intentresolver/platform/ImageEditorModule.kt (renamed from java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt)18
-rw-r--r--java/src/com/android/intentresolver/platform/NearbyShareModule.kt (renamed from java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt)18
-rw-r--r--java/src/com/android/intentresolver/platform/PlatformSecureSettings.kt (renamed from java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt)18
-rw-r--r--java/src/com/android/intentresolver/platform/SecureSettings.kt (renamed from java/src/com/android/intentresolver/v2/platform/SecureSettings.kt)18
-rw-r--r--java/src/com/android/intentresolver/platform/SecureSettingsModule.kt30
-rw-r--r--java/src/com/android/intentresolver/profiles/AdapterBinder.java31
-rw-r--r--java/src/com/android/intentresolver/profiles/ChooserMultiProfilePagerAdapter.java (renamed from java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java)77
-rw-r--r--java/src/com/android/intentresolver/profiles/MultiProfilePagerAdapter.java (renamed from java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java)527
-rw-r--r--java/src/com/android/intentresolver/profiles/OnProfileSelectedListener.java46
-rw-r--r--java/src/com/android/intentresolver/profiles/OnSwitchOnWorkSelectedListener.java27
-rw-r--r--java/src/com/android/intentresolver/profiles/ProfileDescriptor.java82
-rw-r--r--java/src/com/android/intentresolver/profiles/ResolverMultiProfilePagerAdapter.java (renamed from java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java)50
-rw-r--r--java/src/com/android/intentresolver/profiles/TabConfig.java38
-rw-r--r--java/src/com/android/intentresolver/shared/model/Profile.kt52
-rw-r--r--java/src/com/android/intentresolver/shared/model/User.kt (renamed from java/src/com/android/intentresolver/v2/data/model/User.kt)42
-rw-r--r--java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt42
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt5
-rw-r--r--java/src/com/android/intentresolver/ui/ActionTitle.java (renamed from java/src/com/android/intentresolver/v2/ui/ActionTitle.java)3
-rw-r--r--java/src/com/android/intentresolver/ui/ProfilePagerResources.kt61
-rw-r--r--java/src/com/android/intentresolver/ui/ShareResultSender.kt163
-rw-r--r--java/src/com/android/intentresolver/ui/ShortcutPolicyModule.kt94
-rw-r--r--java/src/com/android/intentresolver/ui/model/ActivityModel.kt81
-rw-r--r--java/src/com/android/intentresolver/ui/model/ResolverRequest.kt68
-rw-r--r--java/src/com/android/intentresolver/ui/model/ShareAction.kt23
-rw-r--r--java/src/com/android/intentresolver/ui/viewmodel/ChooserRequestReader.kt198
-rw-r--r--java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt94
-rw-r--r--java/src/com/android/intentresolver/ui/viewmodel/ResolverRequestReader.kt59
-rw-r--r--java/src/com/android/intentresolver/ui/viewmodel/ResolverViewModel.kt70
-rw-r--r--java/src/com/android/intentresolver/util/CancellationSignalUtils.kt41
-rw-r--r--java/src/com/android/intentresolver/util/Flow.kt10
-rw-r--r--java/src/com/android/intentresolver/util/ParallelIteration.kt50
-rw-r--r--java/src/com/android/intentresolver/util/SyncUtils.kt33
-rw-r--r--java/src/com/android/intentresolver/util/cursor/CursorView.kt59
-rw-r--r--java/src/com/android/intentresolver/util/cursor/Cursors.kt87
-rw-r--r--java/src/com/android/intentresolver/util/cursor/PagedCursor.kt52
-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/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/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/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/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/SecureSettingsModule.kt14
-rw-r--r--java/src/com/android/intentresolver/v2/util/MutableLazy.kt36
-rw-r--r--java/src/com/android/intentresolver/v2/validation/ValidationResult.kt39
-rw-r--r--java/src/com/android/intentresolver/v2/validation/types/Validators.kt45
-rw-r--r--java/src/com/android/intentresolver/validation/Findings.kt (renamed from java/src/com/android/intentresolver/v2/validation/Findings.kt)23
-rw-r--r--java/src/com/android/intentresolver/validation/Validation.kt (renamed from java/src/com/android/intentresolver/v2/validation/Validation.kt)24
-rw-r--r--java/src/com/android/intentresolver/validation/ValidationResult.kt26
-rw-r--r--java/src/com/android/intentresolver/validation/types/IntentOrUri.kt (renamed from java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt)25
-rw-r--r--java/src/com/android/intentresolver/validation/types/ParceledArray.kt (renamed from java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt)31
-rw-r--r--java/src/com/android/intentresolver/validation/types/SimpleValue.kt (renamed from java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt)36
-rw-r--r--java/src/com/android/intentresolver/validation/types/Validators.kt (renamed from java/src/com/android/intentresolver/v2/listcontroller/ListController.kt)13
-rw-r--r--java/src/com/android/intentresolver/widget/ActionRow.kt4
-rw-r--r--java/src/com/android/intentresolver/widget/BadgeTextView.kt104
-rw-r--r--java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt16
-rw-r--r--java/src/com/android/intentresolver/widget/ImagePreviewView.kt13
-rw-r--r--java/src/com/android/intentresolver/widget/RecyclerViewExtensions.kt8
-rw-r--r--java/src/com/android/intentresolver/widget/ViewExtensions.kt27
-rw-r--r--lint-baseline.xml2425
-rw-r--r--tests/activity/Android.bp2
-rw-r--r--tests/activity/AndroidManifest.xml4
-rw-r--r--tests/activity/AndroidTest.xml1
-rw-r--r--tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java53
-rw-r--r--tests/activity/src/com/android/intentresolver/ChooserActivityTest.java (renamed from tests/activity/src/com/android/intentresolver/UnbundledChooserActivityTest.java)288
-rw-r--r--tests/activity/src/com/android/intentresolver/ChooserActivityWorkProfileTest.java (renamed from tests/activity/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java)64
-rw-r--r--tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java88
-rw-r--r--tests/activity/src/com/android/intentresolver/ResolverActivityTest.java84
-rw-r--r--tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java92
-rw-r--r--tests/activity/src/com/android/intentresolver/ext/RecyclerViewExt.kt28
-rw-r--r--tests/activity/src/com/android/intentresolver/logging/TestEventLogModule.kt8
-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.bp3
-rw-r--r--tests/shared/Android.bp4
-rw-r--r--tests/shared/src/com/android/intentresolver/CoroutinesKosmos.kt22
-rw-r--r--tests/shared/src/com/android/intentresolver/FakeImageLoader.kt (renamed from tests/shared/src/com/android/intentresolver/TestPreviewImageLoader.kt)8
-rw-r--r--tests/shared/src/com/android/intentresolver/FrameworkMocksKosmos.kt25
-rw-r--r--tests/shared/src/com/android/intentresolver/MockitoKotlinHelpers.kt207
-rw-r--r--tests/shared/src/com/android/intentresolver/TestContentPreviewViewModel.kt27
-rw-r--r--tests/shared/src/com/android/intentresolver/contentpreview/MimetypeClassifierKosmos.kt21
-rw-r--r--tests/shared/src/com/android/intentresolver/contentpreview/UriMetadataReaderKosmos.kt28
-rw-r--r--tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PayloadToggleRepoKosmos.kt25
-rw-r--r--tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/CursorResolverKosmos.kt33
-rw-r--r--tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/PendingIntentSenderKosmos.kt22
-rw-r--r--tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifierKosmos.kt22
-rw-r--r--tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/PayloadToggleInteractorKosmos.kt120
-rw-r--r--tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallbackKosmos.kt35
-rw-r--r--tests/shared/src/com/android/intentresolver/data/repository/FakeUserRepository.kt65
-rw-r--r--tests/shared/src/com/android/intentresolver/data/repository/V2RepositoryKosmos.kt29
-rw-r--r--tests/shared/src/com/android/intentresolver/ext/ParcelableExt.kt45
-rw-r--r--tests/shared/src/com/android/intentresolver/inject/ActivityModelKosmos.kt26
-rw-r--r--tests/shared/src/com/android/intentresolver/inject/ChooserServiceFlagsKosmos.kt24
-rw-r--r--tests/shared/src/com/android/intentresolver/logging/EventLogKosmos.kt23
-rw-r--r--tests/shared/src/com/android/intentresolver/platform/FakeSecureSettings.kt (renamed from tests/shared/src/com/android/intentresolver/v2/platform/FakeSecureSettings.kt)18
-rw-r--r--tests/shared/src/com/android/intentresolver/platform/FakeUserManager.kt (renamed from tests/shared/src/com/android/intentresolver/v2/platform/FakeUserManager.kt)82
-rw-r--r--tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt22
-rw-r--r--tests/unit/Android.bp4
-rw-r--r--tests/unit/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt79
-rw-r--r--tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt153
-rw-r--r--tests/unit/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt71
-rw-r--r--tests/unit/src/com/android/intentresolver/ChooserListAdapterDataTest.kt20
-rw-r--r--tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt18
-rw-r--r--tests/unit/src/com/android/intentresolver/ChooserRefinementManagerTest.kt20
-rw-r--r--tests/unit/src/com/android/intentresolver/ChooserRequestParametersTest.kt87
-rw-r--r--tests/unit/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt31
-rw-r--r--tests/unit/src/com/android/intentresolver/FakeResolverListCommunicator.kt6
-rw-r--r--tests/unit/src/com/android/intentresolver/MultiProfilePagerAdapterTest.kt277
-rw-r--r--tests/unit/src/com/android/intentresolver/ProfileAvailabilityTest.kt74
-rw-r--r--tests/unit/src/com/android/intentresolver/ProfileHelperTest.kt275
-rw-r--r--tests/unit/src/com/android/intentresolver/ResolverListAdapterTest.kt527
-rw-r--r--tests/unit/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt227
-rw-r--r--tests/unit/src/com/android/intentresolver/TestHelpers.kt23
-rw-r--r--tests/unit/src/com/android/intentresolver/chooser/TargetInfoTest.kt430
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt96
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/FileContentPreviewUiTest.kt40
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt296
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt55
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt476
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt137
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/TextContentPreviewUiTest.kt60
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt237
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/UriMetadataReaderTest.kt100
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifierImplTest.kt80
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt282
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractorTest.kt120
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt316
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractorTest.kt91
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractorTest.kt139
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SetCursorPreviewsInteractorTest.kt101
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractorTest.kt48
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallbackImplTest.kt462
-rw-r--r--tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt244
-rw-r--r--tests/unit/src/com/android/intentresolver/coroutines/Flow.kt (renamed from tests/unit/src/com/android/intentresolver/v2/coroutines/Flow.kt)2
-rw-r--r--tests/unit/src/com/android/intentresolver/data/repository/FakeUserRepositoryTest.kt108
-rw-r--r--tests/unit/src/com/android/intentresolver/data/repository/UserRepositoryImplTest.kt (renamed from tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt)150
-rw-r--r--tests/unit/src/com/android/intentresolver/domain/interactor/UserInteractorTest.kt206
-rw-r--r--tests/unit/src/com/android/intentresolver/emptystate/CrossProfileIntentsCheckerTest.kt34
-rw-r--r--tests/unit/src/com/android/intentresolver/emptystate/EmptyStateUiHelperTest.kt140
-rw-r--r--tests/unit/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProviderTest.kt156
-rw-r--r--tests/unit/src/com/android/intentresolver/ext/CreationExtrasExtTest.kt54
-rw-r--r--tests/unit/src/com/android/intentresolver/ext/IntentExtTest.kt85
-rw-r--r--tests/unit/src/com/android/intentresolver/logging/EventLogImplTest.java2
-rw-r--r--tests/unit/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java10
-rw-r--r--tests/unit/src/com/android/intentresolver/platform/FakeSecureSettingsTest.kt (renamed from tests/unit/src/com/android/intentresolver/v2/platform/FakeSecureSettingsTest.kt)18
-rw-r--r--tests/unit/src/com/android/intentresolver/platform/FakeUserManagerTest.kt (renamed from tests/unit/src/com/android/intentresolver/v2/platform/FakeUserManagerTest.kt)20
-rw-r--r--tests/unit/src/com/android/intentresolver/platform/NearbyShareModuleTest.kt (renamed from tests/unit/src/com/android/intentresolver/v2/platform/NearbyShareModuleTest.kt)30
-rw-r--r--tests/unit/src/com/android/intentresolver/profiles/MultiProfilePagerAdapterTest.kt (renamed from tests/unit/src/com/android/intentresolver/v2/MultiProfilePagerAdapterTest.kt)95
-rw-r--r--tests/unit/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt42
-rw-r--r--tests/unit/src/com/android/intentresolver/ui/ShareResultSenderImplTest.kt190
-rw-r--r--tests/unit/src/com/android/intentresolver/ui/model/ActivityModelTest.kt107
-rw-r--r--tests/unit/src/com/android/intentresolver/ui/viewmodel/ChooserRequestTest.kt297
-rw-r--r--tests/unit/src/com/android/intentresolver/ui/viewmodel/ResolverRequestTest.kt128
-rw-r--r--tests/unit/src/com/android/intentresolver/util/TestKosmos.kt51
-rw-r--r--tests/unit/src/com/android/intentresolver/util/TruthUtils.kt26
-rw-r--r--tests/unit/src/com/android/intentresolver/util/UriFiltersTest.kt16
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/ChooserActionFactoryTest.kt244
-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/validation/ValidationTest.kt99
-rw-r--r--tests/unit/src/com/android/intentresolver/v2/validation/types/SimpleValueTest.kt52
-rw-r--r--tests/unit/src/com/android/intentresolver/validation/ValidationTest.kt132
-rw-r--r--tests/unit/src/com/android/intentresolver/validation/types/IntentOrUriTest.kt (renamed from tests/unit/src/com/android/intentresolver/v2/validation/types/IntentOrUriTest.kt)80
-rw-r--r--tests/unit/src/com/android/intentresolver/validation/types/ParceledArrayTest.kt (renamed from tests/unit/src/com/android/intentresolver/v2/validation/types/ParceledArrayTest.kt)60
-rw-r--r--tests/unit/src/com/android/intentresolver/validation/types/SimpleValueTest.kt92
432 files changed, 20689 insertions, 21150 deletions
diff --git a/Android.bp b/Android.bp
index 2e67398d..d0d20cdb 100644
--- a/Android.bp
+++ b/Android.bp
@@ -34,6 +34,7 @@ java_defaults {
strict_updatability_linting: false,
extra_check_modules: ["SystemUILintChecker"],
warning_checks: ["MissingApacheLicenseDetector"],
+ baseline_filename: "lint-baseline.xml",
},
}
@@ -59,6 +60,20 @@ android_library {
"kotlinx-coroutines-android",
"//external/kotlinc:kotlin-annotations",
"guava",
+ "PlatformComposeCore",
+ "PlatformComposeSceneTransitionLayout",
+ "androidx.compose.runtime_runtime",
+ "androidx.compose.material3_material3",
+ "androidx.compose.material_material-icons-extended",
+ "androidx.activity_activity-compose",
+ "androidx.compose.animation_animation-graphics",
+ "androidx.lifecycle_lifecycle-viewmodel-compose",
+ "androidx.lifecycle_lifecycle-runtime-compose",
+ ],
+ javacflags: [
+ "-Adagger.fastInit=enabled",
+ "-Adagger.explicitBindingConflictsWithInject=ERROR",
+ "-Adagger.strictMultibindingValidation=enabled",
],
}
diff --git a/AndroidManifest-app.xml b/AndroidManifest-app.xml
index ec4fec85..7338dd08 100644
--- a/AndroidManifest-app.xml
+++ b/AndroidManifest-app.xml
@@ -32,43 +32,8 @@
android:requiredForAllUsers="true"
android:supportsRtl="true">
- <!-- This alias needs to be maintained until there are no more devices that could be
- upgrading from T QPR3. (b/283722356) -->
- <activity-alias
- android:name=".ChooserActivityLauncher"
- android:targetActivity=".ChooserActivity"
- android:exported="true">
-
- <!-- This intent filter is assigned a priority greater than 100 so
- that it will take precedence over the framework ChooserActivity
- in the process of resolving implicit action.CHOOSER intents
- whenever this activity is enabled by the experiment flag. -->
- <intent-filter android:priority="500">
- <action android:name="android.intent.action.CHOOSER" />
- <category android:name="android.intent.category.DEFAULT" />
- <category android:name="android.intent.category.VOICE" />
- </intent-filter>
- </activity-alias>
-
<activity android:name=".ChooserActivity"
- 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="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:enabled="true"
android:theme="@style/Theme.DeviceDefault.Chooser"
android:finishOnCloseSystemDialogs="true"
android:excludeFromRecents="true"
@@ -76,19 +41,22 @@
android:relinquishTaskIdentity="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
android:visibleToInstantApps="true"
+ android:exported="false" />
+
+ <!-- This alias needs to be maintained until there are no more devices that could be
+ upgrading from T QPR3. (b/283722356) -->
+ <activity-alias
+ android:name=".ChooserActivityLauncher"
+ android:targetActivity=".ChooserActivity"
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">
+ <intent-filter android:priority="500">
<action android:name="android.intent.action.CHOOSER" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.VOICE" />
</intent-filter>
- </activity>
+ </activity-alias>
<provider android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
diff --git a/AndroidManifest-lib.xml b/AndroidManifest-lib.xml
index b3a43eb3..bdb94232 100644
--- a/AndroidManifest-lib.xml
+++ b/AndroidManifest-lib.xml
@@ -32,4 +32,6 @@
<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" />
+ <uses-permission android:name="android.permission.LOG_COMPAT_CHANGE" />
+ <uses-permission android:name="android.permission.READ_COMPAT_CHANGE_CONFIG" />
</manifest>
diff --git a/TEST_MAPPING b/TEST_MAPPING
index de28a495..51c7bd43 100644
--- a/TEST_MAPPING
+++ b/TEST_MAPPING
@@ -2,13 +2,13 @@
"presubmit": [
{
"name": "IntentResolver-tests-unit"
+ },
+ {
+ "name": "IntentResolver-tests-activity"
}
],
"postsubmit": [
{
- "name": "IntentResolver-tests-activity"
- },
- {
"name": "IntentResolver-tests-integration"
}
]
diff --git a/aconfig/FeatureFlags.aconfig b/aconfig/FeatureFlags.aconfig
index d67d7abe..b7d9ea0d 100644
--- a/aconfig/FeatureFlags.aconfig
+++ b/aconfig/FeatureFlags.aconfig
@@ -6,17 +6,13 @@ container: "system"
# bug: "Feature_Bug_#" or "<none>"
flag {
- name: "example_new_sharing_method"
+ name: "fix_target_list_footer"
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"
+ description: "Update app target grid footer on window insets change"
+ bug: "324011248"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
flag {
@@ -32,3 +28,33 @@ flag {
description: "Enables the new modular framework"
bug: "302113519"
}
+
+flag {
+ name: "bespoke_label_view"
+ namespace: "intentresolver"
+ description: "Use a custom view to draw target labels"
+ bug: "302188527"
+}
+
+flag {
+ name: "enable_private_profile"
+ namespace: "intentresolver"
+ description: "Enable private profile support"
+ bug: "328029692"
+}
+
+flag {
+ name: "refine_system_actions"
+ namespace: "intentresolver"
+ description: "This flag enables sending system actions to the caller refinement flow"
+ bug: "331206205"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+flag {
+ name: "fix_empty_state_padding"
+ namespace: "intentresolver"
+ description: "Always apply systemBar window insets regardless of profiles present"
+ bug: "338447666"
+}
diff --git a/java/res/drawable/checkbox.xml b/java/res/drawable/checkbox.xml
new file mode 100644
index 00000000..189d01ff
--- /dev/null
+++ b/java/res/drawable/checkbox.xml
@@ -0,0 +1,10 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="20dp"
+ android:height="20dp"
+ android:viewportWidth="20"
+ android:viewportHeight="20">
+ <path
+ android:pathData="M10,0C4.48,0 0,4.48 0,10C0,15.52 4.48,20 10,20C15.52,20 20,15.52 20,10C20,4.48 15.52,0 10,0ZM10,18C5.59,18 2,14.41 2,10C2,5.59 5.59,2 10,2C14.41,2 18,5.59 18,10C18,14.41 14.41,18 10,18ZM5.4,9.6L8,12.2L14.6,5.6L16,7L8,15L4,11L5.4,9.6Z"
+ android:fillColor="#ffffff"
+ android:fillType="evenOdd"/>
+</vector>
diff --git a/java/res/drawable/ic_play_circle_filled_24px.xml b/java/res/drawable/ic_play_circle_filled_24px.xml
new file mode 100644
index 00000000..f67127ca
--- /dev/null
+++ b/java/res/drawable/ic_play_circle_filled_24px.xml
@@ -0,0 +1,3 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="20dp" android:viewportHeight="20" android:viewportWidth="20" android:width="20dp">
+ <path android:fillColor="#ffffff" android:fillType="evenOdd" android:pathData="M0,10C0,4.48 4.48,0 10,0C15.52,0 20,4.48 20,10C20,15.52 15.52,20 10,20C4.48,20 0,15.52 0,10ZM14,10L8,5.5V14.5L14,10Z"/>
+</vector>
diff --git a/java/res/drawable/resolver_profile_tab_bg.xml b/java/res/drawable/resolver_profile_tab_bg.xml
index 8bb23a53..97f3b7e2 100644
--- a/java/res/drawable/resolver_profile_tab_bg.xml
+++ b/java/res/drawable/resolver_profile_tab_bg.xml
@@ -25,7 +25,7 @@
</item>
<item>
- <selector android:enterFadeDuration="100">
+ <selector>
<item android:state_selected="false">
<shape android:shape="rectangle">
<corners android:radius="12dp" />
diff --git a/java/res/layout/chooser_action_view.xml b/java/res/layout/chooser_action_view.xml
index e17dce0e..d045a7e3 100644
--- a/java/res/layout/chooser_action_view.xml
+++ b/java/res/layout/chooser_action_view.xml
@@ -27,6 +27,5 @@
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
- android:maxWidth="@dimen/chooser_action_max_width"
android:textColor="?androidprv:attr/materialColorOnSurface"
android:textSize="12sp" />
diff --git a/java/res/layout/chooser_grid.xml b/java/res/layout/chooser_grid.xml
deleted file mode 100644
index 8320b284..00000000
--- a/java/res/layout/chooser_grid.xml
+++ /dev/null
@@ -1,97 +0,0 @@
-<?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"
- 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="@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>
-
-</com.android.intentresolver.widget.ResolverDrawerLayout>
diff --git a/java/res/layout/chooser_grid_item.xml b/java/res/layout/chooser_grid_item.xml
new file mode 100644
index 00000000..18abc7bc
--- /dev/null
+++ b/java/res/layout/chooser_grid_item.xml
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2006, 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.
+*/
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:id="@androidprv:id/item"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="100dp"
+ android:gravity="top|center_horizontal"
+ android:paddingVertical="@dimen/grid_padding"
+ android:paddingHorizontal="4dp"
+ android:focusable="true"
+ android:background="?android:attr/selectableItemBackgroundBorderless">
+
+ <ImageView android:id="@android:id/icon"
+ android:layout_width="@dimen/chooser_icon_size"
+ android:layout_height="@dimen/chooser_icon_size"
+ android:layout_marginHorizontal="8dp"
+ android:scaleType="fitCenter" />
+
+ <!-- Size manually tuned to match specs -->
+ <Space android:layout_width="1dp"
+ android:layout_height="7dp"/>
+
+ <!-- NOTE: for id/text1 and id/text2 below set the width to match parent as a workaround for
+ b/269395540 i.e. prevent views bounds change during a transition animation. It does not
+ affect pinned views as we change their layout parameters programmatically (but that's even
+ more narrow possibility and it's not clear if the root cause or the bug would affect it).
+ -->
+ <!-- App name or Direct Share target name, DS set to 2 lines -->
+ <com.android.intentresolver.widget.BadgeTextView
+ android:id="@android:id/text1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?androidprv:attr/materialColorOnSurface"
+ android:textSize="12sp"
+ android:maxLines="1"
+ android:ellipsize="end" />
+
+ <!-- Activity name if set, gone for Direct Share targets -->
+ <TextView android:id="@android:id/text2"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textSize="12sp"
+ android:textColor="?androidprv:attr/materialColorOnSurfaceVariant"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:lines="1"
+ android:gravity="top|center_horizontal"
+ android:ellipsize="end"/>
+
+</LinearLayout>
+
diff --git a/java/res/layout/chooser_grid_preview_file.xml b/java/res/layout/chooser_grid_preview_file.xml
index 90832d23..4e8cf7ba 100644
--- a/java/res/layout/chooser_grid_preview_file.xml
+++ b/java/res/layout/chooser_grid_preview_file.xml
@@ -26,14 +26,6 @@
android:orientation="vertical"
android:background="?androidprv:attr/materialColorSurfaceContainer">
- <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"
android:layout_height="wrap_content"
diff --git a/java/res/layout/chooser_grid_preview_files_text.xml b/java/res/layout/chooser_grid_preview_files_text.xml
index e7747496..2756e800 100644
--- a/java/res/layout/chooser_grid_preview_files_text.xml
+++ b/java/res/layout/chooser_grid_preview_files_text.xml
@@ -25,14 +25,6 @@
android:orientation="vertical"
android:background="?androidprv:attr/materialColorSurfaceContainer">
- <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"
android:layout_height="wrap_content"
diff --git a/java/res/layout/chooser_grid_preview_text.xml b/java/res/layout/chooser_grid_preview_text.xml
index f3045c34..ee54c0ae 100644
--- a/java/res/layout/chooser_grid_preview_text.xml
+++ b/java/res/layout/chooser_grid_preview_text.xml
@@ -27,14 +27,6 @@
android:orientation="vertical"
android:background="?androidprv:attr/materialColorSurfaceContainer">
- <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"
android:layout_height="wrap_content"
@@ -122,4 +114,3 @@
<include layout="@layout/chooser_action_row" />
</LinearLayout>
-
diff --git a/java/res/layout/chooser_headline_row.xml b/java/res/layout/chooser_headline_row.xml
index 62781847..bfce7473 100644
--- a/java/res/layout/chooser_headline_row.xml
+++ b/java/res/layout/chooser_headline_row.xml
@@ -38,6 +38,21 @@
android:textSize="18sp"
/>
+ <TextView
+ android:id="@+id/metadata"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toStartOf="@id/barrier"
+ app:layout_constraintHorizontal_bias="0.0"
+ app:layout_constrainedWidth="true"
+ app:layout_constraintTop_toBottomOf="@id/headline"
+ style="@style/TextAppearance.ChooserDefault"
+ android:fontFamily="@androidprv:string/config_bodyFontFamily"
+ android:textSize="12sp"
+ />
+
<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier"
android:layout_width="wrap_content"
@@ -50,13 +65,17 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxWidth="@dimen/modify_share_text_toggle_max_width"
+ android:background="@drawable/chooser_action_button_bg"
app:layout_constraintEnd_toEndOf="parent"
android:maxLines="2"
android:ellipsize="end"
android:visibility="gone"
- android:paddingTop="3dp"
- style="@style/TextAppearance.ChooserDefault"
+ android:paddingVertical="3dp"
+ android:paddingHorizontal="@dimen/chooser_edge_margin_normal_half"
+ style="?android:attr/borderlessButtonStyle"
android:drawableEnd="@drawable/chevron_right"
+ android:textColor="?androidprv:attr/materialColorOnSurface"
+ android:textSize="12sp"
/>
<!-- This is only relevant for image+text preview, but needs to be in this layout so it can
diff --git a/java/res/layout/chooser_list_per_profile.xml b/java/res/layout/chooser_list_per_profile.xml
deleted file mode 100644
index ef82090c..00000000
--- a/java/res/layout/chooser_list_per_profile.xml
+++ /dev/null
@@ -1,34 +0,0 @@
-<!--
- ~ 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="match_parent">
- <androidx.recyclerview.widget.RecyclerView
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- 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/layout/chooser_list_per_profile_wrap.xml b/java/res/layout/chooser_list_per_profile_wrap.xml
index 157fa75d..fc0431d7 100644
--- a/java/res/layout/chooser_list_per_profile_wrap.xml
+++ b/java/res/layout/chooser_list_per_profile_wrap.xml
@@ -35,7 +35,6 @@
android:clipToPadding="false"
android:background="?androidprv:attr/materialColorSurfaceContainer"
android:scrollbars="none"
- android:elevation="1dp"
android:nestedScrollingEnabled="true" />
<include layout="@layout/resolver_empty_states" />
diff --git a/java/res/values-af/strings.xml b/java/res/values-af/strings.xml
index e0a73836..67ea4d7b 100644
--- a/java/res/values-af/strings.xml
+++ b/java/res/values-af/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Deel tans album"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privaat"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Persoonlike aansig"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Werkaansig"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privaat aansig"</string>
<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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"Geen private apps nie"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Maak <xliff:g id="APP">%s</xliff:g> in jou persoonlike profiel oop?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Maak <xliff:g id="APP">%s</xliff:g> in jou werkprofiel oop?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Gebruik persoonlike blaaier"</string>
diff --git a/java/res/values-am/strings.xml b/java/res/values-am/strings.xml
index ba6409fd..7482d692 100644
--- a/java/res/values-am/strings.xml
+++ b/java/res/values-am/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"የተጋራ አልበም"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"የግል"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"የግል ዕይታ"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"የስራ ዕይታ"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"የግል ዕይታ"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> በግል መገለጫዎ ውስጥ ይከፈት?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"<xliff:g id="APP">%s</xliff:g> በስራ መገለጫዎ ውስጥ ይከፈት?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"የግል አሳሽ ተጠቀም"</string>
diff --git a/java/res/values-ar/strings.xml b/java/res/values-ar/strings.xml
index da8d4de2..1a232c41 100644
--- a/java/res/values-ar/strings.xml
+++ b/java/res/values-ar/strings.xml
@@ -49,7 +49,7 @@
<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="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>
@@ -66,18 +66,21 @@
<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_album" msgid="191743129899503345">"مشاركة الألبوم"</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_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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"المساحة الخاصّة"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"عرض المحتوى الشخصي"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"عرض محتوى العمل"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"عرض المساحة الخاصّة"</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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"ما مِن تطبيقات خاصة"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"هل تريد فتح <xliff:g id="APP">%s</xliff:g> في ملفك الشخصي؟"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"هل تريد فتح <xliff:g id="APP">%s</xliff:g> في ملفك الشخصي للعمل؟"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"استخدام المتصفّح الشخصي"</string>
diff --git a/java/res/values-as/strings.xml b/java/res/values-as/strings.xml
index 14bd864e..67bbf6c8 100644
--- a/java/res/values-as/strings.xml
+++ b/java/res/values-as/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"এলবাম শ্বেয়াৰ কৰি থকা হৈছে"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"ব্যক্তিগত"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ব্যক্তিগত ভিউ"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"কৰ্মস্থানৰ ভিউ"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ব্যক্তিগত ভিউ"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"আপোনাৰ ব্যক্তিগত প্ৰ’ফাইলত <xliff:g id="APP">%s</xliff:g> খুলিবনে?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"আপোনাৰ কর্মস্থানৰ প্ৰ\'ফাইলত <xliff:g id="APP">%s</xliff:g> খুলিবনে?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"ব্যক্তিগত ব্ৰাউজাৰ ব্যৱহাৰ কৰক"</string>
diff --git a/java/res/values-az/strings.xml b/java/res/values-az/strings.xml
index a31df362..a073ea20 100644
--- a/java/res/values-az/strings.xml
+++ b/java/res/values-az/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Albom 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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Şəxsi"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Şəxsi məzmuna baxış"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"İş məzmununa baxış"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Şəxsi baxış"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"IT admininiz tərəfindən bloklanıb"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Bu kontenti iş tətbiqləri ilə paylaşmaq mümkün deyil"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Bu kontenti iş tətbiqləri ilə açmaq mümkün deyil"</string>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"Şə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>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"İş profilinizdə <xliff:g id="APP">%s</xliff:g> tətbiqi açılsın?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Şəxsi brauzerdən istifadə edin"</string>
diff --git a/java/res/values-b+sr+Latn/strings.xml b/java/res/values-b+sr+Latn/strings.xml
index ea0d87b3..4f016f19 100644
--- a/java/res/values-b+sr+Latn/strings.xml
+++ b/java/res/values-b+sr+Latn/strings.xml
@@ -57,7 +57,7 @@
<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_images" msgid="5251443722186962006">"{count,plural, =1{Deljenje slike}one{Deljenje # slike}few{Deljenje # slike}other{Deljenje # 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 # 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>
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Deljeni album"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privatno"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Lični prikaz"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Prikaz za posao"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privatni prikaz"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokira IT administrator"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Ovaj sadržaj ne može da se deli pomoću poslovnih aplikacija"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Ovaj sadržaj ne može da se otvara pomoću poslovnih aplikacija"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Želite da na ličnom profilu otvorite: <xliff:g id="APP">%s</xliff:g>?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Želite da na poslovnom profilu otvorite: <xliff:g id="APP">%s</xliff:g>?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Koristi lični pregledač"</string>
diff --git a/java/res/values-be/strings.xml b/java/res/values-be/strings.xml
index aecc1cbd..9ab80a2f 100644
--- a/java/res/values-be/strings.xml
+++ b/java/res/values-be/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Ідзе абагульванне альбома"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"Прыватная прастора"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Прагляд асабістага змесціва"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Прагляд працоўнага змесціва"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Прыватная прастора"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Адкрыць праграму \"<xliff:g id="APP">%s</xliff:g>\" з выкарыстаннем асабістага профілю?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Адкрыць праграму \"<xliff:g id="APP">%s</xliff:g>\" з выкарыстаннем працоўнага профілю?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Скарыстаць асабісты браўзер"</string>
diff --git a/java/res/values-bg/strings.xml b/java/res/values-bg/strings.xml
index 5bc22d73..0c6d1249 100644
--- a/java/res/values-bg/strings.xml
+++ b/java/res/values-bg/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Споделяне на албума"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"Частно"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Личен изглед"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Служебен изглед"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Частен изглед"</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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"Няма частни приложения"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Искате ли да отворите <xliff:g id="APP">%s</xliff:g> в личния си потребителски профил?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Искате ли да отворите <xliff:g id="APP">%s</xliff:g> в служебния си потребителски профил?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Използване на личния браузър"</string>
diff --git a/java/res/values-bn/strings.xml b/java/res/values-bn/strings.xml
index 0561cf99..83c76752 100644
--- a/java/res/values-bn/strings.xml
+++ b/java/res/values-bn/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"অ্যালবাম শেয়ার করা হচ্ছে"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"ব্যক্তিগত"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ব্যক্তিগত ভিউ"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"অফিসের ভিউ"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ব্যক্তিগত ভিউ"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"আপনার ব্যক্তিগত প্রোফাইল থেকে <xliff:g id="APP">%s</xliff:g> খুলবেন?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"আপনার অফিস প্রোফাইল থেকে <xliff:g id="APP">%s</xliff:g> খুলবেন?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"ব্যক্তিগত ব্রাউজার ব্যবহার করুন"</string>
diff --git a/java/res/values-bs/strings.xml b/java/res/values-bs/strings.xml
index 3c88d9c1..a29f3638 100644
--- a/java/res/values-bs/strings.xml
+++ b/java/res/values-bs/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Dijeljenje albuma"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privatno"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Prikaz ličnog sadržaja"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Prikaz poslovnog sadržaja"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privatan prikaz"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokirao je vaš IT administrator"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Ovaj sadržaj nije moguće dijeliti pomoću poslovnih aplikacija"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Ovaj sadržaj nije moguće otvoriti pomoću poslovnih aplikacija"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Otvoriti aplikaciju <xliff:g id="APP">%s</xliff:g> na ličnom profilu?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Otvoriti aplikaciju <xliff:g id="APP">%s</xliff:g> na radnom profilu?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Koristi lični preglednik"</string>
diff --git a/java/res/values-ca/strings.xml b/java/res/values-ca/strings.xml
index bd0416a5..daac39ef 100644
--- a/java/res/values-ca/strings.xml
+++ b/java/res/values-ca/strings.xml
@@ -57,7 +57,7 @@
<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 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_images" msgid="5251443722186962006">"{count,plural, =1{Comparteix una imatge}many{Comparteix # d\'imatges}other{Comparteix # 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_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>
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"S\'està compartint l\'àlbum"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privat"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Visualització personal"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Visualització de treball"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Visualització privada"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bloquejat per l\'administrador de TI"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"No es pot compartir aquest contingut amb aplicacions de treball"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"No es pot obrir aquest contingut amb aplicacions de treball"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vols obrir <xliff:g id="APP">%s</xliff:g> al teu perfil personal?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Vols obrir <xliff:g id="APP">%s</xliff:g> al teu perfil de treball?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Utilitza el navegador personal"</string>
diff --git a/java/res/values-cs/strings.xml b/java/res/values-cs/strings.xml
index a5deed60..4f0eca35 100644
--- a/java/res/values-cs/strings.xml
+++ b/java/res/values-cs/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Sdílení alba"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Soukromé"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Osobní zobrazení"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Pracovní zobrazení"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Soukromé zobrazení"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokováno administrátorem IT"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Tento obsah nelze sdílet pomocí pracovních aplikací"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Tento obsah nelze otevřít pomocí pracovních aplikací"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Otevřít aplikaci <xliff:g id="APP">%s</xliff:g> v osobním profilu?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Otevřít aplikaci <xliff:g id="APP">%s</xliff:g> v pracovním profilu?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Použít osobní prohlížeč"</string>
diff --git a/java/res/values-da/strings.xml b/java/res/values-da/strings.xml
index 8d226d44..784a2efd 100644
--- a/java/res/values-da/strings.xml
+++ b/java/res/values-da/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Deling af albummet"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privat"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Visningen Personligt"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Visningen Arbejde"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Visningen Privat"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokeret af din it-administrator"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Dette indhold kan ikke deles med arbejdsapps"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Dette indhold kan ikke åbnes med arbejdsapps"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vil du åbne <xliff:g id="APP">%s</xliff:g> på din personlige profil?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Vil du åbne <xliff:g id="APP">%s</xliff:g> på din arbejdsprofil?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Brug personlig browser"</string>
diff --git a/java/res/values-de/strings.xml b/java/res/values-de/strings.xml
index dc476fa7..07be8072 100644
--- a/java/res/values-de/strings.xml
+++ b/java/res/values-de/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Album teilen"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privat"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Private Ansicht"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Geschäftliche Ansicht"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Private Ansicht"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Von deinem IT-Administrator blockiert"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Diese Art von Inhalt kann nicht über geschäftliche Apps geteilt werden"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Diese Art von Inhalt kann nicht mit geschäftlichen Apps geöffnet werden"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> in deinem privaten Profil öffnen?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"<xliff:g id="APP">%s</xliff:g> in deinem Arbeitsprofil öffnen?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Privaten Browser verwenden"</string>
diff --git a/java/res/values-el/strings.xml b/java/res/values-el/strings.xml
index e760e00c..b62d6687 100644
--- a/java/res/values-el/strings.xml
+++ b/java/res/values-el/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Κοινοποίηση λευκώματος"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"Ιδιωτική"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Προσωπική προβολή"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Προβολή εργασίας"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Ιδιωτική προβολή"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Θέλετε να ανοίξετε την εφαρμογή <xliff:g id="APP">%s</xliff:g> στο προσωπικό σας προφίλ;"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Θέλετε να ανοίξετε την εφαρμογή <xliff:g id="APP">%s</xliff:g> στο προφίλ σας εργασίας;"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Χρήση προσωπικού προγράμματος περιήγησης"</string>
diff --git a/java/res/values-en-rAU/strings.xml b/java/res/values-en-rAU/strings.xml
index a1438ed9..537606cc 100644
--- a/java/res/values-en-rAU/strings.xml
+++ b/java/res/values-en-rAU/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Sharing album"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Private"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personal view"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Work view"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Private view"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blocked by your IT admin"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"This content can’t be shared with work apps"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"No private apps"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Open <xliff:g id="APP">%s</xliff:g> in your personal profile?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Open <xliff:g id="APP">%s</xliff:g> in your work profile?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Use personal browser"</string>
diff --git a/java/res/values-en-rCA/strings.xml b/java/res/values-en-rCA/strings.xml
index a1438ed9..537606cc 100644
--- a/java/res/values-en-rCA/strings.xml
+++ b/java/res/values-en-rCA/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Sharing album"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Private"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personal view"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Work view"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Private view"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blocked by your IT admin"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"This content can’t be shared with work apps"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"No private apps"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Open <xliff:g id="APP">%s</xliff:g> in your personal profile?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Open <xliff:g id="APP">%s</xliff:g> in your work profile?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Use personal browser"</string>
diff --git a/java/res/values-en-rGB/strings.xml b/java/res/values-en-rGB/strings.xml
index a1438ed9..537606cc 100644
--- a/java/res/values-en-rGB/strings.xml
+++ b/java/res/values-en-rGB/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Sharing album"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Private"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personal view"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Work view"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Private view"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blocked by your IT admin"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"This content can’t be shared with work apps"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"No private apps"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Open <xliff:g id="APP">%s</xliff:g> in your personal profile?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Open <xliff:g id="APP">%s</xliff:g> in your work profile?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Use personal browser"</string>
diff --git a/java/res/values-en-rIN/strings.xml b/java/res/values-en-rIN/strings.xml
index a1438ed9..537606cc 100644
--- a/java/res/values-en-rIN/strings.xml
+++ b/java/res/values-en-rIN/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Sharing album"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Private"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personal view"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Work view"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Private view"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blocked by your IT admin"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"This content can’t be shared with work apps"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"This content can’t be opened with work apps"</string>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"No private apps"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Open <xliff:g id="APP">%s</xliff:g> in your personal profile?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Open <xliff:g id="APP">%s</xliff:g> in your work profile?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Use personal browser"</string>
diff --git a/java/res/values-en-rXC/strings.xml b/java/res/values-en-rXC/strings.xml
index 56574b6c..70cafe8e 100644
--- a/java/res/values-en-rXC/strings.xml
+++ b/java/res/values-en-rXC/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‎‏‎‏‎‏‎‏‎‏‎‎‏‎‎‏‏‎‏‎‏‎‏‎‏‏‏‎‎‎‏‎‏‏‎‎‏‏‏‎‎‏‎‎‎‎‎‎‎‏‏‏‎‏‏‏‏‎‎‎‏‎Sharing album‎‏‎‎‏‎"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‏‏‎‏‏‏‎‎‏‏‏‏‎‏‏‏‎‎‎‎‎‎‎‎‏‏‏‎‎‏‏‎‎‎‎‏‎‎‏‎‏‎‏‎‏‏‎‎‏‏‎‎‏‏‎‏‎‏‎Private‎‏‎‎‏‎"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‏‏‎‎‎‎‎‎‎‎‎‏‏‎‎‎‎‏‎‎‎‏‏‎‎‏‎‎‏‏‎‏‏‏‏‏‏‎‏‏‏‏‏‎‎‏‎‏‏‎‏‏‎‏‎‏‏‏‎‎Personal view‎‏‎‎‏‎"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‏‏‏‎‎‎‎‎‏‏‏‏‏‎‏‎‏‎‎‎‎‏‏‎‏‎‎‎‎‎‏‏‎‏‏‏‏‎‎‎‏‎‎‏‎‏‎‏‏‏‎‎‎‎‎Work view‎‏‎‎‏‎"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‏‎‎‏‏‏‏‏‏‎‎‏‏‏‎‎‎‏‏‎‎‏‏‎‎‏‎‏‏‏‎‏‏‎‏‏‎‎‏‏‎‎‎‏‎‎‎Private view‎‏‎‎‏‎"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‎‎‎‎‏‏‎‎‏‎‎‎‎‏‏‏‏‎‏‎‎‏‏‎‎‎‎‎‎‏‏‎‏‏‏‎‏‏‏‎‎‏‎‎‏‎‏‏‏‏‎‎‎‏‎‎‎‎‎‏‎Blocked by your IT admin‎‏‎‎‏‎"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‎‏‎‎‏‎‏‏‎‏‎‎‏‏‎‏‎‏‏‎‎‎‏‏‏‎‏‏‎‏‎‎‎‎‏‎‎‏‏‏‎‎‏‏‎‎‎‏‎‎‎‎‎‎‏‎‏‎‎‏‏‎This content can’t be shared with work apps‎‏‎‎‏‎"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‏‎‎‎‏‎‎‏‏‎‏‏‏‏‏‎‏‎‎‎‎‎‎‏‎‎‎‏‏‏‏‏‎‎‏‏‎‎‎‏‏‎‏‎‏‏‎‎‏‎‎‏‏‎‏‏‎‏‎‎This content can’t be opened with work apps‎‏‎‎‏‎"</string>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‎‏‏‏‎‎‏‎‏‏‎‎‏‎‏‏‏‎‏‎‏‎‎‎‎‎‏‎‏‎‏‎‏‎‏‎‏‎‏‏‏‏‎‏‎‏‎‎‏‏‎‏‏‏‎‎‎‎‎No private apps‎‏‎‎‏‎"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‏‏‏‏‎‏‎‎‏‎‎‎‏‎‎‏‎‏‏‏‏‎‏‎‎‎‎‎‎‎‏‏‏‏‏‎‏‎‏‏‏‎‎‏‎‏‏‎‏‏‎‎‎‎‎‎‏‏‏‏‏‏‏‏‎Open ‎‏‎‎‏‏‎<xliff:g id="APP">%s</xliff:g>‎‏‎‎‏‏‏‎ in your personal profile?‎‏‎‎‏‎"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‏‎‏‏‏‎‏‏‎‏‎‎‎‏‏‏‏‏‏‎‏‎‎‎‎‎‏‏‏‏‎‏‎‏‏‎‏‏‏‏‎‏‎‏‎‎‏‏‏‏‏‎‏‎‏‏‏‎‏‏‎‏‎‏‎Open ‎‏‎‎‏‏‎<xliff:g id="APP">%s</xliff:g>‎‏‎‎‏‏‏‎ in your work profile?‎‏‎‎‏‎"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"‎‏‎‎‎‎‎‏‎‏‏‏‎‎‎‎‎‎‏‎‎‏‎‎‎‎‏‏‏‏‏‎‏‏‎‎‏‏‏‏‎‏‎‏‎‎‏‎‎‎‎‎‏‏‏‎‏‎‎‏‏‎‎‏‏‎‎‏‏‏‏‎‎‏‎‏‏‏‎‏‎‎‏‏‏‏‏‏‏‎‏‏‎‎‎Use personal browser‎‏‎‎‏‎"</string>
diff --git a/java/res/values-es-rUS/strings.xml b/java/res/values-es-rUS/strings.xml
index 97ae9a6c..2f33f965 100644
--- a/java/res/values-es-rUS/strings.xml
+++ b/java/res/values-es-rUS/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Se comparte este álbum"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privada"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Vista personal"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Vista de trabajo"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Vista privada"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bloqueado por tu administrador de TI"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"No se pueden usar apps de trabajo para compartir este contenido"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"No se puede abrir este contenido con apps de trabajo"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"¿Quieres abrir <xliff:g id="APP">%s</xliff:g> en tu perfil personal?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"¿Quieres abrir <xliff:g id="APP">%s</xliff:g> en tu perfil de trabajo?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Usar un navegador personal"</string>
diff --git a/java/res/values-es/strings.xml b/java/res/values-es/strings.xml
index 0c42bb82..92aed933 100644
--- a/java/res/values-es/strings.xml
+++ b/java/res/values-es/strings.xml
@@ -57,7 +57,7 @@
<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_images" msgid="5251443722186962006">"{count,plural, =1{Compartir imagen}many{Compartir # imágenes}other{Compartir # 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_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>
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Compartiendo álbum"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privada"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Ver contenido personal"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Ver contenido de trabajo"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Vista privada"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bloqueado por tu administrador de TI"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Este contenido no se puede compartir con aplicaciones de trabajo"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Este contenido no se puede abrir con aplicaciones de trabajo"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"¿Abrir <xliff:g id="APP">%s</xliff:g> en tu perfil personal?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"¿Abrir <xliff:g id="APP">%s</xliff:g> en tu perfil de trabajo?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Usar navegador personal"</string>
@@ -95,5 +100,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>
+ <string name="pinned" msgid="7623664001331394139">"Fijado"</string>
</resources>
diff --git a/java/res/values-et/strings.xml b/java/res/values-et/strings.xml
index bc960699..6c5cd952 100644
--- a/java/res/values-et/strings.xml
+++ b/java/res/values-et/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Albumi 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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privaatne"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Isiklik vaade"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Töövaade"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privaatne vaade"</string>
<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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"Privaatseid rakendusi pole"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Kas avada <xliff:g id="APP">%s</xliff:g> teie isiklikul profiilil?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Kas avada <xliff:g id="APP">%s</xliff:g> teie tööprofiilil?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Kasuta isiklikku brauserit"</string>
diff --git a/java/res/values-eu/strings.xml b/java/res/values-eu/strings.xml
index 1cc7576b..7570e7dc 100644
--- a/java/res/values-eu/strings.xml
+++ b/java/res/values-eu/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Albuma partekatzea"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Pribatua"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Ikuspegi pertsonala"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Laneko ikuspegia"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Ikuspegi pribatua"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"IKT saileko administratzaileak blokeatu egin du"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Eduki hau ezin da laneko aplikazioekin partekatu"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Eduki hau ezin da laneko aplikazioekin ireki"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Profil pertsonalean ireki nahi duzu <xliff:g id="APP">%s</xliff:g>?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Laneko profilean ireki nahi duzu <xliff:g id="APP">%s</xliff:g>?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Erabili arakatzaile pertsonala"</string>
diff --git a/java/res/values-fa/strings.xml b/java/res/values-fa/strings.xml
index 58313f70..66b03cfc 100644
--- a/java/res/values-fa/strings.xml
+++ b/java/res/values-fa/strings.xml
@@ -66,18 +66,21 @@
<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_album" msgid="191743129899503345">"هم‌رسانی آلبوم"</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_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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"خصوصی"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"نمای شخصی"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"نمای کاری"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"نمای خصوصی"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> در نمایه شخصی باز شود؟"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"<xliff:g id="APP">%s</xliff:g> در نمایه کاری باز شود؟"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"استفاده از مرورگر شخصی"</string>
diff --git a/java/res/values-fi/strings.xml b/java/res/values-fi/strings.xml
index 53537e67..3b79b195 100644
--- a/java/res/values-fi/strings.xml
+++ b/java/res/values-fi/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Albumia 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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Yksityinen"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Henkilökohtainen näkymä"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Työnäkymä"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Yksityinen näkymä"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"IT-järjestelmänvalvojasi estämä"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Tätä sisältöä ei voi jakaa työsovelluksilla"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Tätä sisältöä ei voi avata työsovelluksilla"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Avataanko <xliff:g id="APP">%s</xliff:g> henkilökohtaisessa profiilissa?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Avataanko <xliff:g id="APP">%s</xliff:g> työprofiilissa?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Käytä henkilökohtaista selainta"</string>
diff --git a/java/res/values-fr-rCA/strings.xml b/java/res/values-fr-rCA/strings.xml
index 5595b6cc..074ce258 100644
--- a/java/res/values-fr-rCA/strings.xml
+++ b/java/res/values-fr-rCA/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Album partagé"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privé"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Affichage personnel"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Affichage professionnel"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Affichage privé"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bloqué par votre administrateur informatique"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Impossible de partager ce contenu avec des applications professionnelles"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Impossible d\'ouvrir ce contenu avec des applications professionnelles"</string>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"Aucune application privée"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Ouvrir <xliff:g id="APP">%s</xliff:g> dans votre profil personnel?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Ouvrir <xliff:g id="APP">%s</xliff:g> dans votre profil professionnel?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Utiliser le navigateur du profil personnel"</string>
diff --git a/java/res/values-fr/strings.xml b/java/res/values-fr/strings.xml
index 5f0c85e0..c87644a6 100644
--- a/java/res/values-fr/strings.xml
+++ b/java/res/values-fr/strings.xml
@@ -55,7 +55,7 @@
<string name="screenshot_edit" msgid="3857183660047569146">"Modifier"</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_text" msgid="8137537443603304062">"Texte à partager"</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>
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Partage de l\'album"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Mode privé"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Vue personnelle"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Vue professionnelle"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Affichage en mode privé"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bloqué par votre administrateur informatique"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Impossible de partager ce contenu avec des applis professionnelles"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Impossible d\'ouvrir ce contenu avec des applis professionnelles"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Ouvrir <xliff:g id="APP">%s</xliff:g> dans votre profil personnel ?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Ouvrir <xliff:g id="APP">%s</xliff:g> dans votre profil professionnel ?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Utiliser le navigateur personnel"</string>
diff --git a/java/res/values-gl/strings.xml b/java/res/values-gl/strings.xml
index 60dc78de..6b8a4151 100644
--- a/java/res/values-gl/strings.xml
+++ b/java/res/values-gl/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Compartindo álbum"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privada"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Vista persoal"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Vista de traballo"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Vista privada"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"O teu administrador de TI bloqueou a instalación"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Este contido non pode compartirse con aplicacións do traballo"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Este contido non pode abrirse con aplicacións do traballo"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Queres abrir <xliff:g id="APP">%s</xliff:g> no teu perfil persoal?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Queres abrir <xliff:g id="APP">%s</xliff:g> no teu perfil de traballo?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Utilizar navegador persoal"</string>
diff --git a/java/res/values-gu/strings.xml b/java/res/values-gu/strings.xml
index db3bd59a..d9dd48f4 100644
--- a/java/res/values-gu/strings.xml
+++ b/java/res/values-gu/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"આલ્બમ શેર કરી રહ્યાં છીએ"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"ખાનગી"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"વ્યક્તિગત વ્યૂ"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"ઑફિસ વ્યૂ"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ખાનગી વ્યૂ"</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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"કોઈ ખાનગી ઍપ નથી"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"તમારી વ્યક્તિગત પ્રોફાઇલમાં <xliff:g id="APP">%s</xliff:g> ખોલીએ?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"તમારી ઑફિસની પ્રોફાઇલમાં <xliff:g id="APP">%s</xliff:g> ખોલીએ?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"વ્યક્તિગત બ્રાઉઝરનો ઉપયોગ કરો"</string>
diff --git a/java/res/values-h480dp/dimens.xml b/java/res/values-h480dp/dimens.xml
index b5c86c77..74fab4ea 100644
--- a/java/res/values-h480dp/dimens.xml
+++ b/java/res/values-h480dp/dimens.xml
@@ -22,7 +22,7 @@
<dimen name="resolver_button_bar_spacing">8dp</dimen>
<dimen name="chooser_preview_width">-1px</dimen>
- <dimen name="chooser_preview_image_height_tall">192dp</dimen>
+ <dimen name="chooser_preview_image_height_tall">284dp</dimen>
<dimen name="grid_padding">10dp</dimen>
<dimen name="width_text_image_preview_size">56dp</dimen>
</resources>
diff --git a/java/res/values-h480dp/integers.xml b/java/res/values-h480dp/integers.xml
index c1693057..1195d6b7 100644
--- a/java/res/values-h480dp/integers.xml
+++ b/java/res/values-h480dp/integers.xml
@@ -15,5 +15,5 @@
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
- <integer name="text_preview_lines">3</integer>
+ <integer name="text_preview_lines">8</integer>
</resources>
diff --git a/java/res/values-hi/strings.xml b/java/res/values-hi/strings.xml
index b722e0ce..071b2b54 100644
--- a/java/res/values-hi/strings.xml
+++ b/java/res/values-hi/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"एल्बम शेयर किया जा रहा है"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"निजी"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"निजी व्यू"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"वर्क व्यू"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"निजी व्यू"</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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"कोई निजी ऐप्लिकेशन उपलब्ध नहीं है"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"क्या <xliff:g id="APP">%s</xliff:g> को निजी प्रोफ़ाइल में खोलना है?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"क्या <xliff:g id="APP">%s</xliff:g> को वर्क प्रोफ़ाइल में खोलना है?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"निजी ब्राउज़र का इस्तेमाल करें"</string>
diff --git a/java/res/values-hr/strings.xml b/java/res/values-hr/strings.xml
index e2d71b37..ebfe6fe4 100644
--- a/java/res/values-hr/strings.xml
+++ b/java/res/values-hr/strings.xml
@@ -57,7 +57,7 @@
<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_images" msgid="5251443722186962006">"{count,plural, =1{Podijelite sliku}one{Podijelite # sliku}few{Podijelite # slike}other{Podijelite # 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_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>
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Dijeljenje albuma"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privatno"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Osobni prikaz"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Poslovni prikaz"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privatni prikaz"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokirao vaš IT administrator"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Taj se sadržaj ne može dijeliti pomoću poslovnih aplikacija"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Taj se sadržaj ne može otvoriti pomoću poslovnih aplikacija"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Želite li otvoriti aplikaciju <xliff:g id="APP">%s</xliff:g> na osobnom profilu?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Želite li otvoriti aplikaciju <xliff:g id="APP">%s</xliff:g> na poslovnom profilu?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Koristi osobni preglednik"</string>
diff --git a/java/res/values-hu/strings.xml b/java/res/values-hu/strings.xml
index 53ddba7f..10f6d027 100644
--- a/java/res/values-hu/strings.xml
+++ b/java/res/values-hu/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Album megosztása"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privát"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Személyes nézet"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Munkanézet"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privát nézet"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Rendszergazda által letiltva"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Ez a tartalom nem osztható meg munkahelyi alkalmazásokkal"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Ez a tartalom nem nyitható meg munkahelyi alkalmazásokkal"</string>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"Nincsenek privát alkalmazások"</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>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Megnyitja a(z) <xliff:g id="APP">%s</xliff:g> alkalmazást a munkaprofil használatával?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Személyes böngésző használata"</string>
diff --git a/java/res/values-hy/strings.xml b/java/res/values-hy/strings.xml
index 6a83cdaa..8c46bd07 100644
--- a/java/res/values-hy/strings.xml
+++ b/java/res/values-hy/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Ալբոմը դարձվում է ընդհանուր"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"Մասնավոր"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Անձնական"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Աշխատանքային"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Անձնական դիտակերպ"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Բացե՞լ <xliff:g id="APP">%s</xliff:g> հավելվածը ձեր անձնական պրոֆիլում"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Բացե՞լ <xliff:g id="APP">%s</xliff:g> հավելվածը ձեր աշխատանքային պրոֆիլում"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Օգտագործել անձնական դիտարկիչը"</string>
diff --git a/java/res/values-in/strings.xml b/java/res/values-in/strings.xml
index d7400b80..02b00466 100644
--- a/java/res/values-in/strings.xml
+++ b/java/res/values-in/strings.xml
@@ -55,7 +55,7 @@
<string name="screenshot_edit" msgid="3857183660047569146">"Edit"</string>
<string name="other_files" msgid="4501185823517473875">"{count,plural, =1{+ # file}other{+ # file}}"</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_text" msgid="8137537443603304062">"Teks yang akan dibagikan"</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>
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Berbagi album"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Pribadi"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Tampilan pribadi"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Tampilan kerja"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Tampilan pribadi"</string>
<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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"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>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Buka <xliff:g id="APP">%s</xliff:g> di profil kerja?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Gunakan browser pribadi"</string>
diff --git a/java/res/values-is/strings.xml b/java/res/values-is/strings.xml
index 8e0a9f4f..b6e6e758 100644
--- a/java/res/values-is/strings.xml
+++ b/java/res/values-is/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Deilir albúmi"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Lokað"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Persónulegt yfirlit"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Vinnuyfirlit"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Lokuð stilling"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Útilokað af kerfisstjóra"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Ekki er hægt að deila þessu efni með vinnuforritum"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Ekki er hægt að opna þetta efni með vinnuforritum"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Opna <xliff:g id="APP">%s</xliff:g> í þínu eigin sniði?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Opna <xliff:g id="APP">%s</xliff:g> í vinnusniðinu þínu?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Nota einkavafra"</string>
diff --git a/java/res/values-it/strings.xml b/java/res/values-it/strings.xml
index 38aba0c2..c011fa3c 100644
--- a/java/res/values-it/strings.xml
+++ b/java/res/values-it/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Condivisione album"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privata"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Visualizzazione personale"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Visualizzazione di lavoro"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Visualizzazione privata"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bloccati dall\'amministratore IT"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Questi contenuti non possono essere condivisi con app di lavoro"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Questi contenuti non possono essere aperti con app di lavoro"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Aprire <xliff:g id="APP">%s</xliff:g> nel tuo profilo personale?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Aprire <xliff:g id="APP">%s</xliff:g> nel tuo profilo di lavoro?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Usa il browser personale"</string>
diff --git a/java/res/values-iw/strings.xml b/java/res/values-iw/strings.xml
index c79425d8..c1740360 100644
--- a/java/res/values-iw/strings.xml
+++ b/java/res/values-iw/strings.xml
@@ -66,18 +66,21 @@
<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_album" msgid="191743129899503345">"שיתוף האלבום"</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_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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"פרטי"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"תצוגה אישית"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"תצוגת עבודה"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"תצוגה פרטית"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"לפתוח את <xliff:g id="APP">%s</xliff:g> בפרופיל האישי?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"לפתוח את <xliff:g id="APP">%s</xliff:g> בפרופיל העבודה?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"בדפדפן האישי"</string>
diff --git a/java/res/values-ja/strings.xml b/java/res/values-ja/strings.xml
index 15c2277b..73d838e5 100644
--- a/java/res/values-ja/strings.xml
+++ b/java/res/values-ja/strings.xml
@@ -55,7 +55,7 @@
<string name="screenshot_edit" msgid="3857183660047569146">"編集"</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_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>
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"アルバムの共有"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"プライベート"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"個人用ビュー"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"仕事用ビュー"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"プライベート ビュー"</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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"限定公開アプリは対応していません"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"個人用プロファイルで <xliff:g id="APP">%s</xliff:g> を開きますか?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"仕事用プロファイルで <xliff:g id="APP">%s</xliff:g> を開きますか?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"個人用ブラウザを使用"</string>
diff --git a/java/res/values-ka/strings.xml b/java/res/values-ka/strings.xml
index 88bc15ac..2c86006b 100644
--- a/java/res/values-ka/strings.xml
+++ b/java/res/values-ka/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"გაზიარებული ალბომი"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"კერძო"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"პირადი ხედი"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"სამსახურის ხედი"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"პირადი სივრცე"</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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"არსებული პირადი აპებით მხარდაჭერილი არ არის"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"გსურთ <xliff:g id="APP">%s</xliff:g>-ის გახსნა თქვენს პირად პროფილში?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"გსურთ <xliff:g id="APP">%s</xliff:g>-ის გახსნა თქვენს სამსახურის პროფილში?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"პირადი ბრაუზერის გამოყენება"</string>
diff --git a/java/res/values-kk/strings.xml b/java/res/values-kk/strings.xml
index 7b195799..1819fc34 100644
--- a/java/res/values-kk/strings.xml
+++ b/java/res/values-kk/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Альбомды бөлісу"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"Құпия"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Жеке көру"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Жұмыс деректерін көру"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Құпия көрініс"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> қолданбасын жеке профиліңізде ашу керек пе?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"<xliff:g id="APP">%s</xliff:g> қолданбасын жұмыс профиліңізде ашу керек пе?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Жеке браузерді пайдалану"</string>
diff --git a/java/res/values-km/strings.xml b/java/res/values-km/strings.xml
index ae956af3..93c2c5f0 100644
--- a/java/res/values-km/strings.xml
+++ b/java/res/values-km/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"កំពុងចែករំលែកអាល់ប៊ុម"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"ឯកជន"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ទិដ្ឋភាពផ្ទាល់ខ្លួន"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"ទិដ្ឋភាព​ការងារ"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ទិដ្ឋភាពឯកជន"</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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"គ្មានកម្មវិធីឯកជនទេ"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"បើក <xliff:g id="APP">%s</xliff:g> នៅក្នុងកម្រង​ព័ត៌មាន​ផ្ទាល់​ខ្លួនរបស់អ្នកឬ?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"បើក <xliff:g id="APP">%s</xliff:g> នៅក្នុងកម្រងព័ត៌មានការងាររបស់អ្នកឬ?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"ប្រើ​កម្មវិធីរុករក​តាមអ៊ីនធឺណិត​ផ្ទាល់ខ្លួន"</string>
diff --git a/java/res/values-kn/strings.xml b/java/res/values-kn/strings.xml
index 505277c6..d8af7445 100644
--- a/java/res/values-kn/strings.xml
+++ b/java/res/values-kn/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"ಆಲ್ಬಮ್ ಅನ್ನು ಹಂಚಿಕೊಳ್ಳಲಾಗುತ್ತಿದೆ"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"ಖಾಸಗಿ"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ವೈಯಕ್ತಿಕ ವೀಕ್ಷಣೆ"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"ಕೆಲಸದ ವೀಕ್ಷಣೆ"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ಖಾಸಗಿ ವೀಕ್ಷಣೆ"</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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"ಯಾವುದೇ ಖಾಸಗಿ ಆ್ಯಪ್‍‍ಗಳಿಲ್ಲ"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"ನಿಮ್ಮ ವೈಯಕ್ತಿಕ ಪ್ರೊಫೈಲ್‌ನಲ್ಲಿ <xliff:g id="APP">%s</xliff:g> ಅನ್ನು ತೆರೆಯಬೇಕೆ?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"ನಿಮ್ಮ ಉದ್ಯೋಗದ ಪ್ರೊಫೈಲ್‌ನಲ್ಲಿ <xliff:g id="APP">%s</xliff:g> ಅನ್ನು ತೆರೆಯಬೇಕೆ?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"ವೈಯಕ್ತಿಕ ಬ್ರೌಸರ್ ಬಳಸಿ"</string>
diff --git a/java/res/values-ko/strings.xml b/java/res/values-ko/strings.xml
index e9e908be..5e1903af 100644
--- a/java/res/values-ko/strings.xml
+++ b/java/res/values-ko/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"앨범 공유"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"비공개"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"개인 뷰"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"직장 뷰"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"비공개 뷰"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"개인 프로필에서 <xliff:g id="APP">%s</xliff:g> 앱을 여시겠습니까?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"직장 프로필에서 <xliff:g id="APP">%s</xliff:g> 앱을 여시겠습니까?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"개인 브라우저 사용"</string>
diff --git a/java/res/values-ky/strings.xml b/java/res/values-ky/strings.xml
index 311a2169..56915f4b 100644
--- a/java/res/values-ky/strings.xml
+++ b/java/res/values-ky/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Альбом бөлүшүлүүдө"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"Купуя"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Жеке көрүнүш"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Жумуш көрүнүшү"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Купуя көрүнүш"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> колдонмосу жеке профилде ачылсынбы?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"<xliff:g id="APP">%s</xliff:g> колдонмосу жумуш профилинде ачылсынбы?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Жеке серепчини колдонуу"</string>
diff --git a/java/res/values-lo/strings.xml b/java/res/values-lo/strings.xml
index 48e9a074..314a3b05 100644
--- a/java/res/values-lo/strings.xml
+++ b/java/res/values-lo/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"ກຳລັງແບ່ງປັນອະລະບ້ຳ"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"ສ່ວນຕົວ"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ມຸມມອງສ່ວນຕົວ"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"ມຸມມອງວຽກ"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ມຸມມອງສ່ວນຕົວ"</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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"ບໍ່ມີແອັບສ່ວນຕົວ"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"ເປີດ <xliff:g id="APP">%s</xliff:g> ໃນໂປຣໄຟລ໌ສ່ວນຕົວຂອງທ່ານບໍ?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"ເປີດ <xliff:g id="APP">%s</xliff:g> ໃນ​ໂປຣ​ໄຟລ໌​ບ່ອນ​ເຮັດ​ວຽກຂອງທ່ານບໍ?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"ໃຊ້ໂປຣແກຣມທ່ອງເວັບສ່ວນຕົວ"</string>
diff --git a/java/res/values-lt/strings.xml b/java/res/values-lt/strings.xml
index 51ffbbff..bfec820a 100644
--- a/java/res/values-lt/strings.xml
+++ b/java/res/values-lt/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Bendrinamas albumas"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privatus"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Asmeninė peržiūra"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Darbo peržiūra"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privatus rodinys"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Užblokavo jūsų IT administratorius"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Šio turinio negalima bendrinti su darbo programomis"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Šio turinio negalima atidaryti naudojant darbo programas"</string>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"Nėra privačių programų"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Atidaryti „<xliff:g id="APP">%s</xliff:g>“ asmeniniame profilyje?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Atidaryti „<xliff:g id="APP">%s</xliff:g>“ darbo profilyje?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Naudoti asmeninę naršyklę"</string>
diff --git a/java/res/values-lv/strings.xml b/java/res/values-lv/strings.xml
index de5c352b..e405b66a 100644
--- a/java/res/values-lv/strings.xml
+++ b/java/res/values-lv/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Notiek albuma kopīgošana"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privāts"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personisks skats"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Darba skats"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privātais skats"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bloķējis jūsu IT administrators"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Šo saturu nevar kopīgot ar darba lietotnēm"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Šo saturu nevar atvērt darba lietotnēs"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<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>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Vai atvērt lietotni <xliff:g id="APP">%s</xliff:g> jūsu darba profilā?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Izmantot personīgo pārlūku"</string>
diff --git a/java/res/values-mk/strings.xml b/java/res/values-mk/strings.xml
index 7ef3a9ca..df46dc98 100644
--- a/java/res/values-mk/strings.xml
+++ b/java/res/values-mk/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Споделување албум"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"Приватно"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Личен приказ"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Работен приказ"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Приватен приказ"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Да се отвори <xliff:g id="APP">%s</xliff:g> во личниот профил?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Да се отвори <xliff:g id="APP">%s</xliff:g> во работниот профил?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Користи личен прелистувач"</string>
diff --git a/java/res/values-ml/strings.xml b/java/res/values-ml/strings.xml
index 03b01db9..90eb4bf7 100644
--- a/java/res/values-ml/strings.xml
+++ b/java/res/values-ml/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"ആൽബം പങ്കിടൽ"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"സ്വകാര്യം"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"വ്യക്തിപര കാഴ്‌ച"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"ഔദ്യോഗിക കാഴ്‌ച"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"സ്വകാര്യ കാഴ്ച"</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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"സ്വകാര്യ ആപ്പുകൾ ഇല്ല"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g>, നിങ്ങളുടെ വ്യക്തിപരമായ പ്രൊഫൈലിൽ തുറക്കണോ?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"<xliff:g id="APP">%s</xliff:g>, നിങ്ങളുടെ ഔദ്യോഗിക പ്രൊഫൈലിൽ തുറക്കണോ?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"വ്യക്തിപരമായ ബ്രൗസർ ഉപയോഗിക്കുക"</string>
diff --git a/java/res/values-mn/strings.xml b/java/res/values-mn/strings.xml
index 339ca5e4..469afa50 100644
--- a/java/res/values-mn/strings.xml
+++ b/java/res/values-mn/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Цомог хуваалцаж байна"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"Хувийн"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Хувийн харагдах байдал"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Ажлын харагдах байдал"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Хувийн харагдах байдал"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Хувийн профайл дээрээ <xliff:g id="APP">%s</xliff:g>-г нээх үү?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Ажлын профайл дээрээ <xliff:g id="APP">%s</xliff:g>-г нээх үү?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Хувийн хөтөч ашиглах"</string>
diff --git a/java/res/values-mr/strings.xml b/java/res/values-mr/strings.xml
index 5202a3b7..c4c0818c 100644
--- a/java/res/values-mr/strings.xml
+++ b/java/res/values-mr/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"अल्बम शेअर करत आहे"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"खाजगी"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"वैयक्तिक दृश्य"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"कार्य दृश्य"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"खाजगी व्ह्यू"</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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"कोणतीही खाजगी अ‍ॅप्स नाहीत"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"तुमच्या वैयक्तिक प्रोफाइलमध्ये <xliff:g id="APP">%s</xliff:g> उघडायचे आहे का?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"तुमच्या कार्य प्रोफाइलमध्ये <xliff:g id="APP">%s</xliff:g> उघडायचे आहे का?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"वैयक्तिक ब्राउझर वापरा"</string>
diff --git a/java/res/values-ms/strings.xml b/java/res/values-ms/strings.xml
index f1ac4d1d..b5e99492 100644
--- a/java/res/values-ms/strings.xml
+++ b/java/res/values-ms/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Berkongsi album"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Peribadi"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Paparan peribadi"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Paparan kerja"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Paparan peribadi"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Disekat oleh pentadbir IT anda"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Kandungan ini tidak boleh dikongsi dengan apl kerja"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Kandungan ini tidak boleh dibuka dengan apl kerja"</string>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"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>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Buka <xliff:g id="APP">%s</xliff:g> dalam profil kerja anda?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Gunakan penyemak imbas peribadi"</string>
diff --git a/java/res/values-my/strings.xml b/java/res/values-my/strings.xml
index c3ab1ee2..475a755f 100644
--- a/java/res/values-my/strings.xml
+++ b/java/res/values-my/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"အယ်လ်ဘမ် မျှဝေနေသည်"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"သီးသန့်"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ပုဂ္ဂိုလ်ရေးဆိုင်ရာ မြင်ကွင်း"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"အလုပ် မြင်ကွင်း"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"သီးသန့်ပြသခြင်း"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> ကို သင့်ကိုယ်ပိုင်ပရိုဖိုင်တွင် ဖွင့်မလား။"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"<xliff:g id="APP">%s</xliff:g> ကို သင့်အလုပ်ပရိုဖိုင်တွင် ဖွင့်မလား။"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"ကိုယ်ပိုင်ဘရောင်ဇာ သုံးရန်"</string>
diff --git a/java/res/values-nb/strings.xml b/java/res/values-nb/strings.xml
index a2c6da68..e455a2b6 100644
--- a/java/res/values-nb/strings.xml
+++ b/java/res/values-nb/strings.xml
@@ -55,7 +55,7 @@
<string name="screenshot_edit" msgid="3857183660047569146">"Endre"</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_text" msgid="8137537443603304062">"Deler tekst"</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>
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Deler album"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privat"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personlig visning"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Jobbvisning"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privat visning"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokkert av IT-administratoren din"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Dette innholdet kan ikke deles med jobbapper"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Dette innholdet kan ikke åpnes med jobbapper"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vil du åpne <xliff:g id="APP">%s</xliff:g> i den personlige profilen din?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Vil du åpne <xliff:g id="APP">%s</xliff:g> i jobbprofilen din?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Bruk den personlige nettleseren"</string>
diff --git a/java/res/values-ne/strings.xml b/java/res/values-ne/strings.xml
index 176067f2..614ecfe5 100644
--- a/java/res/values-ne/strings.xml
+++ b/java/res/values-ne/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"एल्बम सेयर गरिँदै छ"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"निजी"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"व्यक्तिगत दृश्य"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"कार्य दृश्य"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"निजी भ्यू"</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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"कुनै पनि निजी एप छैन"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> तपाईंको व्यक्तिगत प्रोफाइलमा खोल्ने हो?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"<xliff:g id="APP">%s</xliff:g> तपाईंको कार्य प्रोफाइलमा खोल्ने हो?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"व्यक्तिगत ब्राउजर प्रयोग गर्नुहोस्"</string>
diff --git a/java/res/values-nl/strings.xml b/java/res/values-nl/strings.xml
index 7ef1513b..70832458 100644
--- a/java/res/values-nl/strings.xml
+++ b/java/res/values-nl/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Album wordt gedeeld"</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>
@@ -76,17 +77,20 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privé"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Persoonlijke weergave"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Werkweergave"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privéweergave"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Geblokkeerd door je IT-beheerder"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Deze content kan niet worden gedeeld met werk-apps"</string>
<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="7115260573975624516">"Werk-apps zijn onderbroken"</string>
+ <string name="resolver_turn_on_work_apps" msgid="7115260573975624516">"Werk-apps zijn gepauzeerd"</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="resolver_no_private_apps_available" msgid="4164473548027417456">"Geen privé-apps"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> openen in je persoonlijke profiel?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"<xliff:g id="APP">%s</xliff:g> openen in je werkprofiel?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Persoonlijke browser gebruiken"</string>
diff --git a/java/res/values-or/strings.xml b/java/res/values-or/strings.xml
index 93c60db2..9d36c473 100644
--- a/java/res/values-or/strings.xml
+++ b/java/res/values-or/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"ଆଲବମ ସେୟାର କରାଯାଉଛି"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"ପ୍ରାଇଭେଟ"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ବ୍ୟକ୍ତିଗତ ଭ୍ୟୁ"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"ୱାର୍କ ଭ୍ୟୁ"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ପ୍ରାଇଭେଟ ଭ୍ୟୁ"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g>କୁ ଆପଣଙ୍କ ବ୍ୟକ୍ତିଗତ ପ୍ରୋଫାଇଲରେ ଖୋଲିବେ?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"<xliff:g id="APP">%s</xliff:g>କୁ ଆପଣଙ୍କ ୱାର୍କ ପ୍ରୋଫାଇଲରେ ଖୋଲିବେ?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"ବ୍ୟକ୍ତିଗତ ବ୍ରାଉଜର୍ ବ୍ୟବହାର କରନ୍ତୁ"</string>
diff --git a/java/res/values-pa/strings.xml b/java/res/values-pa/strings.xml
index 872168d6..60a9c0f5 100644
--- a/java/res/values-pa/strings.xml
+++ b/java/res/values-pa/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"ਐਲਬਮ ਨੂੰ ਸਾਂਝਾ ਕੀਤਾ ਜਾ ਰਿਹਾ ਹੈ"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"ਨਿੱਜੀ"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ਵਿਅਕਤੀਗਤ ਦ੍ਰਿਸ਼"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"ਕਾਰਜ ਦ੍ਰਿਸ਼"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ਨਿੱਜੀ ਦ੍ਰਿਸ਼"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"ਕੀ ਆਪਣੇ ਨਿੱਜੀ ਪ੍ਰੋਫਾਈਲ ਵਿੱਚ <xliff:g id="APP">%s</xliff:g> ਨੂੰ ਖੋਲ੍ਹਣਾ ਹੈ?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"ਕੀ ਆਪਣੇ ਕਾਰਜ ਪ੍ਰੋਫਾਈਲ ਵਿੱਚ <xliff:g id="APP">%s</xliff:g> ਨੂੰ ਖੋਲ੍ਹਣਾ ਹੈ?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"ਨਿੱਜੀ ਬ੍ਰਾਊਜ਼ਰ ਵਰਤੋ"</string>
diff --git a/java/res/values-pl/strings.xml b/java/res/values-pl/strings.xml
index 40fe5860..48c1ca28 100644
--- a/java/res/values-pl/strings.xml
+++ b/java/res/values-pl/strings.xml
@@ -57,7 +57,7 @@
<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_images" msgid="5251443722186962006">"{count,plural, =1{Udostępniam obraz}few{Udostępniam # obrazy}many{Udostępniam # obrazów}other{Udostępniam # 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_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>
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Udostępnij album"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Prywatna"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Widok osobisty"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Widok służbowy"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Widok prywatny"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Działanie zablokowane przez administratora IT"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Tych treści nie można udostępniać w aplikacjach służbowych"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Tych treści nie można otworzyć w aplikacjach służbowych"</string>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"Brak prywatnych aplikacji"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Otworzyć aplikację <xliff:g id="APP">%s</xliff:g> w profilu osobistym?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Otworzyć aplikację <xliff:g id="APP">%s</xliff:g> w profilu służbowym?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Użyj przeglądarki osobistej"</string>
diff --git a/java/res/values-pt-rBR/strings.xml b/java/res/values-pt-rBR/strings.xml
index ec52fd28..665de8b6 100644
--- a/java/res/values-pt-rBR/strings.xml
+++ b/java/res/values-pt-rBR/strings.xml
@@ -57,7 +57,7 @@
<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_images" msgid="5251443722186962006">"{count,plural, =1{Compartilhar imagem}one{Compartilhar # imagem}many{Compartilhar # de imagens}other{Compartilhar # 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_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>
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Compartilhando álbum"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privado"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Visualização pessoal"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Visualização de trabalho"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Visualização particular"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Compartilhamento bloqueado pelo administrador de TI"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Não é possível compartilhar esse conteúdo com apps de trabalho"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Não é possível abrir esse conteúdo com apps de trabalho"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Abrir o app <xliff:g id="APP">%s</xliff:g> no seu perfil pessoal?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Abrir o app <xliff:g id="APP">%s</xliff:g> no seu perfil de trabalho?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Usar o navegador pessoal"</string>
diff --git a/java/res/values-pt-rPT/strings.xml b/java/res/values-pt-rPT/strings.xml
index c60b923b..08694c9d 100644
--- a/java/res/values-pt-rPT/strings.xml
+++ b/java/res/values-pt-rPT/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Partilhar álbum"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privada"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Vista pessoal"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Vista de trabalho"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Vista privada"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bloqueado pelo administrador de TI"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Não é possível partilhar este conteúdo com apps de trabalho"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Não é possível abrir este conteúdo com apps de trabalho"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Abrir a app <xliff:g id="APP">%s</xliff:g> no seu perfil pessoal?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Abrir a app <xliff:g id="APP">%s</xliff:g> no seu perfil de trabalho?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Usar navegador pessoal"</string>
diff --git a/java/res/values-pt/strings.xml b/java/res/values-pt/strings.xml
index ec52fd28..665de8b6 100644
--- a/java/res/values-pt/strings.xml
+++ b/java/res/values-pt/strings.xml
@@ -57,7 +57,7 @@
<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_images" msgid="5251443722186962006">"{count,plural, =1{Compartilhar imagem}one{Compartilhar # imagem}many{Compartilhar # de imagens}other{Compartilhar # 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_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>
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Compartilhando álbum"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privado"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Visualização pessoal"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Visualização de trabalho"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Visualização particular"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Compartilhamento bloqueado pelo administrador de TI"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Não é possível compartilhar esse conteúdo com apps de trabalho"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Não é possível abrir esse conteúdo com apps de trabalho"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Abrir o app <xliff:g id="APP">%s</xliff:g> no seu perfil pessoal?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Abrir o app <xliff:g id="APP">%s</xliff:g> no seu perfil de trabalho?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Usar o navegador pessoal"</string>
diff --git a/java/res/values-ro/strings.xml b/java/res/values-ro/strings.xml
index d6cae158..8620e2a5 100644
--- a/java/res/values-ro/strings.xml
+++ b/java/res/values-ro/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Se permite accesul la album"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privat"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Afișarea conținutului personal"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Afișarea conținutului de lucru"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Secțiunea Privat"</string>
<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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Deschizi <xliff:g id="APP">%s</xliff:g> în profilul personal?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Deschizi <xliff:g id="APP">%s</xliff:g> în profilul de serviciu?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Folosește browserul personal"</string>
diff --git a/java/res/values-ru/strings.xml b/java/res/values-ru/strings.xml
index 618e0a6f..ca852709 100644
--- a/java/res/values-ru/strings.xml
+++ b/java/res/values-ru/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Вы делитесь альбомом"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"Личное пространство"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Просмотр личных данных"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Просмотр рабочих данных"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Личное пространство"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Открыть приложение \"<xliff:g id="APP">%s</xliff:g>\" в личном профиле?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Открыть приложение \"<xliff:g id="APP">%s</xliff:g>\" в рабочем профиле?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Использовать личный браузер"</string>
diff --git a/java/res/values-si/strings.xml b/java/res/values-si/strings.xml
index 176206e8..f79f70d3 100644
--- a/java/res/values-si/strings.xml
+++ b/java/res/values-si/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"ඇල්බමය බෙදා ගැනීම"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"පෞද්ගලික"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"පෞද්ගලික දසුන"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"කාර්යාල දසුන"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"පෞද්ගලික දසුන"</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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"පුද්ගලික යෙදුම් නැත"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> ඔබගේ පුද්ගලික පැතිකඩ තුළ විවෘත කරන්නද?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"<xliff:g id="APP">%s</xliff:g> ඔබගේ කාර්යාල පැතිකඩ තුළ විවෘත කරන්නද?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"පුද්ගලික බ්‍රව්සරය භාවිත කරන්න"</string>
diff --git a/java/res/values-sk/strings.xml b/java/res/values-sk/strings.xml
index 1ac43e60..e87800d4 100644
--- a/java/res/values-sk/strings.xml
+++ b/java/res/values-sk/strings.xml
@@ -55,9 +55,9 @@
<string name="screenshot_edit" msgid="3857183660047569146">"Upraviť"</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_text" msgid="8137537443603304062">"Zdieľanie textu"</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_images" msgid="5251443722186962006">"{count,plural, =1{Zdieľanie obrázku}few{Zdieľanie # obrázkov}many{Sharing # images}other{Zdieľanie # 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_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>
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Zdieľanie albumu"</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>
@@ -76,8 +77,10 @@
<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">"Pracovné"</string>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Súkromné"</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_private_tab_accessibility" msgid="2513122834337197252">"Súkromné zobrazenie"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokované vaším správcom IT"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Tento obsah sa nedá zdieľať pomocou pracovných aplikácií"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Tento obsah sa nedá otvoriť pomocou pracovných aplikácií"</string>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"Žiadne súkromné aplikácie"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Chcete otvoriť <xliff:g id="APP">%s</xliff:g> v osobnom profile?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Chcete otvoriť <xliff:g id="APP">%s</xliff:g> v pracovnom profile?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Použiť osobný prehliadač"</string>
diff --git a/java/res/values-sl/strings.xml b/java/res/values-sl/strings.xml
index 0ef88727..29ff09e0 100644
--- a/java/res/values-sl/strings.xml
+++ b/java/res/values-sl/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Deljenje albuma"</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>
@@ -76,8 +77,10 @@
<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">"Delo"</string>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Zasebno"</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_private_tab_accessibility" msgid="2513122834337197252">"Zasebni pogled"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blokiral skrbnik za IT"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Te vsebine ni mogoče deliti z delovnimi aplikacijami."</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Te vsebine ni mogoče odpreti z delovnimi aplikacijami."</string>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"Ni zasebnih aplikacij"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Želite aplikacijo <xliff:g id="APP">%s</xliff:g> odpreti v osebnem profilu?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Želite aplikacijo <xliff:g id="APP">%s</xliff:g> odpreti v delovnem profilu?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Uporabi osebni brskalnik"</string>
diff --git a/java/res/values-sq/strings.xml b/java/res/values-sq/strings.xml
index 95c3e57c..8043a15c 100644
--- a/java/res/values-sq/strings.xml
+++ b/java/res/values-sq/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Albumi po ndahet"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"Private"</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_private_tab_accessibility" msgid="2513122834337197252">"Pamja private"</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ë 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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Të hapet <xliff:g id="APP">%s</xliff:g> në profilin tënd personal?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Të hapet <xliff:g id="APP">%s</xliff:g> në profilin tënd të punës?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Përdor shfletuesin personal"</string>
diff --git a/java/res/values-sr/strings.xml b/java/res/values-sr/strings.xml
index 511a1293..0359c894 100644
--- a/java/res/values-sr/strings.xml
+++ b/java/res/values-sr/strings.xml
@@ -57,7 +57,7 @@
<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_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_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>
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Дељени албум"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"Приватно"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Лични приказ"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Приказ за посао"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Приватни приказ"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Желите да на личном профилу отворите: <xliff:g id="APP">%s</xliff:g>?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Желите да на пословном профилу отворите: <xliff:g id="APP">%s</xliff:g>?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Користи лични прегледач"</string>
diff --git a/java/res/values-sv/strings.xml b/java/res/values-sv/strings.xml
index 7ed2d3f1..a459f69c 100644
--- a/java/res/values-sv/strings.xml
+++ b/java/res/values-sv/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Delar album"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Privat"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personlig vy"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Jobbvy"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Privat vy"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Blockeras av IT-administratören"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Det här innehållet kan inte delas med jobbappar"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Det här innehållet kan inte öppnas med jobbappar"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vill du öppna <xliff:g id="APP">%s</xliff:g> i din privata profil?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Vill du öppna <xliff:g id="APP">%s</xliff:g> i din jobbprofil?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Använd privat webbläsare"</string>
diff --git a/java/res/values-sw/strings.xml b/java/res/values-sw/strings.xml
index de45a78c..63dabd19 100644
--- a/java/res/values-sw/strings.xml
+++ b/java/res/values-sw/strings.xml
@@ -55,7 +55,7 @@
<string name="screenshot_edit" msgid="3857183660047569146">"Badilisha"</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_text" msgid="8137537443603304062">"Kutuma 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>
@@ -66,18 +66,21 @@
<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_album" msgid="191743129899503345">"Kutumia albamu pamoja"</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_no_direct_share_targets" msgid="4233416657754261844">"Hujapendekezewa watu wa kuwatumia"</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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Wa faragha"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Mwonekano wa binafsi"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Mwonekano wa kazini"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Mwonekano wa faragha"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Imezuiwa na msimamizi wako wa Tehama"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Huwezi kushiriki maudhui haya na programu za kazini"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Huwezi kufungua maudhui haya ukitumia programu za kazini"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Je, unataka kufungua <xliff:g id="APP">%s</xliff:g> katika wasifu wako binafsi?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Je, unataka kufungua <xliff:g id="APP">%s</xliff:g> katika wasifu wako wa kazi?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Tumia kivinjari cha binafsi"</string>
diff --git a/java/res/values-ta/strings.xml b/java/res/values-ta/strings.xml
index c95e5cb1..dcddcf0c 100644
--- a/java/res/values-ta/strings.xml
+++ b/java/res/values-ta/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"ஆல்பத்தைப் பகிர்தல்"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"ரகசியம்"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"தனிப்பட்ட காட்சி"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"பணிக் காட்சி"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ரகசியக் காட்சி"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"உங்கள் தனிப்பட்ட கணக்கில் <xliff:g id="APP">%s</xliff:g> ஆப்ஸைத் திறக்கவா?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"உங்கள் பணிக் கணக்கில் <xliff:g id="APP">%s</xliff:g> ஆப்ஸைத் திறக்கவா?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"தனிப்பட்ட உலாவியைப் பயன்படுத்து"</string>
diff --git a/java/res/values-te/strings.xml b/java/res/values-te/strings.xml
index a8b9457a..73dc3a1c 100644
--- a/java/res/values-te/strings.xml
+++ b/java/res/values-te/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>
@@ -57,7 +57,7 @@
<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_files" msgid="1275646542246028823">"{count,plural, =1{# ఫైల్‌ను షేర్ చేస్తోంది}other{# ఫైళ్లను షేర్ చేస్తోంది}}"</string>
<string name="sharing_images_with_text" msgid="9005717434461730242">"{count,plural, =1{టెక్స్ట్ మెసేజ్ పంపడం ద్వారా ఇమేజ్‌ను షేర్ చేయడం}other{టెక్స్ట్ మెసేజ్ పంపడం ద్వారా # ఇమేజ్‌లను షేర్ చేయడం}}"</string>
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"ఆల్బమ్ షేర్ చేయబడుతోంది"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"ప్రైవేట్"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"వ్యక్తిగత వీక్షణ"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"పని వీక్షణ"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"ప్రైవేట్ వీక్షణ"</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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"ప్రైవేట్ యాప్‌లు ఏవీ లేవు"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g>ను మీ వ్యక్తిగత ప్రొఫైల్‌లో తెరవాలా?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"<xliff:g id="APP">%s</xliff:g>ను మీ వర్క్ ప్రొఫైల్‌లో తెరవాలా?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"వ్యక్తిగత బ్రౌజర్‌ను ఉపయోగించండి"</string>
diff --git a/java/res/values-th/strings.xml b/java/res/values-th/strings.xml
index af91064b..2deef229 100644
--- a/java/res/values-th/strings.xml
+++ b/java/res/values-th/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"กำลังแชร์อัลบั้ม"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"ส่วนตัว"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"มุมมองส่วนตัว"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"ดูงาน"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"มุมมองส่วนตัว"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"เปิด <xliff:g id="APP">%s</xliff:g> ในโปรไฟล์ส่วนตัวไหม"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"เปิด <xliff:g id="APP">%s</xliff:g> ในโปรไฟล์งานไหม"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"ใช้เบราว์เซอร์ส่วนตัว"</string>
diff --git a/java/res/values-tl/strings.xml b/java/res/values-tl/strings.xml
index cb4ff654..ccf43d7b 100644
--- a/java/res/values-tl/strings.xml
+++ b/java/res/values-tl/strings.xml
@@ -32,7 +32,7 @@
<string name="whichEditApplicationLabel" msgid="5992662938338600364">"I-edit"</string>
<string name="whichSendApplication" msgid="59510564281035884">"Ibahagi"</string>
<string name="whichSendApplicationNamed" msgid="495577664218765855">"Ibahagi gamit ang <xliff:g id="APP">%1$s</xliff:g>"</string>
- <string name="whichSendApplicationLabel" msgid="2391198069286568035">"Ibahagi"</string>
+ <string name="whichSendApplicationLabel" msgid="2391198069286568035">"I-share"</string>
<string name="whichSendToApplication" msgid="2724450540348806267">"Ipadala gamit ang"</string>
<string name="whichSendToApplicationNamed" msgid="1996548940365954543">"Ipadala gamit ang <xliff:g id="APP">%1$s</xliff:g>"</string>
<string name="whichSendToApplicationLabel" msgid="6909037198280591110">"Ipadala"</string>
@@ -57,7 +57,7 @@
<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_images" msgid="5251443722186962006">"{count,plural, =1{Shine-share ang larawan}one{Shine-share ang # larawan}other{Shine-share 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_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>
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Ibinabahagi ang album"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Pribado"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Personal na view"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"View ng trabaho"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Pribadong view"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Na-block ng iyong IT admin"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Hindi puwedeng ibahagi sa mga app para sa trabaho ang content na ito"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Hindi puwedeng buksan sa mga app para sa trabaho ang content na ito"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Buksan ang <xliff:g id="APP">%s</xliff:g> sa iyong personal na profile?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Buksan ang <xliff:g id="APP">%s</xliff:g> sa iyong profile sa trabaho?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Gamitin ang personal na browser"</string>
diff --git a/java/res/values-tr/strings.xml b/java/res/values-tr/strings.xml
index 53d74bb9..e671cf89 100644
--- a/java/res/values-tr/strings.xml
+++ b/java/res/values-tr/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Albüm 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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Gizli"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Kişisel görünüm"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"İş görünümü"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Gizli görünüm"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"BT yöneticiniz tarafından engellendi"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Bu içerik, iş uygulamalarıyla paylaşılamaz"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Bu içerik, iş uygulamalarıyla açılamaz"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> uygulaması kişisel profilinizde açılsın mı?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"<xliff:g id="APP">%s</xliff:g> uygulaması iş profilinizde açılsın mı?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Kişisel tarayıcıyı kullan"</string>
diff --git a/java/res/values-uk/strings.xml b/java/res/values-uk/strings.xml
index f9d810af..90ca8213 100644
--- a/java/res/values-uk/strings.xml
+++ b/java/res/values-uk/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Надання спільного доступу до альбома"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"Приватний простір"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Особистий перегляд"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Робочий перегляд"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Приватний перегляд"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Відкрити додаток <xliff:g id="APP">%s</xliff:g> в особистому профілі?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Відкрити додаток <xliff:g id="APP">%s</xliff:g> у робочому профілі?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Використати особистий веб-переглядач"</string>
diff --git a/java/res/values-ur/strings.xml b/java/res/values-ur/strings.xml
index 6a101d98..252e87e9 100644
--- a/java/res/values-ur/strings.xml
+++ b/java/res/values-ur/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"البم کا اشتراک کیا جا رہا ہے"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"نجی"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"ذاتی ملاحظہ"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"دفتری ملاحظہ"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"نجی ملاحظہ"</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>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"کوئی نجی ایپ نہیں ہے"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"اپنی ذاتی پروفائل میں <xliff:g id="APP">%s</xliff:g> کھولیں؟"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"اپنی دفتری پروفائل میں <xliff:g id="APP">%s</xliff:g> کھولیں؟"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"ذاتی براؤزر استعمال کریں"</string>
diff --git a/java/res/values-uz/strings.xml b/java/res/values-uz/strings.xml
index 24249f50..482f0a90 100644
--- a/java/res/values-uz/strings.xml
+++ b/java/res/values-uz/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Albom ulashilmoqda"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Maxfiy"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Shaxsiy rejim"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Ishchi rejim"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Maxfiy rejim"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Administratoringiz tomonidan bloklangan"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Bu kontent ishga oid ilovalar bilan ulashilmaydi"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Bu kontent ishga oid ilovalar bilan ochilmaydi"</string>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"<xliff:g id="APP">%s</xliff:g> shaxsiy profilda ochilsinmi?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"<xliff:g id="APP">%s</xliff:g> shaxsiy profilda ochilsinmi?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Shaxsiy brauzerdan foydalanish"</string>
diff --git a/java/res/values-vi/strings.xml b/java/res/values-vi/strings.xml
index b08d9a3a..beacc185 100644
--- a/java/res/values-vi/strings.xml
+++ b/java/res/values-vi/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"Chia sẻ album"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Riêng tư"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Chế độ xem cá nhân"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Chế độ xem công việc"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Chế độ xem riêng tư"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Bị quản trị viên CNTT chặn"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Bạn không thể chia sẻ nội dung này bằng ứng dụng công việc"</string>
<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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<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>
diff --git a/java/res/values-zh-rCN/strings.xml b/java/res/values-zh-rCN/strings.xml
index e208e106..afe104b4 100644
--- a/java/res/values-zh-rCN/strings.xml
+++ b/java/res/values-zh-rCN/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"分享影集"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"私密"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"个人视图"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"工作视图"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"私密视图"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"要使用个人资料打开 <xliff:g id="APP">%s</xliff:g> 吗?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"要使用工作资料打开 <xliff:g id="APP">%s</xliff:g> 吗?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"使用个人浏览器"</string>
diff --git a/java/res/values-zh-rHK/strings.xml b/java/res/values-zh-rHK/strings.xml
index 837b1587..e65b6dc8 100644
--- a/java/res/values-zh-rHK/strings.xml
+++ b/java/res/values-zh-rHK/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"共享相簿"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"私人"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"個人檢視模式"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"工作檢視模式"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"私人檢視模式"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"要在個人設定檔中開啟「<xliff:g id="APP">%s</xliff:g>」嗎?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"要在工作設定檔中開啟「<xliff:g id="APP">%s</xliff:g>」嗎?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"使用個人瀏覽器"</string>
diff --git a/java/res/values-zh-rTW/strings.xml b/java/res/values-zh-rTW/strings.xml
index 0fddc70e..f90ef68b 100644
--- a/java/res/values-zh-rTW/strings.xml
+++ b/java/res/values-zh-rTW/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"共享相簿"</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>
@@ -76,8 +77,10 @@
<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_private_tab" msgid="3707548826254095157">"私人"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"個人檢視模式"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"工作檢視模式"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"私人檢視模式"</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>
@@ -87,6 +90,8 @@
<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>
+ <!-- no translation found for resolver_no_private_apps_available (4164473548027417456) -->
+ <skip />
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"要在個人資料夾中開啟「<xliff:g id="APP">%s</xliff:g>」嗎?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"要在工作資料夾中開啟「<xliff:g id="APP">%s</xliff:g>」嗎?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"使用個人瀏覽器"</string>
diff --git a/java/res/values-zu/strings.xml b/java/res/values-zu/strings.xml
index b651eb06..f053260c 100644
--- a/java/res/values-zu/strings.xml
+++ b/java/res/values-zu/strings.xml
@@ -66,6 +66,7 @@
<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_album" msgid="191743129899503345">"I-albhamu eyabiwe"</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>
@@ -76,8 +77,10 @@
<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>
+ <string name="resolver_private_tab" msgid="3707548826254095157">"Okuyimfihlo"</string>
<string name="resolver_personal_tab_accessibility" msgid="4467784352232582574">"Ukubuka komuntu siqu"</string>
<string name="resolver_work_tab_accessibility" msgid="7581878836587799920">"Ukubuka komsebenzi"</string>
+ <string name="resolver_private_tab_accessibility" msgid="2513122834337197252">"Ukubuka okuyimfihlo"</string>
<string name="resolver_cross_profile_blocked" msgid="3515194063758605377">"Kuvinjelwe umlawuli wakho we-IT"</string>
<string name="resolver_cant_share_with_work_apps_explanation" msgid="2984105853145456723">"Lokhu okuqukethwe akukwazi ukwabiwa nama-app womsebenzi"</string>
<string name="resolver_cant_access_work_apps_explanation" msgid="1463093773348988122">"Lokhu okuqukethwe akukwazi ukukopishwa ngama-app womsebenzi"</string>
@@ -87,6 +90,7 @@
<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="resolver_no_private_apps_available" msgid="4164473548027417456">"Awekho ama-app ayimfihlo"</string>
<string name="miniresolver_open_in_personal" msgid="8397377137465016575">"Vula i-<xliff:g id="APP">%s</xliff:g> kwiphrofayela yakho siqu?"</string>
<string name="miniresolver_open_in_work" msgid="4271638122142624693">"Vula i-<xliff:g id="APP">%s</xliff:g> kwiphrofayela yakho yomsebenzi?"</string>
<string name="miniresolver_use_personal_browser" msgid="1428911732509069292">"Sebenzisa isiphequluli somuntu siqu"</string>
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index 8843c81a..bd868c9f 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -19,7 +19,6 @@
<!-- chooser/resolver (sharesheet) spacing -->
<dimen name="chooser_action_corner_radius">28dp</dimen>
<dimen name="chooser_action_horizontal_margin">2dp</dimen>
- <dimen name="chooser_action_max_width">200dp</dimen>
<dimen name="chooser_width">450dp</dimen>
<dimen name="chooser_corner_radius">28dp</dimen>
<dimen name="chooser_corner_radius_small">14dp</dimen>
@@ -59,7 +58,7 @@
<!-- Note that the values in this section are for landscape phones. For screen configs taller
than 480dp, the values are set in values-h480dp/dimens.xml -->
<dimen name="chooser_preview_width">412dp</dimen>
- <dimen name="chooser_preview_image_height_tall">46dp</dimen>
+ <dimen name="chooser_preview_image_height_tall">124dp</dimen>
<dimen name="grid_padding">8dp</dimen>
<dimen name="width_text_image_preview_size">46dp</dimen>
<!-- END SECTION -->
diff --git a/java/res/values/integers.xml b/java/res/values/integers.xml
index 6d57e43e..8d203bca 100644
--- a/java/res/values/integers.xml
+++ b/java/res/values/integers.xml
@@ -17,5 +17,5 @@
<resources>
<!-- Note that this is the value for landscape phones, the value for all screens taller than
480dp is set in values-h480dp/integers.xml -->
- <integer name="text_preview_lines">1</integer>
+ <integer name="text_preview_lines">3</integer>
</resources>
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index 0c772573..17a514d7 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -210,6 +210,11 @@
}
</string>
+
+ <!-- Title atop a sharing UI indicating that an album (typically of photos/videos) is being
+ shared [CHAR_LIMIT=50] -->
+ <string name="sharing_album">Sharing album</string>
+
<!-- Message indicating that the attached text has been removed from this share and only the
images are being shared. [CHAR LIMIT=none] -->
<string name="sharing_images_only">{count, plural,
@@ -250,16 +255,20 @@
<!-- Prompt for the USB device resolver dialog with warning text for USB device dialogs. [CHAR LIMIT=200] -->
<string name="usb_device_resolve_prompt_warn">This app has not been granted record permission but could capture audio through this USB device.</string>
- <!-- ResolverActivity - profile tabs -->
+ <!-- ChooserActivity + ResolverActivity - profile tabs -->
<!-- Label of a tab on a screen. A user can tap this tap to switch to the 'Personal' view (that shows their personal content) if they have a work profile on their device. [CHAR LIMIT=NONE] -->
<string name="resolver_personal_tab">Personal</string>
<!-- Label of a tab on a screen. A user can tap this tab to switch to the 'Work' view (that shows their work content) if they have a work profile on their device. [CHAR LIMIT=NONE] -->
<string name="resolver_work_tab">Work</string>
+ <!-- Label of a tab on a screen. A user can tap this tab to switch to the 'Private' view (that shows their Private Space content) if they have private space configured on their device. [CHAR LIMIT=NONE] -->
+ <string name="resolver_private_tab">Private</string>
<!-- Accessibility label for the personal tab button. [CHAR LIMIT=NONE] -->
<string name="resolver_personal_tab_accessibility">Personal view</string>
<!-- Accessibility label for the work tab button. [CHAR LIMIT=NONE] -->
<string name="resolver_work_tab_accessibility">Work view</string>
+ <!-- Accessibility label for the private tab button. [CHAR LIMIT=NONE] -->
+ <string name="resolver_private_tab_accessibility">Private view</string>
<!-- Title of a screen. This text lets the user know that their IT admin doesn't allow them to share this content across profiles. [CHAR LIMIT=NONE] -->
<string name="resolver_cross_profile_blocked">Blocked by your IT admin</string>
@@ -286,6 +295,9 @@
<!-- Error message. This text lets the user know that their current personal apps don't support the specific content. [CHAR LIMIT=NONE] -->
<string name="resolver_no_personal_apps_available">No personal apps</string>
+ <!-- Error message. This text lets the user know that their current private apps don't support the specific content. [CHAR LIMIT=NONE] -->
+ <string name="resolver_no_private_apps_available">No private apps</string>
+
<!-- Dialog title. User must choose between opening content in a cross-profile app or same-profile browser. [CHAR LIMIT=NONE] -->
<string name="miniresolver_open_in_personal">Open <xliff:g id="app" example="YouTube">%s</xliff:g> in your personal profile?</string>
<!-- Dialog title. User must choose between opening content in a cross-profile app or same-profile browser. [CHAR LIMIT=NONE] -->
diff --git a/java/res/values/styles.xml b/java/res/values/styles.xml
index 0ccab4c0..143009d0 100644
--- a/java/res/values/styles.xml
+++ b/java/res/values/styles.xml
@@ -45,7 +45,7 @@
<style name="Theme.DeviceDefault.Chooser" parent="Theme.DeviceDefault.Resolver">
<item name="*android:iconfactoryIconSize">@dimen/chooser_icon_size</item>
<item name="*android:iconfactoryBadgeSize">@dimen/chooser_badge_size</item>
- <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
+ <item name="android:windowLayoutInDisplayCutoutMode">always</item>
</style>
<style name="TextAppearance.ChooserDefault"
diff --git a/java/src/com/android/intentresolver/AnnotatedUserHandles.java b/java/src/com/android/intentresolver/AnnotatedUserHandles.java
deleted file mode 100644
index 3565e757..00000000
--- a/java/src/com/android/intentresolver/AnnotatedUserHandles.java
+++ /dev/null
@@ -1,217 +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.app.Activity;
-import android.app.ActivityManager;
-import android.os.UserHandle;
-import android.os.UserManager;
-
-import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
-
-/**
- * Helper class to precompute the (immutable) designations of various user handles in the system
- * that may contribute to the current Sharesheet session.
- */
-public final class AnnotatedUserHandles {
- /** The user id of the app that started the share activity. */
- public final int userIdOfCallingApp;
-
- /**
- * 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
- * Sharesheet as a different user than they themselves were running as. Verify and document.
- */
- public final UserHandle userHandleSharesheetLaunchedAs;
-
- /**
- * The {@link UserHandle} that owns the "personal tab" in a tabbed share UI (or the *only* 'tab'
- * in a non-tabbed UI).
- *
- * This is never a work or clone user, but may either be the root user (0) or a "secondary"
- * multi-user profile (i.e., one that's not root, work, nor clone). This is a "secondary"
- * profile only when that user is the active "foreground" user.
- *
- * In the current implementation, we can assert that this is the root user (0) any time we
- * display a tabbed UI (i.e., any time `workProfileUserHandle` is non-null), or any time that we
- * have a clone profile. This note is only provided for informational purposes; clients should
- * avoid making any reliances on that assumption.
- */
- public final UserHandle personalProfileUserHandle;
-
- /**
- * 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}.
- */
- @Nullable
- public final UserHandle workProfileUserHandle;
-
- /**
- * 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}.
- *
- * 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
- * the "personal" profile owning that clone profile (which we currently know must belong to
- * user 0, but clients should avoid making any reliances on that assumption).
- */
- public final UserHandle tabOwnerUserHandleForLaunch;
-
- /** Compute all handle designations for a new Sharesheet session in the specified activity. */
- public static AnnotatedUserHandles forShareActivity(Activity shareActivity) {
- // TODO: consider integrating logic for `ResolverActivity.EXTRA_CALLING_USER`?
- UserHandle userHandleSharesheetLaunchedAs = UserHandle.of(UserHandle.myUserId());
-
- // ActivityManager.getCurrentUser() refers to the current Foreground user. When clone/work
- // profile is active, we always make the personal tab from the foreground user.
- // Outside profiles, current foreground user is potentially the same as the sharesheet
- // process's user (UserHandle.myUserId()), so we continue to create personal tab with the
- // current foreground user.
- UserHandle personalProfileUserHandle = UserHandle.of(ActivityManager.getCurrentUser());
-
- UserManager userManager = shareActivity.getSystemService(UserManager.class);
-
- return newBuilder()
- .setUserIdOfCallingApp(shareActivity.getLaunchedFromUid())
- .setUserHandleSharesheetLaunchedAs(userHandleSharesheetLaunchedAs)
- .setPersonalProfileUserHandle(personalProfileUserHandle)
- .setWorkProfileUserHandle(
- getWorkProfileForUser(userManager, personalProfileUserHandle))
- .setCloneProfileUserHandle(
- getCloneProfileForUser(userManager, personalProfileUserHandle))
- .build();
- }
-
- @VisibleForTesting public static Builder newBuilder() {
- return new Builder();
- }
-
- /**
- * Returns the {@link UserHandle} to use when querying resolutions for intents in a
- * {@link ResolverListController} configured for the provided {@code userHandle}.
- */
- public UserHandle getQueryIntentsUser(UserHandle userHandle) {
- // In case launching app is in clonedProfile, and we are building the personal tab, intent
- // resolution will be attempted as clonedUser instead of user 0. This is because intent
- // resolution from user 0 and clonedUser is not guaranteed to return same results.
- // We do not care about the case when personal adapter is started with non-root user
- // (secondary user case), as clone profile is guaranteed to be non-active in that case.
- UserHandle queryIntentsUser = userHandle;
- if (isLaunchedAsCloneProfile() && userHandle.equals(personalProfileUserHandle)) {
- queryIntentsUser = cloneProfileUserHandle;
- }
- return queryIntentsUser;
- }
-
- private Boolean isLaunchedAsCloneProfile() {
- return userHandleSharesheetLaunchedAs.equals(cloneProfileUserHandle);
- }
-
- private AnnotatedUserHandles(
- int userIdOfCallingApp,
- UserHandle userHandleSharesheetLaunchedAs,
- UserHandle personalProfileUserHandle,
- @Nullable UserHandle workProfileUserHandle,
- @Nullable UserHandle cloneProfileUserHandle) {
- if ((userIdOfCallingApp < 0) || UserHandle.isIsolated(userIdOfCallingApp)) {
- throw new SecurityException("Can't start a resolver from uid " + userIdOfCallingApp);
- }
-
- this.userIdOfCallingApp = userIdOfCallingApp;
- this.userHandleSharesheetLaunchedAs = userHandleSharesheetLaunchedAs;
- this.personalProfileUserHandle = personalProfileUserHandle;
- this.workProfileUserHandle = workProfileUserHandle;
- this.cloneProfileUserHandle = cloneProfileUserHandle;
- this.tabOwnerUserHandleForLaunch =
- (userHandleSharesheetLaunchedAs == workProfileUserHandle)
- ? workProfileUserHandle : personalProfileUserHandle;
- }
-
- @Nullable
- private static UserHandle getWorkProfileForUser(
- UserManager userManager, UserHandle profileOwnerUserHandle) {
- return userManager.getProfiles(profileOwnerUserHandle.getIdentifier())
- .stream()
- .filter(info -> info.isManagedProfile())
- .findFirst()
- .map(info -> info.getUserHandle())
- .orElse(null);
- }
-
- @Nullable
- private static UserHandle getCloneProfileForUser(
- UserManager userManager, UserHandle profileOwnerUserHandle) {
- return userManager.getProfiles(profileOwnerUserHandle.getIdentifier())
- .stream()
- .filter(info -> info.isCloneProfile())
- .findFirst()
- .map(info -> info.getUserHandle())
- .orElse(null);
- }
-
- @VisibleForTesting
- public static class Builder {
- private int mUserIdOfCallingApp;
- private UserHandle mUserHandleSharesheetLaunchedAs;
- private UserHandle mPersonalProfileUserHandle;
- private UserHandle mWorkProfileUserHandle;
- private UserHandle mCloneProfileUserHandle;
-
- public Builder setUserIdOfCallingApp(int id) {
- mUserIdOfCallingApp = id;
- return this;
- }
-
- public Builder setUserHandleSharesheetLaunchedAs(UserHandle user) {
- mUserHandleSharesheetLaunchedAs = user;
- return this;
- }
-
- public Builder setPersonalProfileUserHandle(UserHandle user) {
- mPersonalProfileUserHandle = user;
- return this;
- }
-
- public Builder setWorkProfileUserHandle(UserHandle user) {
- mWorkProfileUserHandle = user;
- return this;
- }
-
- public Builder setCloneProfileUserHandle(UserHandle user) {
- mCloneProfileUserHandle = user;
- return this;
- }
-
- public AnnotatedUserHandles build() {
- return new AnnotatedUserHandles(
- mUserIdOfCallingApp,
- mUserHandleSharesheetLaunchedAs,
- mPersonalProfileUserHandle,
- mWorkProfileUserHandle,
- mCloneProfileUserHandle);
- }
- }
-}
diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java
index 310fcc27..79998fbc 100644
--- a/java/src/com/android/intentresolver/ChooserActionFactory.java
+++ b/java/src/com/android/intentresolver/ChooserActionFactory.java
@@ -39,6 +39,8 @@ 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.ui.ShareResultSender;
+import com.android.intentresolver.ui.model.ShareAction;
import com.android.intentresolver.widget.ActionRow;
import com.android.internal.annotations.VisibleForTesting;
@@ -46,6 +48,7 @@ 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;
@@ -53,8 +56,11 @@ 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. */
+ /**
+ * 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
@@ -92,19 +98,17 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
private final Context mContext;
- @Nullable
- private final Runnable mCopyButtonRunnable;
- private final Runnable mEditButtonRunnable;
+ @Nullable private Runnable mCopyButtonRunnable;
+ @Nullable private Runnable mEditButtonRunnable;
private final ImmutableList<ChooserAction> mCustomActions;
- private final @Nullable ChooserAction mModifyShareAction;
private final Consumer<Boolean> mExcludeSharedTextAction;
+ @Nullable private final ShareResultSender mShareResultSender;
private final Consumer</* @Nullable */ Integer> mFinishCallback;
private final EventLog mLog;
/**
* @param context
- * @param chooserRequest data about the invocation of the current Sharesheet session.
- * device to implement the supported action types.
+ * @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
@@ -115,54 +119,74 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
*/
public ChooserActionFactory(
Context context,
- ChooserRequestParameters chooserRequest,
- ChooserIntegratedDeviceComponents integratedDeviceComponents,
+ Intent targetIntent,
+ String referrerPackageName,
+ List<ChooserAction> chooserActions,
+ Optional<ComponentName> imageEditor,
EventLog log,
Consumer<Boolean> onUpdateSharedTextIsExcluded,
Callable</* @Nullable */ View> firstVisibleImageQuery,
ActionActivityStarter activityStarter,
- Consumer</* @Nullable */ Integer> finishCallback) {
+ @Nullable ShareResultSender shareResultSender,
+ Consumer</* @Nullable */ Integer> finishCallback,
+ ClipboardManager clipboardManager) {
this(
context,
makeCopyButtonRunnable(
- context,
- chooserRequest.getTargetIntent(),
- chooserRequest.getReferrerPackageName(),
+ clipboardManager,
+ targetIntent,
+ referrerPackageName,
finishCallback,
log),
makeEditButtonRunnable(
getEditSharingTarget(
context,
- chooserRequest.getTargetIntent(),
- integratedDeviceComponents),
+ targetIntent,
+ imageEditor),
firstVisibleImageQuery,
activityStarter,
log),
- chooserRequest.getChooserActions(),
- chooserRequest.getModifyShareAction(),
+ chooserActions,
onUpdateSharedTextIsExcluded,
log,
+ shareResultSender,
finishCallback);
+
}
@VisibleForTesting
ChooserActionFactory(
Context context,
@Nullable Runnable copyButtonRunnable,
- Runnable editButtonRunnable,
+ @Nullable Runnable editButtonRunnable,
List<ChooserAction> customActions,
- @Nullable ChooserAction modifyShareAction,
Consumer<Boolean> onUpdateSharedTextIsExcluded,
EventLog log,
+ @Nullable ShareResultSender shareResultSender,
Consumer</* @Nullable */ Integer> finishCallback) {
mContext = context;
mCopyButtonRunnable = copyButtonRunnable;
mEditButtonRunnable = editButtonRunnable;
mCustomActions = ImmutableList.copyOf(customActions);
- mModifyShareAction = modifyShareAction;
mExcludeSharedTextAction = onUpdateSharedTextIsExcluded;
mLog = log;
+ mShareResultSender = shareResultSender;
mFinishCallback = finishCallback;
+
+ if (mShareResultSender != null) {
+ if (mEditButtonRunnable != null) {
+ mEditButtonRunnable = () -> {
+ mShareResultSender.onActionSelected(ShareAction.SYSTEM_EDIT);
+ editButtonRunnable.run();
+ };
+ }
+ if (mCopyButtonRunnable != null) {
+ mCopyButtonRunnable = () -> {
+ mShareResultSender.onActionSelected(ShareAction.SYSTEM_COPY);
+ copyButtonRunnable.run();
+ };
+ }
+ }
}
@Override
@@ -186,11 +210,9 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
ActionRow.Action actionRow = createCustomAction(
mContext,
mCustomActions.get(i),
- mFinishCallback,
- () -> {
- mLog.logCustomActionSelected(position);
- }
- );
+ () -> logCustomAction(position),
+ mShareResultSender,
+ mFinishCallback);
if (actionRow != null) {
actions.add(actionRow);
}
@@ -199,21 +221,6 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
}
/**
- * 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.
@@ -229,7 +236,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
@Nullable
private static Runnable makeCopyButtonRunnable(
- Context context,
+ ClipboardManager clipboardManager,
Intent targetIntent,
String referrerPackageName,
Consumer<Integer> finishCallback,
@@ -245,8 +252,6 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
return null;
}
return () -> {
- ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(
- Context.CLIPBOARD_SERVICE);
clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName);
log.logActionSelected(EventLog.SELECTION_TYPE_COPY);
@@ -278,18 +283,18 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
return clipData;
}
+ @Nullable
private static TargetInfo getEditSharingTarget(
Context context,
Intent originalIntent,
- ChooserIntegratedDeviceComponents integratedComponents) {
- final ComponentName editorComponent = integratedComponents.getEditSharingComponent();
+ 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);
- resolveIntent.setComponent(editorComponent);
+ imageEditor.ifPresent(resolveIntent::setComponent);
resolveIntent.setAction(Intent.ACTION_EDIT);
resolveIntent.putExtra(EDIT_SOURCE, EDIT_SOURCE_SHARESHEET);
String originalAction = originalIntent.getAction();
@@ -308,7 +313,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
final ResolveInfo ri = context.getPackageManager().resolveActivity(
resolveIntent, PackageManager.GET_META_DATA);
if (ri == null || ri.activityInfo == null) {
- Log.e(TAG, "Device-specified editor (" + editorComponent + ") not available");
+ Log.e(TAG, "Device-specified editor (" + imageEditor + ") not available");
return null;
}
@@ -323,11 +328,13 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
return dri;
}
+ @Nullable
private static Runnable makeEditButtonRunnable(
- TargetInfo editSharingTarget,
+ @Nullable TargetInfo editSharingTarget,
Callable</* @Nullable */ View> firstVisibleImageQuery,
ActionActivityStarter activityStarter,
EventLog log) {
+ if (editSharingTarget == null) return null;
return () -> {
// Log share completion via edit.
log.logActionSelected(EventLog.SELECTION_TYPE_EDIT);
@@ -347,12 +354,13 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
}
@Nullable
- private static ActionRow.Action createCustomAction(
+ static ActionRow.Action createCustomAction(
Context context,
- ChooserAction action,
- Consumer<Integer> finishCallback,
- Runnable loggingRunnable) {
- if (action == null || action.getAction() == null) {
+ @Nullable ChooserAction action,
+ Runnable loggingRunnable,
+ ShareResultSender shareResultSender,
+ Consumer</* @Nullable */ Integer> finishCallback) {
+ if (action == null) {
return null;
}
Drawable icon = action.getIcon().loadDrawable(context);
@@ -382,8 +390,15 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
if (loggingRunnable != null) {
loggingRunnable.run();
}
+ if (shareResultSender != null) {
+ shareResultSender.onActionSelected(ShareAction.APPLICATION_DEFINED);
+ }
finishCallback.accept(Activity.RESULT_OK);
}
);
}
+
+ void logCustomAction(int position) {
+ mLog.logCustomActionSelected(position);
+ }
}
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 9000ab3a..1922c05c 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2008 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,25 +16,37 @@
package com.android.intentresolver;
+import static android.app.VoiceInteractor.PickOptionRequest.Option;
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.content.Intent.FLAG_ACTIVITY_NEW_TASK;
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 androidx.lifecycle.LifecycleKt.getCoroutineScope;
+import static com.android.intentresolver.ext.CreationExtrasExtKt.addDefaultArgs;
+import static com.android.intentresolver.profiles.MultiProfilePagerAdapter.PROFILE_PERSONAL;
+import static com.android.intentresolver.profiles.MultiProfilePagerAdapter.PROFILE_WORK;
+import static com.android.intentresolver.ui.model.ActivityModel.ACTIVITY_MODEL_KEY;
import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
-import android.app.Activity;
+import static java.util.Objects.requireNonNull;
+
import android.app.ActivityManager;
import android.app.ActivityOptions;
+import android.app.ActivityThread;
+import android.app.VoiceInteractor;
+import android.app.admin.DevicePolicyEventLogger;
import android.app.prediction.AppPredictor;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.AppTargetId;
+import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
@@ -51,29 +63,42 @@ import android.database.Cursor;
import android.graphics.Insets;
import android.net.Uri;
import android.os.Bundle;
+import android.os.StrictMode;
import android.os.SystemClock;
+import android.os.Trace;
import android.os.UserHandle;
-import android.os.UserManager;
import android.service.chooser.ChooserTarget;
+import android.stats.devicepolicy.DevicePolicyEnums;
+import android.text.TextUtils;
import android.util.Log;
import android.util.Slog;
-import android.util.SparseArray;
+import android.view.Gravity;
+import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
+import android.view.Window;
import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.TabHost;
+import android.widget.TabWidget;
import android.widget.TextView;
+import android.widget.Toast;
-import androidx.annotation.IntDef;
import androidx.annotation.MainThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
import androidx.lifecycle.ViewModelProvider;
+import androidx.lifecycle.viewmodel.CreationExtras;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.ViewPager;
+import com.android.intentresolver.ChooserRefinementManager.RefinementType;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
@@ -81,40 +106,73 @@ 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.data.model.ChooserRequest;
+import com.android.intentresolver.data.repository.DevicePolicyResources;
+import com.android.intentresolver.domain.interactor.UserInteractor;
+import com.android.intentresolver.emptystate.CompositeEmptyStateProvider;
+import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
+import com.android.intentresolver.emptystate.DevicePolicyBlockerEmptyState;
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.grid.ChooserGridAdapter;
-import com.android.intentresolver.icons.DefaultTargetDataLoader;
import com.android.intentresolver.icons.TargetDataLoader;
+import com.android.intentresolver.inject.Background;
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.platform.AppPredictionAvailable;
+import com.android.intentresolver.platform.ImageEditor;
+import com.android.intentresolver.platform.NearbyShare;
+import com.android.intentresolver.profiles.ChooserMultiProfilePagerAdapter;
+import com.android.intentresolver.profiles.MultiProfilePagerAdapter.ProfileType;
+import com.android.intentresolver.profiles.OnProfileSelectedListener;
+import com.android.intentresolver.profiles.OnSwitchOnWorkSelectedListener;
+import com.android.intentresolver.profiles.TabConfig;
+import com.android.intentresolver.shared.model.Profile;
import com.android.intentresolver.shortcuts.AppPredictorFactory;
import com.android.intentresolver.shortcuts.ShortcutLoader;
+import com.android.intentresolver.ui.ActionTitle;
+import com.android.intentresolver.ui.ProfilePagerResources;
+import com.android.intentresolver.ui.ShareResultSender;
+import com.android.intentresolver.ui.ShareResultSenderFactory;
+import com.android.intentresolver.ui.model.ActivityModel;
+import com.android.intentresolver.ui.viewmodel.ChooserViewModel;
+import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.ImagePreviewView;
+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.MetricsEvent;
+import com.android.internal.util.LatencyTracker;
+
+import com.google.common.collect.ImmutableList;
import dagger.hilt.android.AndroidEntryPoint;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.text.Collator;
+import kotlin.Pair;
+
+import kotlinx.coroutines.CoroutineDispatcher;
+
import java.util.ArrayList;
+import java.util.Arrays;
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.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
+import java.util.function.Supplier;
import javax.inject.Inject;
@@ -123,9 +181,10 @@ import javax.inject.Inject;
* for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}.
*
*/
-@AndroidEntryPoint(ResolverActivity.class)
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+@AndroidEntryPoint(FragmentActivity.class)
public class ChooserActivity extends Hilt_ChooserActivity implements
- ResolverListAdapter.ResolverListCommunicator {
+ ResolverListAdapter.ResolverListCommunicator, PackagesChangedListener, StartsSelectedItem {
private static final String TAG = "ChooserActivity";
/**
@@ -139,7 +198,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
/**
* 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";
@@ -148,6 +206,39 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share";
private static final String SHORTCUT_TARGET = "shortcut_target";
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ // Inherited properties.
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ private static final String TAB_TAG_PERSONAL = "personal";
+ private static final String TAB_TAG_WORK = "work";
+
+ private static final String LAST_SHOWN_TAB_KEY = "last_shown_tab_key";
+ protected static final String METRICS_CATEGORY_CHOOSER = "intent_chooser";
+
+ private int mLayoutId;
+ private UserHandle mHeaderCreatorUser;
+ private boolean mRegistered;
+ private PackageMonitor mPersonalPackageMonitor;
+ private PackageMonitor mWorkPackageMonitor;
+
+ protected ResolverDrawerLayout mResolverDrawerLayout;
+ private TabHost mTabHost;
+ private ResolverViewPager mViewPager;
+ protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter;
+ protected final LatencyTracker mLatencyTracker = getLatencyTracker();
+
+ /** See {@link #setRetainInOnStop}. */
+ private boolean mRetainInOnStop;
+ protected Insets mSystemWindowInsets = null;
+ private ResolverActivity.PickTargetOptionRequest mPickOptionRequest;
+
+ @Nullable
+ private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
+
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////////////////////
+
+
// 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`.
@@ -156,37 +247,37 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
private final Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache = new HashMap<>();
private final Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache = new HashMap<>();
- public static final int TARGET_TYPE_DEFAULT = 0;
- public static final int TARGET_TYPE_CHOOSER_TARGET = 1;
- public static final int TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER = 2;
- public static final int TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE = 3;
+ static final int TARGET_TYPE_DEFAULT = 0;
+ static final int TARGET_TYPE_CHOOSER_TARGET = 1;
+ static final int TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER = 2;
+ 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;
- @IntDef({
- TARGET_TYPE_DEFAULT,
- TARGET_TYPE_CHOOSER_TARGET,
- TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER,
- TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
- })
- @Retention(RetentionPolicy.SOURCE)
- public @interface ShareTargetType {}
-
+ @Inject public UserInteractor mUserInteractor;
+ @Inject @Background public CoroutineDispatcher mBackgroundDispatcher;
+ @Inject public ChooserHelper mChooserHelper;
@Inject public FeatureFlags mFeatureFlags;
+ @Inject public android.service.chooser.FeatureFlags mChooserServiceFeatureFlags;
@Inject public EventLog mEventLog;
-
- private ChooserIntegratedDeviceComponents mIntegratedDeviceComponents;
-
- /* TODO: this is `nullable` because we have to defer the assignment til onCreate(). We make the
- * only assignment there, and expect it to be ready by the time we ever use it --
- * someday if we move all the usage to a component with a narrower lifecycle (something that
- * matches our Activity's create/destroy lifecycle, not its Java object lifecycle) then we
- * should be able to make this assignment as "final."
- */
- @Nullable
- private ChooserRequestParameters mChooserRequest;
+ @Inject @AppPredictionAvailable public boolean mAppPredictionAvailable;
+ @Inject @ImageEditor public Optional<ComponentName> mImageEditor;
+ @Inject @NearbyShare public Optional<ComponentName> mNearbyShare;
+ @Inject public TargetDataLoader mTargetDataLoader;
+ @Inject public DevicePolicyResources mDevicePolicyResources;
+ @Inject public ProfilePagerResources mProfilePagerResources;
+ @Inject public PackageManager mPackageManager;
+ @Inject public ClipboardManager mClipboardManager;
+ @Inject public IntentForwarding mIntentForwarding;
+ @Inject public ShareResultSenderFactory mShareResultSenderFactory;
+
+ private ActivityModel mActivityModel;
+ private ChooserRequest mRequest;
+ private ProfileHelper mProfiles;
+ private ProfileAvailability mProfileAvailability;
+ @Nullable private ShareResultSender mShareResultSender;
private ChooserRefinementManager mRefinementManager;
@@ -214,14 +305,10 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
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 final Map<Integer, ProfileRecord> mProfileRecords = new HashMap<>();
private boolean mExcludeSharedText = false;
/**
@@ -232,98 +319,339 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
*/
private boolean mFinishWhenStopped = false;
+ private final AtomicLong mIntentReceivedTime = new AtomicLong(-1);
+
+ protected ActivityModel createActivityModel() {
+ return ActivityModel.createFrom(this);
+ }
+
+ private ChooserViewModel mViewModel;
+
+ @NonNull
+ @Override
+ public CreationExtras getDefaultViewModelCreationExtras() {
+ return addDefaultArgs(
+ super.getDefaultViewModelCreationExtras(),
+ new Pair<>(ACTIVITY_MODEL_KEY, createActivityModel()));
+ }
+
@Override
protected void onCreate(Bundle savedInstanceState) {
- Tracer.INSTANCE.markLaunched();
- final long intentReceivedTime = System.currentTimeMillis();
- mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
+ super.onCreate(savedInstanceState);
+ Log.i(TAG, "onCreate");
- try {
- mChooserRequest = new ChooserRequestParameters(
- getIntent(),
- getReferrerPackageName(),
- getReferrer());
- } catch (IllegalArgumentException e) {
- Log.e(TAG, "Caller provided invalid Chooser request parameters", e);
+ setTheme(R.style.Theme_DeviceDefault_Chooser);
+
+ // Initializer is invoked when this function returns, via Lifecycle.
+ mChooserHelper.setInitializer(this::initialize);
+ if (mChooserServiceFeatureFlags.chooserPayloadToggling()) {
+ mChooserHelper.setOnChooserRequestChanged(this::onChooserRequestChanged);
+ mChooserHelper.setOnPendingSelection(this::onPendingSelection);
+ }
+ }
+
+ @Override
+ protected final void onStart() {
+ super.onStart();
+ this.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+ }
+
+ @Override
+ protected final void onResume() {
+ super.onResume();
+ Log.d(TAG, "onResume: " + getComponentName().flattenToShortString());
+ mFinishWhenStopped = false;
+ mRefinementManager.onActivityResume();
+ }
+
+ @Override
+ protected final 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()
+ && !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();
+ }
+ }
+
+ if (mRefinementManager != null) {
+ mRefinementManager.onActivityStop(isChangingConfigurations());
+ }
+
+ if (mFinishWhenStopped) {
+ mFinishWhenStopped = false;
finish();
- super_onCreate(null);
- return;
}
+ }
+
+ @Override
+ protected final void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ if (mViewPager != null) {
+ outState.putInt(LAST_SHOWN_TAB_KEY, mViewPager.getCurrentItem());
+ }
+ }
+
+ @Override
+ protected final void onRestart() {
+ super.onRestart();
+ if (!mRegistered) {
+ mPersonalPackageMonitor.register(
+ this,
+ getMainLooper(),
+ mProfiles.getPersonalHandle(),
+ false);
+ if (mProfiles.getWorkProfilePresent()) {
+ if (mWorkPackageMonitor == null) {
+ mWorkPackageMonitor = createPackageMonitor(
+ mChooserMultiProfilePagerAdapter.getWorkListAdapter());
+ }
+ mWorkPackageMonitor.register(
+ this,
+ getMainLooper(),
+ mProfiles.getWorkHandle(),
+ false);
+ }
+ mRegistered = true;
+ }
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (!isChangingConfigurations() && mPickOptionRequest != null) {
+ mPickOptionRequest.cancel();
+ }
+ if (mChooserMultiProfilePagerAdapter != null) {
+ mChooserMultiProfilePagerAdapter.destroy();
+ }
+
+ if (isFinishing()) {
+ mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET);
+ }
+
+ mBackgroundThreadPoolExecutor.shutdownNow();
+
+ destroyProfileRecords();
+ }
+
+ /** DO NOT CALL. Only for use from ChooserHelper as a callback. */
+ private void initialize() {
+
+ mViewModel = new ViewModelProvider(this).get(ChooserViewModel.class);
+ mRequest = mViewModel.getRequest().getValue();
+ mActivityModel = mViewModel.getActivityModel();
+
+ mProfiles = new ProfileHelper(
+ mUserInteractor,
+ getCoroutineScope(getLifecycle()),
+ mBackgroundDispatcher,
+ mFeatureFlags);
+
+ mProfileAvailability = new ProfileAvailability(
+ mUserInteractor,
+ getCoroutineScope(getLifecycle()),
+ mBackgroundDispatcher);
+
+ mProfileAvailability.setOnProfileStatusChange(this::onWorkProfileStatusUpdated);
+
+ mIntentReceivedTime.set(System.currentTimeMillis());
+ mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
+
mPinnedSharedPrefs = getPinnedSharedPrefs(this);
- mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
+ updateShareResultSender();
+
+ mMaxTargetsPerRow =
+ getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
mShouldDisplayLandscape =
shouldDisplayLandscape(getResources().getConfiguration().orientation);
- setRetainInOnStop(mChooserRequest.shouldRetainInOnStop());
+ setRetainInOnStop(mRequest.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);
+ Objects.toString(mRequest.getSharedText(), null),
+ mRequest.getShareTargetFilter(),
+ mAppPredictionAvailable
+ ),
+ mRequest.getShareTargetFilter()
+ );
- getEventLog().logSharesheetTriggered();
- mIntegratedDeviceComponents = getIntegratedDeviceComponents();
+ mChooserMultiProfilePagerAdapter = createMultiProfilePagerAdapter(
+ /* context = */ this,
+ mProfilePagerResources,
+ mRequest,
+ mProfiles,
+ mProfileAvailability,
+ mRequest.getInitialIntents(),
+ mMaxTargetsPerRow);
+
+ maybeDisableRecentsScreenshot(mProfiles, mProfileAvailability);
+
+ if (!configureContentView(mTargetDataLoader)) {
+ mPersonalPackageMonitor = createPackageMonitor(
+ mChooserMultiProfilePagerAdapter.getPersonalListAdapter());
+ mPersonalPackageMonitor.register(
+ this,
+ getMainLooper(),
+ mProfiles.getPersonalHandle(),
+ false
+ );
+ if (mProfiles.getWorkProfilePresent()) {
+ mWorkPackageMonitor = createPackageMonitor(
+ mChooserMultiProfilePagerAdapter.getWorkListAdapter());
+ mWorkPackageMonitor.register(
+ this,
+ getMainLooper(),
+ mProfiles.getWorkHandle(),
+ 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();
+ }
+ });
- mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class);
+ boolean hasTouchScreen = mPackageManager
+ .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;
+ }
+
+ Intent intent = mRequest.getTargetIntent();
+ final Set<String> categories = intent.getCategories();
+ MetricsLogger.action(this,
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()
+ ? MetricsEvent.ACTION_SHOW_APP_DISAMBIG_APP_FEATURED
+ : MetricsEvent.ACTION_SHOW_APP_DISAMBIG_NONE_FEATURED,
+ intent.getAction() + ":" + intent.getType() + ":"
+ + (categories != null ? Arrays.toString(categories.toArray())
+ : ""));
+ }
+
+ 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);
+ if (completion.getRefinedIntent() == null) {
+ finish();
+ return;
+ }
+
+ // Prepare to regenerate our "system actions" based on the refined intent.
+ // TODO: optimize if needed. `TARGET_INFO` cases don't require a new action
+ // factory at all. And if we break up `ChooserActionFactory`, we could avoid
+ // resolving a new editor intent unless we're handling an `EDIT_ACTION`.
+ ChooserActionFactory refinedActionFactory =
+ createChooserActionFactory(completion.getRefinedIntent());
+ switch (completion.getType()) {
+ case TARGET_INFO: {
+ TargetInfo refinedTarget = completion
+ .getOriginalTargetInfo()
+ .tryToCloneWithAppliedRefinement(
+ completion.getRefinedIntent());
+ if (refinedTarget == null) {
+ Log.e(TAG, "Failed to apply refinement to any matching source intent");
+ } else {
+ maybeRemoveSharedText(refinedTarget);
+
+ // 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, and make sure Sharesheet gets cleaned up regardless of the
+ // outcome of that launch.launch; ignore
+
+ safelyStartActivity(refinedTarget);
+ }
+ }
+ break;
+
+ case COPY_ACTION: {
+ if (refinedActionFactory.getCopyButtonRunnable() != null) {
+ refinedActionFactory.getCopyButtonRunnable().run();
+ }
+ }
+ break;
+
+ case EDIT_ACTION: {
+ if (refinedActionFactory.getEditButtonRunnable() != null) {
+ refinedActionFactory.getEditButtonRunnable().run();
+ }
+ }
+ break;
}
finish();
}
});
-
BasePreviewViewModel previewViewModel =
new ViewModelProvider(this, createPreviewViewModelFactory())
.get(BasePreviewViewModel.class);
+ previewViewModel.init(
+ mRequest.getTargetIntent(),
+ mRequest.getAdditionalContentUri(),
+ mChooserServiceFeatureFlags.chooserPayloadToggling());
+ ChooserContentPreviewUi.ActionFactory actionFactory =
+ decorateActionFactoryWithRefinement(
+ createChooserActionFactory(mRequest.getTargetIntent()));
mChooserContentPreviewUi = new ChooserContentPreviewUi(
getCoroutineScope(getLifecycle()),
- previewViewModel.createOrReuseProvider(mChooserRequest.getTargetIntent()),
- mChooserRequest.getTargetIntent(),
- previewViewModel.createOrReuseImageLoader(),
- createChooserActionFactory(),
+ previewViewModel.getPreviewDataProvider(),
+ mRequest.getTargetIntent(),
+ previewViewModel.getImageLoader(),
+ actionFactory,
+ createModifyShareActionFactory(),
mEnterTransitionAnimationDelegate,
- new HeadlineGeneratorImpl(this));
-
+ new HeadlineGeneratorImpl(this),
+ mRequest.getContentTypeHint(),
+ mRequest.getMetadataText(),
+ mChooserServiceFeatureFlags.chooserPayloadToggling());
updateStickyContentPreview();
- if (shouldShowStickyContentPreview()
- || mChooserMultiProfilePagerAdapter
- .getCurrentRootAdapter().getSystemRowCount() != 0) {
+ if (shouldShowStickyContentPreview()) {
getEventLog().logActionShareWithPreview(
mChooserContentPreviewUi.getPreferredContentPreview());
}
-
mChooserShownTime = System.currentTimeMillis();
- final long systemCost = mChooserShownTime - intentReceivedTime;
+ final long systemCost = mChooserShownTime - mIntentReceivedTime.get();
getEventLog().logChooserActivityShown(
- isWorkProfile(), mChooserRequest.getTargetType(), systemCost);
-
+ isWorkProfile(), mRequest.getTargetType(), systemCost);
if (mResolverDrawerLayout != null) {
mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange);
@@ -333,49 +661,686 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
getEventLog().logSharesheetExpansionChanged(isCollapsed);
});
}
-
if (DEBUG) {
Log.d(TAG, "System Time Cost is " + systemCost);
}
-
getEventLog().logShareStarted(
- getReferrerPackageName(),
- mChooserRequest.getTargetType(),
- mChooserRequest.getCallerChooserTargets().size(),
- (mChooserRequest.getInitialIntents() == null)
- ? 0 : mChooserRequest.getInitialIntents().length,
+ mRequest.getReferrerPackage(),
+ mRequest.getTargetType(),
+ mRequest.getCallerChooserTargets().size(),
+ mRequest.getInitialIntents().size(),
isWorkProfile(),
mChooserContentPreviewUi.getPreferredContentPreview(),
- mChooserRequest.getTargetAction(),
- mChooserRequest.getChooserActions().size(),
- mChooserRequest.getModifyShareAction() != null
+ mRequest.getTargetAction(),
+ mRequest.getChooserActions().size(),
+ mRequest.getModifyShareAction() != null
);
-
mEnterTransitionAnimationDelegate.postponeTransition();
+ Tracer.INSTANCE.markLaunched();
}
- @VisibleForTesting
- protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() {
- return ChooserIntegratedDeviceComponents.get(this, new SecureSettings());
+ private void maybeDisableRecentsScreenshot(
+ ProfileHelper profileHelper, ProfileAvailability profileAvailability) {
+ for (Profile profile : profileHelper.getProfiles()) {
+ if (profile.getType() == Profile.Type.PRIVATE) {
+ if (profileAvailability.isAvailable(profile)) {
+ // Show blank screen in Recent preview if private profile is available
+ // to not leak its presence.
+ setRecentsScreenshotEnabled(false);
+ }
+ return;
+ }
+ }
+ }
+
+ private void onChooserRequestChanged(ChooserRequest chooserRequest) {
+ // intentional reference comparison
+ if (mRequest == chooserRequest) {
+ return;
+ }
+ boolean recreateAdapters = shouldUpdateAdapters(mRequest, chooserRequest);
+ mRequest = chooserRequest;
+ updateShareResultSender();
+ mChooserContentPreviewUi.updateModifyShareAction();
+ if (recreateAdapters) {
+ recreatePagerAdapter();
+ } else {
+ setTabsViewEnabled(true);
+ }
+ }
+
+ private void onPendingSelection() {
+ setTabsViewEnabled(false);
+ }
+
+ private void onAppTargetsLoaded(ResolverListAdapter listAdapter) {
+ if (mChooserMultiProfilePagerAdapter == null) {
+ return;
+ }
+ if (!isProfilePagerAdapterAttached()
+ && listAdapter == mChooserMultiProfilePagerAdapter.getActiveListAdapter()) {
+ mChooserMultiProfilePagerAdapter.setupViewPager(mViewPager);
+ setTabsViewEnabled(true);
+ }
+ }
+
+ private void updateShareResultSender() {
+ IntentSender chosenComponentSender = mRequest.getChosenComponentSender();
+ if (chosenComponentSender != null) {
+ mShareResultSender = mShareResultSenderFactory.create(
+ mViewModel.getActivityModel().getLaunchedFromUid(), chosenComponentSender);
+ } else {
+ mShareResultSender = null;
+ }
+ }
+
+ private boolean shouldUpdateAdapters(
+ ChooserRequest oldChooserRequest, ChooserRequest newChooserRequest) {
+ Intent oldTargetIntent = oldChooserRequest.getTargetIntent();
+ Intent newTargetIntent = newChooserRequest.getTargetIntent();
+ List<Intent> oldAltIntents = oldChooserRequest.getAdditionalTargets();
+ List<Intent> newAltIntents = newChooserRequest.getAdditionalTargets();
+
+ // TODO: a workaround for the unnecessary target reloading caused by multiple flow updates -
+ // an artifact of the current implementation; revisit.
+ return !oldTargetIntent.equals(newTargetIntent) || !oldAltIntents.equals(newAltIntents);
+ }
+
+ private void recreatePagerAdapter() {
+ if (!mChooserServiceFeatureFlags.chooserPayloadToggling()) {
+ return;
+ }
+ destroyProfileRecords();
+ createProfileRecords(
+ new AppPredictorFactory(
+ this,
+ Objects.toString(mRequest.getSharedText(), null),
+ mRequest.getShareTargetFilter(),
+ mAppPredictionAvailable
+ ),
+ mRequest.getShareTargetFilter()
+ );
+
+ int currentPage = mChooserMultiProfilePagerAdapter.getCurrentPage();
+ if (mChooserMultiProfilePagerAdapter != null) {
+ mChooserMultiProfilePagerAdapter.destroy();
+ }
+ // Update the pager adapter but do not attach it to the view till the targets are reloaded,
+ // see onChooserAppTargetsLoaded method.
+ mChooserMultiProfilePagerAdapter = createMultiProfilePagerAdapter(
+ /* context = */ this,
+ mProfilePagerResources,
+ mRequest,
+ mProfiles,
+ mProfileAvailability,
+ mRequest.getInitialIntents(),
+ mMaxTargetsPerRow);
+ mChooserMultiProfilePagerAdapter.setCurrentPage(currentPage);
+ if (mPersonalPackageMonitor != null) {
+ mPersonalPackageMonitor.unregister();
+ }
+ mPersonalPackageMonitor = createPackageMonitor(
+ mChooserMultiProfilePagerAdapter.getPersonalListAdapter());
+ mPersonalPackageMonitor.register(
+ this,
+ getMainLooper(),
+ mProfiles.getPersonalHandle(),
+ false);
+ if (mProfiles.getWorkProfilePresent()) {
+ if (mWorkPackageMonitor != null) {
+ mWorkPackageMonitor.unregister();
+ }
+ mWorkPackageMonitor = createPackageMonitor(
+ mChooserMultiProfilePagerAdapter.getWorkListAdapter());
+ mWorkPackageMonitor.register(
+ this,
+ getMainLooper(),
+ mProfiles.getWorkHandle(),
+ false);
+ }
+ postRebuildList(
+ mChooserMultiProfilePagerAdapter.rebuildTabs(
+ mProfiles.getWorkProfilePresent() || mProfiles.getPrivateProfilePresent()));
+ setTabsViewEnabled(false);
+ }
+
+ private void setTabsViewEnabled(boolean isEnabled) {
+ TabWidget tabs = mTabHost.getTabWidget();
+ if (tabs != null) {
+ tabs.setEnabled(isEnabled);
+ }
+ View tabContent = mTabHost.findViewById(com.android.internal.R.id.profile_pager);
+ if (tabContent != null) {
+ tabContent.setEnabled(isEnabled);
+ }
}
@Override
- protected int appliedThemeResId() {
- return R.style.Theme_DeviceDefault_Chooser;
+ protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
+ if (mViewPager != null) {
+ mViewPager.setCurrentItem(savedInstanceState.getInt(LAST_SHOWN_TAB_KEY));
+ }
+ mChooserMultiProfilePagerAdapter.clearInactiveProfileCache();
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ // Inherited methods
+ //////////////////////////////////////////////////////////////////////////////////////////////
+
+ private boolean isAutolaunching() {
+ return !mRegistered && isFinishing();
}
+ private boolean maybeAutolaunchIfSingleTarget() {
+ int count = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();
+ if (count != 1) {
+ return false;
+ }
+
+ if (mChooserMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null) {
+ return false;
+ }
+
+ // Only one target, so we're a candidate to auto-launch!
+ final TargetInfo target = mChooserMultiProfilePagerAdapter.getActiveListAdapter()
+ .targetInfoForPosition(0, false);
+ if (shouldAutoLaunchSingleChoice(target)) {
+ safelyStartActivity(target);
+ finish();
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isTwoPagePersonalAndWorkConfiguration() {
+ return (mChooserMultiProfilePagerAdapter.getCount() == 2)
+ && mChooserMultiProfilePagerAdapter.hasPageForProfile(PROFILE_PERSONAL)
+ && mChooserMultiProfilePagerAdapter.hasPageForProfile(PROFILE_WORK);
+ }
+
+ /**
+ * When we have 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 =
+ (mChooserMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mChooserMultiProfilePagerAdapter.getPersonalListAdapter()
+ : mChooserMultiProfilePagerAdapter.getWorkListAdapter();
+
+ ResolverListAdapter inactiveListAdapter =
+ (mChooserMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mChooserMultiProfilePagerAdapter.getWorkListAdapter()
+ : mChooserMultiProfilePagerAdapter.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 (!mIntentForwarding.canAppInteractAcrossProfiles(this, packageName)) {
+ return false;
+ }
+
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET)
+ .setBoolean(activeListAdapter.getUserHandle()
+ .equals(mProfiles.getPersonalHandle()))
+ .setStrings(getMetricsCategory())
+ .write();
+ safelyStartActivity(activeProfileTarget);
+ finish();
+ return true;
+ }
+
+ /**
+ * @return {@code true} if a resolved target is autolaunched, otherwise {@code false}
+ */
+ private boolean maybeAutolaunchActivity() {
+ int numberOfProfiles = mChooserMultiProfilePagerAdapter.getItemCount();
+ // 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.
+ if (numberOfProfiles == 1 && maybeAutolaunchIfSingleTarget()) {
+ return true;
+ } else if (maybeAutolaunchIfCrossProfileSupported()) {
+ return true;
+ }
+ return false;
+ }
+
+ @Override // ResolverListCommunicator
+ public final void onPostListReady(ResolverListAdapter listAdapter, boolean doPostProcessing,
+ boolean rebuildCompleted) {
+ if (isAutolaunching()) {
+ return;
+ }
+ if (mChooserMultiProfilePagerAdapter
+ .shouldShowEmptyStateScreen((ChooserListAdapter) listAdapter)) {
+ mChooserMultiProfilePagerAdapter
+ .showEmptyResolverListEmptyState((ChooserListAdapter) listAdapter);
+ } else {
+ mChooserMultiProfilePagerAdapter.showListView((ChooserListAdapter) listAdapter);
+ }
+ // showEmptyResolverListEmptyState can mark the tab as loaded,
+ // which is a precondition for auto launching
+ if (rebuildCompleted && maybeAutolaunchActivity()) {
+ return;
+ }
+ if (doPostProcessing) {
+ maybeCreateHeader(listAdapter);
+ onListRebuilt(listAdapter, rebuildCompleted);
+ }
+ }
+
+ private CharSequence getOrLoadDisplayLabel(TargetInfo info) {
+ if (info.isDisplayResolveInfo()) {
+ mTargetDataLoader.getOrLoadLabel((DisplayResolveInfo) info);
+ }
+ CharSequence displayLabel = info.getDisplayLabel();
+ return displayLabel == null ? "" : displayLabel;
+ }
+
+ protected final CharSequence getTitleForAction(Intent intent, int defaultTitleRes) {
+ final ActionTitle title = 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 =
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter().getFilteredPosition() >= 0;
+ if (title == ActionTitle.DEFAULT && defaultTitleRes != 0) {
+ return getString(defaultTitleRes);
+ } else {
+ return named
+ ? getString(
+ title.namedTitleRes,
+ getOrLoadDisplayLabel(
+ mChooserMultiProfilePagerAdapter
+ .getActiveListAdapter().getFilteredItem()))
+ : getString(title.titleRes);
+ }
+ }
+
+ /**
+ * 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 (!mProfiles.getWorkProfilePresent()
+ && 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 = mRequest.getTitle() != null
+ ? mRequest.getTitle()
+ : getTitleForAction(mRequest.getTargetIntent(),
+ mRequest.getDefaultTitleResource());
+
+ 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();
+ }
+
+ /** 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);
+ }
+
+ 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 = mIntentForwarding.forwardMessageFor(
+ mRequest.getTargetIntent());
+ if (profileSwitchMessage != null) {
+ Toast.makeText(this, profileSwitchMessage, Toast.LENGTH_LONG).show();
+ }
+ try {
+ if (cti.startAsCaller(this, options, user.getIdentifier())) {
+ maybeSendShareResult(cti);
+ maybeLogCrossProfileTargetLaunch(cti, user);
+ }
+ } catch (RuntimeException e) {
+ Slog.wtf(TAG,
+ "Unable to launch as uid " + mActivityModel.getLaunchedFromUid()
+ + " package " + mActivityModel.getLaunchedFromPackage()
+ + ", while running in " + ActivityThread.currentProcessName(), e);
+ }
+ }
+
+ private void maybeLogCrossProfileTargetLaunch(TargetInfo cti, UserHandle currentUserHandle) {
+ if (!mProfiles.getWorkProfilePresent() || currentUserHandle.equals(getUser())) {
+ return;
+ }
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_CROSS_PROFILE_TARGET_OPENED)
+ .setBoolean(currentUserHandle.equals(mProfiles.getPersonalHandle()))
+ .setStrings(getMetricsCategory(),
+ cti.isInDirectShareMetricsCategory() ? "direct_share" : "other_target")
+ .write();
+ }
+
+ private LatencyTracker getLatencyTracker() {
+ return LatencyTracker.getInstance(this);
+ }
+
+ /**
+ * 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;
+ }
+
+ // @NonFinalForTesting
+ @VisibleForTesting
+ protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() {
+ return new CrossProfileIntentsChecker(getContentResolver());
+ }
+
+ protected final EmptyStateProvider createEmptyStateProvider(
+ ProfileHelper profileHelper,
+ ProfileAvailability profileAvailability) {
+ EmptyStateProvider blockerEmptyStateProvider = createBlockerEmptyStateProvider();
+
+ EmptyStateProvider workProfileOffEmptyStateProvider =
+ new WorkProfilePausedEmptyStateProvider(
+ this,
+ profileHelper,
+ profileAvailability,
+ /* onSwitchOnWorkSelectedListener = */
+ () -> {
+ if (mOnSwitchOnWorkSelectedListener != null) {
+ mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected();
+ }
+ },
+ getMetricsCategory());
+
+ EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider(
+ mProfiles,
+ mProfileAvailability,
+ getMetricsCategory(),
+ mProfilePagerResources
+ );
+
+ // Return composite provider, the order matters (the higher, the more priority)
+ return new CompositeEmptyStateProvider(
+ blockerEmptyStateProvider,
+ workProfileOffEmptyStateProvider,
+ noAppsEmptyStateProvider
+ );
+ }
+
+ /**
+ * Returns the {@link List} of {@link UserHandle} to pass on to the
+ * {@link ResolverRankerServiceResolverComparator} as per the provided {@code userHandle}.
+ */
+ private List<UserHandle> getResolverRankerServiceUserHandleList(UserHandle userHandle) {
+ return getResolverRankerServiceUserHandleListInternal(userHandle);
+ }
+
+ private 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(mProfiles.getPersonalHandle())
+ && mProfiles.getCloneUserPresent()) {
+ userList.add(mProfiles.getCloneHandle());
+ }
+ return userList;
+ }
+
+ /**
+ * 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);
+ }
+
+ @Override // ResolverListCommunicator
+ public final void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
+ if (!mChooserMultiProfilePagerAdapter.onHandlePackagesChanged(
+ (ChooserListAdapter) listAdapter,
+ mProfileAvailability.getWaitingToEnableProfile())) {
+ // We no longer have any items... just finish the activity.
+ finish();
+ }
+ }
+
+ final Option optionForChooserTarget(TargetInfo target, int index) {
+ return new Option(getOrLoadDisplayLabel(target), index);
+ }
+
+ @Override // ResolverListCommunicator
+ public final void sendVoiceChoicesIfNeeded() {
+ if (!isVoiceInteraction()) {
+ // Clearly not needed.
+ return;
+ }
+
+ int count = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getCount();
+ final Option[] options = new Option[count];
+ for (int i = 0; i < options.length; i++) {
+ TargetInfo target = mChooserMultiProfilePagerAdapter.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 ResolverActivity.PickTargetOptionRequest(
+ new VoiceInteractor.Prompt(getTitle()), options, null);
+ getVoiceInteractor().submitRequest(mPickOptionRequest);
+ }
+
+ /**
+ * Sets up the content view.
+ * @return <code>true</code> if the activity is finishing and creation should halt.
+ */
+ private boolean configureContentView(TargetDataLoader targetDataLoader) {
+ if (mChooserMultiProfilePagerAdapter.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.
+ boolean rebuildCompleted = mChooserMultiProfilePagerAdapter.rebuildTabs(
+ mProfiles.getWorkProfilePresent());
+
+ mLayoutId = R.layout.chooser_grid_scrollable_preview;
+
+ setContentView(mLayoutId);
+ mTabHost = findViewById(com.android.internal.R.id.profile_tabhost);
+ mViewPager = requireViewById(com.android.internal.R.id.profile_pager);
+ mChooserMultiProfilePagerAdapter.setupViewPager(mViewPager);
+ boolean result = postRebuildList(rebuildCompleted);
+ Trace.endSection();
+ return result;
+ }
+
+ /**
+ * 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);
+ }
+
+ /**
+ * 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 (mProfiles.getWorkProfilePresent()) {
+ textView.setGravity(Gravity.CENTER);
+ }
+ stub.addView(textView);
+ }
+ }
+ private void setupViewVisibilities() {
+ ChooserListAdapter activeListAdapter =
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter();
+ if (!mChooserMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)) {
+ addUseDifferentAppLabelIfNecessary(activeListAdapter);
+ }
+ }
+ /**
+ * 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 = mChooserMultiProfilePagerAdapter.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 (mProfiles.getWorkProfilePresent()
+ || (mProfiles.getPrivateProfilePresent()
+ && mProfileAvailability.isAvailable(
+ requireNonNull(mProfiles.getPrivateProfile())))) {
+ setupProfileTabs();
+ }
+
+ return false;
+ }
+
+ private void setupProfileTabs() {
+ mChooserMultiProfilePagerAdapter.setupProfileTabs(
+ getLayoutInflater(),
+ mTabHost,
+ mViewPager,
+ R.layout.resolver_profile_tab_button,
+ com.android.internal.R.id.profile_pager,
+ () -> onProfileTabSelected(mViewPager.getCurrentItem()),
+ new OnProfileSelectedListener() {
+ @Override
+ public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) {}
+
+ @Override
+ public void onProfilePageStateChanged(int state) {
+ onHorizontalSwipeStateChanged(state);
+ }
+ });
+ mOnSwitchOnWorkSelectedListener = () -> {
+ View workTab = mTabHost.getTabWidget().getChildAt(
+ mChooserMultiProfilePagerAdapter.getPageNumberForProfile(PROFILE_WORK));
+ workTab.setFocusable(true);
+ workTab.setFocusableInTouchMode(true);
+ workTab.requestFocus();
+ };
+ }
+
+ //////////////////////////////////////////////////////////////////////////////////////////////
+ //////////////////////////////////////////////////////////////////////////////////////////////
+
private void createProfileRecords(
AppPredictorFactory factory, IntentFilter targetIntentFilter) {
- UserHandle mainUserHandle = getAnnotatedUserHandles().personalProfileUserHandle;
+ UserHandle mainUserHandle = mProfiles.getPersonalHandle();
ProfileRecord record = createProfileRecord(mainUserHandle, targetIntentFilter, factory);
if (record.shortcutLoader == null) {
Tracer.INSTANCE.endLaunchToShortcutTrace();
}
- UserHandle workUserHandle = getAnnotatedUserHandles().workProfileUserHandle;
+ UserHandle workUserHandle = mProfiles.getWorkHandle();
if (workUserHandle != null) {
createProfileRecord(workUserHandle, targetIntentFilter, factory);
}
+
+ UserHandle privateUserHandle = mProfiles.getPrivateHandle();
+ if (privateUserHandle != null && mProfileAvailability.isAvailable(
+ requireNonNull(mProfiles.getPrivateProfile()))) {
+ createProfileRecord(privateUserHandle, targetIntentFilter, factory);
+ }
}
private ProfileRecord createProfileRecord(
@@ -396,7 +1361,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
@Nullable
private ProfileRecord getProfileRecord(UserHandle userHandle) {
- return mProfileRecords.get(userHandle.getIdentifier(), null);
+ return mProfileRecords.get(userHandle.getIdentifier());
}
@VisibleForTesting
@@ -419,25 +1384,73 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
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;
+ protected ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter() {
+ return createMultiProfilePagerAdapter(
+ /* context = */ this,
+ mProfilePagerResources,
+ mViewModel.getRequest().getValue(),
+ mProfiles,
+ mProfileAvailability,
+ mRequest.getInitialIntents(),
+ mMaxTargetsPerRow);
+ }
+
+ private ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter(
+ Context context,
+ ProfilePagerResources profilePagerResources,
+ ChooserRequest request,
+ ProfileHelper profileHelper,
+ ProfileAvailability profileAvailability,
+ List<Intent> initialIntents,
+ int maxTargetsPerRow) {
+ Log.d(TAG, "createMultiProfilePagerAdapter");
+
+ Profile launchedAs = profileHelper.getLaunchedAsProfile();
+
+ Intent[] initialIntentArray = initialIntents.toArray(new Intent[0]);
+ List<Intent> payloadIntents = request.getPayloadIntents();
+
+ List<TabConfig<ChooserGridAdapter>> tabs = new ArrayList<>();
+ for (Profile profile : profileHelper.getProfiles()) {
+ if (profile.getType() == Profile.Type.PRIVATE
+ && !profileAvailability.isAvailable(profile)) {
+ continue;
+ }
+ ChooserGridAdapter adapter = createChooserGridAdapter(
+ context,
+ payloadIntents,
+ profile.equals(launchedAs) ? initialIntentArray : null,
+ profile.getPrimary().getHandle()
+ );
+ tabs.add(new TabConfig<>(
+ /* profile = */ profile.getType().ordinal(),
+ profilePagerResources.profileTabLabel(profile.getType()),
+ profilePagerResources.profileTabAccessibilityLabel(profile.getType()),
+ /* tabTag = */ profile.getType().name(),
+ adapter));
+ }
+
+ EmptyStateProvider emptyStateProvider =
+ createEmptyStateProvider(profileHelper, profileAvailability);
+
+ Supplier<Boolean> workProfileQuietModeChecker =
+ () -> !(profileHelper.getWorkProfilePresent()
+ && profileAvailability.isAvailable(
+ requireNonNull(profileHelper.getWorkProfile())));
+
+ return new ChooserMultiProfilePagerAdapter(
+ /* context */ this,
+ ImmutableList.copyOf(tabs),
+ emptyStateProvider,
+ workProfileQuietModeChecker,
+ launchedAs.getType().ordinal(),
+ profileHelper.getWorkHandle(),
+ profileHelper.getCloneHandle(),
+ maxTargetsPerRow);
}
- @Override
protected EmptyStateProvider createBlockerEmptyStateProvider() {
- final boolean isSendAction = mChooserRequest.isSendActionTarget();
+ final boolean isSendAction = mRequest.isSendActionTarget();
final EmptyState noWorkToPersonalEmptyState =
new DevicePolicyBlockerEmptyState(
@@ -466,79 +1479,14 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
/* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER);
return new NoCrossProfileEmptyStateProvider(
- getAnnotatedUserHandles().personalProfileUserHandle,
+ mProfiles,
noWorkToPersonalEmptyState,
noPersonalToWorkEmptyState,
- createCrossProfileIntentsChecker(),
- getAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
- }
-
- private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile(
- Intent[] initialIntents,
- List<ResolveInfo> rList,
- boolean filterLastUsed,
- TargetDataLoader targetDataLoader) {
- ChooserGridAdapter adapter = createChooserGridAdapter(
- /* context */ this,
- /* payloadIntents */ mIntents,
- initialIntents,
- rList,
- filterLastUsed,
- /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,
- targetDataLoader);
- return new ChooserMultiProfilePagerAdapter(
- /* context */ this,
- adapter,
- createEmptyStateProvider(/* workProfileUserHandle= */ null),
- /* workProfileQuietModeChecker= */ () -> false,
- /* workProfileUserHandle= */ null,
- getAnnotatedUserHandles().cloneProfileUserHandle,
- mMaxTargetsPerRow,
- mFeatureFlags);
- }
-
- private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles(
- Intent[] initialIntents,
- List<ResolveInfo> rList,
- boolean filterLastUsed,
- TargetDataLoader targetDataLoader) {
- int selectedProfile = findSelectedProfile();
- ChooserGridAdapter personalAdapter = createChooserGridAdapter(
- /* context */ this,
- /* payloadIntents */ mIntents,
- selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
- rList,
- filterLastUsed,
- /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,
- targetDataLoader);
- ChooserGridAdapter workAdapter = createChooserGridAdapter(
- /* context */ this,
- /* payloadIntents */ mIntents,
- selectedProfile == PROFILE_WORK ? initialIntents : null,
- rList,
- filterLastUsed,
- /* userHandle */ getAnnotatedUserHandles().workProfileUserHandle,
- targetDataLoader);
- return new ChooserMultiProfilePagerAdapter(
- /* context */ this,
- personalAdapter,
- workAdapter,
- createEmptyStateProvider(getAnnotatedUserHandles().workProfileUserHandle),
- () -> mWorkProfileAvailability.isQuietModeEnabled(),
- selectedProfile,
- getAnnotatedUserHandles().workProfileUserHandle,
- getAnnotatedUserHandles().cloneProfileUserHandle,
- mMaxTargetsPerRow,
- mFeatureFlags);
+ createCrossProfileIntentsChecker());
}
private int findSelectedProfile() {
- int selectedProfile = getSelectedProfileExtra();
- if (selectedProfile == -1) {
- selectedProfile = getProfileForUser(
- getAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
- }
- return selectedProfile;
+ return mProfiles.getLaunchedAsProfileType().ordinal();
}
/**
@@ -546,12 +1494,11 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
* @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();
+ private boolean isWorkProfile() {
+ return mProfiles.getLaunchedAsProfileType() == Profile.Type.WORK;
}
- @Override
+ //@Override
protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) {
return new PackageMonitor() {
@Override
@@ -564,6 +1511,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
/**
* Update UI to reflect changes in data.
*/
+ @Override
public void handlePackagesChanged() {
handlePackagesChanged(/* listAdapter */ null);
}
@@ -577,39 +1525,23 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
// Refresh pinned items
mPinnedSharedPrefs = getPinnedSharedPrefs(this);
if (listAdapter == null) {
- handlePackageChangePerProfile(mChooserMultiProfilePagerAdapter.getActiveListAdapter());
- if (mChooserMultiProfilePagerAdapter.getCount() > 1) {
- handlePackageChangePerProfile(
- mChooserMultiProfilePagerAdapter.getInactiveListAdapter());
- }
+ mChooserMultiProfilePagerAdapter.refreshPackagesInAllTabs();
} else {
- handlePackageChangePerProfile(listAdapter);
+ listAdapter.handlePackagesChanged();
}
- updateProfileViewButton();
- }
-
- private void handlePackageChangePerProfile(ResolverListAdapter adapter) {
- ProfileRecord record = getProfileRecord(adapter.getUserHandle());
- if (record != null && record.shortcutLoader != null) {
- record.shortcutLoader.reset();
- }
- adapter.handlePackagesChanged();
- }
-
- @Override
- protected void onResume() {
- super.onResume();
- Log.d(TAG, "onResume: " + getComponentName().flattenToShortString());
- mFinishWhenStopped = false;
- mRefinementManager.onActivityResume();
}
@Override
- public void onConfigurationChanged(@NonNull Configuration newConfig) {
+ public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
- ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
- if (viewPager.isLayoutRtl()) {
- mMultiProfilePagerAdapter.setupViewPager(viewPager);
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
+
+ if (mSystemWindowInsets != null) {
+ mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top,
+ mSystemWindowInsets.right, 0);
+ }
+ if (mViewPager.isLayoutRtl()) {
+ mChooserMultiProfilePagerAdapter.setupViewPager(mViewPager);
}
mShouldDisplayLandscape = shouldDisplayLandscape(newConfig.orientation);
@@ -639,7 +1571,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
}
private void updateTabPadding() {
- if (shouldShowTabs()) {
+ if (mProfiles.getWorkProfilePresent()) {
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
@@ -671,9 +1603,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
getResources(),
getLayoutInflater(),
parent,
- mFeatureFlags.scrollablePreview()
- ? findViewById(R.id.chooser_headline_row_container)
- : null);
+ requireViewById(R.id.chooser_headline_row_container));
if (layout != null) {
adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
@@ -699,47 +1629,17 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
return resolver.query(uri, null, null, null, null);
}
- @Override
- protected void onStop() {
- super.onStop();
- 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.values().forEach(ProfileRecord::destroy);
mProfileRecords.clear();
}
@Override // ResolverListCommunicator
public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
- if (mChooserRequest == null) {
- return defIntent;
- }
-
Intent result = defIntent;
- if (mChooserRequest.getReplacementExtras() != null) {
+ if (mRequest.getReplacementExtras() != null) {
final Bundle replExtras =
- mChooserRequest.getReplacementExtras().getBundle(aInfo.packageName);
+ mRequest.getReplacementExtras().getBundle(aInfo.packageName);
if (replExtras != null) {
result = new Intent(defIntent);
result.putExtras(replExtras);
@@ -758,33 +1658,22 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
return result;
}
- @Override
- public void onActivityStarted(TargetInfo cti) {
- if (mChooserRequest.getChosenComponentSender() != null) {
+ private void maybeSendShareResult(TargetInfo cti) {
+ if (mShareResultSender != null) {
final ComponentName target = cti.getResolvedComponentName();
if (target != null) {
- final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target);
- try {
- mChooserRequest.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);
- }
+ mShareResultSender.onComponentSelected(target, cti.isChooserTargetInfo());
}
}
}
private void addCallerChooserTargets() {
- if (!mChooserRequest.getCallerChooserTargets().isEmpty()) {
+ if (!mRequest.getCallerChooserTargets().isEmpty()) {
// Send the caller's chooser targets only to the default profile.
- UserHandle defaultUser = (findSelectedProfile() == PROFILE_WORK)
- ? getAnnotatedUserHandles().workProfileUserHandle
- : getAnnotatedUserHandles().personalProfileUserHandle;
- if (mChooserMultiProfilePagerAdapter.getCurrentUserHandle() == defaultUser) {
+ if (mChooserMultiProfilePagerAdapter.getActiveProfile() == findSelectedProfile()) {
mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults(
/* origTarget */ null,
- new ArrayList<>(mChooserRequest.getCallerChooserTargets()),
+ new ArrayList<>(mRequest.getCallerChooserTargets()),
TARGET_TYPE_DEFAULT,
/* directShareShortcutInfoCache */ Collections.emptyMap(),
/* directShareAppTargetCache */ Collections.emptyMap());
@@ -792,28 +1681,19 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
}
}
- @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)) {
+ if (target.isSuspended()) {
return false;
}
- return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true);
+ // TODO: migrate to ChooserRequest
+ return mViewModel.getActivityModel().getIntent()
+ .getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true);
}
private void showTargetDetails(TargetInfo targetInfo) {
@@ -828,8 +1708,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
// 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()
- ? mChooserRequest.getTargetIntentFilter() : null;
+ IntentFilter intentFilter;
+ intentFilter = targetInfo.isSelectableTargetInfo()
+ ? mRequest.getShareTargetFilter() : null;
String shortcutTitle = targetInfo.isSelectableTargetInfo()
? targetInfo.getDisplayLabel().toString() : null;
String shortcutIdKey = targetInfo.getDirectShareShortcutId();
@@ -846,22 +1727,25 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
intentFilter);
}
- @Override
- protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) {
+ protected boolean onTargetSelected(TargetInfo target) {
if (mRefinementManager.maybeHandleSelection(
target,
- mChooserRequest.getRefinementIntentSender(),
+ mRequest.getRefinementIntentSender(),
getApplication(),
getMainThreadHandler())) {
return false;
}
updateModelAndChooserCounts(target);
maybeRemoveSharedText(target);
- return super.onTargetSelected(target, alwaysCheck);
+ safelyStartActivity(target);
+
+ // Rely on the ActivityManager to pop up a dialog regarding app suspension
+ // and return false
+ return !target.isSuspended();
}
@Override
- public void startSelected(int which, boolean always, boolean filtered) {
+ public void startSelected(int which, /* unused */ boolean always, boolean filtered) {
ChooserListAdapter currentListAdapter =
mChooserMultiProfilePagerAdapter.getActiveListAdapter();
TargetInfo targetInfo = currentListAdapter
@@ -884,8 +1768,23 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
return;
}
}
+ if (isFinishing()) {
+ return;
+ }
- super.startSelected(which, always, filtered);
+ TargetInfo target = mChooserMultiProfilePagerAdapter.getActiveListAdapter()
+ .targetInfoForPosition(which, filtered);
+ if (target != null) {
+ if (onTargetSelected(target)) {
+ MetricsLogger.action(
+ this, MetricsEvent.ACTION_APP_DISAMBIG_TAP);
+ MetricsLogger.action(this,
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()
+ ? MetricsEvent.ACTION_HIDE_APP_DISAMBIG_APP_FEATURED
+ : MetricsEvent.ACTION_HIDE_APP_DISAMBIG_NONE_FEATURED);
+ finish();
+ }
+ }
// 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
@@ -905,7 +1804,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
targetInfo.getResolveInfo().activityInfo.processName,
which,
/* directTargetAlsoRanked= */ getRankedPosition(targetInfo),
- mChooserRequest.getCallerChooserTargets().size(),
+ mRequest.getCallerChooserTargets().size(),
targetInfo.getHashedTargetIdForMetrics(this),
targetInfo.isPinned(),
mIsSuccessfullySelected,
@@ -942,7 +1841,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
mIsSuccessfullySelected,
selectionCost
);
- return;
}
}
}
@@ -964,19 +1862,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
return -1;
}
- @Override
- protected boolean shouldAddFooterView() {
- // To accommodate for window insets
- return true;
- }
-
- @Override
protected void applyFooterView(int height) {
- int count = mChooserMultiProfilePagerAdapter.getItemCount();
-
- for (int i = 0; i < count; i++) {
- mChooserMultiProfilePagerAdapter.getAdapterForIndex(i).setFooterHeight(height);
- }
+ mChooserMultiProfilePagerAdapter.setFooterHeightInEveryAdapter(height);
}
private void logDirectShareTargetReceived(UserHandle forUser) {
@@ -996,7 +1883,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
if (info != null) {
sendClickToAppPredictor(info);
final ResolveInfo ri = info.getResolveInfo();
- Intent targetIntent = getTargetIntent();
+ Intent targetIntent = mRequest.getTargetIntent();
if (ri != null && ri.activityInfo != null && targetIntent != null) {
ChooserListAdapter currentListAdapter =
mChooserMultiProfilePagerAdapter.getActiveListAdapter();
@@ -1024,7 +1911,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
if (targetIntent == null) {
return;
}
- Intent originalTargetIntent = new Intent(mChooserRequest.getTargetIntent());
+ Intent originalTargetIntent = new Intent(mRequest.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) {
@@ -1094,101 +1981,36 @@ public class ChooserActivity extends Hilt_ChooserActivity 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) || (getAnnotatedUserHandles().cloneProfileUserHandle != null))
+ return ((record == null) || (mProfiles.getCloneUserPresent()))
? 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 mChooserRequest.getFilteredComponentNames().contains(name);
- }
-
- @Override
- public boolean isComponentPinned(ComponentName name) {
- return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
- }
- }
-
- @VisibleForTesting
- public ChooserGridAdapter createChooserGridAdapter(
+ private ChooserGridAdapter createChooserGridAdapter(
Context context,
List<Intent> payloadIntents,
Intent[] initialIntents,
- List<ResolveInfo> rList,
- boolean filterLastUsed,
- UserHandle userHandle,
- TargetDataLoader targetDataLoader) {
+ UserHandle userHandle) {
ChooserListAdapter chooserListAdapter = createChooserListAdapter(
context,
payloadIntents,
initialIntents,
- rList,
- filterLastUsed,
+ /* TODO: not used, remove. rList= */ null,
+ /* TODO: not used, remove. filterLastUsed= */ false,
createListController(userHandle),
userHandle,
- getTargetIntent(),
- mChooserRequest.getReferrerFillInIntent(),
- mMaxTargetsPerRow,
- targetDataLoader);
+ mRequest.getTargetIntent(),
+ mRequest.getReferrerFillInIntent(),
+ mMaxTargetsPerRow
+ );
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);
}
@@ -1206,13 +2028,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
showTargetDetails(longPressedTargetInfo);
}
}
-
- @Override
- public void updateProfileViewButton(View newButtonFromProfileRow) {
- mProfileView = newButtonFromProfileRow;
- mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick);
- ChooserActivity.this.updateProfileViewButton();
- }
},
chooserListAdapter,
shouldShowContentPreview(),
@@ -1231,11 +2046,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
UserHandle userHandle,
Intent targetIntent,
Intent referrerFillInIntent,
- int maxTargetsPerRow,
- TargetDataLoader targetDataLoader) {
- UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()
- && userHandle.equals(getAnnotatedUserHandles().personalProfileUserHandle)
- ? getAnnotatedUserHandles().cloneProfileUserHandle : userHandle;
+ int maxTargetsPerRow) {
+ UserHandle initialIntentsUserSpace = mProfiles.getQueryIntentsHandle(userHandle);
return new ChooserListAdapter(
context,
payloadIntents,
@@ -1247,53 +2059,70 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
targetIntent,
referrerFillInIntent,
this,
- context.getPackageManager(),
+ mPackageManager,
getEventLog(),
maxTargetsPerRow,
initialIntentsUserSpace,
- targetDataLoader,
- null);
+ mTargetDataLoader,
+ () -> {
+ ProfileRecord record = getProfileRecord(userHandle);
+ if (record != null && record.shortcutLoader != null) {
+ record.shortcutLoader.reset();
+ }
+ },
+ mFeatureFlags);
}
- @Override
- protected void onWorkProfileStatusUpdated() {
- UserHandle workUser = getAnnotatedUserHandles().workProfileUserHandle;
+ private void onWorkProfileStatusUpdated() {
+ UserHandle workUser = mProfiles.getWorkHandle();
ProfileRecord record = workUser == null ? null : getProfileRecord(workUser);
if (record != null && record.shortcutLoader != null) {
record.shortcutLoader.reset();
}
- super.onWorkProfileStatusUpdated();
+ if (mChooserMultiProfilePagerAdapter.getCurrentUserHandle().equals(
+ mProfiles.getWorkHandle())) {
+ mChooserMultiProfilePagerAdapter.rebuildActiveTab(true);
+ } else {
+ mChooserMultiProfilePagerAdapter.clearInactiveProfileCache();
+ }
}
- @Override
@VisibleForTesting
protected ChooserListController createListController(UserHandle userHandle) {
AppPredictor appPredictor = getAppPredictor(userHandle);
AbstractResolverComparator resolverComparator;
if (appPredictor != null) {
- resolverComparator = new AppPredictionServiceResolverComparator(this, getTargetIntent(),
- getReferrerPackageName(), appPredictor, userHandle, getEventLog(),
- getIntegratedDeviceComponents().getNearbySharingComponent());
+ resolverComparator = new AppPredictionServiceResolverComparator(
+ this,
+ mRequest.getTargetIntent(),
+ mRequest.getLaunchedFromPackage(),
+ appPredictor,
+ userHandle,
+ getEventLog(),
+ mNearbyShare.orElse(null)
+ );
} else {
resolverComparator =
new ResolverRankerServiceResolverComparator(
this,
- getTargetIntent(),
- getReferrerPackageName(),
+ mRequest.getTargetIntent(),
+ mRequest.getReferrerPackage(),
null,
getEventLog(),
getResolverRankerServiceUserHandleList(userHandle),
- getIntegratedDeviceComponents().getNearbySharingComponent());
+ mNearbyShare.orElse(null));
}
return new ChooserListController(
this,
- mPm,
- getTargetIntent(),
- getReferrerPackageName(),
- getAnnotatedUserHandles().userIdOfCallingApp,
+ mPackageManager,
+ mRequest.getTargetIntent(),
+ mRequest.getReferrerPackage(),
+ mViewModel.getActivityModel().getLaunchedFromUid(),
resolverComparator,
- getQueryIntentsUser(userHandle));
+ mProfiles.getQueryIntentsHandle(userHandle),
+ mRequest.getFilteredComponentNames(),
+ mPinnedSharedPrefs);
}
@VisibleForTesting
@@ -1301,11 +2130,70 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
return PreviewViewModel.Companion.getFactory();
}
- private ChooserActionFactory createChooserActionFactory() {
+ private ChooserContentPreviewUi.ActionFactory decorateActionFactoryWithRefinement(
+ ChooserContentPreviewUi.ActionFactory originalFactory) {
+ if (!mFeatureFlags.refineSystemActions()) {
+ return originalFactory;
+ }
+
+ return new ChooserContentPreviewUi.ActionFactory() {
+ @Override
+ @Nullable
+ public Runnable getEditButtonRunnable() {
+ return () -> {
+ if (!mRefinementManager.maybeHandleSelection(
+ RefinementType.EDIT_ACTION,
+ List.of(mRequest.getTargetIntent()),
+ null,
+ mRequest.getRefinementIntentSender(),
+ getApplication(),
+ getMainThreadHandler())) {
+ originalFactory.getEditButtonRunnable().run();
+ }
+ };
+ }
+
+ @Override
+ @Nullable
+ public Runnable getCopyButtonRunnable() {
+ return () -> {
+ if (!mRefinementManager.maybeHandleSelection(
+ RefinementType.COPY_ACTION,
+ List.of(mRequest.getTargetIntent()),
+ null,
+ mRequest.getRefinementIntentSender(),
+ getApplication(),
+ getMainThreadHandler())) {
+ originalFactory.getCopyButtonRunnable().run();
+ }
+ };
+ }
+
+ @Override
+ public List<ActionRow.Action> createCustomActions() {
+ return originalFactory.createCustomActions();
+ }
+
+ @Override
+ @Nullable
+ public ActionRow.Action getModifyShareAction() {
+ return originalFactory.getModifyShareAction();
+ }
+
+ @Override
+ public Consumer<Boolean> getExcludeSharedTextAction() {
+ return originalFactory.getExcludeSharedTextAction();
+ }
+ };
+ }
+
+ private ChooserActionFactory createChooserActionFactory(Intent targetIntent) {
return new ChooserActionFactory(
this,
- mChooserRequest,
- mIntegratedDeviceComponents,
+ targetIntent,
+ mRequest.getLaunchedFromPackage(),
+ mRequest.getChooserActions(),
+ mImageEditor,
getEventLog(),
(isExcluded) -> mExcludeSharedText = isExcluded,
this::getFirstVisibleImgPreviewView,
@@ -1313,7 +2201,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
@Override
public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) {
safelyStartActivityAsUser(
- targetInfo, getAnnotatedUserHandles().personalProfileUserHandle);
+ targetInfo,
+ mProfiles.getPersonalHandle()
+ );
finish();
}
@@ -1324,19 +2214,32 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
ChooserActivity.this, sharedElement, sharedElementName);
safelyStartActivityAsUser(
targetInfo,
- getAnnotatedUserHandles().personalProfileUserHandle,
+ mProfiles.getPersonalHandle(),
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();
- });
+ mShareResultSender,
+ this::finishWithStatus,
+ mClipboardManager);
+ }
+
+ private Supplier<ActionRow.Action> createModifyShareActionFactory() {
+ return () -> ChooserActionFactory.createCustomAction(
+ ChooserActivity.this,
+ mRequest.getModifyShareAction(),
+ () -> getEventLog().logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE),
+ mShareResultSender,
+ this::finishWithStatus);
+ }
+
+ private void finishWithStatus(@Nullable Integer status) {
+ if (status != null) {
+ setResult(status);
+ }
+ finish();
}
/*
@@ -1346,7 +2249,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
*/
private void handleLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
int oldTop, int oldRight, int oldBottom) {
- if (mChooserMultiProfilePagerAdapter == null) {
+ if (mChooserMultiProfilePagerAdapter == null || !isProfilePagerAdapterAttached()) {
return;
}
RecyclerView recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
@@ -1381,8 +2284,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
updateTabPadding();
}
- UserHandle currentUserHandle = mChooserMultiProfilePagerAdapter.getCurrentUserHandle();
- int currentProfile = getProfileForUser(currentUserHandle);
+ int currentProfile = mChooserMultiProfilePagerAdapter.getActiveProfile();
int initialProfile = findSelectedProfile();
if (currentProfile != initialProfile) {
return;
@@ -1408,9 +2310,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
int top, int bottom, RecyclerView recyclerView, ChooserGridAdapter gridAdapter) {
int offset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0;
- int rowsToShow = gridAdapter.getSystemRowCount()
- + gridAdapter.getProfileRowCount()
- + gridAdapter.getServiceTargetRowCount()
+ int rowsToShow = gridAdapter.getServiceTargetRowCount()
+ gridAdapter.getCallerAndRankedTargetRowCount();
// then this is most likely not a SEND_* action, so check
@@ -1432,7 +2332,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
offset += stickyContentPreview.getHeight();
}
- if (shouldShowTabs()) {
+ if (mProfiles.getWorkProfilePresent()) {
offset += findViewById(com.android.internal.R.id.tabs).getHeight();
}
@@ -1455,7 +2355,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
rowsToShow--;
}
} else {
- ViewGroup currentEmptyStateView = getActiveEmptyStateView();
+ ViewGroup currentEmptyStateView =
+ mChooserMultiProfilePagerAdapter.getActiveEmptyStateView();
if (currentEmptyStateView.getVisibility() == View.VISIBLE) {
offset += currentEmptyStateView.getHeight();
}
@@ -1464,43 +2365,21 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
return Math.min(offset, bottom - top);
}
+ private boolean isProfilePagerAdapterAttached() {
+ return mChooserMultiProfilePagerAdapter == mViewPager.getAdapter();
+ }
+
/**
* If we have a tabbed view and are showing 1 row in the current profile and an empty
- * state screen in the other profile, to prevent cropping of the empty state screen we show
+ * 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 shouldShowTabs()
- && rowsToShow == 1
- && mChooserMultiProfilePagerAdapter.shouldShowEmptyStateScreen(
- mChooserMultiProfilePagerAdapter.getInactiveListAdapter());
- }
-
- /**
- * 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(getAnnotatedUserHandles().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;
- }
-
- private ViewGroup getActiveEmptyStateView() {
- int currentPage = mChooserMultiProfilePagerAdapter.getCurrentPage();
- return mChooserMultiProfilePagerAdapter.getEmptyStateView(currentPage);
- }
-
- @Override // ResolverListCommunicator
- public void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
- mChooserMultiProfilePagerAdapter.getActiveListAdapter().notifyDataSetChanged();
- super.onHandlePackagesChanged(listAdapter);
+ return rowsToShow == 1
+ && mChooserMultiProfilePagerAdapter
+ .shouldShowEmptyStateScreenInAnyInactiveAdapter();
}
- @Override
protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) {
setupScrollListener();
maybeSetupGlobalLayoutListener();
@@ -1517,9 +2396,17 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
//TODO: move this block inside ChooserListAdapter (should be called when
// ResolverListAdapter#mPostListReadyRunnable is executed.
if (chooserListAdapter.getDisplayResolveInfoCount() == 0) {
+ if (rebuildComplete && mChooserServiceFeatureFlags.chooserPayloadToggling()) {
+ onAppTargetsLoaded(listAdapter);
+ }
chooserListAdapter.notifyDataSetChanged();
} else {
- chooserListAdapter.updateAlphabeticalList();
+ if (mChooserServiceFeatureFlags.chooserPayloadToggling()) {
+ chooserListAdapter.updateAlphabeticalList(
+ () -> onAppTargetsLoaded(listAdapter));
+ } else {
+ chooserListAdapter.updateAlphabeticalList();
+ }
}
if (rebuildComplete) {
@@ -1570,7 +2457,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
adapter.completeServiceTargetLoading();
}
- if (mMultiProfilePagerAdapter.getActiveListAdapter() == adapter) {
+ if (mChooserMultiProfilePagerAdapter.getActiveListAdapter() == adapter) {
long duration = Tracer.INSTANCE.endLaunchToShortcutTrace();
if (duration >= 0) {
Log.d(TAG, "stat to first shortcut time: " + duration + " ms");
@@ -1585,7 +2472,8 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
if (mResolverDrawerLayout == null) {
return;
}
- int elevatedViewResId = shouldShowTabs() ? com.android.internal.R.id.tabs : com.android.internal.R.id.chooser_header;
+ int elevatedViewResId = mProfiles.getWorkProfilePresent()
+ ? 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 =
@@ -1593,7 +2481,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
mChooserMultiProfilePagerAdapter.getActiveAdapterView().addOnScrollListener(
new RecyclerView.OnScrollListener() {
@Override
- public void onScrollStateChanged(@NonNull RecyclerView view, int scrollState) {
+ public void onScrollStateChanged(RecyclerView view, int scrollState) {
if (scrollState == RecyclerView.SCROLL_STATE_IDLE) {
if (mScrollStatus == SCROLL_STATUS_SCROLLING_VERTICAL) {
mScrollStatus = SCROLL_STATUS_IDLE;
@@ -1608,7 +2496,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
}
@Override
- public void onScrolled(@NonNull RecyclerView view, int dx, int dy) {
+ 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) {
@@ -1623,7 +2511,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
}
private void maybeSetupGlobalLayoutListener() {
- if (shouldShowTabs()) {
+ if (mProfiles.getWorkProfilePresent()) {
return;
}
final View recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
@@ -1657,10 +2545,10 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
if (!shouldShowContentPreview()) {
return false;
}
- boolean isEmpty = mMultiProfilePagerAdapter.getListAdapterForUserHandle(
- UserHandle.of(UserHandle.myUserId())).getCount() == 0;
- return (mFeatureFlags.scrollablePreview() || shouldShowTabs())
- && (!isEmpty || shouldShowContentPreviewWhenEmpty());
+ ResolverListAdapter adapter = mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(
+ UserHandle.of(UserHandle.myUserId()));
+ boolean isEmpty = adapter == null || adapter.getCount() == 0;
+ return !isEmpty || shouldShowContentPreviewWhenEmpty();
}
/**
@@ -1678,7 +2566,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
* @return true if we want to show the content preview area
*/
protected boolean shouldShowContentPreview() {
- return (mChooserRequest != null) && mChooserRequest.isSendActionTarget();
+ return mRequest.isSendActionTarget();
}
private void updateStickyContentPreview() {
@@ -1722,34 +2610,22 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
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() {
+ protected void onProfileTabSelected(int currentPage) {
+ setupViewVisibilities();
+ maybeLogProfileChange();
+ if (mProfiles.getWorkProfilePresent()) {
+ // The device policy logger is only concerned with sessions that include a work profile.
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_SWITCH_TABS)
+ .setInt(currentPage)
+ .setStrings(getMetricsCategory())
+ .write();
+ }
+
// 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
@@ -1759,25 +2635,28 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
}
}
- @Override
protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
- if (shouldShowTabs()) {
+ mSystemWindowInsets = insets.getInsets(WindowInsets.Type.systemBars());
+ if (mFeatureFlags.fixEmptyStatePadding() || mProfiles.getWorkProfilePresent()) {
mChooserMultiProfilePagerAdapter
- .setEmptyStateBottomOffset(insets.getSystemWindowInsetBottom());
- mChooserMultiProfilePagerAdapter.setupContainerPadding(
- getActiveEmptyStateView().findViewById(com.android.internal.R.id.resolver_empty_state_container));
+ .setEmptyStateBottomOffset(mSystemWindowInsets.bottom);
}
- WindowInsets result = super.onApplyWindowInsets(v, insets);
+ mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top,
+ mSystemWindowInsets.right, 0);
+
+ // Need extra padding so the list can fully scroll up
+ // To accommodate for window insets
+ applyFooterView(mSystemWindowInsets.bottom);
+
if (mResolverDrawerLayout != null) {
mResolverDrawerLayout.requestLayout();
}
- return result;
+ return WindowInsets.CONSUMED;
}
private void setHorizontalScrollingEnabled(boolean enabled) {
- ResolverViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
- viewPager.setSwipingEnabled(enabled);
+ mViewPager.setSwipingEnabled(enabled);
}
private void setVerticalScrollEnabled(boolean enabled) {
@@ -1787,7 +2666,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
layoutManager.setVerticalScrollEnabled(enabled);
}
- @Override
void onHorizontalSwipeStateChanged(int state) {
if (state == ViewPager.SCROLL_STATE_DRAGGING) {
if (mScrollStatus == SCROLL_STATUS_IDLE) {
@@ -1802,7 +2680,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements
}
}
- @Override
protected void maybeLogProfileChange() {
getEventLog().logSharesheetProfileChanged();
}
diff --git a/java/src/com/android/intentresolver/ChooserHelper.kt b/java/src/com/android/intentresolver/ChooserHelper.kt
new file mode 100644
index 00000000..6317ee1d
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserHelper.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.app.Activity
+import android.os.UserHandle
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.activity.viewModels
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.intentresolver.annotation.JavaInterop
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ActivityResultRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PendingSelectionCallbackRepository
+import com.android.intentresolver.data.model.ChooserRequest
+import com.android.intentresolver.ui.viewmodel.ChooserViewModel
+import com.android.intentresolver.validation.Invalid
+import com.android.intentresolver.validation.Valid
+import com.android.intentresolver.validation.log
+import dagger.hilt.android.scopes.ActivityScoped
+import java.util.function.Consumer
+import javax.inject.Inject
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.launch
+
+private const val TAG: String = "ChooserHelper"
+
+/**
+ * __Purpose__
+ *
+ * Cleanup aid. Provides a pathway to cleaner code.
+ *
+ * __Incoming References__
+ *
+ * ChooserHelper must not expose any properties or functions directly back to ChooserActivity. If a
+ * value or operation is required by ChooserActivity, then it must be added to ChooserInitializer
+ * (or a new interface as appropriate) with ChooserActivity supplying a callback to receive it at
+ * the appropriate point. This enforces unidirectional control flow.
+ *
+ * __Outgoing References__
+ *
+ * _ChooserActivity_
+ *
+ * This class must only reference it's host as Activity/ComponentActivity; no down-cast to
+ * [ChooserActivity]. Other components should be created here or supplied via Injection, and not
+ * referenced directly within ChooserActivity. This prevents circular dependencies from forming. If
+ * necessary, during cleanup the dependency can be supplied back to ChooserActivity as described
+ * above in 'Incoming References', see [ChooserInitializer].
+ *
+ * _Elsewhere_
+ *
+ * Where possible, Singleton and ActivityScoped dependencies should be injected here instead of
+ * referenced from an existing location. If not available for injection, the value should be
+ * constructed here, then provided to where it is needed.
+ */
+@ActivityScoped
+@JavaInterop
+class ChooserHelper
+@Inject
+constructor(
+ hostActivity: Activity,
+ private val activityResultRepo: ActivityResultRepository,
+ private val pendingSelectionCallbackRepo: PendingSelectionCallbackRepository,
+) : DefaultLifecycleObserver {
+ // This is guaranteed by Hilt, since only a ComponentActivity is injectable.
+ private val activity: ComponentActivity = hostActivity as ComponentActivity
+ private val viewModel by activity.viewModels<ChooserViewModel>()
+
+ // TODO: provide the following through an init object passed into [setInitialize]
+ private lateinit var activityInitializer: Runnable
+ /** Invoked when there are updates to ChooserRequest */
+ var onChooserRequestChanged: Consumer<ChooserRequest> = Consumer {}
+ /** Invoked when there are a new change to payload selection */
+ var onPendingSelection: Runnable = Runnable {}
+
+ init {
+ activity.lifecycle.addObserver(this)
+ }
+
+ /**
+ * Set the initialization hook for the host activity.
+ *
+ * This _must_ be called from [ChooserActivity.onCreate].
+ */
+ fun setInitializer(initializer: Runnable) {
+ check(activity.lifecycle.currentState == Lifecycle.State.INITIALIZED) {
+ "setInitializer must be called before onCreate returns"
+ }
+ activityInitializer = initializer
+ }
+
+ /** Invoked by Lifecycle, after [ChooserActivity.onCreate] _returns_. */
+ override fun onCreate(owner: LifecycleOwner) {
+ Log.i(TAG, "CREATE")
+ Log.i(TAG, "${viewModel.activityModel}")
+
+ val callerUid: Int = viewModel.activityModel.launchedFromUid
+ if (callerUid < 0 || UserHandle.isIsolated(callerUid)) {
+ Log.e(TAG, "Can't start a chooser from uid $callerUid")
+ activity.finish()
+ return
+ }
+
+ when (val request = viewModel.initialRequest) {
+ is Valid -> initializeActivity(request)
+ is Invalid -> reportErrorsAndFinish(request)
+ }
+
+ activity.lifecycleScope.launch {
+ activity.setResult(activityResultRepo.activityResult.filterNotNull().first())
+ activity.finish()
+ }
+
+ activity.lifecycleScope.launch {
+ val hasPendingCallbackFlow =
+ pendingSelectionCallbackRepo.pendingTargetIntent
+ .map { it != null }
+ .distinctUntilChanged()
+ .onEach { hasPendingCallback ->
+ if (hasPendingCallback) {
+ onPendingSelection.run()
+ }
+ }
+ activity.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
+ viewModel.request
+ .combine(hasPendingCallbackFlow) { request, hasPendingCallback ->
+ request to hasPendingCallback
+ }
+ // only take ChooserRequest if there are no pending callbacks
+ .filter { !it.second }
+ .map { it.first }
+ .distinctUntilChanged(areEquivalent = { old, new -> old === new })
+ .collect { onChooserRequestChanged.accept(it) }
+ }
+ }
+ }
+
+ override fun onStart(owner: LifecycleOwner) {
+ Log.i(TAG, "START")
+ }
+
+ override fun onResume(owner: LifecycleOwner) {
+ Log.i(TAG, "RESUME")
+ }
+
+ override fun onPause(owner: LifecycleOwner) {
+ Log.i(TAG, "PAUSE")
+ }
+
+ override fun onStop(owner: LifecycleOwner) {
+ Log.i(TAG, "STOP")
+ }
+
+ override fun onDestroy(owner: LifecycleOwner) {
+ Log.i(TAG, "DESTROY")
+ }
+
+ private fun reportErrorsAndFinish(request: Invalid<ChooserRequest>) {
+ request.errors.forEach { it.log(TAG) }
+ activity.finish()
+ }
+
+ private fun initializeActivity(request: Valid<ChooserRequest>) {
+ request.warnings.forEach { it.log(TAG) }
+ activityInitializer.run()
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
deleted file mode 100644
index 7cd86bf4..00000000
--- a/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
+++ /dev/null
@@ -1,85 +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 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;
-
-/**
- * Helper to look up the components available on this device to handle assorted built-in actions
- * like "Edit" that may be displayed for certain content/preview types. The components are queried
- * when this record is instantiated, and are then immutable for a given instance.
- *
- * Because this describes the app's external execution environment, test methods may prefer to
- * provide explicit values to override the default lookup logic.
- */
-public class ChooserIntegratedDeviceComponents {
- @Nullable
- private final ComponentName mEditSharingComponent;
-
- @Nullable
- private final ComponentName mNearbySharingComponent;
-
- /** Look up the integrated components available on this device. */
- public static ChooserIntegratedDeviceComponents get(
- Context context,
- SecureSettings secureSettings) {
- return new ChooserIntegratedDeviceComponents(
- getEditSharingComponent(context),
- getNearbySharingComponent(context, secureSettings));
- }
-
- @VisibleForTesting
- ChooserIntegratedDeviceComponents(
- @Nullable ComponentName editSharingComponent,
- @Nullable ComponentName nearbySharingComponent) {
- mEditSharingComponent = editSharingComponent;
- mNearbySharingComponent = nearbySharingComponent;
- }
-
- public ComponentName getEditSharingComponent() {
- return mEditSharingComponent;
- }
-
- public ComponentName getNearbySharingComponent() {
- return mNearbySharingComponent;
- }
-
- private static ComponentName getEditSharingComponent(Context context) {
- String editorComponent = context.getApplicationContext().getString(
- R.string.config_systemImageEditor);
- return TextUtils.isEmpty(editorComponent)
- ? null : ComponentName.unflattenFromString(editorComponent);
- }
-
- private static ComponentName getNearbySharingComponent(Context context,
- SecureSettings secureSettings) {
- String nearbyComponent = secureSettings.getString(
- context.getContentResolver(), Settings.Secure.NEARBY_SHARING_COMPONENT);
- if (TextUtils.isEmpty(nearbyComponent)) {
- nearbyComponent = context.getString(R.string.config_defaultNearbySharingComponent);
- }
- return TextUtils.isEmpty(nearbyComponent)
- ? null : ComponentName.unflattenFromString(nearbyComponent);
- }
-}
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java
index 876ad5c3..29b5698b 100644
--- a/java/src/com/android/intentresolver/ChooserListAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -48,12 +48,14 @@ import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.DisplayResolveInfoAzInfoComparator;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.NotSelectableTargetInfo;
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.widget.BadgeTextView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
@@ -109,6 +111,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
// Reserve spots for incoming direct share targets by adding placeholders
private final TargetInfo mPlaceHolderTargetInfo;
private final TargetDataLoader mTargetDataLoader;
+ private final boolean mUseBadgeTextViewForLabels;
private final List<TargetInfo> mServiceTargets = new ArrayList<>();
private final List<DisplayResolveInfo> mCallerTargets = new ArrayList<>();
@@ -166,7 +169,8 @@ public class ChooserListAdapter extends ResolverListAdapter {
int maxRankedTargets,
UserHandle initialIntentsUserSpace,
TargetDataLoader targetDataLoader,
- @Nullable PackageChangeCallback packageChangeCallback) {
+ @Nullable PackageChangeCallback packageChangeCallback,
+ FeatureFlags featureFlags) {
this(
context,
payloadIntents,
@@ -185,7 +189,8 @@ public class ChooserListAdapter extends ResolverListAdapter {
targetDataLoader,
packageChangeCallback,
AsyncTask.SERIAL_EXECUTOR,
- context.getMainExecutor());
+ context.getMainExecutor(),
+ featureFlags);
}
@VisibleForTesting
@@ -207,7 +212,8 @@ public class ChooserListAdapter extends ResolverListAdapter {
TargetDataLoader targetDataLoader,
@Nullable PackageChangeCallback packageChangeCallback,
Executor bgExecutor,
- Executor mainExecutor) {
+ Executor mainExecutor,
+ FeatureFlags featureFlags) {
// Don't send the initial intents through the shared ResolverActivity path,
// we want to separate them into a different section.
super(
@@ -231,6 +237,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
mPlaceHolderTargetInfo = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context);
mTargetDataLoader = targetDataLoader;
mPackageChangeCallback = packageChangeCallback;
+ mUseBadgeTextViewForLabels = featureFlags.bespokeLabelView();
createPlaceHolders();
mEventLog = eventLog;
mShortcutSelectionLogic = new ShortcutSelectionLogic(
@@ -332,15 +339,27 @@ public class ChooserListAdapter extends ResolverListAdapter {
@Override
View onCreateView(ViewGroup parent) {
- return mInflater.inflate(R.layout.resolve_grid_item, parent, false);
+ return mInflater.inflate(
+ mUseBadgeTextViewForLabels
+ ? R.layout.chooser_grid_item
+ : R.layout.resolve_grid_item,
+ parent,
+ false);
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ notifyDataSetChanged();
}
@VisibleForTesting
@Override
public void onBindView(View view, TargetInfo info, int position) {
+ view.setEnabled(!isDestroyed());
final ViewHolder holder = (ViewHolder) view.getTag();
- holder.reset();
+ resetViewHolder(holder);
// Always remove the spacing listener, attach as needed to direct share targets below.
holder.text.removeOnLayoutChangeListener(mPinTextSpacingListener);
@@ -377,16 +396,18 @@ public class ChooserListAdapter extends ResolverListAdapter {
contentDescription,
mContext.getResources().getString(R.string.pinned));
}
- holder.updateContentDescription(contentDescription);
+ updateContentDescription(holder, 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)));
+ updateContentDescription(
+ holder,
+ String.join(
+ ". ",
+ info.getDisplayLabel(),
+ mContext.getResources().getString(R.string.pinned)));
}
DisplayResolveInfo dri = (DisplayResolveInfo) info;
if (!dri.hasDisplayIcon()) {
@@ -398,22 +419,56 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
if (info.isPlaceHolderTargetInfo()) {
- holder.bindPlaceholder();
+ bindPlaceholder(holder);
}
if (info.isMultiDisplayResolveInfo()) {
// If the target is grouped show an indicator
- holder.bindGroupIndicator(
+ bindGroupIndicator(
+ holder,
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
- holder.bindPinnedIndicator(mContext.getDrawable(R.drawable.chooser_pinned_background));
+ bindPinnedIndicator(holder, mContext.getDrawable(R.drawable.chooser_pinned_background));
holder.text.addOnLayoutChangeListener(mPinTextSpacingListener);
}
}
+ private void resetViewHolder(ViewHolder holder) {
+ holder.reset();
+ holder.itemView.setBackground(holder.defaultItemViewBackground);
+
+ if (mUseBadgeTextViewForLabels) {
+ ((BadgeTextView) holder.text).setBadgeDrawable(null);
+ }
+ holder.text.setBackground(null);
+ holder.text.setPaddingRelative(0, 0, 0, 0);
+ }
+
+ private void updateContentDescription(ViewHolder holder, String description) {
+ holder.itemView.setContentDescription(description);
+ }
+
+ private void bindPlaceholder(ViewHolder holder) {
+ holder.itemView.setBackground(null);
+ }
+
+ private void bindGroupIndicator(ViewHolder holder, Drawable indicator) {
+ if (mUseBadgeTextViewForLabels) {
+ ((BadgeTextView) holder.text).setBadgeDrawable(indicator);
+ } else {
+ holder.text.setPaddingRelative(0, 0, /*end = */indicator.getIntrinsicWidth(), 0);
+ holder.text.setBackground(indicator);
+ }
+ }
+
+ private void bindPinnedIndicator(ViewHolder holder, Drawable indicator) {
+ holder.text.setPaddingRelative(/*start = */indicator.getIntrinsicWidth(), 0, 0, 0);
+ holder.text.setBackground(indicator);
+ }
+
private void loadDirectShareIcon(SelectableTargetInfo info) {
if (mRequestedIcons.add(info)) {
mTargetDataLoader.loadDirectShareIcon(
@@ -430,9 +485,19 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
}
+ /**
+ * Group application targets
+ */
public void updateAlphabeticalList() {
- final ChooserActivity.AzInfoComparator comparator =
- new ChooserActivity.AzInfoComparator(mContext);
+ updateAlphabeticalList(() -> {});
+ }
+
+ /**
+ * Group application targets
+ */
+ public void updateAlphabeticalList(Runnable onCompleted) {
+ final DisplayResolveInfoAzInfoComparator
+ comparator = new DisplayResolveInfoAzInfoComparator(mContext);
final List<DisplayResolveInfo> allTargets = new ArrayList<>();
allTargets.addAll(getTargetsInCurrentDisplayList());
allTargets.addAll(mCallerTargets);
@@ -475,6 +540,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
mSortedList.clear();
mSortedList.addAll(newList);
notifyDataSetChanged();
+ onCompleted.run();
}
private void loadMissingLabels(List<DisplayResolveInfo> targets) {
@@ -664,7 +730,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
public void addServiceResults(
@Nullable DisplayResolveInfo origTarget,
List<ChooserTarget> targets,
- @ChooserActivity.ShareTargetType int targetType,
+ int targetType,
Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos,
Map<ChooserTarget, AppTarget> directShareToAppTargets) {
// Avoid inserting any potentially late results.
@@ -701,7 +767,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
*/
public float getBaseScore(
DisplayResolveInfo target,
- @ChooserActivity.ShareTargetType int targetType) {
+ int targetType) {
if (target == null) {
return CALLER_TARGET_SCORE_BOOST;
}
@@ -744,9 +810,6 @@ public class ChooserListAdapter extends ResolverListAdapter {
@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/ChooserListController.java b/java/src/com/android/intentresolver/ChooserListController.java
new file mode 100644
index 00000000..48aa8be1
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserListController.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.os.UserHandle;
+
+import com.android.intentresolver.model.AbstractResolverComparator;
+
+import java.util.List;
+
+public class ChooserListController extends ResolverListController {
+ private final List<ComponentName> mFilteredComponents;
+ private final SharedPreferences mPinnedComponents;
+
+ public ChooserListController(
+ Context context,
+ PackageManager pm,
+ Intent targetIntent,
+ String referrerPackageName,
+ int launchedFromUid,
+ AbstractResolverComparator resolverComparator,
+ UserHandle queryIntentsAsUser,
+ List<ComponentName> filteredComponents,
+ SharedPreferences pinnedComponents) {
+ super(
+ context,
+ pm,
+ targetIntent,
+ referrerPackageName,
+ launchedFromUid,
+ resolverComparator,
+ queryIntentsAsUser);
+ mFilteredComponents = filteredComponents;
+ mPinnedComponents = pinnedComponents;
+ }
+
+ @Override
+ public boolean isComponentFiltered(ComponentName name) {
+ return mFilteredComponents.contains(name);
+ }
+
+ @Override
+ public boolean isComponentPinned(ComponentName name) {
+ return mPinnedComponents.getBoolean(name.flattenToString(), false);
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
deleted file mode 100644
index 080f9d24..00000000
--- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
+++ /dev/null
@@ -1,214 +0,0 @@
-/*
- * 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;
-
-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.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);
- }
-
- /**
- * 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 rebuildActiveTab(boolean doPostProcessing) {
- if (doPostProcessing) {
- Tracer.INSTANCE.beginAppTargetLoadingSection(getActiveListAdapter().getUserHandle());
- }
- return super.rebuildActiveTab(doPostProcessing);
- }
-
- @Override
- public boolean rebuildInactiveTab(boolean doPostProcessing) {
- if (getItemCount() != 1 && doPostProcessing) {
- Tracer.INSTANCE.beginAppTargetLoadingSection(getInactiveListAdapter().getUserHandle());
- }
- return super.rebuildInactiveTab(doPostProcessing);
- }
-
- 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;
- }
-
- 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/ChooserRefinementManager.java b/java/src/com/android/intentresolver/ChooserRefinementManager.java
index 474b240f..5c828a8e 100644
--- a/java/src/com/android/intentresolver/ChooserRefinementManager.java
+++ b/java/src/com/android/intentresolver/ChooserRefinementManager.java
@@ -41,7 +41,6 @@ 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
@@ -60,22 +59,58 @@ public final class ChooserRefinementManager extends ViewModel {
private boolean mConfigurationChangeInProgress = false;
/**
+ * The types of selections that may be sent to refinement.
+ *
+ * The refinement flow results in a refined intent, but the interpretation of that intent
+ * depends on the type of selection that prompted the refinement.
+ */
+ public enum RefinementType {
+ TARGET_INFO, // A normal (`TargetInfo`) target.
+
+ // System actions derived from the refined intent (from `ChooserActionFactory`).
+ COPY_ACTION,
+ EDIT_ACTION
+ }
+
+ /**
* A token for the completion of a refinement process that can be consumed exactly once.
*/
public static class RefinementCompletion {
private TargetInfo mTargetInfo;
private boolean mConsumed;
+ private final RefinementType mType;
+
+ @Nullable
+ private final TargetInfo mOriginalTargetInfo;
+
+ @Nullable
+ private final Intent mRefinedIntent;
+
+ RefinementCompletion(
+ @Nullable RefinementType type,
+ @Nullable TargetInfo originalTargetInfo,
+ @Nullable Intent refinedIntent) {
+ mType = type;
+ mOriginalTargetInfo = originalTargetInfo;
+ mRefinedIntent = refinedIntent;
+ }
- RefinementCompletion(TargetInfo targetInfo) {
- mTargetInfo = targetInfo;
+ public RefinementType getType() {
+ return mType;
+ }
+
+ @Nullable
+ public TargetInfo getOriginalTargetInfo() {
+ return mOriginalTargetInfo;
}
/**
* @return The output of the completed refinement process. Null if the process was aborted
* or failed.
*/
- public TargetInfo getTargetInfo() {
- return mTargetInfo;
+ @Nullable
+ public Intent getRefinedIntent() {
+ return mRefinedIntent;
}
/**
@@ -106,14 +141,11 @@ public final class ChooserRefinementManager extends ViewModel {
* @return true if the selection should wait for a now-started refinement flow, or false if it
* can proceed by the default (non-refinement) logic.
*/
- public boolean maybeHandleSelection(TargetInfo selectedTarget,
- IntentSender refinementIntentSender, Application application, Handler mainHandler) {
- if (refinementIntentSender == null) {
- return false;
- }
- if (selectedTarget.getAllSourceIntents().isEmpty()) {
- return false;
- }
+ public boolean maybeHandleSelection(
+ TargetInfo selectedTarget,
+ IntentSender refinementIntentSender,
+ Application application,
+ Handler mainHandler) {
if (selectedTarget.isSuspended()) {
// We expect all launches to fail for this target, so don't make the user go through the
// refinement flow first. Besides, the default (non-refinement) handling displays a
@@ -122,27 +154,57 @@ public final class ChooserRefinementManager extends ViewModel {
return false;
}
+ return maybeHandleSelection(
+ RefinementType.TARGET_INFO,
+ selectedTarget.getAllSourceIntents(),
+ selectedTarget,
+ refinementIntentSender,
+ application,
+ mainHandler);
+ }
+
+ /**
+ * Delegate the user's selection of targets (with one or more matching {@code sourceIntents} to
+ * the refinement flow, if possible.
+ * @return true if the selection should wait for a now-started refinement flow, or false if it
+ * can proceed by the default (non-refinement) logic.
+ */
+ public boolean maybeHandleSelection(
+ RefinementType refinementType,
+ List<Intent> sourceIntents,
+ @Nullable TargetInfo originalTargetInfo,
+ IntentSender refinementIntentSender,
+ Application application,
+ Handler mainHandler) {
+ // Our requests have a non-null `originalTargetInfo` in exactly the
+ // cases when `refinementType == TARGET_INFO`.
+ assert ((originalTargetInfo == null) == (refinementType == RefinementType.TARGET_INFO));
+
+ if (refinementIntentSender == null) {
+ return false;
+ }
+ if (sourceIntents.isEmpty()) {
+ return false;
+ }
+
destroy(); // Terminate any prior sessions.
mRefinementResultReceiver = new RefinementResultReceiver(
+ refinementType,
refinedIntent -> {
destroy();
-
- TargetInfo refinedTarget =
- selectedTarget.tryToCloneWithAppliedRefinement(refinedIntent);
- if (refinedTarget != null) {
- mRefinementCompletion.setValue(new RefinementCompletion(refinedTarget));
- } else {
- Log.e(TAG, "Failed to apply refinement to any matching source intent");
- mRefinementCompletion.setValue(new RefinementCompletion(null));
- }
+ mRefinementCompletion.setValue(
+ new RefinementCompletion(
+ refinementType, originalTargetInfo, refinedIntent));
},
() -> {
destroy();
- mRefinementCompletion.setValue(new RefinementCompletion(null));
+ mRefinementCompletion.setValue(
+ new RefinementCompletion(
+ refinementType, originalTargetInfo, null));
},
mainHandler);
- Intent refinementRequest = makeRefinementRequest(mRefinementResultReceiver, selectedTarget);
+ Intent refinementRequest = makeRefinementRequest(mRefinementResultReceiver, sourceIntents);
try {
refinementIntentSender.sendIntent(application, 0, refinementRequest, null, null);
return true;
@@ -168,7 +230,7 @@ public final class ChooserRefinementManager extends ViewModel {
// into a valid Chooser session, so we'll treat it as a cancellation instead.
Log.w(TAG, "Chooser resumed while awaiting refinement result; aborting");
destroy();
- mRefinementCompletion.setValue(new RefinementCompletion(null));
+ mRefinementCompletion.setValue(new RefinementCompletion(null, null, null));
}
}
}
@@ -188,9 +250,8 @@ public final class ChooserRefinementManager extends ViewModel {
}
private static Intent makeRefinementRequest(
- RefinementResultReceiver resultReceiver, TargetInfo originalTarget) {
+ RefinementResultReceiver resultReceiver, List<Intent> sourceIntents) {
final Intent fillIn = new Intent();
- final List<Intent> sourceIntents = originalTarget.getAllSourceIntents();
fillIn.putExtra(Intent.EXTRA_INTENT, sourceIntents.get(0));
final int sourceIntentCount = sourceIntents.size();
if (sourceIntentCount > 1) {
@@ -205,16 +266,19 @@ public final class ChooserRefinementManager extends ViewModel {
}
private static class RefinementResultReceiver extends ResultReceiver {
+ private final RefinementType mType;
private final Consumer<Intent> mOnSelectionRefined;
private final Runnable mOnRefinementCancelled;
private boolean mDestroyed;
RefinementResultReceiver(
+ RefinementType type,
Consumer<Intent> onSelectionRefined,
Runnable onRefinementCancelled,
Handler handler) {
super(handler);
+ mType = type;
mOnSelectionRefined = onSelectionRefined;
mOnRefinementCancelled = onRefinementCancelled;
}
diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java
index 7ad809e9..06f56e3b 100644
--- a/java/src/com/android/intentresolver/ChooserRequestParameters.java
+++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java
@@ -16,6 +16,7 @@
package com.android.intentresolver;
+
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
@@ -41,6 +42,7 @@ import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
+import java.util.Optional;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@@ -101,6 +103,9 @@ public class ChooserRequestParameters {
@Nullable
private final IntentFilter mTargetIntentFilter;
+ @Nullable
+ private final CharSequence mMetadataText;
+
public ChooserRequestParameters(
final Intent clientIntent,
String referrerPackageName,
@@ -125,8 +130,14 @@ public class ChooserRequestParameters {
mReferrerFillInIntent = new Intent().putExtra(Intent.EXTRA_REFERRER, referrer);
- mChosenComponentSender = clientIntent.getParcelableExtra(
- Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER);
+ mChosenComponentSender =
+ Optional.ofNullable(
+ clientIntent.getParcelableExtra(Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER,
+ IntentSender.class))
+ .orElse(clientIntent.getParcelableExtra(
+ Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER,
+ IntentSender.class));
+
mRefinementIntentSender = clientIntent.getParcelableExtra(
Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER);
@@ -147,6 +158,12 @@ public class ChooserRequestParameters {
mChooserActions = getChooserActions(clientIntent);
mModifyShareAction = getModifyShareAction(clientIntent);
+
+ if (android.service.chooser.Flags.enableSharesheetMetadataExtra()) {
+ mMetadataText = clientIntent.getCharSequenceExtra(Intent.EXTRA_METADATA_TEXT);
+ } else {
+ mMetadataText = null;
+ }
}
public Intent getTargetIntent() {
@@ -252,6 +269,11 @@ public class ChooserRequestParameters {
return mTargetIntentFilter;
}
+ @Nullable
+ public CharSequence getMetadataText() {
+ return mMetadataText;
+ }
+
private static boolean isSendAction(@Nullable String action) {
return (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action));
}
diff --git a/java/src/com/android/intentresolver/v2/ChooserSelector.kt b/java/src/com/android/intentresolver/ChooserSelector.kt
index 378bc06c..c1174e95 100644
--- a/java/src/com/android/intentresolver/v2/ChooserSelector.kt
+++ b/java/src/com/android/intentresolver/ChooserSelector.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.intentresolver.v2
import android.content.BroadcastReceiver
diff --git a/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
index f0fcd149..30e69c18 100644
--- a/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
+++ b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
@@ -63,7 +63,7 @@ public class ChooserStackedAppDialogFragment extends ChooserTargetActionsDialogF
@Override
public void onClick(DialogInterface dialog, int which) {
mMultiDisplayResolveInfo.setSelected(which);
- ((ChooserActivity) getActivity()).startSelected(mParentWhich, false, true);
+ ((StartsSelectedItem) getActivity()).startSelected(mParentWhich, false, true);
dismiss();
}
diff --git a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
index b6b7de96..ae80fad4 100644
--- a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
+++ b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
@@ -205,7 +205,7 @@ public class ChooserTargetActionsDialogFragment extends DialogFragment
} else {
pinComponent(mTargetInfos.get(which).getResolvedComponentName());
}
- ((ChooserActivity) getActivity()).handlePackagesChanged();
+ ((PackagesChangedListener) getActivity()).handlePackagesChanged();
dismiss();
}
diff --git a/java/src/com/android/intentresolver/ContentTypeHint.kt b/java/src/com/android/intentresolver/ContentTypeHint.kt
new file mode 100644
index 00000000..f607e4ae
--- /dev/null
+++ b/java/src/com/android/intentresolver/ContentTypeHint.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.content.Intent
+
+/** Enum reflecting the value of [Intent.EXTRA_CHOOSER_CONTENT_TYPE_HINT]. */
+enum class ContentTypeHint {
+ NONE,
+ ALBUM,
+}
diff --git a/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt b/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt
index b1178aa5..6a4fe65a 100644
--- a/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt
+++ b/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt
@@ -21,14 +21,14 @@ import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback
import com.android.internal.annotations.VisibleForTesting
+import java.util.function.Supplier
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-import java.util.function.Supplier
/**
- * A helper class to track app's readiness for the scene transition animation.
- * The app is ready when both the image is laid out and the drawer offset is calculated.
+ * A helper class to track app's readiness for the scene transition animation. The app is ready when
+ * both the image is laid out and the drawer offset is calculated.
*/
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
class EnterTransitionAnimationDelegate(
@@ -45,21 +45,22 @@ class EnterTransitionAnimationDelegate(
activity.setEnterSharedElementCallback(
object : SharedElementCallback() {
override fun onMapSharedElements(
- names: MutableList<String>, sharedElements: MutableMap<String, View>
+ names: MutableList<String>,
+ sharedElements: MutableMap<String, View>
) {
- this@EnterTransitionAnimationDelegate.onMapSharedElements(
- names, sharedElements
- )
+ this@EnterTransitionAnimationDelegate.onMapSharedElements(names, sharedElements)
}
- })
+ }
+ )
}
fun postponeTransition() {
activity.postponeEnterTransition()
- timeoutJob = activity.lifecycleScope.launch {
- delay(activity.resources.getInteger(R.integer.config_shortAnimTime).toLong())
- onTimeout()
- }
+ timeoutJob =
+ activity.lifecycleScope.launch {
+ delay(activity.resources.getInteger(R.integer.config_shortAnimTime).toLong())
+ onTimeout()
+ }
}
private fun onTimeout() {
@@ -110,8 +111,14 @@ class EnterTransitionAnimationDelegate(
override fun onLayoutChange(
v: View,
- left: Int, top: Int, right: Int, bottom: Int,
- oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int
+ left: Int,
+ top: Int,
+ right: Int,
+ bottom: Int,
+ oldLeft: Int,
+ oldTop: Int,
+ oldRight: Int,
+ oldBottom: Int
) {
v.removeOnLayoutChangeListener(this)
startPostponedEnterTransition()
diff --git a/java/src/com/android/intentresolver/IntentForwarderActivity.java b/java/src/com/android/intentresolver/IntentForwarderActivity.java
index 15996d00..db94c918 100644
--- a/java/src/com/android/intentresolver/IntentForwarderActivity.java
+++ b/java/src/com/android/intentresolver/IntentForwarderActivity.java
@@ -20,8 +20,8 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTEN
import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK;
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 static com.android.intentresolver.ui.viewmodel.ResolverRequestReaderKt.EXTRA_CALLING_USER;
+import static com.android.intentresolver.ui.viewmodel.ResolverRequestReaderKt.EXTRA_SELECTED_PROFILE;
import android.app.Activity;
import android.app.ActivityThread;
@@ -46,6 +46,7 @@ import android.widget.Toast;
import androidx.annotation.Nullable;
+import com.android.intentresolver.profiles.MultiProfilePagerAdapter;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -254,9 +255,9 @@ public class IntentForwarderActivity extends Activity {
private int findSelectedProfile(String className) {
if (className.equals(FORWARD_INTENT_TO_PARENT)) {
- return ChooserActivity.PROFILE_PERSONAL;
+ return MultiProfilePagerAdapter.PROFILE_PERSONAL;
} else if (className.equals(FORWARD_INTENT_TO_MANAGED_PROFILE)) {
- return ChooserActivity.PROFILE_WORK;
+ return MultiProfilePagerAdapter.PROFILE_WORK;
}
return -1;
}
diff --git a/java/src/com/android/intentresolver/IntentForwarding.kt b/java/src/com/android/intentresolver/IntentForwarding.kt
new file mode 100644
index 00000000..c8f6cf41
--- /dev/null
+++ b/java/src/com/android/intentresolver/IntentForwarding.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver
+
+import android.Manifest
+import android.Manifest.permission.INTERACT_ACROSS_USERS
+import android.Manifest.permission.INTERACT_ACROSS_USERS_FULL
+import android.app.ActivityManager
+import android.content.Context
+import android.content.Intent
+import android.content.PermissionChecker
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.Log
+import com.android.intentresolver.data.repository.DevicePolicyResources
+import javax.inject.Inject
+import javax.inject.Singleton
+
+private const val TAG: String = "IntentForwarding"
+
+@Singleton
+class IntentForwarding
+@Inject
+constructor(
+ private val resources: DevicePolicyResources,
+ private val userManager: UserManager,
+ private val packageManager: PackageManager
+) {
+
+ 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 -> resources.forwardToPersonalMessage
+ !originIsManaged && targetIsManaged -> resources.forwardToWorkMessage
+ else -> null
+ }
+ }
+ return null
+ }
+
+ private fun isPermissionGranted(permission: String, uid: Int) =
+ ActivityManager.checkComponentPermission(
+ /* permission = */ permission,
+ /* uid = */ uid,
+ /* owningUid= */ -1,
+ /* exported= */ true
+ )
+
+ /**
+ * Returns whether the package has the necessary permissions to interact across profiles on
+ * behalf of a given user.
+ *
+ * This means meeting the following condition:
+ * * The app's [ApplicationInfo.crossProfile] flag must be true, and at least one of the
+ * following conditions must be fulfilled
+ * * `Manifest.permission.INTERACT_ACROSS_USERS_FULL` granted.
+ * * `Manifest.permission.INTERACT_ACROSS_USERS` granted.
+ * * `Manifest.permission.INTERACT_ACROSS_PROFILES` granted, or the corresponding AppOps
+ * `android:interact_across_profiles` is set to "allow".
+ */
+ fun canAppInteractAcrossProfiles(context: Context, packageName: String): Boolean {
+ val applicationInfo: ApplicationInfo
+ try {
+ applicationInfo = packageManager.getApplicationInfo(packageName, 0)
+ } catch (e: PackageManager.NameNotFoundException) {
+ Log.e(TAG, "Package $packageName does not exist on current user.")
+ return false
+ }
+ if (!applicationInfo.crossProfile) {
+ return false
+ }
+
+ val packageUid = applicationInfo.uid
+
+ if (isPermissionGranted(INTERACT_ACROSS_USERS_FULL, packageUid) == PERMISSION_GRANTED) {
+ return true
+ }
+ if (isPermissionGranted(INTERACT_ACROSS_USERS, packageUid) == PERMISSION_GRANTED) {
+ return true
+ }
+ return PermissionChecker.checkPermissionForPreflight(
+ context,
+ Manifest.permission.INTERACT_ACROSS_PROFILES,
+ PermissionChecker.PID_UNKNOWN,
+ packageUid,
+ packageName
+ ) == PERMISSION_GRANTED
+ }
+}
diff --git a/java/src/com/android/intentresolver/ItemRevealAnimationTracker.kt b/java/src/com/android/intentresolver/ItemRevealAnimationTracker.kt
index d3e07c6b..7deb0d10 100644
--- a/java/src/com/android/intentresolver/ItemRevealAnimationTracker.kt
+++ b/java/src/com/android/intentresolver/ItemRevealAnimationTracker.kt
@@ -37,9 +37,7 @@ internal class ItemRevealAnimationTracker {
fun animateLabel(view: View, info: TargetInfo) = animateView(view, info, labelProgress)
private fun animateView(view: View, info: TargetInfo, map: MutableMap<TargetInfo, Record>) {
- val record = map.getOrPut(info) {
- Record()
- }
+ val record = map.getOrPut(info) { Record() }
if ((view.animation as? RevealAnimation)?.record === record) return
view.clearAnimation()
diff --git a/java/src/com/android/intentresolver/JavaFlowHelper.kt b/java/src/com/android/intentresolver/JavaFlowHelper.kt
new file mode 100644
index 00000000..231cb809
--- /dev/null
+++ b/java/src/com/android/intentresolver/JavaFlowHelper.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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("JavaFlowHelper")
+
+package com.android.intentresolver
+
+import com.android.intentresolver.annotation.JavaInterop
+import java.util.function.Consumer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.launch
+
+@JavaInterop
+fun <T> collect(scope: CoroutineScope, flow: Flow<T>, collector: Consumer<T>): Job =
+ scope.launch { flow.collect { collector.accept(it) } }
diff --git a/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java
deleted file mode 100644
index 42a29e55..00000000
--- a/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java
+++ /dev/null
@@ -1,583 +0,0 @@
-/*
- * 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;
-
-import android.os.Trace;
-import android.os.UserHandle;
-import android.view.View;
-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.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).
- *
- * 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 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 Supplier<Optional<Integer>> mContainerBottomPaddingOverrideSupplier;
-
- 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;
- mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier;
-
- ImmutableList.Builder<ProfileDescriptor<PageViewT, SinglePageAdapterT>> items =
- new ImmutableList.Builder<>();
- for (SinglePageAdapterT adapter : adapters) {
- items.add(createProfileDescriptor(adapter));
- }
- mItems = items.build();
- }
-
- private ProfileDescriptor<PageViewT, SinglePageAdapterT> createProfileDescriptor(
- SinglePageAdapterT adapter) {
- return new ProfileDescriptor<>(mPageViewInflater.get(), adapter);
- }
-
- 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);
- }
-
- @NonNull
- @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, @NonNull Object view) {
- container.removeView((View) view);
- }
-
- @Override
- public int getCount() {
- return getItemCount();
- }
-
- public int getCurrentPage() {
- return mCurrentPage;
- }
-
- @VisibleForTesting
- public UserHandle getCurrentUserHandle() {
- return getActiveListAdapter().getUserHandle();
- }
-
- @Override
- public boolean isViewFromObject(@NonNull View view, @NonNull 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);
- }
-
- public ViewGroup getEmptyStateView(int pageIndex) {
- return getItem(pageIndex).getEmptyStateView();
- }
-
- /**
- * 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));
- }
-
- @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());
- }
-
- /**
- * Rebuilds the tab that is currently visible to the user.
- * <p>Returns {@code true} if rebuild has completed.
- */
- public 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.
- */
- public 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;
- }
- }
-
- private 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.
- */
- 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();
- }
- }
-
- protected void showEmptyState(
- ListAdapterT activeListAdapter,
- EmptyState emptyState,
- View.OnClickListener buttonOnClick) {
- ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
- userHandleToPageIndex(activeListAdapter.getUserHandle()));
- descriptor.mRootView.findViewById(
- com.android.internal.R.id.resolver_list).setVisibility(View.GONE);
- descriptor.mEmptyStateUi.resetViewVisibilities();
-
- ViewGroup emptyStateView = descriptor.getEmptyStateView();
-
- 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);
- String title = emptyState.getTitle();
- if (title != null) {
- titleView.setVisibility(View.VISIBLE);
- titleView.setText(title);
- } else {
- titleView.setVisibility(View.GONE);
- }
-
- TextView subtitleView = emptyStateView.findViewById(
- com.android.internal.R.id.resolver_empty_state_subtitle);
- String subtitle = emptyState.getSubtitle();
- if (subtitle != null) {
- subtitleView.setVisibility(View.VISIBLE);
- subtitleView.setText(subtitle);
- } else {
- subtitleView.setVisibility(View.GONE);
- }
-
- 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.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE);
- button.setOnClickListener(buttonOnClick);
-
- activeListAdapter.markTabLoaded();
- }
-
- /**
- * 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.
- */
- 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.mRootView.findViewById(
- com.android.internal.R.id.resolver_list).setVisibility(View.VISIBLE);
- descriptor.mEmptyStateUi.hide();
- }
-
- 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) {
- 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() {
- return mEmptyStateView;
- }
- }
-
- /** 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/PackagesChangedListener.kt b/java/src/com/android/intentresolver/PackagesChangedListener.kt
new file mode 100644
index 00000000..10f0bf51
--- /dev/null
+++ b/java/src/com/android/intentresolver/PackagesChangedListener.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver
+
+/** A component which can be notified when packages have changed. */
+interface PackagesChangedListener {
+ /** Report that packages have changed. */
+ fun handlePackagesChanged()
+}
diff --git a/java/src/com/android/intentresolver/ProfileAvailability.kt b/java/src/com/android/intentresolver/ProfileAvailability.kt
new file mode 100644
index 00000000..c8e78552
--- /dev/null
+++ b/java/src/com/android/intentresolver/ProfileAvailability.kt
@@ -0,0 +1,103 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import androidx.annotation.MainThread
+import com.android.intentresolver.annotation.JavaInterop
+import com.android.intentresolver.domain.interactor.UserInteractor
+import com.android.intentresolver.shared.model.Profile
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+
+/** Provides availability status for profiles */
+@JavaInterop
+class ProfileAvailability(
+ private val userInteractor: UserInteractor,
+ private val scope: CoroutineScope,
+ private val background: CoroutineDispatcher,
+) {
+ /** Used by WorkProfilePausedEmptyStateProvider */
+ var waitingToEnableProfile = false
+ private set
+
+ /** Set by ChooserActivity to call onWorkProfileStatusUpdated */
+ var onProfileStatusChange: Runnable? = null
+
+ private var waitJob: Job? = null
+
+ /** Query current profile availability. An unavailable profile is one which is not active. */
+ @MainThread
+ fun isAvailable(profile: Profile): Boolean {
+ return runBlocking(background) {
+ userInteractor.availability.map { it[profile] == true }.first()
+ }
+ }
+
+ /**
+ * The number of profiles which are visible. All profiles count except for private which is
+ * hidden when locked.
+ */
+ fun visibleProfileCount() =
+ runBlocking(background) {
+ val availability = userInteractor.availability.first()
+ val profiles = userInteractor.profiles.first()
+ profiles
+ .filter {
+ when (it.type) {
+ Profile.Type.PRIVATE -> availability[it] == true
+ else -> true
+ }
+ }
+ .size
+ }
+
+ /** Used by WorkProfilePausedEmptyStateProvider */
+ fun requestQuietModeState(profile: Profile, quietMode: Boolean) {
+ val enableProfile = !quietMode
+
+ // Check if the profile is already in the correct state
+ if (isAvailable(profile) == enableProfile) {
+ return // No-op
+ }
+
+ // Support existing code
+ if (enableProfile) {
+ waitingToEnableProfile = true
+ waitJob?.cancel()
+
+ val job =
+ scope.launch {
+ // Wait for the profile to become available
+ userInteractor.availability.filter { it[profile] == true }.first()
+ }
+ job.invokeOnCompletion {
+ waitingToEnableProfile = false
+ onProfileStatusChange?.run()
+ }
+ waitJob = job
+ }
+
+ // Apply the change
+ scope.launch { userInteractor.updateState(profile, enableProfile) }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ProfileHelper.kt b/java/src/com/android/intentresolver/ProfileHelper.kt
new file mode 100644
index 00000000..e1d912c3
--- /dev/null
+++ b/java/src/com/android/intentresolver/ProfileHelper.kt
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.os.UserHandle
+import androidx.annotation.MainThread
+import com.android.intentresolver.annotation.JavaInterop
+import com.android.intentresolver.domain.interactor.UserInteractor
+import com.android.intentresolver.inject.IntentResolverFlags
+import com.android.intentresolver.shared.model.Profile
+import com.android.intentresolver.shared.model.User
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.runBlocking
+
+@JavaInterop
+@MainThread
+class ProfileHelper
+@Inject
+constructor(
+ interactor: UserInteractor,
+ private val scope: CoroutineScope,
+ private val background: CoroutineDispatcher,
+ private val flags: IntentResolverFlags,
+) {
+ private val launchedByHandle: UserHandle = interactor.launchedAs
+
+ val launchedAsProfile by lazy {
+ runBlocking(background) { interactor.launchedAsProfile.first() }
+ }
+ val profiles by lazy { runBlocking(background) { interactor.profiles.first() } }
+
+ // Map UserHandle back to a user within launchedByProfile
+ private val launchedByUser: User =
+ when (launchedByHandle) {
+ launchedAsProfile.primary.handle -> launchedAsProfile.primary
+ launchedAsProfile.clone?.handle -> requireNotNull(launchedAsProfile.clone)
+ else -> error("launchedByUser must be a member of launchedByProfile")
+ }
+ val launchedAsProfileType: Profile.Type = launchedAsProfile.type
+
+ val personalProfile = profiles.single { it.type == Profile.Type.PERSONAL }
+ val workProfile = profiles.singleOrNull { it.type == Profile.Type.WORK }
+ val privateProfile = profiles.singleOrNull { it.type == Profile.Type.PRIVATE }
+
+ val personalHandle = personalProfile.primary.handle
+ val workHandle = workProfile?.primary?.handle
+ val privateHandle = privateProfile?.primary?.handle
+ val cloneHandle = personalProfile.clone?.handle
+
+ val isLaunchedAsCloneProfile = launchedByUser == launchedAsProfile.clone
+
+ val cloneUserPresent = personalProfile.clone != null
+ val workProfilePresent = workProfile != null
+ val privateProfilePresent = privateProfile != null
+
+ // Name retained for ease of review, to be renamed later
+ val tabOwnerUserHandleForLaunch =
+ if (launchedByUser.role == User.Role.CLONE) {
+ // When started by clone user, return the profile owner instead
+ launchedAsProfile.primary.handle
+ } else {
+ // Otherwise the launched user is used
+ launchedByUser.handle
+ }
+
+ fun findProfileType(handle: UserHandle): Profile.Type? {
+ val matched =
+ profiles.firstOrNull { it.primary.handle == handle || it.clone?.handle == handle }
+ return matched?.type
+ }
+
+ // Name retained for ease of review, to be renamed later
+ fun getQueryIntentsHandle(handle: UserHandle): UserHandle? {
+ return if (isLaunchedAsCloneProfile && handle == personalHandle) {
+ cloneHandle
+ } else {
+ handle
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java
index 0331c33e..1b08d957 100644
--- a/java/src/com/android/intentresolver/ResolverActivity.java
+++ b/java/src/com/android/intentresolver/ResolverActivity.java
@@ -16,39 +16,30 @@
package com.android.intentresolver;
-import static android.Manifest.permission.INTERACT_ACROSS_PROFILES;
-import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL;
-import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK;
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.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB_ACCESSIBILITY;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY;
-import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
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 androidx.lifecycle.LifecycleKt.getCoroutineScope;
+
+import static com.android.intentresolver.ext.CreationExtrasExtKt.addDefaultArgs;
import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED;
-import android.app.Activity;
-import android.app.ActivityManager;
+import static java.util.Objects.requireNonNull;
+
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;
@@ -56,7 +47,6 @@ 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;
@@ -67,7 +57,6 @@ import android.os.StrictMode;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
-import android.provider.MediaStore;
import android.provider.Settings;
import android.stats.devicepolicy.DevicePolicyEnums;
import android.text.TextUtils;
@@ -89,48 +78,66 @@ 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.lifecycle.ViewModelProvider;
+import androidx.lifecycle.viewmodel.CreationExtras;
import androidx.viewpager.widget.ViewPager;
-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.data.repository.DevicePolicyResources;
+import com.android.intentresolver.domain.interactor.UserInteractor;
import com.android.intentresolver.emptystate.CompositeEmptyStateProvider;
import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
+import com.android.intentresolver.emptystate.DevicePolicyBlockerEmptyState;
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.inject.Background;
import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
+import com.android.intentresolver.profiles.MultiProfilePagerAdapter;
+import com.android.intentresolver.profiles.MultiProfilePagerAdapter.ProfileType;
+import com.android.intentresolver.profiles.OnProfileSelectedListener;
+import com.android.intentresolver.profiles.OnSwitchOnWorkSelectedListener;
+import com.android.intentresolver.profiles.ResolverMultiProfilePagerAdapter;
+import com.android.intentresolver.profiles.TabConfig;
+import com.android.intentresolver.shared.model.Profile;
+import com.android.intentresolver.ui.ActionTitle;
+import com.android.intentresolver.ui.ProfilePagerResources;
+import com.android.intentresolver.ui.model.ActivityModel;
+import com.android.intentresolver.ui.model.ResolverRequest;
+import com.android.intentresolver.ui.viewmodel.ResolverViewModel;
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 com.google.common.collect.ImmutableList;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+import kotlin.Pair;
+
+import kotlinx.coroutines.CoroutineDispatcher;
import java.util.ArrayList;
import java.util.Arrays;
-import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
-import java.util.function.Supplier;
+
+import javax.inject.Inject;
/**
* This is a copy of ResolverActivity to support IntentResolver's ChooserActivity. This code is
@@ -138,47 +145,34 @@ import java.util.function.Supplier;
* 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
+@AndroidEntryPoint(FragmentActivity.class)
+public class ResolverActivity extends Hilt_ResolverActivity implements
ResolverListAdapter.ResolverListCommunicator {
- public ResolverActivity() {
- mIsIntentPicker = getClass().equals(ResolverActivity.class);
- }
-
- protected ResolverActivity(boolean isIntentPicker) {
- mIsIntentPicker = isIntentPicker;
- }
-
- /**
- * Whether to enable a launch mode that is safe to use when forwarding intents received from
- * applications and running in system processes. This mode uses Activity.startActivityAsCaller
- * instead of the normal Activity.startActivity for launching the activity selected
- * by the user.
- */
- private boolean mSafeForwardingMode;
+ @Inject @Background public CoroutineDispatcher mBackgroundDispatcher;
+ @Inject public UserInteractor mUserInteractor;
+ @Inject public ResolverHelper mResolverHelper;
+ @Inject public PackageManager mPackageManager;
+ @Inject public DevicePolicyResources mDevicePolicyResources;
+ @Inject public ProfilePagerResources mProfilePagerResources;
+ @Inject public IntentForwarding mIntentForwarding;
+ @Inject public FeatureFlags mFeatureFlags;
+
+ private ResolverViewModel mViewModel;
+ private ResolverRequest mRequest;
+ private ProfileHelper mProfiles;
+ private ProfileAvailability mProfileAvailability;
+ protected TargetDataLoader mTargetDataLoader;
+ private boolean mResolvingHome;
private Button mAlwaysButton;
private Button mOnceButton;
protected View mProfileView;
private int mLastSelected = AbsListView.INVALID_POSITION;
- private boolean mResolvingHome = false;
- private String mProfileSwitchMessage;
private int mLayoutId;
- @VisibleForTesting
- protected final ArrayList<Intent> mIntents = new ArrayList<>();
private PickTargetOptionRequest mPickOptionRequest;
- private String mReferrerPackage;
- private CharSequence mTitle;
- private int mDefaultTitleResId;
// Expected to be true if this object is ResolverActivity or is ResolverWrapperActivity.
- private final boolean mIsIntentPicker;
-
- // Whether or not this activity supports choosing a default handler for the intent.
- @VisibleForTesting
- protected boolean mSupportsAlwaysUseOption;
protected ResolverDrawerLayout mResolverDrawerLayout;
- protected PackageManager mPm;
private static final String TAG = "ResolverActivity";
private static final boolean DEBUG = false;
@@ -189,150 +183,33 @@ public class ResolverActivity extends FragmentActivity implements
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 final boolean mWorkProfileHasBeenEnabled = false;
- private static final String TAB_TAG_PERSONAL = "personal";
- private static final String TAB_TAG_WORK = "work";
+ protected static final String TAB_TAG_PERSONAL = "personal";
+ protected static final String TAB_TAG_WORK = "work";
private PackageMonitor mPersonalPackageMonitor;
private PackageMonitor mWorkPackageMonitor;
- private TargetDataLoader mTargetDataLoader;
-
- @VisibleForTesting
- protected MultiProfilePagerAdapter mMultiProfilePagerAdapter;
-
- protected WorkProfileAvailabilityManager mWorkProfileAvailability;
+ protected ResolverMultiProfilePagerAdapter 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;
+ public static final int PROFILE_PERSONAL = MultiProfilePagerAdapter.PROFILE_PERSONAL;
+ public static final int PROFILE_WORK = MultiProfilePagerAdapter.PROFILE_WORK;
private UserHandle mHeaderCreatorUser;
- // User handle annotations are lazy-initialized to ensure that they're computed exactly once
- // (even though they can't be computed prior to activity creation).
- // TODO: use a less ad-hoc pattern for lazy initialization (by switching to Dagger or
- // introducing a common `LazySingletonSupplier` API, etc), and/or migrate all dependents to a
- // 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 = 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;
- protected final LatencyTracker mLatencyTracker = getLatencyTracker();
-
- private 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;
- }
- }
-
protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) {
return new PackageMonitor() {
@Override
public void onSomePackagesChanged() {
listAdapter.handlePackagesChanged();
- updateProfileViewButton();
}
@Override
@@ -344,123 +221,169 @@ public class ResolverActivity extends FragmentActivity implements
};
}
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- // Use a specialized prompt when we're handling the 'Home' app startActivity()
- final Intent intent = makeMyIntent();
- final Set<String> categories = intent.getCategories();
- if (Intent.ACTION_MAIN.equals(intent.getAction())
- && categories != null
- && categories.size() == 1
- && categories.contains(Intent.CATEGORY_HOME)) {
- // Note: this field is not set to true in the compatibility version.
- mResolvingHome = true;
- }
-
- onCreate(
- savedInstanceState,
- intent,
- /* additionalTargets= */ null,
- /* title= */ null,
- /* defaultTitleRes= */ 0,
- /* initialIntents= */ null,
- /* resolutionList= */ null,
- /* supportsAlwaysUseOption= */ true,
- createIconLoader(),
- /* safeForwardingMode= */ true);
+ protected ActivityModel createActivityModel() {
+ return ActivityModel.createFrom(this);
}
- /**
- * Compatibility version for other bundled services that use this overload without
- * a default title resource
- */
- protected void onCreate(
- Bundle savedInstanceState,
- Intent intent,
- CharSequence title,
- Intent[] initialIntents,
- List<ResolveInfo> resolutionList,
- boolean supportsAlwaysUseOption,
- boolean safeForwardingMode) {
- onCreate(
- savedInstanceState,
- intent,
- null,
- title,
- 0,
- initialIntents,
- resolutionList,
- supportsAlwaysUseOption,
- createIconLoader(),
- safeForwardingMode);
+ @NonNull
+ @Override
+ public CreationExtras getDefaultViewModelCreationExtras() {
+ return addDefaultArgs(
+ super.getDefaultViewModelCreationExtras(),
+ new Pair<>(ActivityModel.ACTIVITY_MODEL_KEY, createActivityModel()));
}
- protected void onCreate(
- Bundle savedInstanceState,
- Intent intent,
- Intent[] additionalTargets,
- CharSequence title,
- int defaultTitleRes,
- Intent[] initialIntents,
- List<ResolveInfo> resolutionList,
- boolean supportsAlwaysUseOption,
- TargetDataLoader targetDataLoader,
- boolean safeForwardingMode) {
- setTheme(appliedThemeResId());
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ Log.i(TAG, "onCreate");
+ setTheme(R.style.Theme_DeviceDefault_Resolver);
+ mResolverHelper.setInitializer(this::initialize);
+ }
- // Determine whether we should show that intent is forwarded
- // from managed profile to owner or other way around.
- setProfileSwitchMessage(intent.getContentUserHint());
+ @Override
+ protected final void onStart() {
+ super.onStart();
+ this.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
- // Force computation of user handle annotations in order to validate the caller ID. (See the
- // associated TODO comment to explain why this is structured as a lazy computation.)
- AnnotatedUserHandles unusedReferenceToHandles = mLazyAnnotatedUserHandles.get();
+ final Window window = this.getWindow();
+ final WindowManager.LayoutParams attrs = window.getAttributes();
+ attrs.privateFlags &= ~SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+ window.setAttributes(attrs);
- mWorkProfileAvailability = createWorkProfileAvailabilityManager();
+ if (mRegistered) {
+ mPersonalPackageMonitor.unregister();
+ if (mWorkPackageMonitor != null) {
+ mWorkPackageMonitor.unregister();
+ }
+ mRegistered = false;
+ }
+ final Intent intent = getIntent();
+ if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction()
+ && !mResolvingHome) {
+ // 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();
+ }
+ }
+ }
- mPm = getPackageManager();
+ @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());
+ }
+ }
- mReferrerPackage = getReferrerPackageName();
+ @Override
+ protected final void onRestart() {
+ super.onRestart();
+ if (!mRegistered) {
+ mPersonalPackageMonitor.register(
+ this,
+ getMainLooper(),
+ mProfiles.getPersonalHandle(),
+ false);
+ if (mProfiles.getWorkProfilePresent()) {
+ if (mWorkPackageMonitor == null) {
+ mWorkPackageMonitor = createPackageMonitor(
+ mMultiProfilePagerAdapter.getWorkListAdapter());
+ }
+ mWorkPackageMonitor.register(
+ this,
+ getMainLooper(),
+ mProfiles.getWorkHandle(),
+ false);
+ }
+ mRegistered = true;
+ }
+ mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
+ }
- // The initial intent must come before any other targets that are to be added.
- mIntents.add(0, new Intent(intent));
- if (additionalTargets != null) {
- Collections.addAll(mIntents, additionalTargets);
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (!isChangingConfigurations() && mPickOptionRequest != null) {
+ mPickOptionRequest.cancel();
+ }
+ if (mMultiProfilePagerAdapter != null
+ && mMultiProfilePagerAdapter.getActiveListAdapter() != null) {
+ mMultiProfilePagerAdapter.getActiveListAdapter().onDestroy();
}
+ }
+
+ private void initialize() {
+ mViewModel = new ViewModelProvider(this).get(ResolverViewModel.class);
+ mRequest = mViewModel.getRequest().getValue();
- mTitle = title;
- mDefaultTitleResId = defaultTitleRes;
+ mProfiles = new ProfileHelper(
+ mUserInteractor,
+ getCoroutineScope(getLifecycle()),
+ mBackgroundDispatcher,
+ mFeatureFlags);
- mSupportsAlwaysUseOption = supportsAlwaysUseOption;
- mSafeForwardingMode = safeForwardingMode;
- mTargetDataLoader = targetDataLoader;
+ mProfileAvailability = new ProfileAvailability(
+ mUserInteractor,
+ getCoroutineScope(getLifecycle()),
+ mBackgroundDispatcher);
+
+ mProfileAvailability.setOnProfileStatusChange(this::onWorkProfileStatusUpdated);
+
+ mResolvingHome = mRequest.isResolvingHome();
+ mTargetDataLoader = new DefaultTargetDataLoader(
+ this,
+ getLifecycle(),
+ mRequest.isAudioCaptureDevice());
// 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 the work tab is shown to simplify the UX.
+ // 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 = mSupportsAlwaysUseOption && !isVoiceInteraction()
- && !shouldShowTabs() && !hasCloneProfile();
+ boolean filterLastUsed = !isVoiceInteraction()
+ && !mProfiles.getWorkProfilePresent() && !mProfiles.getCloneUserPresent();
mMultiProfilePagerAdapter = createMultiProfilePagerAdapter(
- initialIntents, resolutionList, filterLastUsed, targetDataLoader);
- if (configureContentView(targetDataLoader)) {
+ new Intent[0],
+ /* resolutionList = */ mRequest.getResolutionList(),
+ filterLastUsed
+ );
+ if (configureContentView(mTargetDataLoader)) {
return;
}
mPersonalPackageMonitor = createPackageMonitor(
mMultiProfilePagerAdapter.getPersonalListAdapter());
mPersonalPackageMonitor.register(
- this, getMainLooper(), getAnnotatedUserHandles().personalProfileUserHandle, false);
- if (shouldShowTabs()) {
+ this,
+ getMainLooper(),
+ mProfiles.getPersonalHandle(),
+ false
+ );
+ if (mProfiles.getWorkProfilePresent()) {
mWorkPackageMonitor = createPackageMonitor(
mMultiProfilePagerAdapter.getWorkListAdapter());
mWorkPackageMonitor.register(
- this, getMainLooper(), getAnnotatedUserHandles().workProfileUserHandle, false);
+ this,
+ getMainLooper(),
+ mProfiles.getWorkHandle(),
+ false
+ );
}
mRegistered = true;
@@ -474,7 +397,7 @@ public class ResolverActivity extends FragmentActivity implements
}
});
- boolean hasTouchScreen = getPackageManager()
+ boolean hasTouchScreen = mPackageManager
.hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN);
if (isVoiceInteraction() || !hasTouchScreen) {
@@ -487,13 +410,7 @@ public class ResolverActivity extends FragmentActivity implements
mResolverDrawerLayout = rdl;
}
-
- mProfileView = findViewById(com.android.internal.R.id.profile_button);
- if (mProfileView != null) {
- mProfileView.setOnClickListener(this::onProfileClick);
- updateProfileViewButton();
- }
-
+ Intent intent = mViewModel.getRequest().getValue().getIntent();
final Set<String> categories = intent.getCategories();
MetricsLogger.action(this, mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()
? MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_APP_FEATURED
@@ -502,19 +419,31 @@ public class ResolverActivity extends FragmentActivity implements
+ (categories != null ? Arrays.toString(categories.toArray()) : ""));
}
- protected MultiProfilePagerAdapter createMultiProfilePagerAdapter(
+ private void restore(@Nullable Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ // onRestoreInstanceState
+ resetButtonBar();
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ if (viewPager != null) {
+ viewPager.setCurrentItem(savedInstanceState.getInt(LAST_SHOWN_TAB_KEY));
+ }
+ }
+
+ mMultiProfilePagerAdapter.clearInactiveProfileCache();
+ }
+
+ protected ResolverMultiProfilePagerAdapter createMultiProfilePagerAdapter(
Intent[] initialIntents,
List<ResolveInfo> resolutionList,
- boolean filterLastUsed,
- TargetDataLoader targetDataLoader) {
- MultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null;
- if (shouldShowTabs()) {
+ boolean filterLastUsed) {
+ ResolverMultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null;
+ if (mProfiles.getWorkProfilePresent()) {
resolverMultiProfilePagerAdapter =
createResolverMultiProfilePagerAdapterForTwoProfiles(
- initialIntents, resolutionList, filterLastUsed, targetDataLoader);
+ initialIntents, resolutionList, filterLastUsed);
} else {
resolverMultiProfilePagerAdapter = createResolverMultiProfilePagerAdapterForOneProfile(
- initialIntents, resolutionList, filterLastUsed, targetDataLoader);
+ initialIntents, resolutionList, filterLastUsed);
}
return resolverMultiProfilePagerAdapter;
}
@@ -552,15 +481,10 @@ public class ResolverActivity extends FragmentActivity implements
ResolverActivity.METRICS_CATEGORY_RESOLVER);
return new NoCrossProfileEmptyStateProvider(
- getAnnotatedUserHandles().personalProfileUserHandle,
+ mProfiles,
noWorkToPersonalEmptyState,
noPersonalToWorkEmptyState,
- createCrossProfileIntentsChecker(),
- getAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
- }
-
- protected int appliedThemeResId() {
- return R.style.Theme_DeviceDefault_Resolver;
+ createCrossProfileIntentsChecker());
}
/**
@@ -572,9 +496,7 @@ public class ResolverActivity extends FragmentActivity implements
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;
+ return buttonBar == null || buttonBar.getVisibility() == View.GONE;
}
protected void applyFooterView(int height) {
@@ -582,12 +504,12 @@ public class ResolverActivity extends FragmentActivity implements
mFooterSpacer = new Space(getApplicationContext());
} else {
((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
- .getActiveAdapterView().removeFooterView(mFooterSpacer);
+ .getActiveAdapterView().removeFooterView(mFooterSpacer);
}
mFooterSpacer.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT,
- mSystemWindowInsets.bottom));
+ mSystemWindowInsets.bottom));
((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
- .getActiveAdapterView().addFooterView(mFooterSpacer);
+ .getActiveAdapterView().addFooterView(mFooterSpacer);
}
protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
@@ -613,10 +535,10 @@ public class ResolverActivity extends FragmentActivity implements
}
@Override
- public void onConfigurationChanged(@NonNull Configuration newConfig) {
+ public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
- if (mIsIntentPicker && shouldShowTabs() && !useLayoutWithDefault()
+ if (mProfiles.getWorkProfilePresent() && !useLayoutWithDefault()
&& !shouldUseMiniResolver()) {
updateIntentPickerPaddings();
}
@@ -631,52 +553,7 @@ public class ResolverActivity extends FragmentActivity implements
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()
- && !mResolvingHome && !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?
- mWorkProfileAvailability.unregisterWorkProfileStateReceiver(this);
- }
-
- @Override
- protected void onDestroy() {
- super.onDestroy();
- if (!isChangingConfigurations() && mPickOptionRequest != null) {
- mPickOptionRequest.cancel();
- }
- if (mMultiProfilePagerAdapter != null
- && mMultiProfilePagerAdapter.getActiveListAdapter() != null) {
- mMultiProfilePagerAdapter.getActiveListAdapter().onDestroy();
- }
- }
-
+ // referenced by layout XML: android:onClick="onButtonClick"
public void onButtonClick(View v) {
final int id = v.getId();
ListView listView = (ListView) mMultiProfilePagerAdapter.getActiveAdapterView();
@@ -695,9 +572,9 @@ public class ResolverActivity extends FragmentActivity implements
ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter()
.resolveInfoForPosition(which, hasIndexBeenFiltered);
if (mResolvingHome && hasManagedProfile() && !supportsManagedProfiles(ri)) {
+ String launcherName = ri.activityInfo.loadLabel(mPackageManager).toString();
Toast.makeText(this,
- getWorkProfileNotSupportedMsg(
- ri.activityInfo.loadLabel(getPackageManager()).toString()),
+ mDevicePolicyResources.getWorkProfileNotSupportedMessage(launcherName),
Toast.LENGTH_LONG).show();
return;
}
@@ -708,15 +585,12 @@ public class ResolverActivity extends FragmentActivity implements
return;
}
if (onTargetSelected(target, always)) {
- if (always && mSupportsAlwaysUseOption) {
+ if (always) {
MetricsLogger.action(
this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_ALWAYS);
- } else if (mSupportsAlwaysUseOption) {
- MetricsLogger.action(
- this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_JUST_ONCE);
} else {
MetricsLogger.action(
- this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_TAP);
+ this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_JUST_ONCE);
}
MetricsLogger.action(this,
mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()
@@ -726,9 +600,6 @@ public class ResolverActivity extends FragmentActivity implements
}
}
- /**
- * Replace me in subclasses!
- */
@Override // ResolverListCommunicator
public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
return defIntent;
@@ -737,7 +608,7 @@ public class ResolverActivity extends FragmentActivity implements
protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildCompleted) {
final ItemClickListener listener = new ItemClickListener();
setupAdapterListView((ListView) mMultiProfilePagerAdapter.getActiveAdapterView(), listener);
- if (shouldShowTabs() && mIsIntentPicker) {
+ if (mProfiles.getWorkProfilePresent()) {
final ResolverDrawerLayout rdl = findViewById(com.android.internal.R.id.contentPanel);
if (rdl != null) {
rdl.setMaxCollapsedHeight(getResources()
@@ -752,9 +623,9 @@ public class ResolverActivity extends FragmentActivity implements
final ResolveInfo ri = target.getResolveInfo();
final Intent intent = target != null ? target.getResolvedIntent() : null;
- if (intent != null && (mSupportsAlwaysUseOption
- || mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem())
- && mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredResolveList() != null) {
+ if (intent != null /*&& mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()*/
+ && mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredResolveList()
+ != null) {
// Build a reasonable intent filter, based on what matched.
IntentFilter filter = new IntentFilter();
Intent filterIntent;
@@ -796,7 +667,7 @@ public class ResolverActivity extends FragmentActivity implements
// or "content:" schemes (see IntentFilter for the reason).
if (cat != IntentFilter.MATCH_CATEGORY_TYPE
|| (!"file".equals(data.getScheme())
- && !"content".equals(data.getScheme()))) {
+ && !"content".equals(data.getScheme()))) {
filter.addDataScheme(data.getScheme());
// Look through the resolved filter to determine which part
@@ -854,7 +725,7 @@ public class ResolverActivity extends FragmentActivity implements
}
int bestMatch = 0;
- for (int i=0; i<N; i++) {
+ for (int i = 0; i < N; i++) {
ResolveInfo r = mMultiProfilePagerAdapter.getActiveListAdapter()
.getUnfilteredResolveList().get(i).getResolveInfoAt(0);
set[i] = new ComponentName(r.activityInfo.packageName,
@@ -872,7 +743,7 @@ public class ResolverActivity extends FragmentActivity implements
if (always) {
final int userId = getUserId();
- final PackageManager pm = getPackageManager();
+ final PackageManager pm = mPackageManager;
// Set the preferred Activity
pm.addUniquePreferredActivity(filter, bestMatch, set, intent.getComponent());
@@ -881,7 +752,8 @@ public class ResolverActivity extends FragmentActivity implements
// Set default Browser if needed
final String packageName = pm.getDefaultBrowserPackageNameAsUser(userId);
if (TextUtils.isEmpty(packageName)) {
- pm.setDefaultBrowserPackageNameAsUser(ri.activityInfo.packageName, userId);
+ pm.setDefaultBrowserPackageNameAsUser(ri.activityInfo.packageName,
+ userId);
}
}
} else {
@@ -895,21 +767,11 @@ public class ResolverActivity extends FragmentActivity implements
}
}
- 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;
- }
- }
+ safelyStartActivity(target);
- return true;
- }
-
- public void onActivityStarted(TargetInfo cti) {
- // Do nothing
+ // Rely on the ActivityManager to pop up a dialog regarding app suspension
+ // and return false
+ return !target.isSuspended();
}
@Override // ResolverListCommunicator
@@ -921,58 +783,65 @@ public class ResolverActivity extends FragmentActivity implements
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,
- getTargetIntent(),
- getReferrerPackageName(),
+ mRequest.getIntent(),
+ mViewModel.getActivityModel().getReferrerPackage(),
null,
null,
getResolverRankerServiceUserHandleList(userHandle),
null);
return new ResolverListController(
this,
- mPm,
- getTargetIntent(),
- getReferrerPackageName(),
- getAnnotatedUserHandles().userIdOfCallingApp,
+ mPackageManager,
+ mRequest.getIntent(),
+ mViewModel.getActivityModel().getReferrerPackage(),
+ mViewModel.getActivityModel().getLaunchedFromUid(),
resolverComparator,
- getQueryIntentsUser(userHandle));
+ mProfiles.getQueryIntentsHandle(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() { }
+ /* TODO: consider merging with the customized considerations of our implemented
+ * {@link MultiProfilePagerAdapter.OnProfileSelectedListener}. The only apparent distinctions
+ * between the respective listener callbacks would occur in the triggering patterns during init
+ * (when the `OnProfileSelectedListener` is registered after a possible tab-change), or possibly
+ * if there's some way to trigger an update in one model but not the other. If there's an
+ * initialization dependency, we can probably reason about it with confidence. If there's a
+ * discrepancy between the `TabHost` and pager-adapter data models, that inconsistency is
+ * likely to be a bug that would benefit from consolidation.
+ */
+ protected void onProfileTabSelected(int currentPage) {
+ setupViewVisibilities();
+ maybeLogProfileChange();
+ if (mProfiles.getWorkProfilePresent()) {
+ // The device policy logger is only concerned with sessions that include a work profile.
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_SWITCH_TABS)
+ .setInt(currentPage)
+ .setStrings(getMetricsCategory())
+ .write();
+ }
+ }
/**
* 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) {
@@ -982,7 +851,7 @@ public class ResolverActivity extends FragmentActivity implements
stub.setVisibility(View.VISIBLE);
TextView textView = (TextView) LayoutInflater.from(this).inflate(
R.layout.resolver_different_item_header, null, false);
- if (shouldShowTabs()) {
+ if (mProfiles.getWorkProfilePresent()) {
textView.setGravity(Gravity.CENTER);
}
stub.addView(textView);
@@ -990,9 +859,6 @@ public class ResolverActivity extends FragmentActivity implements
}
protected void resetButtonBar() {
- if (!mSupportsAlwaysUseOption) {
- 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");
@@ -1034,55 +900,24 @@ public class ResolverActivity extends FragmentActivity implements
}
@Override // ResolverListCommunicator
- public void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
- if (listAdapter == mMultiProfilePagerAdapter.getActiveListAdapter()) {
- 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
- // point in reloading the list now, since the work profile user is still
- // turning on.
- return;
- }
- boolean listRebuilt = mMultiProfilePagerAdapter.rebuildActiveTab(true);
- if (listRebuilt) {
- ResolverListAdapter activeListAdapter =
- mMultiProfilePagerAdapter.getActiveListAdapter();
- activeListAdapter.notifyDataSetChanged();
- if (activeListAdapter.getCount() == 0 && !inactiveListAdapterHasItems()) {
- // We no longer have any items... just finish the activity.
- finish();
- }
- }
- } else {
- mMultiProfilePagerAdapter.clearInactiveProfileCache();
+ public final void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
+ if (!mMultiProfilePagerAdapter.onHandlePackagesChanged(
+ listAdapter,
+ mProfileAvailability.getWaitingToEnableProfile())) {
+ // 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 WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() {
- return new WorkProfileAvailabilityManager(
- getSystemService(UserManager.class),
- getAnnotatedUserHandles().workProfileUserHandle,
- this::onWorkProfileStatusUpdated);
- }
-
- protected void onWorkProfileStatusUpdated() {
- if (mMultiProfilePagerAdapter.getCurrentUserHandle().equals(
- getAnnotatedUserHandles().workProfileUserHandle)) {
+ private void onWorkProfileStatusUpdated() {
+ if (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_WORK) {
mMultiProfilePagerAdapter.rebuildActiveTab(true);
} else {
mMultiProfilePagerAdapter.clearInactiveProfileCache();
@@ -1097,11 +932,8 @@ public class ResolverActivity extends FragmentActivity implements
Intent[] initialIntents,
List<ResolveInfo> resolutionList,
boolean filterLastUsed,
- UserHandle userHandle,
- TargetDataLoader targetDataLoader) {
- UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()
- && userHandle.equals(getAnnotatedUserHandles().personalProfileUserHandle)
- ? getAnnotatedUserHandles().cloneProfileUserHandle : userHandle;
+ UserHandle userHandle) {
+ UserHandle initialIntentsUserSpace = mProfiles.getQueryIntentsHandle(userHandle);
return new ResolverListAdapter(
context,
payloadIntents,
@@ -1110,33 +942,10 @@ public class ResolverActivity extends FragmentActivity implements
filterLastUsed,
createListController(userHandle),
userHandle,
- getTargetIntent(),
+ mRequest.getIntent(),
this,
initialIntentsUserSpace,
- targetDataLoader);
- }
-
- private TargetDataLoader createIconLoader() {
- Intent startIntent = getIntent();
- boolean isAudioCaptureDevice =
- startIntent.getBooleanExtra(EXTRA_IS_AUDIO_CAPTURE_DEVICE, false);
- return new DefaultTargetDataLoader(this, getLifecycle(), isAudioCaptureDevice);
- }
-
- 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;
+ mTargetDataLoader);
}
protected final EmptyStateProvider createEmptyStateProvider(
@@ -1144,8 +953,10 @@ public class ResolverActivity extends FragmentActivity implements
final EmptyStateProvider blockerEmptyStateProvider = createBlockerEmptyStateProvider();
final EmptyStateProvider workProfileOffEmptyStateProvider =
- new WorkProfilePausedEmptyStateProvider(this, workProfileUserHandle,
- mWorkProfileAvailability,
+ new WorkProfilePausedEmptyStateProvider(
+ this,
+ mProfiles,
+ mProfileAvailability,
/* onSwitchOnWorkSelectedListener= */
() -> {
if (mOnSwitchOnWorkSelectedListener != null) {
@@ -1154,12 +965,11 @@ public class ResolverActivity extends FragmentActivity implements
},
getMetricsCategory());
- final EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider(
- this,
- workProfileUserHandle,
- getAnnotatedUserHandles().personalProfileUserHandle,
+ EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider(
+ mProfiles,
+ mProfileAvailability,
getMetricsCategory(),
- getAnnotatedUserHandles().tabOwnerUserHandleForLaunch
+ mProfilePagerResources
);
// Return composite provider, the order matters (the higher, the more priority)
@@ -1170,76 +980,52 @@ public class ResolverActivity extends FragmentActivity implements
);
}
- private Intent makeMyIntent() {
- Intent intent = new Intent(getIntent());
- 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.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
-
- // 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.getFlags() & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0) {
- intent.setFlags(intent.getFlags() & ~FLAG_ACTIVITY_LAUNCH_ADJACENT);
- }
- return intent;
- }
-
- /**
- * Call {@link Activity#onCreate} without initializing anything further. This should
- * only be used when the activity is about to be immediately finished to avoid wasting
- * initializing steps and leaking resources.
- */
- protected final void super_onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- }
-
- private ResolverMultiProfilePagerAdapter
- createResolverMultiProfilePagerAdapterForOneProfile(
- Intent[] initialIntents,
- List<ResolveInfo> resolutionList,
- boolean filterLastUsed,
- TargetDataLoader targetDataLoader) {
- ResolverListAdapter adapter = createResolverListAdapter(
+ private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForOneProfile(
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean filterLastUsed) {
+ ResolverListAdapter personalAdapter = createResolverListAdapter(
/* context */ this,
- /* payloadIntents */ mIntents,
+ mRequest.getPayloadIntents(),
initialIntents,
resolutionList,
filterLastUsed,
- /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,
- targetDataLoader);
+ /* userHandle */ mProfiles.getPersonalHandle()
+ );
return new ResolverMultiProfilePagerAdapter(
/* context */ this,
- adapter,
+ ImmutableList.of(
+ new TabConfig<>(
+ PROFILE_PERSONAL,
+ mDevicePolicyResources.getPersonalTabLabel(),
+ mDevicePolicyResources.getPersonalTabAccessibilityLabel(),
+ TAB_TAG_PERSONAL,
+ personalAdapter)),
createEmptyStateProvider(/* workProfileUserHandle= */ null),
/* workProfileQuietModeChecker= */ () -> false,
+ /* defaultProfile= */ PROFILE_PERSONAL,
/* workProfileUserHandle= */ null,
- getAnnotatedUserHandles().cloneProfileUserHandle);
+ mProfiles.getCloneHandle());
}
private UserHandle getIntentUser() {
- return getIntent().hasExtra(EXTRA_CALLING_USER)
- ? getIntent().getParcelableExtra(EXTRA_CALLING_USER)
- : getAnnotatedUserHandles().tabOwnerUserHandleForLaunch;
+ return Objects.requireNonNullElse(mRequest.getCallingUser(),
+ mProfiles.getTabOwnerUserHandleForLaunch());
}
private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles(
Intent[] initialIntents,
List<ResolveInfo> resolutionList,
- boolean filterLastUsed,
- TargetDataLoader targetDataLoader) {
+ boolean filterLastUsed) {
// 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 (!getAnnotatedUserHandles().tabOwnerUserHandleForLaunch.equals(intentUser)) {
- if (getAnnotatedUserHandles().personalProfileUserHandle.equals(intentUser)) {
+ if (!mProfiles.getTabOwnerUserHandleForLaunch().equals(intentUser)) {
+ if (mProfiles.getPersonalHandle().equals(intentUser)) {
selectedProfile = PROFILE_PERSONAL;
- } else if (getAnnotatedUserHandles().workProfileUserHandle.equals(intentUser)) {
+ } else if (mProfiles.getWorkHandle().equals(intentUser)) {
selectedProfile = PROFILE_WORK;
}
} else {
@@ -1253,95 +1039,70 @@ public class ResolverActivity extends FragmentActivity implements
// resolver list. So filterLastUsed should be false for the other profile.
ResolverListAdapter personalAdapter = createResolverListAdapter(
/* context */ this,
- /* payloadIntents */ mIntents,
+ mRequest.getPayloadIntents(),
selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
resolutionList,
(filterLastUsed && UserHandle.myUserId()
- == getAnnotatedUserHandles().personalProfileUserHandle.getIdentifier()),
- /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,
- targetDataLoader);
- UserHandle workProfileUserHandle = getAnnotatedUserHandles().workProfileUserHandle;
+ == mProfiles.getPersonalHandle().getIdentifier()),
+ /* userHandle */ mProfiles.getPersonalHandle()
+ );
+ UserHandle workProfileUserHandle = mProfiles.getWorkHandle();
ResolverListAdapter workAdapter = createResolverListAdapter(
/* context */ this,
- /* payloadIntents */ mIntents,
+ mRequest.getPayloadIntents(),
selectedProfile == PROFILE_WORK ? initialIntents : null,
resolutionList,
(filterLastUsed && UserHandle.myUserId()
== workProfileUserHandle.getIdentifier()),
- /* userHandle */ workProfileUserHandle,
- targetDataLoader);
+ /* userHandle */ workProfileUserHandle
+ );
return new ResolverMultiProfilePagerAdapter(
/* context */ this,
- personalAdapter,
- workAdapter,
+ ImmutableList.of(
+ new TabConfig<>(
+ PROFILE_PERSONAL,
+ mDevicePolicyResources.getPersonalTabLabel(),
+ mDevicePolicyResources.getPersonalTabAccessibilityLabel(),
+ TAB_TAG_PERSONAL,
+ personalAdapter),
+ new TabConfig<>(
+ PROFILE_WORK,
+ mDevicePolicyResources.getWorkTabLabel(),
+ mDevicePolicyResources.getWorkTabAccessibilityLabel(),
+ TAB_TAG_WORK,
+ workAdapter)),
createEmptyStateProvider(workProfileUserHandle),
- () -> mWorkProfileAvailability.isQuietModeEnabled(),
+ /* Supplier<Boolean> (QuietMode enabled) == !(available) */
+ () -> !(mProfiles.getWorkProfilePresent()
+ && mProfileAvailability.isAvailable(
+ requireNonNull(mProfiles.getWorkProfile()))),
selectedProfile,
workProfileUserHandle,
- getAnnotatedUserHandles().cloneProfileUserHandle);
+ mProfiles.getCloneHandle());
}
/**
* 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.");
- }
+ Profile.Type selected = mRequest.getSelectedProfile();
+ if (selected == null) {
+ return -1;
+ }
+ switch (selected) {
+ case PERSONAL: return PROFILE_PERSONAL;
+ case WORK: return PROFILE_WORK;
+ default: return -1;
}
- return selectedProfile;
}
- protected final @Profile int getCurrentProfile() {
- UserHandle launchUser = getAnnotatedUserHandles().tabOwnerUserHandleForLaunch;
- UserHandle personalUser = getAnnotatedUserHandles().personalProfileUserHandle;
+ protected final @ProfileType int getCurrentProfile() {
+ UserHandle launchUser = mProfiles.getTabOwnerUserHandleForLaunch();
+ UserHandle personalUser = mProfiles.getPersonalHandle();
return launchUser.equals(personalUser) ? PROFILE_PERSONAL : PROFILE_WORK;
}
- protected final AnnotatedUserHandles getAnnotatedUserHandles() {
- return mLazyAnnotatedUserHandles.get();
- }
-
- private boolean hasWorkProfile() {
- return getAnnotatedUserHandles().workProfileUserHandle != null;
- }
-
- private boolean hasCloneProfile() {
- return getAnnotatedUserHandles().cloneProfileUserHandle != null;
- }
-
- protected final boolean isLaunchedAsCloneProfile() {
- UserHandle launchUser = getAnnotatedUserHandles().userHandleSharesheetLaunchedAs;
- UserHandle cloneUser = getAnnotatedUserHandles().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.
- mProfileSwitchMessage = null;
-
- onTargetSelected(dri, false);
- finish();
- }
-
private void updateIntentPickerPaddings() {
View titleCont = findViewById(com.android.internal.R.id.title_container);
titleCont.setPadding(
@@ -1358,14 +1119,15 @@ public class ResolverActivity extends FragmentActivity implements
}
private void maybeLogCrossProfileTargetLaunch(TargetInfo cti, UserHandle currentUserHandle) {
- if (!hasWorkProfile() || currentUserHandle.equals(getUser())) {
+ // TODO: Test isolation bug, referencing getUser() will break tests with faked profiles
+ if (!mProfiles.getWorkProfilePresent() || currentUserHandle.equals(getUser())) {
return;
}
DevicePolicyEventLogger
.createEvent(DevicePolicyEnums.RESOLVER_CROSS_PROFILE_TARGET_OPENED)
.setBoolean(
currentUserHandle.equals(
- getAnnotatedUserHandles().personalProfileUserHandle))
+ mProfiles.getPersonalHandle()))
.setStrings(getMetricsCategory(),
cti.isInDirectShareMetricsCategory() ? "direct_share" : "other_target")
.write();
@@ -1399,66 +1161,6 @@ public class ResolverActivity extends FragmentActivity implements
return new Option(getOrLoadDisplayLabel(target), index);
}
- public final Intent getTargetIntent() {
- return mIntents.isEmpty() ? null : mIntents.get(0);
- }
-
- protected final String getReferrerPackageName() {
- final Uri referrer = getReferrer();
- if (referrer != null && "android-app".equals(referrer.getScheme())) {
- return referrer.getHost();
- }
- return null;
- }
-
- @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);
- }
- }
-
- private void setProfileSwitchMessage(int contentUserHint) {
- if ((contentUserHint != UserHandle.USER_CURRENT)
- && (contentUserHint != UserHandle.myUserId())) {
- UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
- UserInfo originUserInfo = userManager.getUserInfo(contentUserHint);
- boolean originIsManaged = originUserInfo != null ? originUserInfo.isManagedProfile()
- : false;
- boolean targetIsManaged = userManager.isManagedProfile();
- if (originIsManaged && !targetIsManaged) {
- mProfileSwitchMessage = getForwardToPersonalMsg();
- } else if (!originIsManaged && targetIsManaged) {
- mProfileSwitchMessage = getForwardToWorkMsg();
- }
- }
- }
-
- private String getForwardToPersonalMsg() {
- return getSystemService(DevicePolicyManager.class).getResources().getString(
- FORWARD_INTENT_TO_PERSONAL,
- () -> getString(R.string.forward_intent_to_owner));
- }
-
- private String getForwardToWorkMsg() {
- return getSystemService(DevicePolicyManager.class).getResources().getString(
- FORWARD_INTENT_TO_WORK,
- () -> getString(R.string.forward_intent_to_work));
- }
-
protected final CharSequence getTitleForAction(Intent intent, int defaultTitleRes) {
final ActionTitle title = mResolvingHome
? ActionTitle.HOME
@@ -1481,73 +1183,6 @@ public class ResolverActivity extends FragmentActivity implements
}
}
- final void dismiss() {
- if (!isFinishing()) {
- finish();
- }
- }
-
- @Override
- protected final void onRestart() {
- super.onRestart();
- if (!mRegistered) {
- mPersonalPackageMonitor.register(
- this,
- getMainLooper(),
- getAnnotatedUserHandles().personalProfileUserHandle,
- false);
- if (shouldShowTabs()) {
- if (mWorkPackageMonitor == null) {
- mWorkPackageMonitor = createPackageMonitor(
- mMultiProfilePagerAdapter.getWorkListAdapter());
- }
- mWorkPackageMonitor.register(
- this,
- getMainLooper(),
- getAnnotatedUserHandles().workProfileUserHandle,
- false);
- }
- mRegistered = true;
- }
- if (shouldShowTabs() && mWorkProfileAvailability.isWaitingToEnableWorkProfile()) {
- if (mWorkProfileAvailability.isQuietModeEnabled()) {
- mWorkProfileAvailability.markWorkProfileEnabledBroadcastReceived();
- }
- }
- mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
- updateProfileViewButton();
- }
-
- @Override
- protected final void onStart() {
- super.onStart();
-
- this.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
- if (shouldShowTabs()) {
- mWorkProfileAvailability.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());
- }
- }
-
- @Override
- protected final void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
- super.onRestoreInstanceState(savedInstanceState);
- 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 boolean hasManagedProfile() {
UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
if (userManager == null) {
@@ -1569,7 +1204,7 @@ public class ResolverActivity extends FragmentActivity implements
private boolean supportsManagedProfiles(ResolveInfo resolveInfo) {
try {
- ApplicationInfo appInfo = getPackageManager().getApplicationInfo(
+ ApplicationInfo appInfo = mPackageManager.getApplicationInfo(
resolveInfo.activityInfo.packageName, 0 /* default flags */);
return appInfo.targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP;
} catch (NameNotFoundException e) {
@@ -1587,7 +1222,8 @@ public class ResolverActivity extends FragmentActivity implements
// 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)) {
+ if (mProfiles.getCloneUserPresent()
+ && (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)) {
mAlwaysButton.setEnabled(false);
return;
}
@@ -1613,41 +1249,28 @@ public class ResolverActivity extends FragmentActivity implements
if (ri != null) {
ActivityInfo activityInfo = ri.activityInfo;
- boolean hasRecordPermission =
- mPm.checkPermission(android.Manifest.permission.RECORD_AUDIO,
+ boolean hasRecordPermission = mPackageManager
+ .checkPermission(android.Manifest.permission.RECORD_AUDIO,
activityInfo.packageName)
- == android.content.pm.PackageManager.PERMISSION_GRANTED;
+ == 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);
+ boolean hasAudioCapture = mViewModel.getRequest().getValue().isAudioCaptureDevice();
enabled = !hasAudioCapture;
}
}
mAlwaysButton.setEnabled(enabled);
}
- private String getWorkProfileNotSupportedMsg(String launcherName) {
- return getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_WORK_PROFILE_NOT_SUPPORTED,
- () -> getString(
- R.string.activity_resolver_work_profiles_support,
- launcherName),
- launcherName);
- }
-
@Override // ResolverListCommunicator
public final void onPostListReady(ResolverListAdapter listAdapter, boolean doPostProcessing,
boolean rebuildCompleted) {
if (isAutolaunching()) {
return;
}
- if (mIsIntentPicker) {
- ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
- .setUseLayoutWithDefault(useLayoutWithDefault());
- }
+ mMultiProfilePagerAdapter.setUseLayoutWithDefault(useLayoutWithDefault());
+
if (mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(listAdapter)) {
mMultiProfilePagerAdapter.showEmptyResolverListEmptyState(listAdapter);
} else {
@@ -1696,45 +1319,6 @@ public class ResolverActivity extends FragmentActivity implements
}
}
- @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.
- if (mProfileSwitchMessage != null) {
- Toast.makeText(this, mProfileSwitchMessage, Toast.LENGTH_LONG).show();
- }
- if (!mSafeForwardingMode) {
- if (cti.startAsUser(this, options, user)) {
- onActivityStarted(cti);
- maybeLogCrossProfileTargetLaunch(cti, user);
- }
- return;
- }
- try {
- if (cti.startAsCaller(this, options, user.getIdentifier())) {
- onActivityStarted(cti);
- maybeLogCrossProfileTargetLaunch(cti, user);
- }
- } catch (RuntimeException e) {
- Slog.wtf(TAG,
- "Unable to launch as uid " + getAnnotatedUserHandles().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))
@@ -1754,13 +1338,9 @@ public class ResolverActivity extends FragmentActivity implements
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.
- boolean rebuildCompleted = mMultiProfilePagerAdapter.rebuildActiveTab(true)
- || mMultiProfilePagerAdapter.getActiveListAdapter().isTabLoaded();
- if (shouldShowTabs()) {
- boolean rebuildInactiveCompleted = mMultiProfilePagerAdapter.rebuildInactiveTab(false)
- || mMultiProfilePagerAdapter.getInactiveListAdapter().isTabLoaded();
- rebuildCompleted = rebuildCompleted && rebuildInactiveCompleted;
- }
+ // To date, we really only care about "partially rebuilding" tabs for work and/or personal.
+ boolean rebuildCompleted =
+ mMultiProfilePagerAdapter.rebuildTabs(mProfiles.getWorkProfilePresent());
if (shouldUseMiniResolver()) {
configureMiniResolverContent(targetDataLoader);
@@ -1774,7 +1354,8 @@ public class ResolverActivity extends FragmentActivity implements
mLayoutId = getLayoutResource();
}
setContentView(mLayoutId);
- mMultiProfilePagerAdapter.setupViewPager(findViewById(com.android.internal.R.id.profile_pager));
+ mMultiProfilePagerAdapter.setupViewPager(
+ findViewById(com.android.internal.R.id.profile_pager));
boolean result = postRebuildList(rebuildCompleted);
Trace.endSection();
return result;
@@ -1790,12 +1371,20 @@ public class ResolverActivity extends FragmentActivity implements
mLayoutId = R.layout.miniresolver;
setContentView(mLayoutId);
- DisplayResolveInfo sameProfileResolveInfo =
- mMultiProfilePagerAdapter.getActiveListAdapter().getFirstDisplayResolveInfo();
boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK;
- final ResolverListAdapter inactiveAdapter =
- mMultiProfilePagerAdapter.getInactiveListAdapter();
+ 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();
@@ -1834,6 +1423,69 @@ public class ResolverActivity extends FragmentActivity implements
});
}
+ private boolean isTwoPagePersonalAndWorkConfiguration() {
+ return (mMultiProfilePagerAdapter.getCount() == 2)
+ && mMultiProfilePagerAdapter.hasPageForProfile(PROFILE_PERSONAL)
+ && mMultiProfilePagerAdapter.hasPageForProfile(PROFILE_WORK);
+ }
+
+ @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 =
+ mIntentForwarding.forwardMessageFor(mRequest.getIntent());
+ if (profileSwitchMessage != null) {
+ Toast.makeText(this, profileSwitchMessage, Toast.LENGTH_LONG).show();
+ }
+ try {
+ if (cti.startAsCaller(this, options, user.getIdentifier())) {
+ maybeLogCrossProfileTargetLaunch(cti, user);
+ }
+ } catch (RuntimeException e) {
+ Slog.wtf(TAG,
+ "Unable to launch as uid "
+ + mViewModel.getActivityModel().getLaunchedFromUid()
+ + " package " + mViewModel.getActivityModel().getLaunchedFromPackage()
+ + ", while running in " + ActivityThread.currentProcessName(), e);
+ }
+ }
+
+ /**
+ * 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 (mProfiles.getWorkProfilePresent()) {
+ setupProfileTabs();
+ }
+
+ return false;
+ }
+
/**
* Mini resolver should be used when all of the following are true:
* 1. This is the intent picker (ResolverActivity).
@@ -1841,17 +1493,19 @@ public class ResolverActivity extends FragmentActivity implements
* 3. The other profile has a single non-browser match.
*/
private boolean shouldUseMiniResolver() {
- if (!mIsIntentPicker) {
- return false;
- }
- if (mMultiProfilePagerAdapter.getActiveListAdapter() == null
- || mMultiProfilePagerAdapter.getInactiveListAdapter() == null) {
+ if (!isTwoPagePersonalAndWorkConfiguration()) {
return false;
}
+
ResolverListAdapter sameProfileAdapter =
- mMultiProfilePagerAdapter.getActiveListAdapter();
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getPersonalListAdapter()
+ : mMultiProfilePagerAdapter.getWorkListAdapter();
+
ResolverListAdapter otherProfileAdapter =
- mMultiProfilePagerAdapter.getInactiveListAdapter();
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getWorkListAdapter()
+ : mMultiProfilePagerAdapter.getPersonalListAdapter();
if (sameProfileAdapter.getDisplayResolveInfoCount() == 0) {
Log.d(TAG, "No targets in the current profile");
@@ -1876,53 +1530,6 @@ public class ResolverActivity extends FragmentActivity implements
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 (numberOfProfiles == 2
- && mMultiProfilePagerAdapter.getActiveListAdapter().isTabLoaded()
- && mMultiProfilePagerAdapter.getInactiveListAdapter().isTabLoaded()
- && 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) {
@@ -1945,42 +1552,57 @@ public class ResolverActivity extends FragmentActivity implements
}
/**
- * When we have a personal and a work profile, we auto launch in the following scenario:
+ * 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() {
- ResolverListAdapter activeListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();
- int count = activeListAdapter.getUnfilteredCount();
- if (count != 1) {
+ if (!isTwoPagePersonalAndWorkConfiguration()) {
return false;
}
+
+ ResolverListAdapter activeListAdapter =
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getPersonalListAdapter()
+ : mMultiProfilePagerAdapter.getWorkListAdapter();
+
ResolverListAdapter inactiveListAdapter =
- mMultiProfilePagerAdapter.getInactiveListAdapter();
- if (inactiveListAdapter.getUnfilteredCount() != 1) {
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getWorkListAdapter()
+ : mMultiProfilePagerAdapter.getPersonalListAdapter();
+
+ if (!activeListAdapter.isTabLoaded() || !inactiveListAdapter.isTabLoaded()) {
return false;
}
- TargetInfo activeProfileTarget = activeListAdapter
- .targetInfoForPosition(0, 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(),
+ if (!Objects.equals(
+ activeProfileTarget.getResolvedComponentName(),
inactiveProfileTarget.getResolvedComponentName())) {
return false;
}
+
if (!shouldAutoLaunchSingleChoice(activeProfileTarget)) {
return false;
}
+
String packageName = activeProfileTarget.getResolvedComponentName().getPackageName();
- if (!canAppInteractCrossProfiles(packageName)) {
+ if (!mIntentForwarding.canAppInteractAcrossProfiles(this, packageName)) {
return false;
}
DevicePolicyEventLogger
.createEvent(DevicePolicyEnums.RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET)
.setBoolean(activeListAdapter.getUserHandle()
- .equals(getAnnotatedUserHandles().personalProfileUserHandle))
+ .equals(mProfiles.getPersonalHandle()))
.setStrings(getMetricsCategory())
.write();
safelyStartActivity(activeProfileTarget);
@@ -1988,140 +1610,66 @@ public class ResolverActivity extends FragmentActivity implements
return true;
}
+ private boolean isAutolaunching() {
+ return !mRegistered && isFinishing();
+ }
+
/**
- * 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>
- *
+ * @return {@code true} if a resolved target is autolaunched, otherwise {@code false}
*/
- 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) {
+ private boolean maybeAutolaunchActivity() {
+ if (!isTwoPagePersonalAndWorkConfiguration()) {
return false;
}
- int packageUid = applicationInfo.uid;
+ ResolverListAdapter activeListAdapter =
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getPersonalListAdapter()
+ : mMultiProfilePagerAdapter.getWorkListAdapter();
- 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;
- }
+ ResolverListAdapter inactiveListAdapter =
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getWorkListAdapter()
+ : mMultiProfilePagerAdapter.getPersonalListAdapter();
- private boolean isAutolaunching() {
- return !mRegistered && isFinishing();
- }
+ if (!activeListAdapter.isTabLoaded() || !inactiveListAdapter.isTabLoaded()) {
+ return false;
+ }
- 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(getPersonalTabLabel());
- personalButton.setContentDescription(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(getWorkTabLabel());
- workButton.setContentDescription(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();
- });
+ if ((activeListAdapter.getUnfilteredCount() != 1)
+ || (inactiveListAdapter.getUnfilteredCount() != 1)) {
+ return false;
+ }
- viewPager.setVisibility(View.VISIBLE);
- tabHost.setCurrentTab(mMultiProfilePagerAdapter.getCurrentPage());
- mMultiProfilePagerAdapter.setOnProfileSelectedListener(
- new MultiProfilePagerAdapter.OnProfileSelectedListener() {
- @Override
- public void onProfileSelected(int index) {
- tabHost.setCurrentTab(index);
- resetButtonBar();
- resetCheckedItem();
- }
+ TargetInfo activeProfileTarget = activeListAdapter.targetInfoForPosition(0, false);
+ TargetInfo inactiveProfileTarget = inactiveListAdapter.targetInfoForPosition(0, false);
+ if (!Objects.equals(
+ activeProfileTarget.getResolvedComponentName(),
+ inactiveProfileTarget.getResolvedComponentName())) {
+ return false;
+ }
- @Override
- public void onProfilePageStateChanged(int state) {
- onHorizontalSwipeStateChanged(state);
- }
- });
- mOnSwitchOnWorkSelectedListener = () -> {
- final View workTab = tabHost.getTabWidget().getChildAt(1);
- workTab.setFocusable(true);
- workTab.setFocusableInTouchMode(true);
- workTab.requestFocus();
- };
- }
+ if (!shouldAutoLaunchSingleChoice(activeProfileTarget)) {
+ return false;
+ }
- private String getPersonalTabLabel() {
- return getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_PERSONAL_TAB, () -> getString(R.string.resolver_personal_tab));
- }
+ String packageName = activeProfileTarget.getResolvedComponentName().getPackageName();
+ if (!mIntentForwarding.canAppInteractAcrossProfiles(this, packageName)) {
+ return false;
+ }
- private String getWorkTabLabel() {
- return getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_WORK_TAB, () -> getString(R.string.resolver_work_tab));
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET)
+ .setBoolean(activeListAdapter.getUserHandle()
+ .equals(mProfiles.getPersonalHandle()))
+ .setStrings(getMetricsCategory())
+ .write();
+ safelyStartActivity(activeProfileTarget);
+ finish();
+ return true;
}
private void maybeHideDivider() {
- if (!mIsIntentPicker) {
- return;
- }
final View divider = findViewById(com.android.internal.R.id.divider);
if (divider == null) {
return;
@@ -2130,41 +1678,9 @@ public class ResolverActivity extends FragmentActivity implements
}
private void resetCheckedItem() {
- if (!mIsIntentPicker) {
- return;
- }
mLastSelected = ListView.INVALID_POSITION;
- ListView inactiveListView = (ListView) mMultiProfilePagerAdapter.getInactiveAdapterView();
- if (inactiveListView.getCheckedItemCount() > 0) {
- inactiveListView.setItemChecked(inactiveListView.getCheckedItemPosition(), false);
- }
- }
-
- private String getPersonalTabAccessibilityLabel() {
- return getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_PERSONAL_TAB_ACCESSIBILITY,
- () -> getString(R.string.resolver_personal_tab_accessibility));
- }
-
- private String getWorkTabAccessibilityLabel() {
- return getSystemService(DevicePolicyManager.class).getResources().getString(
- RESOLVER_WORK_TAB_ACCESSIBILITY,
- () -> getString(R.string.resolver_work_tab_accessibility));
- }
-
- 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);
+ ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
+ .clearCheckedItemsInInactiveProfiles();
}
private void setupViewVisibilities() {
@@ -2192,10 +1708,7 @@ public class ResolverActivity extends FragmentActivity implements
private void setupAdapterListView(ListView listView, ItemClickListener listener) {
listView.setOnItemClickListener(listener);
listView.setOnItemLongClickListener(listener);
-
- if (mSupportsAlwaysUseOption) {
- listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
- }
+ listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
}
/**
@@ -2206,17 +1719,17 @@ public class ResolverActivity extends FragmentActivity implements
&& !listAdapter.getUserHandle().equals(mHeaderCreatorUser)) {
return;
}
- if (!shouldShowTabs()
+ if (!mProfiles.getWorkProfilePresent()
&& 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 = mTitle != null
- ? mTitle
- : getTitleForAction(getTargetIntent(), mDefaultTitleResId);
+ ResolverRequest request = mViewModel.getRequest().getValue();
+ CharSequence title = mViewModel.getRequest().getValue().getTitle() != null
+ ? request.getTitle()
+ : getTitleForAction(request.getIntent(), 0);
if (!TextUtils.isEmpty(title)) {
final TextView titleView = findViewById(com.android.internal.R.id.title);
@@ -2261,25 +1774,9 @@ public class ResolverActivity extends FragmentActivity implements
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(
- getAnnotatedUserHandles().tabOwnerUserHandleForLaunch).hasFilteredItem();
- return mSupportsAlwaysUseOption && 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;
- }
-
- private boolean inactiveListAdapterHasItems() {
- if (!shouldShowTabs()) {
- return false;
- }
- return mMultiProfilePagerAdapter.getInactiveListAdapter().getCount() > 0;
+ return mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+ mProfiles.getTabOwnerUserHandleForLaunch()
+ ).hasFilteredItem();
}
final class ItemClickListener implements AdapterView.OnItemClickListener,
@@ -2336,11 +1833,37 @@ public class ResolverActivity extends FragmentActivity implements
}
- /** 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;
+ private void setupProfileTabs() {
+ maybeHideDivider();
+
+ TabHost tabHost = findViewById(com.android.internal.R.id.profile_tabhost);
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+
+ mMultiProfilePagerAdapter.setupProfileTabs(
+ getLayoutInflater(),
+ tabHost,
+ viewPager,
+ R.layout.resolver_profile_tab_button,
+ com.android.internal.R.id.profile_pager,
+ () -> onProfileTabSelected(viewPager.getCurrentItem()),
+ new OnProfileSelectedListener() {
+ @Override
+ public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) {
+ resetButtonBar();
+ resetCheckedItem();
+ }
+
+ @Override
+ public void onProfilePageStateChanged(int state) {}
+ });
+ mOnSwitchOnWorkSelectedListener = () -> {
+ final View workTab =
+ tabHost.getTabWidget().getChildAt(
+ mMultiProfilePagerAdapter.getPageNumberForProfile(PROFILE_WORK));
+ workTab.setFocusable(true);
+ workTab.setFocusableInTouchMode(true);
+ workTab.requestFocus();
+ };
}
static final class PickTargetOptionRequest extends PickOptionRequest {
@@ -2384,7 +1907,7 @@ public class ResolverActivity extends FragmentActivity implements
* {@link ResolverListController} configured for the provided {@code userHandle}.
*/
protected final UserHandle getQueryIntentsUser(UserHandle userHandle) {
- return getAnnotatedUserHandles().getQueryIntentsUser(userHandle);
+ return mProfiles.getQueryIntentsHandle(userHandle);
}
/**
@@ -2404,9 +1927,9 @@ 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(getAnnotatedUserHandles().personalProfileUserHandle)
- && hasCloneProfile()) {
- userList.add(getAnnotatedUserHandles().cloneProfileUserHandle);
+ if (userHandle.equals(mProfiles.getPersonalHandle())
+ && mProfiles.getCloneUserPresent()) {
+ userList.add(mProfiles.getCloneHandle());
}
return userList;
}
diff --git a/java/src/com/android/intentresolver/ResolverHelper.kt b/java/src/com/android/intentresolver/ResolverHelper.kt
new file mode 100644
index 00000000..d12ba7d5
--- /dev/null
+++ b/java/src/com/android/intentresolver/ResolverHelper.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.app.Activity
+import android.os.UserHandle
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.activity.viewModels
+import androidx.lifecycle.DefaultLifecycleObserver
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import com.android.intentresolver.annotation.JavaInterop
+import com.android.intentresolver.domain.interactor.UserInteractor
+import com.android.intentresolver.inject.Background
+import com.android.intentresolver.ui.model.ResolverRequest
+import com.android.intentresolver.ui.viewmodel.ResolverViewModel
+import com.android.intentresolver.validation.Invalid
+import com.android.intentresolver.validation.Valid
+import com.android.intentresolver.validation.log
+import dagger.hilt.android.scopes.ActivityScoped
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+
+private const val TAG: String = "ResolverHelper"
+
+/**
+ * __Purpose__
+ *
+ * Cleanup aid. Provides a pathway to cleaner code.
+ *
+ * __Incoming References__
+ *
+ * ResolverHelper must not expose any properties or functions directly back to ResolverActivity. If
+ * a value or operation is required by ResolverActivity, then it must be added to
+ * ResolverInitializer (or a new interface as appropriate) with ResolverActivity supplying a
+ * callback to receive it at the appropriate point. This enforces unidirectional control flow.
+ *
+ * __Outgoing References__
+ *
+ * _ResolverActivity_
+ *
+ * This class must only reference it's host as Activity/ComponentActivity; no down-cast to
+ * [ResolverActivity]. Other components should be created here or supplied via Injection, and not
+ * referenced directly from the activity. This prevents circular dependencies from forming. If
+ * necessary, during cleanup the dependency can be supplied back to ChooserActivity as described
+ * above in 'Incoming References', see [ResolverInitializer].
+ *
+ * _Elsewhere_
+ *
+ * Where possible, Singleton and ActivityScoped dependencies should be injected here instead of
+ * referenced from an existing location. If not available for injection, the value should be
+ * constructed here, then provided to where it is needed.
+ */
+@ActivityScoped
+@JavaInterop
+class ResolverHelper
+@Inject
+constructor(
+ hostActivity: Activity,
+ private val userInteractor: UserInteractor,
+ @Background private val background: CoroutineDispatcher,
+) : DefaultLifecycleObserver {
+ // This is guaranteed by Hilt, since only a ComponentActivity is injectable.
+ private val activity: ComponentActivity = hostActivity as ComponentActivity
+ private val viewModel by activity.viewModels<ResolverViewModel>()
+
+ private lateinit var activityInitializer: Runnable
+
+ init {
+ activity.lifecycle.addObserver(this)
+ }
+
+ /**
+ * Set the initialization hook for the host activity.
+ *
+ * This _must_ be called from [ResolverActivity.onCreate].
+ */
+ fun setInitializer(initializer: Runnable) {
+ if (activity.lifecycle.currentState != Lifecycle.State.INITIALIZED) {
+ error("setInitializer must be called before onCreate returns")
+ }
+ activityInitializer = initializer
+ }
+
+ /** Invoked by Lifecycle, after Activity.onCreate() _returns_. */
+ override fun onCreate(owner: LifecycleOwner) {
+ Log.i(TAG, "CREATE")
+ Log.i(TAG, "${viewModel.activityModel}")
+
+ val callerUid: Int = viewModel.activityModel.launchedFromUid
+ if (callerUid < 0 || UserHandle.isIsolated(callerUid)) {
+ Log.e(TAG, "Can't start a resolver from uid $callerUid")
+ activity.finish()
+ return
+ }
+
+ when (val request = viewModel.initialRequest) {
+ is Valid -> initializeActivity(request)
+ is Invalid -> reportErrorsAndFinish(request)
+ }
+ }
+
+ private fun reportErrorsAndFinish(request: Invalid<ResolverRequest>) {
+ request.errors.forEach { it.log(TAG) }
+ activity.finish()
+ }
+
+ private fun initializeActivity(request: Valid<ResolverRequest>) {
+ Log.d(TAG, "initializeActivity")
+ request.warnings.forEach { it.log(TAG) }
+
+ activityInitializer.run()
+ }
+}
diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java
index 564d8d19..2a8fcfa4 100644
--- a/java/src/com/android/intentresolver/ResolverListAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverListAdapter.java
@@ -25,7 +25,6 @@ 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;
@@ -449,6 +448,9 @@ public class ResolverListAdapter extends BaseAdapter {
// Send an "incomplete" list-ready while the async task is running.
postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ false);
mBgExecutor.execute(() -> {
+ if (isDestroyed()) {
+ return;
+ }
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
@@ -477,9 +479,6 @@ public class ResolverListAdapter extends BaseAdapter {
@Nullable List<ResolvedComponentInfo> sortedComponents, boolean doPostProcessing) {
processSortedList(sortedComponents, doPostProcessing);
notifyDataSetChanged();
- if (doPostProcessing) {
- mResolverListCommunicator.updateProfileViewButton();
- }
}
protected void processSortedList(
@@ -651,6 +650,7 @@ public class ResolverListAdapter extends BaseAdapter {
return null;
}
+ @Override
public int getCount() {
int totalSize = mDisplayList == null || mDisplayList.isEmpty() ? mPlaceholderCount :
mDisplayList.size();
@@ -664,6 +664,7 @@ public class ResolverListAdapter extends BaseAdapter {
return mDisplayList.size();
}
+ @Override
@Nullable
public TargetInfo getItem(int position) {
if (mFilterLastUsed && mLastChosenPosition >= 0 && position >= mLastChosenPosition) {
@@ -676,6 +677,7 @@ public class ResolverListAdapter extends BaseAdapter {
}
}
+ @Override
public long getItemId(int position) {
return position;
}
@@ -693,6 +695,7 @@ public class ResolverListAdapter extends BaseAdapter {
return mDisplayList.get(index);
}
+ @Override
public final View getView(int position, View convertView, ViewGroup parent) {
View view = convertView;
if (view == null) {
@@ -753,9 +756,7 @@ public class ResolverListAdapter extends BaseAdapter {
}
private void onIconLoaded(DisplayResolveInfo displayResolveInfo, Drawable drawable) {
- if (getOtherProfile() == displayResolveInfo) {
- mResolverListCommunicator.updateProfileViewButton();
- } else if (!displayResolveInfo.hasDisplayIcon()) {
+ if (!displayResolveInfo.hasDisplayIcon()) {
displayResolveInfo.getDisplayIconHolder().setDisplayIcon(drawable);
notifyDataSetChanged();
}
@@ -787,6 +788,10 @@ public class ResolverListAdapter extends BaseAdapter {
mRequestedLabels.clear();
}
+ public final boolean isDestroyed() {
+ return mDestroyed.get();
+ }
+
private static ColorMatrixColorFilter getSuspendedColorMatrix() {
if (sSuspendedMatrixColorFilter == null) {
@@ -835,7 +840,7 @@ public class ResolverListAdapter extends BaseAdapter {
userHandle);
}
- public final List<Intent> getIntents() {
+ public List<Intent> getIntents() {
// TODO: immutable copy?
return mIntents;
}
@@ -903,14 +908,18 @@ public class ResolverListAdapter extends BaseAdapter {
Intent getReplacementIntent(ActivityInfo activityInfo, Intent defIntent);
+ // ResolverListCommunicator
+ default void updateProfileViewButton() {
+ }
+
void onPostListReady(ResolverListAdapter listAdapter, boolean updateUi,
boolean rebuildCompleted);
void sendVoiceChoicesIfNeeded();
- void updateProfileViewButton();
-
- boolean useLayoutWithDefault();
+ default boolean useLayoutWithDefault() {
+ return false;
+ }
boolean shouldGetActivityMetadata();
@@ -918,7 +927,9 @@ public class ResolverListAdapter extends BaseAdapter {
* @return true to filter only apps that can handle
* {@link android.content.Intent#CATEGORY_DEFAULT} intents
*/
- default boolean shouldGetOnlyDefaultActivities() { return true; };
+ default boolean shouldGetOnlyDefaultActivities() {
+ return true;
+ }
void onHandlePackagesChanged(ResolverListAdapter listAdapter);
}
@@ -930,7 +941,7 @@ public class ResolverListAdapter extends BaseAdapter {
@VisibleForTesting
public static class ViewHolder {
public View itemView;
- public Drawable defaultItemViewBackground;
+ public final Drawable defaultItemViewBackground;
public TextView text;
public TextView text2;
@@ -940,8 +951,6 @@ public class ResolverListAdapter extends BaseAdapter {
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("");
@@ -982,10 +991,6 @@ public class ResolverListAdapter extends BaseAdapter {
itemView.setContentDescription(null);
}
- public void updateContentDescription(String description) {
- itemView.setContentDescription(description);
- }
-
/**
* Bind view holder to a TargetInfo.
*/
@@ -998,19 +1003,5 @@ 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/ResolverViewPager.java b/java/src/com/android/intentresolver/ResolverViewPager.java
index 0496579d..891ace87 100644
--- a/java/src/com/android/intentresolver/ResolverViewPager.java
+++ b/java/src/com/android/intentresolver/ResolverViewPager.java
@@ -75,6 +75,12 @@ public class ResolverViewPager extends ViewPager {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
- return !isLayoutRtl() && mSwipingEnabled && super.onInterceptTouchEvent(ev);
+ return !isEnabled()
+ || (!isLayoutRtl() && mSwipingEnabled && super.onInterceptTouchEvent(ev));
+ }
+
+ @Override
+ public boolean onTouchEvent(MotionEvent ev) {
+ return isEnabled() && super.onTouchEvent(ev);
}
}
diff --git a/java/src/com/android/intentresolver/SecureSettings.kt b/java/src/com/android/intentresolver/SecureSettings.kt
index a4853fd8..1e938895 100644
--- a/java/src/com/android/intentresolver/SecureSettings.kt
+++ b/java/src/com/android/intentresolver/SecureSettings.kt
@@ -19,9 +19,7 @@ package com.android.intentresolver
import android.content.ContentResolver
import android.provider.Settings
-/**
- * A proxy class for secure settings, for easier testing.
- */
+/** A proxy class for secure settings, for easier testing. */
open class SecureSettings {
open fun getString(resolver: ContentResolver, name: String): String? {
return Settings.Secure.getString(resolver, name)
diff --git a/java/src/com/android/intentresolver/ShortcutSelectionLogic.java b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java
index efaaf894..2d5ec451 100644
--- a/java/src/com/android/intentresolver/ShortcutSelectionLogic.java
+++ b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java
@@ -30,13 +30,19 @@ import androidx.annotation.Nullable;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.SelectableTargetInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.ui.AppShortcutLimit;
+import com.android.intentresolver.ui.EnforceShortcutLimit;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
-class ShortcutSelectionLogic {
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+@Singleton
+public class ShortcutSelectionLogic {
private static final String TAG = "ShortcutSelectionLogic";
private static final boolean DEBUG = false;
private static final float PINNED_SHORTCUT_TARGET_SCORE_BOOST = 1000.f;
@@ -49,9 +55,10 @@ class ShortcutSelectionLogic {
private final Comparator<ChooserTarget> mBaseTargetComparator =
(lhs, rhs) -> Float.compare(rhs.getScore(), lhs.getScore());
- ShortcutSelectionLogic(
- int maxShortcutTargetsPerApp,
- boolean applySharingAppLimits) {
+ @Inject
+ public ShortcutSelectionLogic(
+ @AppShortcutLimit int maxShortcutTargetsPerApp,
+ @EnforceShortcutLimit boolean applySharingAppLimits) {
mMaxShortcutTargetsPerApp = maxShortcutTargetsPerApp;
mApplySharingAppLimits = applySharingAppLimits;
}
@@ -78,7 +85,7 @@ class ShortcutSelectionLogic {
+ targets.size()
+ " targets");
}
- if (targets.size() == 0) {
+ if (targets.isEmpty()) {
return false;
}
Collections.sort(targets, mBaseTargetComparator);
diff --git a/java/src/com/android/intentresolver/SimpleIconFactory.java b/java/src/com/android/intentresolver/SimpleIconFactory.java
index 750b24ac..f4871e36 100644
--- a/java/src/com/android/intentresolver/SimpleIconFactory.java
+++ b/java/src/com/android/intentresolver/SimpleIconFactory.java
@@ -58,7 +58,6 @@ import org.xmlpull.v1.XmlPullParser;
import java.nio.ByteBuffer;
import java.util.Optional;
-
/**
* @deprecated Use the Launcher3 Iconloaderlib at packages/apps/Launcher3/iconloaderlib. This class
* is a temporary fork of Iconloader. It combines all necessary methods to render app icons that are
diff --git a/java/src/com/android/intentresolver/StartsSelectedItem.kt b/java/src/com/android/intentresolver/StartsSelectedItem.kt
new file mode 100644
index 00000000..01cdf124
--- /dev/null
+++ b/java/src/com/android/intentresolver/StartsSelectedItem.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver
+
+interface StartsSelectedItem {
+ /** Start the selected item. */
+ fun startSelected(which: Int, always: Boolean, filtered: Boolean)
+}
diff --git a/java/src/com/android/intentresolver/annotation/JavaInterop.kt b/java/src/com/android/intentresolver/annotation/JavaInterop.kt
new file mode 100644
index 00000000..e268af98
--- /dev/null
+++ b/java/src/com/android/intentresolver/annotation/JavaInterop.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.annotation
+
+/**
+ * Apply to code which exists specifically to easy integration with existing Java and Java APIs.
+ *
+ * The goal is to prevent usage from Kotlin when a more idiomatic alternative is available.
+ */
+@RequiresOptIn(
+ "This is a a property, function or class specifically supporting Java " +
+ "interoperability. Usage from Kotlin should be limited to interactions with Java."
+)
+annotation class JavaInterop
diff --git a/java/src/com/android/intentresolver/chooser/DisplayResolveInfoAzInfoComparator.java b/java/src/com/android/intentresolver/chooser/DisplayResolveInfoAzInfoComparator.java
new file mode 100644
index 00000000..3462b726
--- /dev/null
+++ b/java/src/com/android/intentresolver/chooser/DisplayResolveInfoAzInfoComparator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.chooser;
+
+
+import android.content.Context;
+
+import java.text.Collator;
+import java.util.Comparator;
+
+/**
+ * Sort intents alphabetically based on display label.
+ */
+public class DisplayResolveInfoAzInfoComparator implements Comparator<DisplayResolveInfo> {
+ Comparator<DisplayResolveInfo> mComparator;
+ public DisplayResolveInfoAzInfoComparator(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);
+ }
+}
diff --git a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
index b97e6b45..4fe28384 100644
--- a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
+++ b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
@@ -17,9 +17,11 @@
package com.android.intentresolver.chooser;
import android.app.Activity;
+import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;
import android.os.UserHandle;
+import android.util.Log;
import androidx.annotation.Nullable;
@@ -121,6 +123,19 @@ public class MultiDisplayResolveInfo extends DisplayResolveInfo {
}
@Override
+ public ComponentName getResolvedComponentName() {
+ if (hasSelected()) {
+ return mTargetInfos.get(mSelected).getResolvedComponentName();
+ }
+ // It is not expected to have this method be called on an unselected multi-display item.
+ // Call super to preserve the legacy (most likely erroneous) behavior.
+ Log.wtf(
+ "ChooserActivity",
+ "retrieving ResolvedComponentName from an unselected MultiDisplayResolveInfo");
+ return super.getResolvedComponentName();
+ }
+
+ @Override
public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
return mTargetInfos.get(mSelected).startAsUser(activity, options, user);
}
diff --git a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt
index 10ee5af1..dc36e584 100644
--- a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt
+++ b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt
@@ -17,16 +17,19 @@
package com.android.intentresolver.contentpreview
import android.content.Intent
+import android.net.Uri
import androidx.annotation.MainThread
import androidx.lifecycle.ViewModel
-import com.android.intentresolver.ChooserRequestParameters
/** A contract for the preview view model. Added for testing. */
abstract class BasePreviewViewModel : ViewModel() {
- @MainThread
- abstract fun createOrReuseProvider(
- targetIntent: Intent
- ): PreviewDataProvider
+ @get:MainThread abstract val previewDataProvider: PreviewDataProvider
+ @get:MainThread abstract val imageLoader: ImageLoader
- @MainThread abstract fun createOrReuseImageLoader(): ImageLoader
+ @MainThread
+ abstract fun init(
+ targetIntent: Intent,
+ additionalContentUri: Uri?,
+ isPayloadTogglingEnabled: Boolean,
+ )
}
diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
index a015147d..4b955c49 100644
--- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
@@ -18,6 +18,7 @@ package com.android.intentresolver.contentpreview;
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_PAYLOAD_SELECTION;
import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT;
import android.content.ClipData;
@@ -32,13 +33,15 @@ import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.intentresolver.ContentTypeHint;
import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback;
+import kotlinx.coroutines.CoroutineScope;
+
import java.util.List;
import java.util.function.Consumer;
-
-import kotlinx.coroutines.CoroutineScope;
+import java.util.function.Supplier;
/**
* Collection of helpers for building the content preview UI displayed in
@@ -48,6 +51,7 @@ import kotlinx.coroutines.CoroutineScope;
public final class ChooserContentPreviewUi {
private final CoroutineScope mScope;
+ private final boolean mIsPayloadTogglingEnabled;
/**
* Delegate to build the default system action buttons to display in the preview layout, if/when
@@ -74,7 +78,9 @@ public final class ChooserContentPreviewUi {
* Provides a share modification action, if any.
*/
@Nullable
- ActionRow.Action getModifyShareAction();
+ default ActionRow.Action getModifyShareAction() {
+ return null;
+ }
/**
* <p>
@@ -90,6 +96,8 @@ public final class ChooserContentPreviewUi {
@VisibleForTesting
final ContentPreviewUi mContentPreviewUi;
+ private final Supplier</*@Nullable*/ActionRow.Action> mModifyShareActionFactory;
+ private View mHeadlineParent;
public ChooserContentPreviewUi(
CoroutineScope scope,
@@ -97,9 +105,16 @@ public final class ChooserContentPreviewUi {
Intent targetIntent,
ImageLoader imageLoader,
ActionFactory actionFactory,
+ Supplier</*@Nullable*/ActionRow.Action> modifyShareActionFactory,
TransitionElementStatusCallback transitionElementStatusCallback,
- HeadlineGenerator headlineGenerator) {
+ HeadlineGenerator headlineGenerator,
+ ContentTypeHint contentTypeHint,
+ @Nullable CharSequence metadata,
+ // TODO: replace with the FeatureFlag ref when v1 is gone
+ boolean isPayloadTogglingEnabled) {
mScope = scope;
+ mIsPayloadTogglingEnabled = isPayloadTogglingEnabled;
+ mModifyShareActionFactory = modifyShareActionFactory;
mContentPreviewUi = createContentPreview(
previewData,
targetIntent,
@@ -107,7 +122,10 @@ public final class ChooserContentPreviewUi {
imageLoader,
actionFactory,
transitionElementStatusCallback,
- headlineGenerator);
+ headlineGenerator,
+ contentTypeHint,
+ metadata
+ );
if (mContentPreviewUi.getType() != CONTENT_PREVIEW_IMAGE) {
transitionElementStatusCallback.onAllTransitionElementsReady();
}
@@ -120,8 +138,10 @@ public final class ChooserContentPreviewUi {
ImageLoader imageLoader,
ActionFactory actionFactory,
TransitionElementStatusCallback transitionElementStatusCallback,
- HeadlineGenerator headlineGenerator) {
-
+ HeadlineGenerator headlineGenerator,
+ ContentTypeHint contentTypeHint,
+ @Nullable CharSequence metadata
+ ) {
int previewType = previewData.getPreviewType();
if (previewType == CONTENT_PREVIEW_TEXT) {
return createTextPreview(
@@ -129,20 +149,31 @@ public final class ChooserContentPreviewUi {
targetIntent,
actionFactory,
imageLoader,
- headlineGenerator);
+ headlineGenerator,
+ contentTypeHint,
+ metadata
+ );
}
if (previewType == CONTENT_PREVIEW_FILE) {
FileContentPreviewUi fileContentPreviewUi = new FileContentPreviewUi(
previewData.getUriCount(),
actionFactory,
- headlineGenerator);
+ headlineGenerator,
+ metadata
+ );
if (previewData.getUriCount() > 0) {
previewData.getFirstFileName(mScope, fileContentPreviewUi::setFirstFileName);
}
return fileContentPreviewUi;
}
+
+ if (previewType == CONTENT_PREVIEW_PAYLOAD_SELECTION && mIsPayloadTogglingEnabled) {
+ transitionElementStatusCallback.onAllTransitionElementsReady(); // TODO
+ return new ShareouselContentPreviewUi();
+ }
+
boolean isSingleImageShare = previewData.getUriCount() == 1
- && typeClassifier.isImageType(previewData.getFirstFileInfo().getMimeType());
+ && typeClassifier.isImageType(previewData.getFirstFileInfo().getMimeType());
CharSequence text = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
if (!TextUtils.isEmpty(text)) {
FilesPlusTextContentPreviewUi previewUi =
@@ -155,7 +186,9 @@ public final class ChooserContentPreviewUi {
actionFactory,
imageLoader,
typeClassifier,
- headlineGenerator);
+ headlineGenerator,
+ metadata
+ );
if (previewData.getUriCount() > 0) {
JavaFlowHelper.collectToList(
mScope,
@@ -175,7 +208,9 @@ public final class ChooserContentPreviewUi {
transitionElementStatusCallback,
previewData.getImagePreviewFileInfoFlow(),
previewData.getUriCount(),
- headlineGenerator);
+ headlineGenerator,
+ metadata
+ );
}
public int getPreferredContentPreview() {
@@ -190,9 +225,20 @@ public final class ChooserContentPreviewUi {
Resources resources,
LayoutInflater layoutInflater,
ViewGroup parent,
- @Nullable View headlineViewParent) {
+ View headlineViewParent) {
- return mContentPreviewUi.display(resources, layoutInflater, parent, headlineViewParent);
+ ViewGroup layout =
+ mContentPreviewUi.display(resources, layoutInflater, parent, headlineViewParent);
+ mHeadlineParent = headlineViewParent;
+ ContentPreviewUi.displayModifyShareAction(mHeadlineParent, mModifyShareActionFactory.get());
+ return layout;
+ }
+
+ /**
+ * Update Modify Share Action, if it is inflated.
+ */
+ public void updateModifyShareAction() {
+ ContentPreviewUi.displayModifyShareAction(mHeadlineParent, mModifyShareActionFactory.get());
}
private static TextContentPreviewUi createTextPreview(
@@ -200,7 +246,10 @@ public final class ChooserContentPreviewUi {
Intent targetIntent,
ChooserContentPreviewUi.ActionFactory actionFactory,
ImageLoader imageLoader,
- HeadlineGenerator headlineGenerator) {
+ HeadlineGenerator headlineGenerator,
+ ContentTypeHint contentTypeHint,
+ @Nullable CharSequence metadata
+ ) {
CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
CharSequence previewTitle = targetIntent.getCharSequenceExtra(Intent.EXTRA_TITLE);
ClipData previewData = targetIntent.getClipData();
@@ -211,13 +260,16 @@ public final class ChooserContentPreviewUi {
previewThumbnail = previewDataItem.getUri();
}
}
+
return new TextContentPreviewUi(
scope,
sharingText,
previewTitle,
+ metadata,
previewThumbnail,
actionFactory,
imageLoader,
- headlineGenerator);
+ headlineGenerator,
+ contentTypeHint);
}
}
diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java
index ad1c6c01..79bb9d3c 100644
--- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java
+++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java
@@ -25,11 +25,13 @@ import java.lang.annotation.Retention;
@Retention(SOURCE)
@IntDef({ContentPreviewType.CONTENT_PREVIEW_FILE,
ContentPreviewType.CONTENT_PREVIEW_IMAGE,
- ContentPreviewType.CONTENT_PREVIEW_TEXT})
+ ContentPreviewType.CONTENT_PREVIEW_TEXT,
+ ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION})
public @interface ContentPreviewType {
// Starting at 1 since 0 is considered "undefined" for some of the database transformations
// of tron logs.
int CONTENT_PREVIEW_IMAGE = 1;
int CONTENT_PREVIEW_FILE = 2;
int CONTENT_PREVIEW_TEXT = 3;
+ int CONTENT_PREVIEW_PAYLOAD_SELECTION = 4;
}
diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
index dce146b0..8eaf3568 100644
--- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
@@ -30,12 +30,14 @@ import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
import com.android.intentresolver.R;
import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.ScrollableImagePreviewView;
-abstract class ContentPreviewUi {
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+public abstract class ContentPreviewUi {
private static final int IMAGE_FADE_IN_MILLIS = 150;
static final String TAG = "ChooserPreview";
@@ -46,7 +48,7 @@ abstract class ContentPreviewUi {
Resources resources,
LayoutInflater layoutInflater,
ViewGroup parent,
- @Nullable View headlineViewParent);
+ View headlineViewParent);
protected static void updateViewWithImage(ImageView imageView, Bitmap image) {
if (image == null) {
@@ -83,16 +85,32 @@ abstract class ContentPreviewUi {
}
}
- protected static void displayModifyShareAction(
- View layout, ChooserContentPreviewUi.ActionFactory actionFactory) {
- ActionRow.Action modifyShareAction = actionFactory.getModifyShareAction();
- if (modifyShareAction != null && layout != null) {
- TextView modifyShareView = layout.findViewById(R.id.reselection_action);
- if (modifyShareView != null) {
- modifyShareView.setText(modifyShareAction.getLabel());
- modifyShareView.setVisibility(View.VISIBLE);
- modifyShareView.setOnClickListener(view -> modifyShareAction.getOnClicked().run());
- }
+ protected static void displayMetadata(View layout, @Nullable CharSequence metadata) {
+ TextView metadataView = layout == null ? null : layout.findViewById(R.id.metadata);
+ if (metadataView == null) {
+ return;
+ }
+ if (!TextUtils.isEmpty(metadata)) {
+ metadataView.setText(metadata);
+ metadataView.setVisibility(View.VISIBLE);
+ } else {
+ metadataView.setVisibility(View.GONE);
+ }
+ }
+
+ static void displayModifyShareAction(
+ View layout, @Nullable ActionRow.Action modifyShareAction) {
+ TextView modifyShareView =
+ layout == null ? null : layout.findViewById(R.id.reselection_action);
+ if (modifyShareView == null) {
+ return;
+ }
+ if (modifyShareAction != null) {
+ modifyShareView.setText(modifyShareAction.getLabel());
+ modifyShareView.setVisibility(View.VISIBLE);
+ modifyShareView.setOnClickListener(view -> modifyShareAction.getOnClicked().run());
+ } else {
+ modifyShareView.setVisibility(View.GONE);
}
}
diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java
index 89e7e528..1749c6f7 100644
--- a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java
@@ -43,15 +43,20 @@ class FileContentPreviewUi extends ContentPreviewUi {
private final ChooserContentPreviewUi.ActionFactory mActionFactory;
private final HeadlineGenerator mHeadlineGenerator;
@Nullable
+ private final CharSequence mMetadata;
+ @Nullable
private ViewGroup mContentPreview = null;
FileContentPreviewUi(
int fileCount,
ChooserContentPreviewUi.ActionFactory actionFactory,
- HeadlineGenerator headlineGenerator) {
+ HeadlineGenerator headlineGenerator,
+ @Nullable CharSequence metadata
+ ) {
mFileCount = fileCount;
mActionFactory = actionFactory;
mHeadlineGenerator = headlineGenerator;
+ mMetadata = metadata;
}
@Override
@@ -71,26 +76,21 @@ class FileContentPreviewUi extends ContentPreviewUi {
Resources resources,
LayoutInflater layoutInflater,
ViewGroup parent,
- @Nullable View headlineViewParent) {
- ViewGroup layout = displayInternal(resources, layoutInflater, parent, headlineViewParent);
- displayModifyShareAction(
- headlineViewParent == null ? layout : headlineViewParent, mActionFactory);
- return layout;
+ View headlineViewParent) {
+ return displayInternal(resources, layoutInflater, parent, headlineViewParent);
}
private ViewGroup displayInternal(
Resources resources,
LayoutInflater layoutInflater,
ViewGroup parent,
- @Nullable View headlineViewParent) {
+ View headlineViewParent) {
mContentPreview = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_file, parent, false);
- if (headlineViewParent == null) {
- headlineViewParent = mContentPreview;
- }
inflateHeadline(headlineViewParent);
displayHeadline(headlineViewParent, mHeadlineGenerator.getFilesHeadline(mFileCount));
+ displayMetadata(headlineViewParent, mMetadata);
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 78fc6586..b50f5bc8 100644
--- a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java
@@ -36,12 +36,12 @@ import com.android.intentresolver.R;
import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.ScrollableImagePreviewView;
+import kotlinx.coroutines.CoroutineScope;
+
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
@@ -57,6 +57,8 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
private final ImageLoader mImageLoader;
private final MimeTypeClassifier mTypeClassifier;
private final HeadlineGenerator mHeadlineGenerator;
+ @Nullable
+ private final CharSequence mMetadata;
private final boolean mIsSingleImage;
private final int mFileCount;
private ViewGroup mContentPreviewView;
@@ -78,7 +80,8 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
ChooserContentPreviewUi.ActionFactory actionFactory,
ImageLoader imageLoader,
MimeTypeClassifier typeClassifier,
- HeadlineGenerator headlineGenerator) {
+ HeadlineGenerator headlineGenerator,
+ @Nullable CharSequence metadata) {
if (isSingleImage && fileCount != 1) {
throw new IllegalArgumentException(
"fileCount = " + fileCount + " and isSingleImage = true");
@@ -92,6 +95,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
mImageLoader = imageLoader;
mTypeClassifier = typeClassifier;
mHeadlineGenerator = headlineGenerator;
+ mMetadata = metadata;
}
@Override
@@ -104,11 +108,8 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
Resources resources,
LayoutInflater layoutInflater,
ViewGroup parent,
- @Nullable View headlineViewParent) {
- ViewGroup layout = displayInternal(layoutInflater, parent, headlineViewParent);
- displayModifyShareAction(
- headlineViewParent == null ? layout : headlineViewParent, mActionFactory);
- return layout;
+ View headlineViewParent) {
+ return displayInternal(layoutInflater, parent, headlineViewParent);
}
public void updatePreviewMetadata(List<FileInfo> files) {
@@ -132,10 +133,10 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
private ViewGroup displayInternal(
LayoutInflater layoutInflater,
ViewGroup parent,
- @Nullable View headlineViewParent) {
+ View headlineViewParent) {
mContentPreviewView = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_files_text, parent, false);
- mHeadliveView = headlineViewParent == null ? mContentPreviewView : headlineViewParent;
+ mHeadliveView = headlineViewParent;
inflateHeadline(mHeadliveView);
final ActionRow actionRow =
@@ -204,6 +205,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
}
displayHeadline(headlineView, headline);
+ displayMetadata(headlineView, mMetadata);
}
private void prepareTextPreview(
diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt
index 5f87c924..21308341 100644
--- a/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt
+++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt
@@ -17,12 +17,14 @@
package com.android.intentresolver.contentpreview
/**
- * 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.
*/
interface HeadlineGenerator {
fun getTextHeadline(text: CharSequence): String
+ fun getAlbumHeadline(): String
+
fun getImagesWithTextHeadline(text: CharSequence, count: Int): String
fun getVideosWithTextHeadline(text: CharSequence, count: Int): String
diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt
index ef1e55d8..e92d9bc6 100644
--- a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt
+++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt
@@ -20,6 +20,12 @@ import android.content.Context
import android.util.PluralsMessageFormatter
import androidx.annotation.StringRes
import com.android.intentresolver.R
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Inject
private const val PLURALS_COUNT = "count"
@@ -27,13 +33,21 @@ 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.
*/
-class HeadlineGeneratorImpl(private val context: Context) : HeadlineGenerator {
+class HeadlineGeneratorImpl
+@Inject
+constructor(
+ @ApplicationContext private val context: Context,
+) : HeadlineGenerator {
override fun getTextHeadline(text: CharSequence): String {
return context.getString(
getTemplateResource(text, R.string.sharing_link, R.string.sharing_text)
)
}
+ override fun getAlbumHeadline(): String {
+ return context.getString(R.string.sharing_album)
+ }
+
override fun getImagesWithTextHeadline(text: CharSequence, count: Int): String {
return getPluralString(
getTemplateResource(
@@ -96,3 +110,9 @@ class HeadlineGeneratorImpl(private val context: Context) : HeadlineGenerator {
return if (text.toString().isHttpUri()) linkResource else nonLinkResource
}
}
+
+@Module
+@InstallIn(SingletonComponent::class)
+interface HeadlineGeneratorModule {
+ @Binds fun bind(impl: HeadlineGeneratorImpl): HeadlineGenerator
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/ImageLoaderModule.kt b/java/src/com/android/intentresolver/contentpreview/ImageLoaderModule.kt
new file mode 100644
index 00000000..b861a24a
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/ImageLoaderModule.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.content.res.Resources
+import com.android.intentresolver.R
+import com.android.intentresolver.inject.ApplicationOwned
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityRetainedComponent
+import dagger.hilt.android.scopes.ActivityRetainedScoped
+
+@Module
+@InstallIn(ActivityRetainedComponent::class)
+interface ImageLoaderModule {
+ @Binds
+ @ActivityRetainedScoped
+ fun imageLoader(previewImageLoader: ImagePreviewImageLoader): ImageLoader
+
+ companion object {
+ @Provides
+ @ThumbnailSize
+ fun thumbnailSize(@ApplicationOwned resources: Resources): Int =
+ resources.getDimensionPixelSize(R.dimen.chooser_preview_image_max_dimen)
+
+ @Provides @PreviewCacheSize fun cacheSize() = 16
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt
index 572ccf0b..fab7203e 100644
--- a/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt
+++ b/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt
@@ -24,17 +24,31 @@ import android.util.Size
import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting
import androidx.collection.LruCache
+import com.android.intentresolver.inject.Background
import java.util.function.Consumer
+import javax.inject.Inject
+import javax.inject.Qualifier
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
private const val TAG = "ImagePreviewImageLoader"
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.BINARY) annotation class ThumbnailSize
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.BINARY)
+annotation class PreviewCacheSize
+
/**
* Implements preview image loading for the content preview UI. Provides requests deduplication,
* image caching, and a limit on the number of parallel loadings.
@@ -52,6 +66,26 @@ constructor(
private val contentResolverSemaphore: Semaphore,
) : ImageLoader {
+ @Inject
+ constructor(
+ @Background dispatcher: CoroutineDispatcher,
+ @ThumbnailSize thumbnailSize: Int,
+ contentResolver: ContentResolver,
+ @PreviewCacheSize cacheSize: Int,
+ ) : this(
+ CoroutineScope(
+ SupervisorJob() +
+ dispatcher +
+ CoroutineExceptionHandler { _, exception ->
+ Log.w(TAG, "Uncaught exception in ImageLoader", exception)
+ } +
+ CoroutineName("ImageLoader")
+ ),
+ thumbnailSize,
+ contentResolver,
+ cacheSize,
+ )
+
constructor(
scope: CoroutineScope,
thumbnailSize: Int,
diff --git a/java/src/com/android/intentresolver/contentpreview/IsHttpUri.kt b/java/src/com/android/intentresolver/contentpreview/IsHttpUri.kt
index 80232537..ac002ab6 100644
--- a/java/src/com/android/intentresolver/contentpreview/IsHttpUri.kt
+++ b/java/src/com/android/intentresolver/contentpreview/IsHttpUri.kt
@@ -15,13 +15,16 @@
*/
@file:JvmName("HttpUriMatcher")
+
package com.android.intentresolver.contentpreview
import java.net.URI
internal fun String.isHttpUri() =
- kotlin.runCatching {
- URI(this).scheme.takeIf { scheme ->
- "http".compareTo(scheme, true) == 0 || "https".compareTo(scheme, true) == 0
+ kotlin
+ .runCatching {
+ URI(this).scheme.takeIf { scheme ->
+ "http".compareTo(scheme, true) == 0 || "https".compareTo(scheme, true) == 0
+ }
}
- }.getOrNull() != null
+ .getOrNull() != null
diff --git a/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt b/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt
index 31a7006c..924e6499 100644
--- a/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt
+++ b/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt
@@ -29,7 +29,7 @@ internal class NoContextPreviewUi(private val type: Int) : ContentPreviewUi() {
resources: Resources?,
layoutInflater: LayoutInflater?,
parent: ViewGroup?,
- headlineViewParent: View?,
+ 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 38918d79..96bb8258 100644
--- a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt
+++ b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt
@@ -18,7 +18,6 @@ package com.android.intentresolver.contentpreview
import android.content.ContentInterface
import android.content.Intent
-import android.database.Cursor
import android.media.MediaMetadata
import android.net.Uri
import android.provider.DocumentsContract
@@ -31,6 +30,7 @@ import androidx.annotation.OpenForTesting
import androidx.annotation.VisibleForTesting
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_PAYLOAD_SELECTION
import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT
import com.android.intentresolver.measurements.runTracing
import com.android.intentresolver.util.ownedByCurrentUser
@@ -74,7 +74,11 @@ open class PreviewDataProvider
constructor(
private val scope: CoroutineScope,
private val targetIntent: Intent,
+ private val additionalContentUri: Uri?,
private val contentResolver: ContentInterface,
+ // TODO: replace with the ChooserServiceFlags ref when PreviewViewModel dependencies are sorted
+ // out
+ private val isPayloadTogglingEnabled: Boolean,
private val typeClassifier: MimeTypeClassifier = DefaultMimeTypeClassifier,
) {
@@ -100,6 +104,9 @@ constructor(
open val uriCount: Int
get() = records.size
+ val uris: List<Uri>
+ get() = records.map { it.uri }
+
/**
* Returns a [Flow] of [FileInfo], for each shared URI in order, with [FileInfo.mimeType] and
* [FileInfo.previewUri] set (a data projection tailored for the image preview UI).
@@ -122,6 +129,9 @@ constructor(
* IMAGE, FILE, TEXT. */
if (!targetIntent.isSend || records.isEmpty()) {
CONTENT_PREVIEW_TEXT
+ } else if (isPayloadTogglingEnabled && shouldShowPayloadSelection()) {
+ // TODO: replace with the proper flags injection
+ CONTENT_PREVIEW_PAYLOAD_SELECTION
} else {
try {
runBlocking(scope.coroutineContext) {
@@ -140,6 +150,22 @@ constructor(
}
}
+ private fun shouldShowPayloadSelection(): Boolean {
+ val extraContentUri = additionalContentUri ?: return false
+ return runCatching {
+ val authority = extraContentUri.authority
+ records.firstOrNull { authority == it.uri.authority } == null
+ }
+ .onFailure {
+ Log.w(
+ ContentPreviewUi.TAG,
+ "Failed to check URI authorities; no payload toggling",
+ it
+ )
+ }
+ .getOrDefault(false)
+ }
+
/**
* The first shared URI's metadata. This call wait's for the data to be loaded and falls back to
* a crude value if the data is not loaded within a time limit.
@@ -250,8 +276,7 @@ constructor(
val isImageType: Boolean
get() = typeClassifier.isImageType(mimeType)
val supportsImageType: Boolean by lazy {
- contentResolver.getStreamTypesSafe(uri)?.firstOrNull(typeClassifier::isImageType) !=
- null
+ contentResolver.getStreamTypesSafe(uri).firstOrNull(typeClassifier::isImageType) != null
}
val supportsThumbnail: Boolean
get() = query.supportsThumbnail
@@ -263,7 +288,8 @@ constructor(
private val query by lazy { readQueryResult() }
private fun readQueryResult(): QueryResult =
- contentResolver.querySafe(uri)?.use { cursor ->
+ // TODO: rewrite using methods from UiMetadataHelpers.kt
+ contentResolver.querySafe(uri, METADATA_COLUMNS)?.use { cursor ->
if (!cursor.moveToFirst()) return@use null
var flagColIdx = -1
@@ -344,51 +370,3 @@ private fun getFileName(uri: Uri): String {
fileName.substring(index + 1)
}
}
-
-private fun ContentInterface.getTypeSafe(uri: Uri): String? =
- runTracing("getType") {
- try {
- getType(uri)
- } catch (e: SecurityException) {
- logProviderPermissionWarning(uri, "mime type")
- null
- } catch (t: Throwable) {
- Log.e(ContentPreviewUi.TAG, "Failed to read metadata, uri: $uri", t)
- null
- }
- }
-
-private fun ContentInterface.getStreamTypesSafe(uri: Uri): Array<String>? =
- runTracing("getStreamTypes") {
- try {
- getStreamTypes(uri, "*/*")
- } catch (e: SecurityException) {
- logProviderPermissionWarning(uri, "stream types")
- null
- } catch (t: Throwable) {
- Log.e(ContentPreviewUi.TAG, "Failed to read stream types, uri: $uri", t)
- null
- }
- }
-
-private fun ContentInterface.querySafe(uri: Uri): Cursor? =
- runTracing("query") {
- try {
- query(uri, METADATA_COLUMNS, null, null)
- } catch (e: SecurityException) {
- logProviderPermissionWarning(uri, "metadata")
- null
- } catch (t: Throwable) {
- Log.e(ContentPreviewUi.TAG, "Failed to read metadata, uri: $uri", t)
- null
- }
- }
-
-private fun logProviderPermissionWarning(uri: Uri, dataName: String) {
- // The ContentResolver already logs the exception. Log something more informative.
- Log.w(
- ContentPreviewUi.TAG,
- "Could not read $uri $dataName. If a preview is desired, call Intent#setClipData() to" +
- " ensure that the sharesheet is given permission."
- )
-}
diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
index 6350756e..6a729945 100644
--- a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
+++ b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
@@ -17,58 +17,64 @@
package com.android.intentresolver.contentpreview
import android.app.Application
+import android.content.ContentResolver
import android.content.Intent
+import android.net.Uri
import androidx.annotation.MainThread
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
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 */
-@HiltViewModel
-class PreviewViewModel
-@Inject
-constructor(
- private val application: Application,
+/** A view model for the preview logic */
+class PreviewViewModel(
+ private val contentResolver: ContentResolver,
+ // TODO: inject ImageLoader instead
+ private val thumbnailSize: Int,
@Background private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : BasePreviewViewModel() {
- private var previewDataProvider: PreviewDataProvider? = null
- private var imageLoader: ImagePreviewImageLoader? = null
+ private var targetIntent: Intent? = null
+ private var additionalContentUri: Uri? = null
+ private var isPayloadTogglingEnabled = false
- @MainThread
- override fun createOrReuseProvider(
- targetIntent: Intent
- ): PreviewDataProvider =
- previewDataProvider
- ?: PreviewDataProvider(
- viewModelScope + dispatcher,
- targetIntent,
- application.contentResolver
- )
- .also { previewDataProvider = it }
+ override val previewDataProvider by lazy {
+ val targetIntent = requireNotNull(this.targetIntent) { "Not initialized" }
+ PreviewDataProvider(
+ viewModelScope + dispatcher,
+ targetIntent,
+ additionalContentUri,
+ contentResolver,
+ isPayloadTogglingEnabled,
+ )
+ }
+ override val imageLoader by lazy {
+ ImagePreviewImageLoader(
+ viewModelScope + dispatcher,
+ thumbnailSize,
+ contentResolver,
+ cacheSize = 16
+ )
+ }
+
+ // TODO: make the view model injectable and inject these dependencies instead
@MainThread
- override fun createOrReuseImageLoader(): ImageLoader =
- imageLoader
- ?: ImagePreviewImageLoader(
- viewModelScope + dispatcher,
- thumbnailSize =
- application.resources.getDimensionPixelSize(
- R.dimen.chooser_preview_image_max_dimen
- ),
- application.contentResolver,
- cacheSize = 16
- )
- .also { imageLoader = it }
+ override fun init(
+ targetIntent: Intent,
+ additionalContentUri: Uri?,
+ isPayloadTogglingEnabled: Boolean,
+ ) {
+ if (this.targetIntent != null) return
+ this.targetIntent = targetIntent
+ this.additionalContentUri = additionalContentUri
+ this.isPayloadTogglingEnabled = isPayloadTogglingEnabled
+ }
companion object {
val Factory: ViewModelProvider.Factory =
@@ -77,7 +83,16 @@ constructor(
override fun <T : ViewModel> create(
modelClass: Class<T>,
extras: CreationExtras
- ): T = PreviewViewModel(checkNotNull(extras[APPLICATION_KEY])) as T
+ ): T {
+ val application: Application = checkNotNull(extras[APPLICATION_KEY])
+ return PreviewViewModel(
+ application.contentResolver,
+ application.resources.getDimensionPixelSize(
+ R.dimen.chooser_preview_image_max_dimen
+ )
+ )
+ as T
+ }
}
}
}
diff --git a/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt b/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt
new file mode 100644
index 00000000..57a51239
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.contentpreview
+
+import android.content.res.Resources
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.annotation.VisibleForTesting
+import androidx.compose.foundation.isSystemInDarkTheme
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.dynamicDarkColorScheme
+import androidx.compose.material3.dynamicLightColorScheme
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.ui.platform.ComposeView
+import androidx.compose.ui.platform.LocalContext
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.android.intentresolver.R
+import com.android.intentresolver.contentpreview.payloadtoggle.ui.composable.Shareousel
+import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselViewModel
+import com.android.intentresolver.ui.viewmodel.ChooserViewModel
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+@VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE)
+class ShareouselContentPreviewUi : ContentPreviewUi() {
+
+ override fun getType(): Int = ContentPreviewType.CONTENT_PREVIEW_IMAGE
+
+ override fun display(
+ resources: Resources,
+ layoutInflater: LayoutInflater,
+ parent: ViewGroup,
+ headlineViewParent: View,
+ ): ViewGroup = displayInternal(parent, headlineViewParent)
+
+ private fun displayInternal(parent: ViewGroup, headlineViewParent: View): ViewGroup {
+ inflateHeadline(headlineViewParent)
+ return ComposeView(parent.context).apply {
+ setContent {
+ val vm: ChooserViewModel = viewModel()
+ val viewModel: ShareouselViewModel = vm.shareouselViewModel
+
+ LaunchedEffect(viewModel) { bindHeader(viewModel, headlineViewParent) }
+
+ MaterialTheme(
+ colorScheme =
+ if (isSystemInDarkTheme()) {
+ dynamicDarkColorScheme(LocalContext.current)
+ } else {
+ dynamicLightColorScheme(LocalContext.current)
+ },
+ ) {
+ Shareousel(viewModel)
+ }
+ }
+ }
+ }
+
+ private suspend fun bindHeader(viewModel: ShareouselViewModel, headlineViewParent: View) {
+ coroutineScope {
+ launch { bindHeadline(viewModel, headlineViewParent) }
+ launch { bindMetadataText(viewModel, headlineViewParent) }
+ }
+ }
+
+ private suspend fun bindHeadline(viewModel: ShareouselViewModel, headlineViewParent: View) {
+ viewModel.headline.collect { headline ->
+ headlineViewParent.findViewById<TextView>(R.id.headline)?.apply {
+ if (headline.isNotBlank()) {
+ text = headline
+ visibility = View.VISIBLE
+ } else {
+ visibility = View.GONE
+ }
+ }
+ }
+ }
+
+ private suspend fun bindMetadataText(viewModel: ShareouselViewModel, headlineViewParent: View) {
+ viewModel.metadataText.collect { metadata ->
+ headlineViewParent.findViewById<TextView>(R.id.metadata)?.apply {
+ if (metadata?.isNotBlank() == true) {
+ text = metadata
+ visibility = View.VISIBLE
+ } else {
+ visibility = View.GONE
+ }
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java
index b0dc3c58..ae7ddcd9 100644
--- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java
@@ -30,6 +30,7 @@ import android.widget.TextView;
import androidx.annotation.Nullable;
+import com.android.intentresolver.ContentTypeHint;
import com.android.intentresolver.R;
import com.android.intentresolver.widget.ActionRow;
@@ -42,26 +43,33 @@ class TextContentPreviewUi extends ContentPreviewUi {
@Nullable
private final CharSequence mPreviewTitle;
@Nullable
+ private final CharSequence mMetadata;
+ @Nullable
private final Uri mPreviewThumbnail;
private final ImageLoader mImageLoader;
private final ChooserContentPreviewUi.ActionFactory mActionFactory;
private final HeadlineGenerator mHeadlineGenerator;
+ private final ContentTypeHint mContentTypeHint;
TextContentPreviewUi(
CoroutineScope scope,
@Nullable CharSequence sharingText,
@Nullable CharSequence previewTitle,
+ @Nullable CharSequence metadata,
@Nullable Uri previewThumbnail,
ChooserContentPreviewUi.ActionFactory actionFactory,
ImageLoader imageLoader,
- HeadlineGenerator headlineGenerator) {
+ HeadlineGenerator headlineGenerator,
+ ContentTypeHint contentTypeHint) {
mScope = scope;
mSharingText = sharingText;
mPreviewTitle = previewTitle;
+ mMetadata = metadata;
mPreviewThumbnail = previewThumbnail;
mImageLoader = imageLoader;
mActionFactory = actionFactory;
mHeadlineGenerator = headlineGenerator;
+ mContentTypeHint = contentTypeHint;
}
@Override
@@ -74,22 +82,16 @@ class TextContentPreviewUi extends ContentPreviewUi {
Resources resources,
LayoutInflater layoutInflater,
ViewGroup parent,
- @Nullable View headlineViewParent) {
- ViewGroup layout = displayInternal(layoutInflater, parent, headlineViewParent);
- displayModifyShareAction(
- headlineViewParent == null ? layout : headlineViewParent, mActionFactory);
- return layout;
+ View headlineViewParent) {
+ return displayInternal(layoutInflater, parent, headlineViewParent);
}
private ViewGroup displayInternal(
LayoutInflater layoutInflater,
ViewGroup parent,
- @Nullable View headlineViewParent) {
+ 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 =
@@ -139,7 +141,11 @@ class TextContentPreviewUi extends ContentPreviewUi {
copyButton.setVisibility(View.GONE);
}
- displayHeadline(headlineViewParent, mHeadlineGenerator.getTextHeadline(mSharingText));
+ String headlineText = (mContentTypeHint == ContentTypeHint.ALBUM)
+ ? mHeadlineGenerator.getAlbumHeadline()
+ : mHeadlineGenerator.getTextHeadline(mSharingText);
+ displayHeadline(headlineViewParent, headlineText);
+ displayMetadata(headlineViewParent, mMetadata);
return contentPreviewLayout;
}
diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
index 8ddd5273..88311016 100644
--- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
@@ -31,12 +31,12 @@ import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback;
import com.android.intentresolver.widget.ScrollableImagePreviewView;
-import java.util.List;
-import java.util.Objects;
-
import kotlinx.coroutines.CoroutineScope;
import kotlinx.coroutines.flow.Flow;
+import java.util.List;
+import java.util.Objects;
+
class UnifiedContentPreviewUi extends ContentPreviewUi {
private final boolean mShowEditAction;
@Nullable
@@ -46,13 +46,14 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
private final MimeTypeClassifier mTypeClassifier;
private final TransitionElementStatusCallback mTransitionElementStatusCallback;
private final HeadlineGenerator mHeadlineGenerator;
+ @Nullable
+ private final CharSequence mMetadata;
private final Flow<FileInfo> mFileInfoFlow;
private final int mItemCount;
@Nullable
private List<FileInfo> mFiles;
@Nullable
private ViewGroup mContentPreviewView;
- @Nullable
private View mHeadlineView;
UnifiedContentPreviewUi(
@@ -65,7 +66,8 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
TransitionElementStatusCallback transitionElementStatusCallback,
Flow<FileInfo> fileInfoFlow,
int itemCount,
- HeadlineGenerator headlineGenerator) {
+ HeadlineGenerator headlineGenerator,
+ @Nullable CharSequence metadata) {
mShowEditAction = isSingleImage;
mIntentMimeType = intentMimeType;
mActionFactory = actionFactory;
@@ -75,6 +77,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
mFileInfoFlow = fileInfoFlow;
mItemCount = itemCount;
mHeadlineGenerator = headlineGenerator;
+ mMetadata = metadata;
JavaFlowHelper.collectToList(scope, fileInfoFlow, this::setFiles);
}
@@ -89,11 +92,8 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
Resources resources,
LayoutInflater layoutInflater,
ViewGroup parent,
- @Nullable View headlineViewParent) {
- ViewGroup layout = displayInternal(layoutInflater, parent, headlineViewParent);
- displayModifyShareAction(
- headlineViewParent == null ? layout : headlineViewParent, mActionFactory);
- return layout;
+ View headlineViewParent) {
+ return displayInternal(layoutInflater, parent, headlineViewParent);
}
private void setFiles(List<FileInfo> files) {
@@ -108,10 +108,10 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
}
private ViewGroup displayInternal(
- LayoutInflater layoutInflater, ViewGroup parent, @Nullable View headlineViewParent) {
+ LayoutInflater layoutInflater, ViewGroup parent, View headlineViewParent) {
mContentPreviewView = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_image, parent, false);
- mHeadlineView = headlineViewParent == null ? mContentPreviewView : headlineViewParent;
+ mHeadlineView = headlineViewParent;
inflateHeadline(mHeadlineView);
final ActionRow actionRow =
@@ -181,5 +181,6 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
} else {
displayHeadline(layout, mHeadlineGenerator.getFilesHeadline(count));
}
+ displayMetadata(layout, mMetadata);
}
}
diff --git a/java/src/com/android/intentresolver/contentpreview/UriMetadataHelpers.kt b/java/src/com/android/intentresolver/contentpreview/UriMetadataHelpers.kt
new file mode 100644
index 00000000..41638b1f
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/UriMetadataHelpers.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.content.ContentInterface
+import android.database.Cursor
+import android.media.MediaMetadata
+import android.net.Uri
+import android.provider.DocumentsContract
+import android.provider.DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL
+import android.provider.Downloads
+import android.provider.OpenableColumns
+import android.text.TextUtils
+import android.util.Log
+import com.android.intentresolver.measurements.runTracing
+
+internal fun ContentInterface.getTypeSafe(uri: Uri): String? =
+ runTracing("getType") {
+ try {
+ getType(uri)
+ } catch (e: SecurityException) {
+ logProviderPermissionWarning(uri, "mime type")
+ null
+ } catch (t: Throwable) {
+ Log.e(ContentPreviewUi.TAG, "Failed to read metadata, uri: $uri", t)
+ null
+ }
+ }
+
+internal fun ContentInterface.getStreamTypesSafe(uri: Uri): Array<String?> =
+ runTracing("getStreamTypes") {
+ try {
+ getStreamTypes(uri, "*/*") ?: emptyArray()
+ } catch (e: SecurityException) {
+ logProviderPermissionWarning(uri, "stream types")
+ emptyArray<String?>()
+ } catch (t: Throwable) {
+ Log.e(ContentPreviewUi.TAG, "Failed to read stream types, uri: $uri", t)
+ emptyArray<String?>()
+ }
+ }
+
+internal fun ContentInterface.querySafe(uri: Uri, columns: Array<String>): Cursor? =
+ runTracing("query") {
+ try {
+ query(uri, columns, null, null)
+ } catch (e: SecurityException) {
+ logProviderPermissionWarning(uri, "metadata")
+ null
+ } catch (t: Throwable) {
+ Log.e(ContentPreviewUi.TAG, "Failed to read metadata, uri: $uri", t)
+ null
+ }
+ }
+
+internal fun Cursor.readSupportsThumbnail(): Boolean =
+ runCatching {
+ val flagColIdx = columnNames.indexOf(DocumentsContract.Document.COLUMN_FLAGS)
+ flagColIdx >= 0 && ((getInt(flagColIdx) and FLAG_SUPPORTS_THUMBNAIL) != 0)
+ }
+ .getOrDefault(false)
+
+internal fun Cursor.readPreviewUri(): Uri? =
+ runCatching {
+ columnNames
+ .indexOf(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI)
+ .takeIf { it >= 0 }
+ ?.let { getString(it)?.let(Uri::parse) }
+ }
+ .getOrNull()
+
+internal fun Cursor.readTitle(): String =
+ runCatching {
+ var nameColIndex = -1
+ var titleColIndex = -1
+ // TODO: double-check why Cursor#getColumnInded didn't work
+ columnNames.forEachIndexed { i, columnName ->
+ when (columnName) {
+ OpenableColumns.DISPLAY_NAME -> nameColIndex = i
+ Downloads.Impl.COLUMN_TITLE -> titleColIndex = i
+ }
+ }
+
+ var title = ""
+ if (nameColIndex >= 0) {
+ title = getString(nameColIndex) ?: ""
+ }
+ if (TextUtils.isEmpty(title) && titleColIndex >= 0) {
+ title = getString(titleColIndex) ?: ""
+ }
+ title
+ }
+ .getOrDefault("")
+
+private fun logProviderPermissionWarning(uri: Uri, dataName: String) {
+ // The ContentResolver already logs the exception. Log something more informative.
+ Log.w(
+ ContentPreviewUi.TAG,
+ "Could not read $uri $dataName. If a preview is desired, call Intent#setClipData() to" +
+ " ensure that the sharesheet is given permission."
+ )
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/UriMetadataReader.kt b/java/src/com/android/intentresolver/contentpreview/UriMetadataReader.kt
new file mode 100644
index 00000000..b5361889
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/UriMetadataReader.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.content.ContentInterface
+import android.media.MediaMetadata
+import android.net.Uri
+import android.provider.DocumentsContract
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Inject
+
+fun interface UriMetadataReader {
+ fun getMetadata(uri: Uri): FileInfo
+}
+
+class UriMetadataReaderImpl
+@Inject
+constructor(
+ private val contentResolver: ContentInterface,
+ private val typeClassifier: MimeTypeClassifier,
+) : UriMetadataReader {
+ override fun getMetadata(uri: Uri): FileInfo {
+ val builder = FileInfo.Builder(uri)
+ val mimeType = contentResolver.getTypeSafe(uri)
+ builder.withMimeType(mimeType)
+ if (
+ typeClassifier.isImageType(mimeType) ||
+ contentResolver.supportsImageType(uri) ||
+ contentResolver.supportsThumbnail(uri)
+ ) {
+ builder.withPreviewUri(uri)
+ return builder.build()
+ }
+ val previewUri = contentResolver.readPreviewUri(uri)
+ if (previewUri != null) {
+ builder.withPreviewUri(previewUri)
+ }
+ return builder.build()
+ }
+
+ private fun ContentInterface.supportsImageType(uri: Uri): Boolean =
+ getStreamTypesSafe(uri).firstOrNull { typeClassifier.isImageType(it) } != null
+
+ private fun ContentInterface.supportsThumbnail(uri: Uri): Boolean =
+ querySafe(uri, arrayOf(DocumentsContract.Document.COLUMN_FLAGS))?.use { cursor ->
+ cursor.moveToFirst() && cursor.readSupportsThumbnail()
+ }
+ ?: false
+
+ private fun ContentInterface.readPreviewUri(uri: Uri): Uri? =
+ querySafe(uri, arrayOf(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI))?.use { cursor ->
+ if (cursor.moveToFirst()) {
+ cursor.readPreviewUri()
+ } else {
+ null
+ }
+ }
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+interface UriMetadataReaderModule {
+
+ @Binds fun bind(impl: UriMetadataReaderImpl): UriMetadataReader
+
+ companion object {
+ @Provides fun classifier(): MimeTypeClassifier = DefaultMimeTypeClassifier
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/CustomActionModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/CustomActionModel.kt
new file mode 100644
index 00000000..b7945005
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/model/CustomActionModel.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.data.model
+
+import android.graphics.drawable.Icon
+
+/** Data model for a custom action the user can take. */
+data class CustomActionModel(
+ /** Label presented to the user identifying this action. */
+ val label: CharSequence,
+ /** Icon presented to the user for this action. */
+ val icon: Icon,
+ /** When invoked, performs this action. */
+ val performAction: () -> Unit,
+)
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/ActivityResultRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/ActivityResultRepository.kt
new file mode 100644
index 00000000..c3bb88c8
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/ActivityResultRepository.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.data.repository
+
+import dagger.hilt.android.scopes.ActivityRetainedScoped
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/** Tracks the result of the current activity. */
+@ActivityRetainedScoped
+class ActivityResultRepository @Inject constructor() {
+ /** The result of the current activity, or `null` if the activity is still active. */
+ val activityResult = MutableStateFlow<Int?>(null)
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/CursorPreviewsRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/CursorPreviewsRepository.kt
new file mode 100644
index 00000000..b104d4bf
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/CursorPreviewsRepository.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.data.repository
+
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
+import dagger.hilt.android.scopes.ActivityRetainedScoped
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/**
+ * Stores previews for Shareousel UI that have been cached locally from a remote
+ * [android.database.Cursor].
+ */
+@ActivityRetainedScoped
+class CursorPreviewsRepository @Inject constructor() {
+ /** Previews available for display within Shareousel. */
+ val previewsModel = MutableStateFlow<PreviewsModel?>(null)
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PendingSelectionCallbackRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PendingSelectionCallbackRepository.kt
new file mode 100644
index 00000000..1745cd9c
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PendingSelectionCallbackRepository.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.data.repository
+
+import android.content.Intent
+import dagger.hilt.android.scopes.ActivityRetainedScoped
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/** Tracks active async communication with sharing app to notify of target intent update. */
+@ActivityRetainedScoped
+class PendingSelectionCallbackRepository @Inject constructor() {
+ /**
+ * The target [Intent] that is has an active update request with the sharing app, or `null` if
+ * there is no active request.
+ */
+ val pendingTargetIntent: MutableStateFlow<Intent?> = MutableStateFlow(null)
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt
new file mode 100644
index 00000000..9aecc981
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PreviewSelectionsRepository.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.data.repository
+
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import dagger.hilt.android.scopes.ViewModelScoped
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/** Stores set of selected previews. */
+@ViewModelScoped
+class PreviewSelectionsRepository @Inject constructor() {
+ val selections = MutableStateFlow(emptySet<PreviewModel>())
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/CursorResolver.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/CursorResolver.kt
new file mode 100644
index 00000000..3aa0d567
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/CursorResolver.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor
+
+import com.android.intentresolver.util.cursor.CursorView
+
+/** Asynchronously retrieves a [CursorView]. */
+fun interface CursorResolver<out T> {
+ suspend fun getCursor(): CursorView<T>?
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/PayloadToggleCursorResolver.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/PayloadToggleCursorResolver.kt
new file mode 100644
index 00000000..3cf2af13
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/PayloadToggleCursorResolver.kt
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor
+
+import android.content.ContentResolver
+import android.content.Intent
+import android.net.Uri
+import android.service.chooser.AdditionalContentContract.Columns.URI
+import androidx.core.os.bundleOf
+import com.android.intentresolver.inject.AdditionalContent
+import com.android.intentresolver.inject.ChooserIntent
+import com.android.intentresolver.util.cursor.CursorView
+import com.android.intentresolver.util.cursor.viewBy
+import com.android.intentresolver.util.withCancellationSignal
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import javax.inject.Inject
+import javax.inject.Qualifier
+
+/** [CursorResolver] for the [CursorView] underpinning Shareousel. */
+class PayloadToggleCursorResolver
+@Inject
+constructor(
+ private val contentResolver: ContentResolver,
+ @AdditionalContent private val cursorUri: Uri,
+ @ChooserIntent private val chooserIntent: Intent,
+) : CursorResolver<Uri?> {
+ override suspend fun getCursor(): CursorView<Uri?>? = withCancellationSignal { signal ->
+ runCatching {
+ contentResolver.query(
+ cursorUri,
+ arrayOf(URI),
+ bundleOf(Intent.EXTRA_INTENT to chooserIntent),
+ signal,
+ )
+ }
+ .getOrNull()
+ ?.viewBy {
+ getString(0)?.let(Uri::parse)?.takeIf { it.authority != cursorUri.authority }
+ }
+ }
+
+ @Module
+ @InstallIn(ViewModelComponent::class)
+ interface Binding {
+ @Binds
+ @PayloadToggle
+ fun bind(cursorResolver: PayloadToggleCursorResolver): CursorResolver<Uri?>
+ }
+}
+
+/** [CursorResolver] for the [CursorView] underpinning Shareousel. */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class PayloadToggle
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/CustomActionPendingIntentSender.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/CustomActionPendingIntentSender.kt
new file mode 100644
index 00000000..faad5bbf
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/CustomActionPendingIntentSender.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.intent
+
+import android.app.ActivityOptions
+import android.app.PendingIntent
+import android.content.Context
+import com.android.intentresolver.R
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Inject
+import javax.inject.Qualifier
+
+/** [PendingIntentSender] for Shareousel custom actions. */
+class CustomActionPendingIntentSender
+@Inject
+constructor(
+ @ApplicationContext private val context: Context,
+) : PendingIntentSender {
+ override fun send(pendingIntent: PendingIntent) {
+ pendingIntent.send(
+ /* context = */ null,
+ /* code = */ 0,
+ /* intent = */ null,
+ /* onFinished = */ null,
+ /* handler = */ null,
+ /* requiredPermission = */ null,
+ /* options = */ ActivityOptions.makeCustomAnimation(
+ context,
+ R.anim.slide_in_right,
+ R.anim.slide_out_left,
+ )
+ .toBundle()
+ )
+ }
+
+ @Module
+ @InstallIn(SingletonComponent::class)
+ interface Binding {
+ @Binds
+ @CustomAction
+ fun bindSender(sender: CustomActionPendingIntentSender): PendingIntentSender
+ }
+}
+
+/** [PendingIntentSender] for Shareousel custom actions. */
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class CustomAction
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/InitialCustomActionsModule.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/InitialCustomActionsModule.kt
new file mode 100644
index 00000000..d75884d5
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/InitialCustomActionsModule.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.intent
+
+import android.app.PendingIntent
+import android.service.chooser.ChooserAction
+import android.util.Log
+import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+@Module
+@InstallIn(ViewModelComponent::class)
+object InitialCustomActionsModule {
+ @Provides
+ fun initialCustomActionModels(
+ chooserActions: List<ChooserAction>,
+ @CustomAction pendingIntentSender: PendingIntentSender,
+ ): List<CustomActionModel> = chooserActions.map { it.toCustomActionModel(pendingIntentSender) }
+}
+
+/**
+ * Returns a [CustomActionModel] that sends this [ChooserAction]'s
+ * [PendingIntent][ChooserAction.getAction].
+ */
+fun ChooserAction.toCustomActionModel(pendingIntentSender: PendingIntentSender) =
+ CustomActionModel(
+ label = label,
+ icon = icon,
+ performAction = {
+ try {
+ pendingIntentSender.send(action)
+ } catch (_: PendingIntent.CanceledException) {
+ Log.d(TAG, "Custom action, $label, has been cancelled")
+ }
+ }
+ )
+
+private const val TAG = "CustomShareActions"
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/PendingIntentSender.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/PendingIntentSender.kt
new file mode 100644
index 00000000..23ba31ba
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/PendingIntentSender.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.intent
+
+import android.app.PendingIntent
+
+/** Sends [PendingIntent]s. */
+fun interface PendingIntentSender {
+ fun send(pendingIntent: PendingIntent)
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifier.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifier.kt
new file mode 100644
index 00000000..4a2a6932
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifier.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.intent
+
+import android.content.ClipData
+import android.content.ClipDescription.compareMimeTypes
+import android.content.Intent
+import android.content.Intent.ACTION_SEND
+import android.content.Intent.ACTION_SEND_MULTIPLE
+import android.content.Intent.EXTRA_STREAM
+import android.net.Uri
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import com.android.intentresolver.inject.TargetIntent
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+
+/** Modifies target intent based on current payload selection. */
+fun interface TargetIntentModifier<Item> {
+ fun intentFromSelection(selection: Collection<Item>): Intent
+}
+
+class TargetIntentModifierImpl<Item>(
+ private val originalTargetIntent: Intent,
+ private val getUri: Item.() -> Uri,
+ private val getMimeType: Item.() -> String?,
+) : TargetIntentModifier<Item> {
+ override fun intentFromSelection(selection: Collection<Item>): Intent {
+ val uris = selection.mapTo(ArrayList()) { it.getUri() }
+ val targetMimeType =
+ selection.fold(null) { target: String?, item: Item ->
+ updateMimeType(item.getMimeType(), target)
+ }
+ return Intent(originalTargetIntent).apply {
+ if (selection.size == 1) {
+ action = ACTION_SEND
+ putExtra(EXTRA_STREAM, selection.first().getUri())
+ } else {
+ action = ACTION_SEND_MULTIPLE
+ putParcelableArrayListExtra(EXTRA_STREAM, uris)
+ }
+ type = targetMimeType
+ if (uris.isNotEmpty()) {
+ clipData =
+ ClipData("", arrayOf(targetMimeType), ClipData.Item(uris[0])).also {
+ for (i in 1 until uris.size) {
+ it.addItem(ClipData.Item(uris[i]))
+ }
+ }
+ }
+ }
+ }
+
+ private fun updateMimeType(itemMimeType: String?, unitedMimeType: String?): String {
+ itemMimeType ?: return "*/*"
+ unitedMimeType ?: return itemMimeType
+ if (compareMimeTypes(itemMimeType, unitedMimeType)) return unitedMimeType
+ val slashIdx = unitedMimeType.indexOf('/')
+ if (slashIdx >= 0 && unitedMimeType.regionMatches(0, itemMimeType, 0, slashIdx + 1)) {
+ return buildString {
+ append(unitedMimeType.substring(0, slashIdx + 1))
+ append('*')
+ }
+ }
+ return "*/*"
+ }
+}
+
+@Module
+@InstallIn(ViewModelComponent::class)
+object TargetIntentModifierModule {
+ @Provides
+ fun targetIntentModifier(
+ @TargetIntent targetIntent: Intent,
+ ): TargetIntentModifier<PreviewModel> =
+ TargetIntentModifierImpl(targetIntent, { uri }, { mimeType })
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ChooserRequestInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ChooserRequestInteractor.kt
new file mode 100644
index 00000000..953e91b3
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ChooserRequestInteractor.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import android.content.Intent
+import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel
+import com.android.intentresolver.data.repository.ChooserRequestRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asSharedFlow
+import kotlinx.coroutines.flow.map
+
+/** Stores the target intent of the share sheet, and custom actions derived from the intent. */
+class ChooserRequestInteractor
+@Inject
+constructor(
+ private val repository: ChooserRequestRepository,
+) {
+ val targetIntent: Flow<Intent>
+ get() = repository.chooserRequest.map { it.targetIntent }
+
+ val customActions: Flow<List<CustomActionModel>>
+ get() = repository.customActions.asSharedFlow()
+
+ val metadataText: Flow<CharSequence?>
+ get() = repository.chooserRequest.map { it.metadataText }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractor.kt
new file mode 100644
index 00000000..f642f420
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractor.kt
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import android.net.Uri
+import android.service.chooser.AdditionalContentContract.CursorExtraKeys.POSITION
+import com.android.intentresolver.contentpreview.UriMetadataReader
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.LoadDirection
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.LoadedWindow
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.expandWindowLeft
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.expandWindowRight
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.numLoadedPages
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.shiftWindowLeft
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.shiftWindowRight
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import com.android.intentresolver.inject.FocusedItemIndex
+import com.android.intentresolver.util.cursor.CursorView
+import com.android.intentresolver.util.cursor.PagedCursor
+import com.android.intentresolver.util.cursor.get
+import com.android.intentresolver.util.cursor.paged
+import com.android.intentresolver.util.mapParallel
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import java.util.concurrent.ConcurrentHashMap
+import javax.inject.Inject
+import javax.inject.Qualifier
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.mapLatest
+
+/** Queries data from a remote cursor, and caches it locally for presentation in Shareousel. */
+class CursorPreviewsInteractor
+@Inject
+constructor(
+ private val interactor: SetCursorPreviewsInteractor,
+ @FocusedItemIndex private val focusedItemIdx: Int,
+ private val uriMetadataReader: UriMetadataReader,
+ @PageSize private val pageSize: Int,
+ @MaxLoadedPages private val maxLoadedPages: Int,
+) {
+
+ init {
+ check(pageSize > 0) { "pageSize must be greater than zero" }
+ }
+
+ /** Start reading data from [uriCursor], and listen for requests to load more. */
+ suspend fun launch(uriCursor: CursorView<Uri?>, initialPreviews: Iterable<PreviewModel>) {
+ // Unclaimed values from the initial selection set. Entries will be removed as the cursor is
+ // read, and any still present are inserted at the start / end of the cursor when it is
+ // reached by the user.
+ val unclaimedRecords: MutableUnclaimedMap =
+ initialPreviews
+ .asSequence()
+ .mapIndexed { i, m -> Pair(m.uri, Pair(i, m)) }
+ .toMap(ConcurrentHashMap())
+ val pagedCursor: PagedCursor<Uri?> = uriCursor.paged(pageSize)
+ val startPosition = uriCursor.extras?.getInt(POSITION, 0) ?: 0
+ val state = readInitialState(pagedCursor, startPosition, unclaimedRecords)
+ processLoadRequests(state, pagedCursor, unclaimedRecords)
+ }
+
+ /** Loop forever, processing any loading requests from the UI and updating local cache. */
+ private suspend fun processLoadRequests(
+ initialState: CursorWindow,
+ pagedCursor: PagedCursor<Uri?>,
+ unclaimedRecords: MutableUnclaimedMap,
+ ) {
+ var state = initialState
+ while (true) {
+ // Design note: in order to prevent load requests from the UI when it was displaying a
+ // previously-published dataset being accidentally associated with a recently-published
+ // one, we generate a new Flow of load requests for each dataset and only listen to
+ // those.
+ val loadingState: Flow<LoadDirection?> =
+ interactor.setPreviews(
+ previewsByKey = state.merged.values.toSet(),
+ startIndex = 0, // TODO: actually track this as the window changes?
+ hasMoreLeft = state.hasMoreLeft,
+ hasMoreRight = state.hasMoreRight,
+ )
+ state = loadingState.handleOneLoadRequest(state, pagedCursor, unclaimedRecords)
+ }
+ }
+
+ /**
+ * Suspends until a single loading request has been handled, returning the new [CursorWindow]
+ * with the loaded data incorporated.
+ */
+ private suspend fun Flow<LoadDirection?>.handleOneLoadRequest(
+ state: CursorWindow,
+ pagedCursor: PagedCursor<Uri?>,
+ unclaimedRecords: MutableUnclaimedMap,
+ ): CursorWindow =
+ mapLatest { loadDirection ->
+ loadDirection?.let {
+ when (loadDirection) {
+ LoadDirection.Left -> state.loadMoreLeft(pagedCursor, unclaimedRecords)
+ LoadDirection.Right -> state.loadMoreRight(pagedCursor, unclaimedRecords)
+ }
+ }
+ }
+ .filterNotNull()
+ .first()
+
+ /**
+ * Returns the initial [CursorWindow], with a single page loaded that contains the given
+ * [startPosition].
+ */
+ private suspend fun readInitialState(
+ cursor: PagedCursor<Uri?>,
+ startPosition: Int,
+ unclaimedRecords: MutableUnclaimedMap,
+ ): CursorWindow {
+ val startPageIdx = startPosition / pageSize
+ val hasMoreLeft = startPageIdx > 0
+ val hasMoreRight = startPageIdx < cursor.count - 1
+ val page: PreviewMap = buildMap {
+ if (!hasMoreLeft) {
+ // First read the initial page; this might claim some unclaimed Uris
+ val page =
+ cursor.getPageUris(startPageIdx)?.toPage(mutableMapOf(), unclaimedRecords)
+ // Now that unclaimed Uris are up-to-date, add them first.
+ putAllUnclaimedLeft(unclaimedRecords)
+ // Then add the loaded page
+ page?.let(::putAll)
+ } else {
+ cursor.getPageUris(startPageIdx)?.toPage(this, unclaimedRecords)
+ }
+ // Finally, add the remainder of the unclaimed Uris.
+ if (!hasMoreRight) {
+ putAllUnclaimedRight(unclaimedRecords)
+ }
+ }
+ return CursorWindow(
+ firstLoadedPageNum = startPageIdx,
+ lastLoadedPageNum = startPageIdx,
+ pages = listOf(page.keys),
+ merged = page,
+ hasMoreLeft = hasMoreLeft,
+ hasMoreRight = hasMoreRight,
+ )
+ }
+
+ private suspend fun CursorWindow.loadMoreRight(
+ cursor: PagedCursor<Uri?>,
+ unclaimedRecords: MutableUnclaimedMap,
+ ): CursorWindow {
+ val pageNum = lastLoadedPageNum + 1
+ val hasMoreRight = pageNum < cursor.count - 1
+ val newPage: PreviewMap = buildMap {
+ readAndPutPage(this@loadMoreRight, cursor, pageNum, unclaimedRecords)
+ if (!hasMoreRight) {
+ putAllUnclaimedRight(unclaimedRecords)
+ }
+ }
+ return if (numLoadedPages < maxLoadedPages) {
+ expandWindowRight(newPage, hasMoreRight)
+ } else {
+ shiftWindowRight(newPage, hasMoreRight)
+ }
+ }
+
+ private suspend fun CursorWindow.loadMoreLeft(
+ cursor: PagedCursor<Uri?>,
+ unclaimedRecords: MutableUnclaimedMap,
+ ): CursorWindow {
+ val pageNum = firstLoadedPageNum - 1
+ val hasMoreLeft = pageNum > 0
+ val newPage: PreviewMap = buildMap {
+ if (!hasMoreLeft) {
+ // First read the page; this might claim some unclaimed Uris
+ val page = readPage(this@loadMoreLeft, cursor, pageNum, unclaimedRecords)
+ // Now that unclaimed URIs are up-to-date, add them first
+ putAllUnclaimedLeft(unclaimedRecords)
+ // Then add the loaded page
+ putAll(page)
+ } else {
+ readAndPutPage(this@loadMoreLeft, cursor, pageNum, unclaimedRecords)
+ }
+ }
+ return if (numLoadedPages < maxLoadedPages) {
+ expandWindowLeft(newPage, hasMoreLeft)
+ } else {
+ shiftWindowLeft(newPage, hasMoreLeft)
+ }
+ }
+
+ private suspend fun readPage(
+ state: CursorWindow,
+ pagedCursor: PagedCursor<Uri?>,
+ pageNum: Int,
+ unclaimedRecords: MutableUnclaimedMap,
+ ): PreviewMap =
+ mutableMapOf<Uri, PreviewModel>()
+ .readAndPutPage(state, pagedCursor, pageNum, unclaimedRecords)
+
+ private suspend fun <M : MutablePreviewMap> M.readAndPutPage(
+ state: CursorWindow,
+ pagedCursor: PagedCursor<Uri?>,
+ pageNum: Int,
+ unclaimedRecords: MutableUnclaimedMap,
+ ): M =
+ pagedCursor
+ .getPageUris(pageNum) // TODO: what do we do if the load fails?
+ ?.filter { it !in state.merged }
+ ?.toPage(this, unclaimedRecords)
+ ?: this
+
+ private suspend fun <M : MutablePreviewMap> Sequence<Uri>.toPage(
+ destination: M,
+ unclaimedRecords: MutableUnclaimedMap,
+ ): M =
+ // Restrict parallelism so as to not overload the metadata reader; anecdotally, too
+ // many parallel queries causes failures.
+ mapParallel(parallelism = 4) { uri -> createPreviewModel(uri, unclaimedRecords) }
+ .associateByTo(destination) { it.uri }
+
+ private fun createPreviewModel(uri: Uri, unclaimedRecords: MutableUnclaimedMap): PreviewModel =
+ unclaimedRecords.remove(uri)?.second
+ ?: PreviewModel(
+ uri = uri,
+ mimeType = uriMetadataReader.getMetadata(uri).mimeType,
+ )
+
+ private fun <M : MutablePreviewMap> M.putAllUnclaimedRight(unclaimed: UnclaimedMap): M =
+ putAllUnclaimedWhere(unclaimed) { it >= focusedItemIdx }
+
+ private fun <M : MutablePreviewMap> M.putAllUnclaimedLeft(unclaimed: UnclaimedMap): M =
+ putAllUnclaimedWhere(unclaimed) { it < focusedItemIdx }
+}
+
+private typealias CursorWindow = LoadedWindow<Uri, PreviewModel>
+
+/**
+ * Values from the initial selection set that have not yet appeared within the Cursor. These values
+ * are appended to the start/end of the cursor dataset, depending on their position relative to the
+ * initially focused value.
+ */
+private typealias UnclaimedMap = Map<Uri, Pair<Int, PreviewModel>>
+
+/** Mutable version of [UnclaimedMap]. */
+private typealias MutableUnclaimedMap = MutableMap<Uri, Pair<Int, PreviewModel>>
+
+private typealias MutablePreviewMap = MutableMap<Uri, PreviewModel>
+
+private typealias PreviewMap = Map<Uri, PreviewModel>
+
+private fun <M : MutablePreviewMap> M.putAllUnclaimedWhere(
+ unclaimedRecords: UnclaimedMap,
+ predicate: (Int) -> Boolean,
+): M =
+ unclaimedRecords
+ .asSequence()
+ .filter { predicate(it.value.first) }
+ .map { it.key to it.value.second }
+ .toMap(this)
+
+private fun PagedCursor<Uri?>.getPageUris(pageNum: Int): Sequence<Uri>? =
+ get(pageNum)?.filterNotNull()
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class PageSize
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class MaxLoadedPages
+
+@Module
+@InstallIn(SingletonComponent::class)
+object ShareouselConstants {
+ @Provides @PageSize fun pageSize(): Int = 16
+
+ @Provides @MaxLoadedPages fun maxLoadedPages(): Int = 3
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractor.kt
new file mode 100644
index 00000000..e973e844
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractor.kt
@@ -0,0 +1,66 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import android.app.Activity
+import android.content.ContentResolver
+import android.content.pm.PackageManager
+import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.ActivityResultRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ActionModel
+import com.android.intentresolver.icon.toComposeIcon
+import com.android.intentresolver.inject.Background
+import com.android.intentresolver.logging.EventLog
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+
+class CustomActionsInteractor
+@Inject
+constructor(
+ private val activityResultRepo: ActivityResultRepository,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ private val contentResolver: ContentResolver,
+ private val eventLog: EventLog,
+ private val packageManager: PackageManager,
+ private val chooserRequestInteractor: ChooserRequestInteractor,
+) {
+ /** List of [ActionModel] that can be presented in Shareousel. */
+ val customActions: Flow<List<ActionModel>>
+ get() =
+ chooserRequestInteractor.customActions
+ .map { actions ->
+ actions.map { action ->
+ ActionModel(
+ label = action.label,
+ icon = action.icon.toComposeIcon(packageManager, contentResolver),
+ performAction = { index -> performAction(action, index) },
+ )
+ }
+ }
+ .flowOn(bgDispatcher)
+ .conflate()
+
+ private fun performAction(action: CustomActionModel, index: Int) {
+ action.performAction()
+ eventLog.logCustomActionSelected(index)
+ activityResultRepo.activityResult.value = Activity.RESULT_OK
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt
new file mode 100644
index 00000000..9bc7ae63
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractor.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import android.net.Uri
+import com.android.intentresolver.contentpreview.UriMetadataReader
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.CursorResolver
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.PayloadToggle
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import com.android.intentresolver.inject.ContentUris
+import com.android.intentresolver.inject.FocusedItemIndex
+import com.android.intentresolver.util.mapParallel
+import javax.inject.Inject
+import kotlinx.coroutines.async
+import kotlinx.coroutines.coroutineScope
+
+/** Populates the data displayed in Shareousel. */
+class FetchPreviewsInteractor
+@Inject
+constructor(
+ private val setCursorPreviews: SetCursorPreviewsInteractor,
+ private val selectionRepository: PreviewSelectionsRepository,
+ private val cursorInteractor: CursorPreviewsInteractor,
+ @FocusedItemIndex private val focusedItemIdx: Int,
+ @ContentUris private val selectedItems: List<@JvmSuppressWildcards Uri>,
+ private val uriMetadataReader: UriMetadataReader,
+ @PayloadToggle private val cursorResolver: CursorResolver<@JvmSuppressWildcards Uri?>,
+) {
+ suspend fun activate() = coroutineScope {
+ val cursor = async { cursorResolver.getCursor() }
+ val initialPreviewMap: Set<PreviewModel> = getInitialPreviews()
+ selectionRepository.selections.value = initialPreviewMap
+ setCursorPreviews.setPreviews(
+ previewsByKey = initialPreviewMap,
+ startIndex = focusedItemIdx,
+ hasMoreLeft = false,
+ hasMoreRight = false,
+ )
+ cursorInteractor.launch(cursor.await() ?: return@coroutineScope, initialPreviewMap)
+ }
+
+ private suspend fun getInitialPreviews(): Set<PreviewModel> =
+ selectedItems
+ // Restrict parallelism so as to not overload the metadata reader; anecdotally, too
+ // many parallel queries causes failures.
+ .mapParallel(parallelism = 4) { uri ->
+ PreviewModel(uri = uri, mimeType = uriMetadataReader.getMetadata(uri).mimeType)
+ }
+ .toSet()
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ProcessTargetIntentUpdatesInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ProcessTargetIntentUpdatesInteractor.kt
new file mode 100644
index 00000000..c202eabf
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ProcessTargetIntentUpdatesInteractor.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PendingSelectionCallbackRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.update.SelectionChangeCallback
+import javax.inject.Inject
+import kotlinx.coroutines.flow.collectLatest
+
+/** Communicates with the sharing application to notify of changes to the target intent. */
+class ProcessTargetIntentUpdatesInteractor
+@Inject
+constructor(
+ private val selectionCallback: SelectionChangeCallback,
+ private val repository: PendingSelectionCallbackRepository,
+ private val chooserRequestInteractor: UpdateChooserRequestInteractor,
+) {
+ /** Listen for events and update state. */
+ suspend fun activate() {
+ repository.pendingTargetIntent.collectLatest { targetIntent ->
+ targetIntent ?: return@collectLatest
+ selectionCallback.onSelectionChanged(targetIntent)?.let { update ->
+ chooserRequestInteractor.applyUpdate(targetIntent, update)
+ }
+ repository.pendingTargetIntent.compareAndSet(targetIntent, null)
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt
new file mode 100644
index 00000000..55a995f5
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractor.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import android.net.Uri
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/** An individual preview in Shareousel. */
+class SelectablePreviewInteractor(
+ private val key: PreviewModel,
+ private val selectionInteractor: SelectionInteractor,
+) {
+ val uri: Uri = key.uri
+
+ /** Whether or not this preview is selected by the user. */
+ val isSelected: Flow<Boolean> = selectionInteractor.selections.map { key in it }
+
+ /** Sets whether this preview is selected by the user. */
+ fun setSelected(isSelected: Boolean) {
+ if (isSelected) {
+ selectionInteractor.select(key)
+ } else {
+ selectionInteractor.unselect(key)
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractor.kt
new file mode 100644
index 00000000..a578d0e2
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractor.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.CursorPreviewsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+
+class SelectablePreviewsInteractor
+@Inject
+constructor(
+ private val previewsRepo: CursorPreviewsRepository,
+ private val selectionInteractor: SelectionInteractor,
+) {
+ /** Keys of previews available for display in Shareousel. */
+ val previews: Flow<PreviewsModel?>
+ get() = previewsRepo.previewsModel
+
+ /**
+ * Returns a [SelectablePreviewInteractor] that can be used to interact with the individual
+ * preview associated with [key].
+ */
+ fun preview(key: PreviewModel) = SelectablePreviewInteractor(key, selectionInteractor)
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt
new file mode 100644
index 00000000..a570f36e
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractor.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PreviewSelectionsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.TargetIntentModifier
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.updateAndGet
+
+class SelectionInteractor
+@Inject
+constructor(
+ private val selectionsRepo: PreviewSelectionsRepository,
+ private val targetIntentModifier: TargetIntentModifier<PreviewModel>,
+ private val updateTargetIntentInteractor: UpdateTargetIntentInteractor,
+) {
+ /** Set of selected previews. */
+ val selections: StateFlow<Set<PreviewModel>>
+ get() = selectionsRepo.selections
+
+ /** Amount of selected previews. */
+ val amountSelected: Flow<Int> = selectionsRepo.selections.map { it.size }
+
+ fun select(model: PreviewModel) {
+ updateChooserRequest(selectionsRepo.selections.updateAndGet { it + model })
+ }
+
+ fun unselect(model: PreviewModel) {
+ updateChooserRequest(selectionsRepo.selections.updateAndGet { it - model })
+ }
+
+ private fun updateChooserRequest(selections: Set<PreviewModel>) {
+ val intent = targetIntentModifier.intentFromSelection(selections)
+ updateTargetIntentInteractor.updateTargetIntent(intent)
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SetCursorPreviewsInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SetCursorPreviewsInteractor.kt
new file mode 100644
index 00000000..21a599fa
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SetCursorPreviewsInteractor.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.CursorPreviewsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.LoadDirection
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+/** Updates [CursorPreviewsRepository] with new previews. */
+class SetCursorPreviewsInteractor
+@Inject
+constructor(private val previewsRepo: CursorPreviewsRepository) {
+ /** Stores new [previewsByKey], and returns a flow of load requests triggered by Shareousel. */
+ fun setPreviews(
+ previewsByKey: Set<PreviewModel>,
+ startIndex: Int,
+ hasMoreLeft: Boolean,
+ hasMoreRight: Boolean,
+ ): Flow<LoadDirection?> {
+ val loadingState = MutableStateFlow<LoadDirection?>(null)
+ previewsRepo.previewsModel.value =
+ PreviewsModel(
+ previewModels = previewsByKey,
+ startIdx = startIndex,
+ loadMoreLeft =
+ if (hasMoreLeft) {
+ ({ loadingState.value = LoadDirection.Left })
+ } else {
+ null
+ },
+ loadMoreRight =
+ if (hasMoreRight) {
+ ({ loadingState.value = LoadDirection.Right })
+ } else {
+ null
+ },
+ )
+ return loadingState.asStateFlow()
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractor.kt
new file mode 100644
index 00000000..dd16f0c1
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractor.kt
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import android.content.Intent
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.CustomAction
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.PendingIntentSender
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.toCustomActionModel
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ShareouselUpdate
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.getOrDefault
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.onValue
+import com.android.intentresolver.data.repository.ChooserRequestRepository
+import javax.inject.Inject
+import kotlinx.coroutines.flow.update
+
+/** Updates the tracked chooser request. */
+class UpdateChooserRequestInteractor
+@Inject
+constructor(
+ private val repository: ChooserRequestRepository,
+ @CustomAction private val pendingIntentSender: PendingIntentSender,
+) {
+ fun applyUpdate(targetIntent: Intent, update: ShareouselUpdate) {
+ repository.chooserRequest.update { current ->
+ current.copy(
+ targetIntent = targetIntent,
+ callerChooserTargets =
+ update.callerTargets.getOrDefault(current.callerChooserTargets),
+ modifyShareAction =
+ update.modifyShareAction.getOrDefault(current.modifyShareAction),
+ additionalTargets = update.alternateIntents.getOrDefault(current.additionalTargets),
+ chosenComponentSender =
+ update.resultIntentSender.getOrDefault(current.chosenComponentSender),
+ refinementIntentSender =
+ update.refinementIntentSender.getOrDefault(current.refinementIntentSender),
+ metadataText = update.metadataText.getOrDefault(current.metadataText),
+ chooserActions = update.customActions.getOrDefault(current.chooserActions),
+ )
+ }
+ update.customActions.onValue { actions ->
+ repository.customActions.value =
+ actions.map { it.toCustomActionModel(pendingIntentSender) }
+ }
+ }
+
+ fun setTargetIntent(targetIntent: Intent) {
+ repository.chooserRequest.update { it.copy(targetIntent = targetIntent) }
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt
new file mode 100644
index 00000000..d99d69ab
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateTargetIntentInteractor.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import android.content.Intent
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.PendingSelectionCallbackRepository
+import javax.inject.Inject
+
+class UpdateTargetIntentInteractor
+@Inject
+constructor(
+ private val repository: PendingSelectionCallbackRepository,
+ private val chooserRequestInteractor: UpdateChooserRequestInteractor,
+) {
+ /**
+ * Updates the target intent for the chooser. This will kick off an asynchronous IPC with the
+ * sharing application, so that it can react to the new intent.
+ */
+ fun updateTargetIntent(targetIntent: Intent) {
+ repository.pendingTargetIntent.value = targetIntent
+ chooserRequestInteractor.setTargetIntent(targetIntent)
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ActionModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ActionModel.kt
new file mode 100644
index 00000000..f69365d7
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ActionModel.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.model
+
+import com.android.intentresolver.icon.ComposeIcon
+
+/** An action that the user can take, provided by the sharing application. */
+data class ActionModel(
+ /** Text shown for this action in the UI. */
+ val label: CharSequence,
+ /** An optional [ComposeIcon] that will be displayed in the UI with this action. */
+ val icon: ComposeIcon?,
+ /**
+ * Performs the action. The argument indicates the index in the UI that this action is shown.
+ */
+ val performAction: (index: Int) -> Unit,
+)
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/LoadDirection.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/LoadDirection.kt
new file mode 100644
index 00000000..23510f15
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/LoadDirection.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.model
+
+/** Specifies which side of the dataset is being loaded. */
+enum class LoadDirection {
+ Left,
+ Right,
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/LoadedWindow.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/LoadedWindow.kt
new file mode 100644
index 00000000..e2e69852
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/LoadedWindow.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.model
+
+/** A window of data loaded from a cursor. */
+data class LoadedWindow<K, V>(
+ /** First cursor page index loaded within this window. */
+ val firstLoadedPageNum: Int,
+ /** Last cursor page index loaded within this window. */
+ val lastLoadedPageNum: Int,
+ /** Keys of cursor data within this window, grouped by loaded page. */
+ val pages: List<Set<K>>,
+ /** Merged set of all cursor data within this window. */
+ val merged: Map<K, V>,
+ /** Is there more data to the left of this window? */
+ val hasMoreLeft: Boolean,
+ /** Is there more data to the right of this window? */
+ val hasMoreRight: Boolean,
+)
+
+/** Number of loaded pages stored within this [LoadedWindow]. */
+val LoadedWindow<*, *>.numLoadedPages: Int
+ get() = (lastLoadedPageNum - firstLoadedPageNum) + 1
+
+/** Inserts [newPage] to the right, and removes the leftmost page from the window. */
+fun <K, V> LoadedWindow<K, V>.shiftWindowRight(
+ newPage: Map<K, V>,
+ hasMore: Boolean,
+): LoadedWindow<K, V> =
+ LoadedWindow(
+ firstLoadedPageNum = firstLoadedPageNum + 1,
+ lastLoadedPageNum = lastLoadedPageNum + 1,
+ pages = pages.drop(1) + listOf(newPage.keys),
+ merged =
+ buildMap {
+ putAll(merged)
+ pages.first().forEach(::remove)
+ putAll(newPage)
+ },
+ hasMoreLeft = true,
+ hasMoreRight = hasMore,
+ )
+
+/** Inserts [newPage] to the right, increasing the size of the window to accommodate it. */
+fun <K, V> LoadedWindow<K, V>.expandWindowRight(
+ newPage: Map<K, V>,
+ hasMore: Boolean,
+): LoadedWindow<K, V> =
+ LoadedWindow(
+ firstLoadedPageNum = firstLoadedPageNum,
+ lastLoadedPageNum = lastLoadedPageNum + 1,
+ pages = pages + listOf(newPage.keys),
+ merged = merged + newPage,
+ hasMoreLeft = hasMoreLeft,
+ hasMoreRight = hasMore,
+ )
+
+/** Inserts [newPage] to the left, and removes the rightmost page from the window. */
+fun <K, V> LoadedWindow<K, V>.shiftWindowLeft(
+ newPage: Map<K, V>,
+ hasMore: Boolean,
+): LoadedWindow<K, V> =
+ LoadedWindow(
+ firstLoadedPageNum = firstLoadedPageNum - 1,
+ lastLoadedPageNum = lastLoadedPageNum - 1,
+ pages = listOf(newPage.keys) + pages.dropLast(1),
+ merged =
+ buildMap {
+ putAll(newPage)
+ putAll(merged - pages.last())
+ },
+ hasMoreLeft = hasMore,
+ hasMoreRight = true,
+ )
+
+/** Inserts [newPage] to the left, increasing the size olf the window to accommodate it. */
+fun <K, V> LoadedWindow<K, V>.expandWindowLeft(
+ newPage: Map<K, V>,
+ hasMore: Boolean,
+): LoadedWindow<K, V> =
+ LoadedWindow(
+ firstLoadedPageNum = firstLoadedPageNum - 1,
+ lastLoadedPageNum = lastLoadedPageNum,
+ pages = listOf(newPage.keys) + pages,
+ merged = newPage + merged,
+ hasMoreLeft = hasMore,
+ hasMoreRight = hasMoreRight,
+ )
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ShareouselUpdate.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ShareouselUpdate.kt
new file mode 100644
index 00000000..821e88a5
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ShareouselUpdate.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.payloadtoggle.domain.model
+
+import android.content.Intent
+import android.content.IntentSender
+import android.service.chooser.ChooserAction
+import android.service.chooser.ChooserTarget
+
+/** Sharing session updates provided by the sharing app from the payload change callback */
+data class ShareouselUpdate(
+ // for all properties, null value means no change
+ val customActions: ValueUpdate<List<ChooserAction>> = ValueUpdate.Absent,
+ val modifyShareAction: ValueUpdate<ChooserAction?> = ValueUpdate.Absent,
+ val alternateIntents: ValueUpdate<List<Intent>> = ValueUpdate.Absent,
+ val callerTargets: ValueUpdate<List<ChooserTarget>> = ValueUpdate.Absent,
+ val refinementIntentSender: ValueUpdate<IntentSender?> = ValueUpdate.Absent,
+ val resultIntentSender: ValueUpdate<IntentSender?> = ValueUpdate.Absent,
+ val metadataText: ValueUpdate<CharSequence?> = ValueUpdate.Absent,
+)
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ValueUpdate.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ValueUpdate.kt
new file mode 100644
index 00000000..bad4eebe
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/model/ValueUpdate.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.payloadtoggle.domain.model
+
+/** Represents an either updated value or the absence of it */
+sealed interface ValueUpdate<out T> {
+ data class Value<T>(val value: T) : ValueUpdate<T>
+ data object Absent : ValueUpdate<Nothing>
+}
+
+/** Return encapsulated value if this instance represent Value or `default` if Absent */
+fun <T> ValueUpdate<T>.getOrDefault(default: T): T =
+ when (this) {
+ is ValueUpdate.Value -> value
+ is ValueUpdate.Absent -> default
+ }
+
+/** Executes the `block` with encapsulated value if this instance represents Value */
+inline fun <T> ValueUpdate<T>.onValue(block: (T) -> Unit) {
+ if (this is ValueUpdate.Value) {
+ block(value)
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallback.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallback.kt
new file mode 100644
index 00000000..1d34dc75
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallback.kt
@@ -0,0 +1,173 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.update
+
+import android.content.ContentInterface
+import android.content.Intent
+import android.content.Intent.EXTRA_ALTERNATE_INTENTS
+import android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS
+import android.content.Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION
+import android.content.Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER
+import android.content.Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER
+import android.content.Intent.EXTRA_CHOOSER_TARGETS
+import android.content.Intent.EXTRA_INTENT
+import android.content.Intent.EXTRA_METADATA_TEXT
+import android.content.IntentSender
+import android.net.Uri
+import android.os.Bundle
+import android.service.chooser.AdditionalContentContract.MethodNames.ON_SELECTION_CHANGED
+import android.service.chooser.ChooserAction
+import android.service.chooser.ChooserTarget
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ShareouselUpdate
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate
+import com.android.intentresolver.inject.AdditionalContent
+import com.android.intentresolver.inject.ChooserIntent
+import com.android.intentresolver.inject.ChooserServiceFlags
+import com.android.intentresolver.ui.viewmodel.readAlternateIntents
+import com.android.intentresolver.ui.viewmodel.readChooserActions
+import com.android.intentresolver.validation.Invalid
+import com.android.intentresolver.validation.Valid
+import com.android.intentresolver.validation.ValidationResult
+import com.android.intentresolver.validation.log
+import com.android.intentresolver.validation.types.array
+import com.android.intentresolver.validation.types.value
+import com.android.intentresolver.validation.validateFrom
+import dagger.Binds
+import dagger.Module
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import javax.inject.Inject
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+
+private const val TAG = "SelectionChangeCallback"
+
+/**
+ * Encapsulates payload change callback invocation to the sharing app; handles callback arguments
+ * and result format mapping.
+ */
+fun interface SelectionChangeCallback {
+ suspend fun onSelectionChanged(targetIntent: Intent): ShareouselUpdate?
+}
+
+class SelectionChangeCallbackImpl
+@Inject
+constructor(
+ @AdditionalContent private val uri: Uri,
+ @ChooserIntent private val chooserIntent: Intent,
+ private val contentResolver: ContentInterface,
+ private val flags: ChooserServiceFlags,
+) : SelectionChangeCallback {
+ private val mutex = Mutex()
+
+ override suspend fun onSelectionChanged(targetIntent: Intent): ShareouselUpdate? =
+ mutex
+ .withLock {
+ contentResolver.call(
+ requireNotNull(uri.authority) { "URI authority can not be null" },
+ ON_SELECTION_CHANGED,
+ uri.toString(),
+ Bundle().apply {
+ putParcelable(
+ EXTRA_INTENT,
+ Intent(chooserIntent).apply { putExtra(EXTRA_INTENT, targetIntent) }
+ )
+ }
+ )
+ }
+ ?.let { bundle ->
+ return when (val result = readCallbackResponse(bundle, flags)) {
+ is Valid -> {
+ result.warnings.forEach { it.log(TAG) }
+ result.value
+ }
+ is Invalid -> {
+ result.errors.forEach { it.log(TAG) }
+ null
+ }
+ }
+ }
+}
+
+private fun readCallbackResponse(
+ bundle: Bundle,
+ flags: ChooserServiceFlags
+): ValidationResult<ShareouselUpdate> {
+ return validateFrom(bundle::get) {
+ // An error is treated as an empty collection or null as the presence of a value indicates
+ // an intention to change the old value implying that the old value is obsolete (and should
+ // not be used).
+ val customActions =
+ bundle.readValueUpdate(EXTRA_CHOOSER_CUSTOM_ACTIONS) {
+ readChooserActions() ?: emptyList()
+ }
+ val modifyShareAction =
+ bundle.readValueUpdate(EXTRA_CHOOSER_MODIFY_SHARE_ACTION) { key ->
+ optional(value<ChooserAction>(key))
+ }
+ val alternateIntents =
+ bundle.readValueUpdate(EXTRA_ALTERNATE_INTENTS) {
+ readAlternateIntents() ?: emptyList()
+ }
+ val callerTargets =
+ bundle.readValueUpdate(EXTRA_CHOOSER_TARGETS) { key ->
+ optional(array<ChooserTarget>(key)) ?: emptyList()
+ }
+ val refinementIntentSender =
+ bundle.readValueUpdate(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER) { key ->
+ optional(value<IntentSender>(key))
+ }
+ val resultIntentSender =
+ bundle.readValueUpdate(EXTRA_CHOOSER_RESULT_INTENT_SENDER) { key ->
+ optional(value<IntentSender>(key))
+ }
+ val metadataText =
+ if (flags.enableSharesheetMetadataExtra()) {
+ bundle.readValueUpdate(EXTRA_METADATA_TEXT) { key ->
+ optional(value<CharSequence>(key))
+ }
+ } else {
+ ValueUpdate.Absent
+ }
+
+ ShareouselUpdate(
+ customActions,
+ modifyShareAction,
+ alternateIntents,
+ callerTargets,
+ refinementIntentSender,
+ resultIntentSender,
+ metadataText,
+ )
+ }
+}
+
+private inline fun <reified T> Bundle.readValueUpdate(
+ key: String,
+ block: (String) -> T
+): ValueUpdate<T> =
+ if (containsKey(key)) {
+ ValueUpdate.Value(block(key))
+ } else {
+ ValueUpdate.Absent
+ }
+
+@Module
+@InstallIn(ViewModelComponent::class)
+interface SelectionChangeCallbackModule {
+ @Binds fun bind(impl: SelectionChangeCallbackImpl): SelectionChangeCallback
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewModel.kt
new file mode 100644
index 00000000..ff96a9f4
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewModel.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.shared.model
+
+import android.net.Uri
+
+/** An individual preview presented in Shareousel. */
+data class PreviewModel(
+ /**
+ * Uri for this preview; if this preview is selected, this will be shared with the target app.
+ */
+ val uri: Uri,
+ /** Mimetype for the data [uri] points to. */
+ val mimeType: String?,
+)
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewsModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewsModel.kt
new file mode 100644
index 00000000..0ac99bd3
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/shared/model/PreviewsModel.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.shared.model
+
+/** A dataset of previews for Shareousel. */
+data class PreviewsModel(
+ /** All available [PreviewModel]s. */
+ val previewModels: Set<PreviewModel>,
+ /** Index into [previewModels] that should be initially displayed to the user. */
+ val startIdx: Int,
+ /**
+ * Signals that more data should be loaded to the left of this dataset. A `null` value indicates
+ * that there is no more data to load in that direction.
+ */
+ val loadMoreLeft: (() -> Unit)?,
+ /**
+ * Signals that more data should be loaded to the right of this dataset. A `null` value
+ * indicates that there is no more data to load in that direction.
+ */
+ val loadMoreRight: (() -> Unit)?,
+)
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ComposeIconComposable.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ComposeIconComposable.kt
new file mode 100644
index 00000000..8cf237da
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ComposeIconComposable.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.contentpreview.payloadtoggle.ui.composable
+
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.res.Resources
+import androidx.compose.foundation.Image
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.painterResource
+import com.android.intentresolver.icon.AdaptiveIcon
+import com.android.intentresolver.icon.BitmapIcon
+import com.android.intentresolver.icon.ComposeIcon
+import com.android.intentresolver.icon.ResourceIcon
+
+@Composable
+fun Image(icon: ComposeIcon, modifier: Modifier = Modifier, colorFilter: ColorFilter? = null) {
+ when (icon) {
+ is AdaptiveIcon -> Image(icon.wrapped, modifier, colorFilter = colorFilter)
+ is BitmapIcon ->
+ Image(
+ icon.bitmap.asImageBitmap(),
+ contentDescription = null,
+ modifier = modifier,
+ colorFilter = colorFilter
+ )
+ is ResourceIcon -> {
+ val localContext = LocalContext.current
+ val wrappedContext: Context =
+ object : ContextWrapper(localContext) {
+ override fun getResources(): Resources = icon.res
+ }
+ CompositionLocalProvider(LocalContext provides wrappedContext) {
+ Image(
+ painterResource(icon.resId),
+ contentDescription = null,
+ modifier = modifier,
+ colorFilter = colorFilter
+ )
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt
new file mode 100644
index 00000000..f33558c7
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselCardComposable.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.contentpreview.payloadtoggle.ui.composable
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.draw.shadow
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.unit.dp
+import com.android.intentresolver.R
+import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ContentType
+
+@Composable
+fun ShareouselCard(
+ image: @Composable () -> Unit,
+ contentType: ContentType,
+ selected: Boolean,
+ modifier: Modifier = Modifier,
+) {
+ Box(modifier) {
+ image()
+ val topButtonPadding = 12.dp
+ Box(modifier = Modifier.padding(topButtonPadding).matchParentSize()) {
+ SelectionIcon(selected, modifier = Modifier.align(Alignment.TopStart))
+ if (contentType == ContentType.Video) {
+ AnimationIcon(modifier = Modifier.align(Alignment.TopEnd))
+ }
+ }
+ }
+}
+
+@Composable
+private fun AnimationIcon(modifier: Modifier = Modifier) {
+ Icon(
+ painterResource(id = R.drawable.ic_play_circle_filled_24px),
+ "animating",
+ tint = Color.White,
+ modifier = Modifier.size(20.dp).then(modifier)
+ )
+}
+
+@Composable
+private fun SelectionIcon(selected: Boolean, modifier: Modifier = Modifier) {
+ if (selected) {
+ val bgColor = MaterialTheme.colorScheme.primary
+ Icon(
+ painter = painterResource(id = R.drawable.checkbox),
+ tint = Color.White,
+ contentDescription = "selected",
+ modifier =
+ Modifier.shadow(
+ elevation = 50.dp,
+ spotColor = Color(0x40000000),
+ ambientColor = Color(0x40000000)
+ )
+ .size(20.dp)
+ .drawBehind {
+ drawCircle(color = bgColor, radius = (this.size.width / 2f) - 1f)
+ }
+ .then(modifier)
+ )
+ } else {
+ Box(
+ modifier =
+ Modifier.shadow(
+ elevation = 50.dp,
+ spotColor = Color(0x40000000),
+ ambientColor = Color(0x40000000),
+ )
+ .border(width = 2.dp, color = Color(0xFFFFFFFF), shape = CircleShape)
+ .clip(CircleShape)
+ .size(20.dp)
+ .background(color = Color(0x7DC4C4C4))
+ .then(modifier)
+ )
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt
new file mode 100644
index 00000000..0a431c2a
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/composable/ShareouselComposable.kt
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.contentpreview.payloadtoggle.ui.composable
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.background
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.layout.width
+import androidx.compose.foundation.lazy.LazyRow
+import androidx.compose.foundation.lazy.items
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material3.AssistChip
+import androidx.compose.material3.AssistChipDefaults
+import androidx.compose.material3.LocalContentColor
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.asImageBitmap
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.dimensionResource
+import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.intentresolver.R
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
+import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ContentType
+import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselPreviewViewModel
+import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselViewModel
+import kotlinx.coroutines.launch
+
+@Composable
+fun Shareousel(viewModel: ShareouselViewModel) {
+ val keySet = viewModel.previews.collectAsStateWithLifecycle(null).value
+ if (keySet != null) {
+ Shareousel(viewModel, keySet)
+ } else {
+ Spacer(
+ Modifier.height(dimensionResource(R.dimen.chooser_preview_image_height_tall) + 64.dp)
+ .background(MaterialTheme.colorScheme.surfaceContainer)
+ )
+ }
+}
+
+@Composable
+private fun Shareousel(viewModel: ShareouselViewModel, keySet: PreviewsModel) {
+ Column(
+ modifier =
+ Modifier.background(MaterialTheme.colorScheme.surfaceContainer)
+ .padding(vertical = 16.dp),
+ ) {
+ PreviewCarousel(keySet, viewModel)
+ ActionCarousel(viewModel)
+ }
+}
+
+@Composable
+private fun PreviewCarousel(
+ previews: PreviewsModel,
+ viewModel: ShareouselViewModel,
+) {
+ val centerIdx = previews.startIdx
+ val carouselState = rememberLazyListState(initialFirstVisibleItemIndex = centerIdx)
+ // TODO: start item needs to be centered, check out ScalingLazyColumn impl or see if
+ // HorizontalPager works for our use-case
+ LazyRow(
+ state = carouselState,
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ modifier =
+ Modifier.fillMaxWidth()
+ .height(dimensionResource(R.dimen.chooser_preview_image_height_tall))
+ ) {
+ items(previews.previewModels.toList(), key = { it.uri }) { model ->
+ ShareouselCard(viewModel.preview(model))
+ }
+ }
+}
+
+@Composable
+private fun ShareouselCard(viewModel: ShareouselPreviewViewModel) {
+ val bitmap by viewModel.bitmap.collectAsStateWithLifecycle(initialValue = null)
+ val selected by viewModel.isSelected.collectAsStateWithLifecycle(initialValue = false)
+ val contentType by
+ viewModel.contentType.collectAsStateWithLifecycle(initialValue = ContentType.Image)
+ val borderColor = MaterialTheme.colorScheme.primary
+ val scope = rememberCoroutineScope()
+ ShareouselCard(
+ image = {
+ bitmap?.let { bitmap ->
+ val aspectRatio =
+ (bitmap.width.toFloat() / bitmap.height.toFloat())
+ // TODO: max ratio is actually equal to the viewport ratio
+ .coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO)
+ Image(
+ bitmap = bitmap.asImageBitmap(),
+ contentDescription = null,
+ contentScale = ContentScale.Crop,
+ modifier = Modifier.aspectRatio(aspectRatio),
+ )
+ }
+ ?: run {
+ // TODO: look at ScrollableImagePreviewView.setLoading()
+ Box(modifier = Modifier.fillMaxHeight().aspectRatio(2f / 5f))
+ }
+ },
+ contentType = contentType,
+ selected = selected,
+ modifier =
+ Modifier.thenIf(selected) {
+ Modifier.border(
+ width = 4.dp,
+ color = borderColor,
+ shape = RoundedCornerShape(size = 12.dp),
+ )
+ }
+ .clip(RoundedCornerShape(size = 12.dp))
+ .clickable { scope.launch { viewModel.setSelected(!selected) } },
+ )
+}
+
+@Composable
+private fun ActionCarousel(viewModel: ShareouselViewModel) {
+ val actions by viewModel.actions.collectAsStateWithLifecycle(initialValue = emptyList())
+ if (actions.isNotEmpty()) {
+ Spacer(Modifier.height(16.dp))
+ LazyRow(
+ horizontalArrangement = Arrangement.spacedBy(4.dp),
+ modifier = Modifier.height(32.dp),
+ ) {
+ itemsIndexed(actions) { idx, actionViewModel ->
+ if (idx == 0) {
+ Spacer(Modifier.width(dimensionResource(R.dimen.chooser_edge_margin_normal)))
+ }
+ ShareouselAction(
+ label = actionViewModel.label,
+ onClick = { actionViewModel.onClicked() },
+ ) {
+ actionViewModel.icon?.let {
+ Image(
+ icon = it,
+ modifier = Modifier.size(16.dp),
+ colorFilter = ColorFilter.tint(LocalContentColor.current)
+ )
+ }
+ }
+ if (idx == actions.size - 1) {
+ Spacer(Modifier.width(dimensionResource(R.dimen.chooser_edge_margin_normal)))
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun ShareouselAction(
+ label: String,
+ onClick: () -> Unit,
+ modifier: Modifier = Modifier,
+ leadingIcon: (@Composable () -> Unit)? = null,
+) {
+ AssistChip(
+ onClick = onClick,
+ label = { Text(label) },
+ leadingIcon = leadingIcon,
+ border = null,
+ shape = RoundedCornerShape(1000.dp), // pill shape.
+ colors =
+ AssistChipDefaults.assistChipColors(
+ containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
+ labelColor = MaterialTheme.colorScheme.onSurface,
+ leadingIconContentColor = MaterialTheme.colorScheme.onSurface
+ ),
+ modifier = modifier,
+ )
+}
+
+inline fun Modifier.thenIf(condition: Boolean, crossinline factory: () -> Modifier): Modifier =
+ if (condition) this.then(factory()) else this
+
+private const val MIN_ASPECT_RATIO = 0.4f
+private const val MAX_ASPECT_RATIO = 2.5f
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ActionChipViewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ActionChipViewModel.kt
new file mode 100644
index 00000000..728c573b
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ActionChipViewModel.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel
+
+import com.android.intentresolver.icon.ComposeIcon
+
+/** An action chip presented to the user underneath Shareousel. */
+data class ActionChipViewModel(
+ /** Text label. */
+ val label: String,
+ /** Optional icon, displayed next to the text label. */
+ val icon: ComposeIcon?,
+ /** Handles user clicks on this action in the UI. */
+ val onClicked: () -> Unit,
+)
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt
new file mode 100644
index 00000000..a245b3e3
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselPreviewViewModel.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel
+
+import android.graphics.Bitmap
+import kotlinx.coroutines.flow.Flow
+
+/** An individual preview within Shareousel. */
+data class ShareouselPreviewViewModel(
+ /** Image to be shared. */
+ val bitmap: Flow<Bitmap?>,
+ /** Type of data to be shared. */
+ val contentType: Flow<ContentType>,
+ /** Whether this preview has been selected by the user. */
+ val isSelected: Flow<Boolean>,
+ /** Sets whether this preview has been selected by the user. */
+ val setSelected: suspend (Boolean) -> Unit,
+)
+
+/** Type of the content being previewed. */
+enum class ContentType {
+ Image,
+ Video,
+ Other
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt
new file mode 100644
index 00000000..082581dc
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModel.kt
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel
+
+import android.content.Context
+import com.android.intentresolver.R
+import com.android.intentresolver.contentpreview.HeadlineGenerator
+import com.android.intentresolver.contentpreview.ImageLoader
+import com.android.intentresolver.contentpreview.ImagePreviewImageLoader
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.PayloadToggle
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.ChooserRequestInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.CustomActionsInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.SelectablePreviewsInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.SelectionInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
+import com.android.intentresolver.inject.Background
+import com.android.intentresolver.inject.ViewModelOwned
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.plus
+
+/** A dynamic carousel of selectable previews within share sheet. */
+data class ShareouselViewModel(
+ /** Text displayed at the top of the share sheet when Shareousel is present. */
+ val headline: Flow<String>,
+ /** App-provided text shown beneath the headline. */
+ val metadataText: Flow<CharSequence?>,
+ /**
+ * Previews which are available for presentation within Shareousel. Use [preview] to create a
+ * [ShareouselPreviewViewModel] for a given [PreviewModel].
+ */
+ val previews: Flow<PreviewsModel?>,
+ /** List of action chips presented underneath Shareousel. */
+ val actions: Flow<List<ActionChipViewModel>>,
+ /** Creates a [ShareouselPreviewViewModel] for a [PreviewModel] present in [previews]. */
+ val preview: (key: PreviewModel) -> ShareouselPreviewViewModel,
+)
+
+@Module
+@InstallIn(ViewModelComponent::class)
+object ShareouselViewModelModule {
+ @Provides
+ fun create(
+ interactor: SelectablePreviewsInteractor,
+ @PayloadToggle imageLoader: ImageLoader,
+ actionsInteractor: CustomActionsInteractor,
+ headlineGenerator: HeadlineGenerator,
+ selectionInteractor: SelectionInteractor,
+ chooserRequestInteractor: ChooserRequestInteractor,
+ // TODO: remove if possible
+ @ViewModelOwned scope: CoroutineScope,
+ ): ShareouselViewModel {
+ val keySet =
+ interactor.previews.stateIn(
+ scope,
+ SharingStarted.Eagerly,
+ initialValue = null,
+ )
+ return ShareouselViewModel(
+ headline =
+ selectionInteractor.amountSelected.map { numItems ->
+ val contentType = ContentType.Image // TODO: convert from metadata
+ when (contentType) {
+ ContentType.Other -> headlineGenerator.getFilesHeadline(numItems)
+ ContentType.Image -> headlineGenerator.getImagesHeadline(numItems)
+ ContentType.Video -> headlineGenerator.getVideosHeadline(numItems)
+ }
+ },
+ metadataText = chooserRequestInteractor.metadataText,
+ previews = keySet,
+ actions =
+ actionsInteractor.customActions.map { actions ->
+ actions.mapIndexedNotNull { i, model ->
+ val icon = model.icon
+ val label = model.label
+ if (icon == null && label.isBlank()) {
+ null
+ } else {
+ ActionChipViewModel(
+ label = label.toString(),
+ icon = model.icon,
+ onClicked = { model.performAction(i) },
+ )
+ }
+ }
+ },
+ preview = { key ->
+ keySet.value?.maybeLoad(key)
+ val previewInteractor = interactor.preview(key)
+ ShareouselPreviewViewModel(
+ bitmap = flow { emit(imageLoader(key.uri)) },
+ contentType = flowOf(ContentType.Image), // TODO: convert from metadata
+ isSelected = previewInteractor.isSelected,
+ setSelected = previewInteractor::setSelected,
+ )
+ },
+ )
+ }
+
+ @Provides
+ @PayloadToggle
+ fun imageLoader(
+ @ViewModelOwned viewModelScope: CoroutineScope,
+ @Background coroutineDispatcher: CoroutineDispatcher,
+ @ApplicationContext context: Context,
+ ): ImageLoader =
+ ImagePreviewImageLoader(
+ viewModelScope + coroutineDispatcher,
+ thumbnailSize =
+ context.resources.getDimensionPixelSize(R.dimen.chooser_preview_image_max_dimen),
+ context.contentResolver,
+ cacheSize = 16,
+ )
+}
+
+private fun PreviewsModel.maybeLoad(key: PreviewModel) {
+ when (key) {
+ previewModels.firstOrNull() -> loadMoreLeft?.invoke()
+ previewModels.lastOrNull() -> loadMoreRight?.invoke()
+ }
+}
diff --git a/java/src/com/android/intentresolver/data/BroadcastSubscriber.kt b/java/src/com/android/intentresolver/data/BroadcastSubscriber.kt
new file mode 100644
index 00000000..cf31ea10
--- /dev/null
+++ b/java/src/com/android/intentresolver/data/BroadcastSubscriber.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.data
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.Handler
+import android.os.UserHandle
+import android.util.Log
+import com.android.intentresolver.inject.Broadcast
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.channels.onFailure
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+
+private const val TAG = "BroadcastSubscriber"
+
+class BroadcastSubscriber
+@Inject
+constructor(
+ @ApplicationContext private val context: Context,
+ @Broadcast private val handler: Handler
+) {
+ /**
+ * 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.
+ */
+ fun <T> createFlow(
+ 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")
+ }
+ }
+
+ @Suppress("MissingPermission")
+ context.registerReceiverAsUser(
+ receiver,
+ user,
+ IntentFilter(filter),
+ null,
+ handler,
+ Context.RECEIVER_NOT_EXPORTED
+ )
+ awaitClose { context.unregisterReceiver(receiver) }
+ }
+}
diff --git a/java/src/com/android/intentresolver/data/model/ChooserRequest.kt b/java/src/com/android/intentresolver/data/model/ChooserRequest.kt
new file mode 100644
index 00000000..045a17f6
--- /dev/null
+++ b/java/src/com/android/intentresolver/data/model/ChooserRequest.kt
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.data.model
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.Intent.ACTION_SEND
+import android.content.Intent.ACTION_SEND_MULTIPLE
+import android.content.Intent.EXTRA_REFERRER
+import android.content.IntentFilter
+import android.content.IntentSender
+import android.net.Uri
+import android.os.Bundle
+import android.service.chooser.ChooserAction
+import android.service.chooser.ChooserTarget
+import androidx.annotation.StringRes
+import com.android.intentresolver.ContentTypeHint
+import com.android.intentresolver.ext.hasAction
+
+const val ANDROID_APP_SCHEME = "android-app"
+
+/** All of the things that are consumed from an incoming share Intent (+Extras). */
+data class ChooserRequest(
+ /** Required. Represents the content being sent. */
+ val targetIntent: Intent,
+
+ /** The action from [targetIntent] as retrieved with [Intent.getAction]. */
+ val targetAction: String? = targetIntent.action,
+
+ /**
+ * Whether [targetAction] is ACTION_SEND or ACTION_SEND_MULTIPLE. These are considered the
+ * canonical "Share" actions. When handling other actions, this flag controls behavioral and
+ * visual changes.
+ */
+ val isSendActionTarget: Boolean = targetIntent.hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE),
+
+ /** The top-level content type as retrieved using [Intent.getType]. */
+ val targetType: String? = targetIntent.type,
+
+ /** The package name of the app which started the current activity instance. */
+ val launchedFromPackage: String,
+
+ /** A custom tile for the main UI. Ignored when the intent is ACTION_SEND(_MULTIPLE). */
+ val title: CharSequence? = null,
+
+ /** A String resource ID to load when [title] is null. */
+ @get:StringRes val defaultTitleResource: Int = 0,
+
+ /**
+ * The referrer value as received by the caller. It may have been supplied via [EXTRA_REFERRER]
+ * or synthesized from callerPackageName. This value is merged into outgoing intents.
+ */
+ val referrer: Uri? = null,
+
+ /**
+ * Choices to exclude from results.
+ *
+ * Any resolved intents with a component in this list will be omitted before presentation.
+ */
+ val filteredComponentNames: List<ComponentName> = emptyList(),
+
+ /**
+ * App provided shortcut share intents (aka "direct share targets")
+ *
+ * Normally share shortcuts are published and consumed using
+ * [ShortcutManager][android.content.pm.ShortcutManager]. This is an alternate channel to allow
+ * apps to directly inject the same information.
+ *
+ * Historical note: This option was initially integrated with other results from the
+ * ChooserTargetService API (since deprecated and removed), hence the name and data format.
+ * These are more correctly called "Share Shortcuts" now.
+ */
+ val callerChooserTargets: List<ChooserTarget> = emptyList(),
+
+ /**
+ * Actions the user may perform. These are presented as separate affordances from the main list
+ * of choices. Selecting a choice is a terminal action which results in finishing. The item
+ * limit is [MAX_CHOOSER_ACTIONS]. This may be further constrained as appropriate.
+ */
+ val chooserActions: List<ChooserAction> = emptyList(),
+
+ /**
+ * An action to start an Activity which for user updating of shared content. Selection is a
+ * terminal action, closing the current activity and launching the target of the action.
+ */
+ val modifyShareAction: ChooserAction? = null,
+
+ /**
+ * When false the host activity will be [finished][android.app.Activity.finish] when stopped.
+ */
+ @get:JvmName("shouldRetainInOnStop") val shouldRetainInOnStop: Boolean = false,
+
+ /**
+ * Intents which contain alternate representations of the content being shared. Any results from
+ * resolving these _alternate_ intents are included with the results of the primary intent as
+ * additional choices (e.g. share as image content vs. link to content).
+ */
+ val additionalTargets: List<Intent> = emptyList(),
+
+ /**
+ * Alternate [extras][Intent.getExtras] to substitute when launching a selected app.
+ *
+ * For a given app (by package name), the Bundle describes what parameters to substitute when
+ * that app is selected.
+ *
+ * // TODO: Map<String, Bundle>
+ */
+ val replacementExtras: Bundle? = null,
+
+ /**
+ * App-supplied choices to be presented first in the list.
+ *
+ * Custom labels and icons may be supplied using
+ * [LabeledIntent][android.content.pm.LabeledIntent].
+ *
+ * Limit 2.
+ */
+ val initialIntents: List<Intent> = emptyList(),
+
+ /**
+ * Provides for callers to be notified when a component is selected.
+ *
+ * The selection is reported in the Intent as [Intent.EXTRA_CHOSEN_COMPONENT] with the
+ * [ComponentName] of the item.
+ */
+ val chosenComponentSender: IntentSender? = null,
+
+ /**
+ * Provides a mechanism for callers to post-process a target when a selection is made.
+ *
+ * The received intent will contain:
+ * * **EXTRA_INTENT** The chosen target
+ * * **EXTRA_ALTERNATE_INTENTS** Additional intents which also match the target
+ * * **EXTRA_RESULT_RECEIVER** A [ResultReceiver][android.os.ResultReceiver] providing a
+ * mechanism for the caller to return information. An updated intent to send must be included
+ * as [Intent.EXTRA_INTENT].
+ */
+ val refinementIntentSender: IntentSender? = null,
+
+ /**
+ * Contains the text content to share supplied by the source app.
+ *
+ * TODO: Constrain length?
+ */
+ val sharedText: CharSequence? = null,
+
+ /**
+ * Supplied to
+ * [ShortcutManager.getShareTargets][android.content.pm.ShortcutManager.getShareTargets] to
+ * query for matching shortcuts. Specifically, only the [dataTypes][IntentFilter.hasDataType]
+ * are considered for matching share shortcuts currently.
+ */
+ val shareTargetFilter: IntentFilter? = null,
+
+ /** A URI for additional content */
+ val additionalContentUri: Uri? = null,
+
+ /** Focused item index (from target intent's STREAM_EXTRA) */
+ val focusedItemPosition: Int = 0,
+
+ /** Value for [Intent.EXTRA_CHOOSER_CONTENT_TYPE_HINT] on the incoming chooser intent. */
+ val contentTypeHint: ContentTypeHint = ContentTypeHint.NONE,
+
+ /**
+ * Metadata to be shown to the user as a part of the sharesheet window.
+ *
+ * Specified by the [Intent.EXTRA_METADATA_TEXT]
+ */
+ val metadataText: CharSequence? = null,
+) {
+ val referrerPackage = referrer?.takeIf { it.scheme == ANDROID_APP_SCHEME }?.authority
+
+ fun getReferrerFillInIntent(): Intent {
+ return Intent().apply {
+ referrerPackage?.also { pkg ->
+ putExtra(EXTRA_REFERRER, Uri.parse("$ANDROID_APP_SCHEME://$pkg"))
+ }
+ }
+ }
+
+ val payloadIntents = listOf(targetIntent) + additionalTargets
+}
diff --git a/java/src/com/android/intentresolver/data/repository/ChooserRequestRepository.kt b/java/src/com/android/intentresolver/data/repository/ChooserRequestRepository.kt
new file mode 100644
index 00000000..14177b1b
--- /dev/null
+++ b/java/src/com/android/intentresolver/data/repository/ChooserRequestRepository.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.data.repository
+
+import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel
+import com.android.intentresolver.data.model.ChooserRequest
+import dagger.hilt.android.scopes.ViewModelScoped
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+@ViewModelScoped
+class ChooserRequestRepository
+@Inject
+constructor(
+ initialRequest: ChooserRequest,
+ initialActions: List<CustomActionModel>,
+) {
+ /** All information from the sharing application pertaining to the chooser. */
+ val chooserRequest: MutableStateFlow<ChooserRequest> = MutableStateFlow(initialRequest)
+
+ /** Custom actions from the sharing app to be presented in the chooser. */
+ // NOTE: this could be derived directly from chooserRequest, but that would require working
+ // directly with PendingIntents, which complicates testing.
+ val customActions: MutableStateFlow<List<CustomActionModel>> = MutableStateFlow(initialActions)
+}
diff --git a/java/src/com/android/intentresolver/data/repository/DevicePolicyResources.kt b/java/src/com/android/intentresolver/data/repository/DevicePolicyResources.kt
new file mode 100644
index 00000000..75faa068
--- /dev/null
+++ b/java/src/com/android/intentresolver/data/repository/DevicePolicyResources.kt
@@ -0,0 +1,143 @@
+/*
+ * 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.data.repository
+
+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.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE
+import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_PERSONAL_APPS
+import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS
+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)
+ }
+ )
+ }
+
+ val forwardToPersonalMessage: String? =
+ devicePolicyManager.resources.getString(FORWARD_INTENT_TO_PERSONAL) {
+ resources.getString(R.string.forward_intent_to_owner)
+ }
+
+ val forwardToWorkMessage by lazy {
+ requireNotNull(
+ policyResources.getString(FORWARD_INTENT_TO_WORK) {
+ resources.getString(R.string.forward_intent_to_work)
+ }
+ )
+ }
+
+ val noPersonalApps by lazy {
+ requireNotNull(
+ policyResources.getString(RESOLVER_NO_PERSONAL_APPS) {
+ resources.getString(R.string.resolver_no_personal_apps_available)
+ }
+ )
+ }
+
+ val noWorkApps by lazy {
+ requireNotNull(
+ policyResources.getString(RESOLVER_NO_WORK_APPS) {
+ resources.getString(R.string.resolver_no_work_apps_available)
+ }
+ )
+ }
+
+ val crossProfileBlocked by lazy {
+ requireNotNull(
+ policyResources.getString(RESOLVER_CROSS_PROFILE_BLOCKED_TITLE) {
+ resources.getString(R.string.resolver_cross_profile_blocked)
+ }
+ )
+ }
+
+ fun toPersonalBlockedByPolicyMessage(sendAction: Boolean): String {
+ return if (sendAction) {
+ resources.getString(R.string.resolver_cant_share_with_personal_apps_explanation)
+ } else {
+ resources.getString(R.string.resolver_cant_access_personal_apps_explanation)
+ }
+ }
+
+ fun toWorkBlockedByPolicyMessage(sendAction: Boolean): String {
+ return if (sendAction) {
+ resources.getString(R.string.resolver_cant_share_with_work_apps_explanation)
+ } else {
+ resources.getString(R.string.resolver_cant_access_work_apps_explanation)
+ }
+ }
+
+ 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
+ )
+ )
+ }
+}
diff --git a/java/src/com/android/intentresolver/data/repository/UserInfoExt.kt b/java/src/com/android/intentresolver/data/repository/UserInfoExt.kt
new file mode 100644
index 00000000..753df93e
--- /dev/null
+++ b/java/src/com/android/intentresolver/data/repository/UserInfoExt.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.data.repository
+
+import android.content.pm.UserInfo
+import com.android.intentresolver.shared.model.User
+import com.android.intentresolver.shared.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/data/repository/UserRepository.kt b/java/src/com/android/intentresolver/data/repository/UserRepository.kt
new file mode 100644
index 00000000..6b5ff4ba
--- /dev/null
+++ b/java/src/com/android/intentresolver/data/repository/UserRepository.kt
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.data.repository
+
+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.Build
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.intentresolver.data.BroadcastSubscriber
+import com.android.intentresolver.inject.Background
+import com.android.intentresolver.inject.Main
+import com.android.intentresolver.inject.ProfileParent
+import com.android.intentresolver.shared.model.User
+import javax.inject.Inject
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted.Companion.WhileSubscribed
+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 list 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<List<User>>
+
+ /**
+ * A [Flow] of availability. Only profile users may become unavailable.
+ *
+ * Availability is currently defined as not being in [quietMode][UserInfo.isQuietModeEnabled].
+ */
+ val availability: Flow<Map<User, 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.availability] 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"
+
+/** The delay between entering the cached process state and entering the frozen cgroup */
+private val cachedProcessFreezeDelay: Duration = 10.seconds
+
+/** How long to continue listening for user state broadcasts while unsubscribed */
+private val stateFlowTimeout = cachedProcessFreezeDelay - 2.seconds
+
+/** How long to retain the previous user state after the state flow stops. */
+private val stateCacheTimeout = 2.seconds
+
+internal data class UserWithState(val user: User, val available: Boolean)
+
+internal typealias UserStates = List<UserWithState>
+
+internal val userBroadcastActions =
+ 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
+ )
+
+/** 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(
+ @ProfileParent profileParent: UserHandle,
+ userManager: UserManager,
+ @Main scope: CoroutineScope,
+ @Background background: CoroutineDispatcher,
+ broadcastSubscriber: BroadcastSubscriber,
+ ) : this(
+ profileParent,
+ userManager,
+ userEvents =
+ broadcastSubscriber.createFlow(
+ createFilter(userBroadcastActions),
+ profileParent,
+ Intent::toUserEvent
+ ),
+ scope,
+ background,
+ )
+
+ private fun debugLog(msg: () -> String) {
+ if (Build.IS_USERDEBUG || Build.IS_ENG) {
+ Log.d(TAG, msg())
+ }
+ }
+
+ private fun errorLog(msg: String, caught: Throwable? = null) {
+ Log.e(TAG, msg, caught)
+ }
+
+ /**
+ * An exception which indicates that an inconsistency exists between the user state map and the
+ * rest of the system.
+ */
+ private class UserStateException(
+ override val message: String,
+ val event: UserEvent,
+ override val cause: Throwable? = null,
+ ) : RuntimeException("$message: event=$event", cause)
+
+ private val sharingScope = CoroutineScope(scope.coroutineContext + backgroundDispatcher)
+ private val usersWithState: Flow<UserStates> =
+ userEvents
+ .onStart { emit(Initialize) }
+ .onEach { debugLog { "userEvent: $it" } }
+ .runningFold(emptyList(), ::handleEvent)
+ .distinctUntilChanged()
+ .onEach { debugLog { "userStateList: $it" } }
+ .stateIn(
+ sharingScope,
+ started =
+ WhileSubscribed(
+ stopTimeoutMillis = stateFlowTimeout.inWholeMilliseconds,
+ replayExpirationMillis = 0
+ /** Immediately on stop */
+ ),
+ listOf()
+ )
+ .filterNot { it.isEmpty() }
+
+ private suspend fun handleEvent(users: UserStates, event: UserEvent): UserStates {
+ return try {
+ // Handle an action by performing some operation, then returning a new map
+ when (event) {
+ is Initialize -> createNewUserStates(profileParent)
+ is ProfileAdded -> handleProfileAdded(event, users)
+ is ProfileRemoved -> handleProfileRemoved(event, users)
+ is AvailabilityChange -> handleAvailability(event, users)
+ is UnknownEvent -> {
+ debugLog { "Unhandled event: $event)" }
+ users
+ }
+ }
+ } catch (e: UserStateException) {
+ errorLog("An error occurred handling an event: ${e.event}")
+ errorLog("Attempting to recover...", e)
+ createNewUserStates(profileParent)
+ }
+ }
+
+ override val users: Flow<List<User>> =
+ usersWithState.map { userStates -> userStates.map { it.user } }.distinctUntilChanged()
+
+ override val availability: Flow<Map<User, Boolean>> =
+ usersWithState
+ .map { list -> list.associate { it.user to it.available } }
+ .distinctUntilChanged()
+
+ override suspend fun requestState(user: User, available: Boolean) {
+ return withContext(backgroundDispatcher) {
+ debugLog { "requestQuietModeEnabled: ${!available} for user $user" }
+ userManager.requestQuietModeEnabled(/* enableQuietMode = */ !available, user.handle)
+ }
+ }
+
+ private fun List<UserWithState>.update(handle: UserHandle, user: UserWithState) =
+ filter { it.user.id != handle.identifier } + user
+
+ private fun handleAvailability(event: AvailabilityChange, current: UserStates): UserStates {
+ val userEntry =
+ current.firstOrNull { it.user.id == event.user.identifier }
+ ?: throw UserStateException("User was not present in the map", event)
+ return current.update(event.user, userEntry.copy(available = !event.quietMode))
+ }
+
+ private fun handleProfileRemoved(event: ProfileRemoved, current: UserStates): UserStates {
+ if (!current.any { it.user.id == event.user.identifier }) {
+ throw UserStateException("User was not present in the map", event)
+ }
+ return current.filter { it.user.id != event.user.identifier }
+ }
+
+ private suspend fun handleProfileAdded(event: ProfileAdded, current: UserStates): UserStates {
+ val user =
+ try {
+ requireNotNull(readUser(event.user))
+ } catch (e: Exception) {
+ throw UserStateException("Failed to read user from UserManager", event, e)
+ }
+ return current + UserWithState(user, true)
+ }
+
+ private suspend fun createNewUserStates(user: UserHandle): UserStates {
+ val profiles = readProfileGroup(user)
+ return profiles.mapNotNull { userInfo ->
+ userInfo.toUser()?.let { user -> UserWithState(user, userInfo.isAvailable()) }
+ }
+ }
+
+ private suspend fun readProfileGroup(member: UserHandle): List<UserInfo> {
+ return withContext(backgroundDispatcher) {
+ @Suppress("DEPRECATION") userManager.getEnabledProfiles(member.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) }
+ }
+ }
+}
+
+/** A Model representing changes to profiles and availability */
+sealed interface UserEvent
+
+/** Used as a an initial value to trigger a fetch of all profile data. */
+data object Initialize : UserEvent
+
+/** A profile was added to the profile group. */
+data class ProfileAdded(
+ /** The handle for the added profile. */
+ val user: UserHandle,
+) : UserEvent
+
+/** A profile was removed from the profile group. */
+data class ProfileRemoved(
+ /** The handle for the removed profile. */
+ val user: UserHandle,
+) : UserEvent
+
+/** A profile has changed availability. */
+data class AvailabilityChange(
+ /** THe handle for the profile with availability change. */
+ val user: UserHandle,
+ /** The new quietMode state. */
+ val quietMode: Boolean = false,
+) : UserEvent
+
+/** An unhandled event, logged and ignored. */
+data class UnknownEvent(
+ /** The broadcast intent action received */
+ val action: String?,
+) : UserEvent
+
+/** Used with [broadcastFlow] to transform a UserManager broadcast action into a [UserEvent]. */
+internal fun Intent.toUserEvent(): UserEvent {
+ val action = action
+ val user = extras?.getParcelable(EXTRA_USER, UserHandle::class.java)
+ val quietMode = extras?.getBoolean(EXTRA_QUIET_MODE, false)
+ return when (action) {
+ ACTION_PROFILE_ADDED -> ProfileAdded(requireNotNull(user))
+ ACTION_PROFILE_REMOVED -> ProfileRemoved(requireNotNull(user))
+ ACTION_MANAGED_PROFILE_UNAVAILABLE,
+ ACTION_MANAGED_PROFILE_AVAILABLE,
+ ACTION_PROFILE_AVAILABLE,
+ ACTION_PROFILE_UNAVAILABLE ->
+ AvailabilityChange(requireNotNull(user), requireNotNull(quietMode))
+ else -> UnknownEvent(action)
+ }
+}
+
+internal fun createFilter(actions: Iterable<String>): IntentFilter {
+ return IntentFilter().apply { actions.forEach(::addAction) }
+}
+
+internal fun UserInfo?.isAvailable(): Boolean {
+ return this?.isQuietModeEnabled != true
+}
diff --git a/java/src/com/android/intentresolver/data/repository/UserRepositoryModule.kt b/java/src/com/android/intentresolver/data/repository/UserRepositoryModule.kt
new file mode 100644
index 00000000..7109d6d4
--- /dev/null
+++ b/java/src/com/android/intentresolver/data/repository/UserRepositoryModule.kt
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.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(
+ @ApplicationContext context: Context,
+ userManager: UserManager
+ ): UserHandle {
+ return userManager.getProfileParent(context.user) ?: context.user
+ }
+ }
+
+ @Binds @Singleton fun userRepository(impl: UserRepositoryImpl): UserRepository
+}
diff --git a/java/src/com/android/intentresolver/data/repository/UserScopedService.kt b/java/src/com/android/intentresolver/data/repository/UserScopedService.kt
new file mode 100644
index 00000000..10a33eb1
--- /dev/null
+++ b/java/src/com/android/intentresolver/data/repository/UserScopedService.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.data.repository
+
+import android.content.Context
+import android.os.UserHandle
+import androidx.core.content.getSystemService
+import dagger.hilt.android.qualifiers.ApplicationContext
+import kotlin.reflect.KClass
+
+/**
+ * Provides instances of a [system service][Context.getSystemService] created with
+ * [the context of a specified user][Context.createContextAsUser].
+ *
+ * Some 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:
+ * ```
+ * @Provides
+ * fun scopedUserManager(@ApplicationContext ctx: Context): UserScopedService<UserManager> {
+ * return UserScopedServiceImpl(ctx, UserManager::class)
+ * }
+ *
+ * class MyUserHelper @Inject constructor(
+ * private val userMgr: UserScopedService<UserManager>,
+ * ) {
+ * fun isPrivateProfile(user: UserHandle): UserManager {
+ * return userMgr.forUser(user).isPrivateProfile()
+ * }
+ * }
+ * ```
+ */
+fun interface UserScopedService<T> {
+ /** Create a service instance for the given user. */
+ fun forUser(user: UserHandle): T
+}
+
+class UserScopedServiceImpl<T : Any>(
+ @ApplicationContext private val context: Context,
+ private val serviceType: KClass<T>,
+) : UserScopedService<T> {
+ override fun forUser(user: UserHandle): T {
+ val context =
+ if (context.user == user) {
+ context
+ } else {
+ context.createContextAsUser(user, 0)
+ }
+ return requireNotNull(context.getSystemService(serviceType.java))
+ }
+}
diff --git a/java/src/com/android/intentresolver/domain/interactor/UserInteractor.kt b/java/src/com/android/intentresolver/domain/interactor/UserInteractor.kt
new file mode 100644
index 00000000..2392a48d
--- /dev/null
+++ b/java/src/com/android/intentresolver/domain/interactor/UserInteractor.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.domain.interactor
+
+import android.os.UserHandle
+import com.android.intentresolver.data.repository.UserRepository
+import com.android.intentresolver.inject.ApplicationUser
+import com.android.intentresolver.shared.model.Profile
+import com.android.intentresolver.shared.model.Profile.Type
+import com.android.intentresolver.shared.model.User
+import com.android.intentresolver.shared.model.User.Role
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.map
+
+/** The high level User interface. */
+class UserInteractor
+@Inject
+constructor(
+ private val userRepository: UserRepository,
+ /** The specific [User] of the application which started this one. */
+ @ApplicationUser val launchedAs: UserHandle,
+) {
+ /** The profile group associated with the launching app user. */
+ val profiles: Flow<List<Profile>> =
+ userRepository.users.map { users ->
+ users.mapNotNull { user ->
+ when (user.role) {
+ // PERSONAL includes CLONE
+ Role.PERSONAL -> {
+ Profile(Type.PERSONAL, user, users.firstOrNull { it.role == Role.CLONE })
+ }
+ Role.CLONE -> {
+ /* ignore, included above */
+ null
+ }
+ // others map 1:1
+ else -> Profile(profileFromRole(user.role), user)
+ }
+ }
+ }
+
+ /** The [Profile] of the application which started this one. */
+ val launchedAsProfile: Flow<Profile> =
+ profiles.map { profiles ->
+ // The launching user profile is the one with a primary id or clone id
+ // matching the application user id. By definition there must always be exactly
+ // one matching profile for the current user.
+ profiles.single {
+ it.primary.id == launchedAs.identifier || it.clone?.id == launchedAs.identifier
+ }
+ }
+ /**
+ * Provides a flow to report on the availability of profile. An unavailable profile may be
+ * hidden or appear disabled within the app.
+ */
+ val availability: Flow<Map<Profile, Boolean>> =
+ combine(profiles, userRepository.availability) { profiles, availability ->
+ profiles.associateWith { availability.getOrDefault(it.primary, false) }
+ }
+
+ /**
+ * Request the profile state be updated. In the case of enabling, the operation could take
+ * significant time and/or require user input.
+ */
+ suspend fun updateState(profile: Profile, available: Boolean) {
+ userRepository.requestState(profile.primary, available)
+ }
+
+ private fun profileFromRole(role: Role): Type =
+ when (role) {
+ Role.PERSONAL -> Type.PERSONAL
+ Role.CLONE -> Type.PERSONAL /* CLONE maps to PERSONAL */
+ Role.PRIVATE -> Type.PRIVATE
+ Role.WORK -> Type.WORK
+ }
+}
diff --git a/java/src/com/android/intentresolver/emptystate/DevicePolicyBlockerEmptyState.java b/java/src/com/android/intentresolver/emptystate/DevicePolicyBlockerEmptyState.java
new file mode 100644
index 00000000..b627636e
--- /dev/null
+++ b/java/src/com/android/intentresolver/emptystate/DevicePolicyBlockerEmptyState.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.emptystate;
+
+import android.app.admin.DevicePolicyEventLogger;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+
+/**
+ * Empty state that gets strings from the device policy manager and tracks events into
+ * event logger of the device policy events.
+ */
+public 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/emptystate/EmptyStateUiHelper.java b/java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java
index d7ef8c75..7524f343 100644
--- a/java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java
+++ b/java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java
@@ -17,47 +17,120 @@ package com.android.intentresolver.emptystate;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+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) {
+ 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);
}
- 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);
+ /**
+ * 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() {
- mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_title)
- .setVisibility(View.INVISIBLE);
+ mEmptyStateTitleView.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);
+ 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.
+ public 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/emptystate/NoAppsAvailableEmptyState.java b/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyState.java
new file mode 100644
index 00000000..b03c730a
--- /dev/null
+++ b/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyState.java
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.emptystate;
+
+import android.app.admin.DevicePolicyEventLogger;
+import android.stats.devicepolicy.nano.DevicePolicyEnums;
+
+import androidx.annotation.NonNull;
+
+public 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/emptystate/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java
index 2653c560..cd1448e4 100644
--- a/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java
+++ b/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java
@@ -16,24 +16,20 @@
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.app.admin.DevicePolicyEventLogger;
-import android.app.admin.DevicePolicyManager;
-import android.content.Context;
-import android.content.pm.ResolveInfo;
+import static com.android.intentresolver.shared.model.Profile.Type.PERSONAL;
+
+import static java.util.Objects.requireNonNull;
+
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.ProfileAvailability;
+import com.android.intentresolver.ProfileHelper;
import com.android.intentresolver.ResolverListAdapter;
-import com.android.internal.R;
-
-import java.util.List;
+import com.android.intentresolver.shared.model.Profile;
+import com.android.intentresolver.ui.ProfilePagerResources;
/**
* Chooser/ResolverActivity empty state provider that returns empty state which is shown when
@@ -41,79 +37,40 @@ import java.util.List;
*/
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;
+ @NonNull private final String mMetricsCategory;
+ private final ProfilePagerResources mProfilePagerResources;
+ private final ProfileHelper mProfileHelper;
+ private final ProfileAvailability mProfileAvailability;
public NoAppsAvailableEmptyStateProvider(
- @NonNull Context context,
- @Nullable UserHandle workProfileUserHandle,
- @Nullable UserHandle personalProfileUserHandle,
+ ProfileHelper profileHelper,
+ ProfileAvailability profileAvailability,
@NonNull String metricsCategory,
- @NonNull UserHandle tabOwnerUserHandleForLaunch) {
- mContext = context;
- mWorkProfileUserHandle = workProfileUserHandle;
- mPersonalProfileUserHandle = personalProfileUserHandle;
+ ProfilePagerResources profilePagerResources) {
+ mProfileHelper = profileHelper;
+ mProfileAvailability = profileAvailability;
mMetricsCategory = metricsCategory;
- mTabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch;
+ mProfilePagerResources = profilePagerResources;
}
- @Nullable
+ @NonNull
@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));
- }
-
+ if (mProfileAvailability.visibleProfileCount() == 1) {
+ return new DefaultEmptyState();
+ } else {
+ Profile.Type profileType =
+ requireNonNull(mProfileHelper.findProfileType(listUserHandle));
+ String title = mProfilePagerResources.noAppsMessage(profileType);
return new NoAppsAvailableEmptyState(
- title, mMetricsCategory,
- /* isPersonalProfile= */ listUserHandle == mPersonalProfileUserHandle
+ title,
+ mMetricsCategory,
+ /* isPersonalProfile= */ profileType == PERSONAL
);
- } 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
@@ -122,37 +79,4 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider {
}
}
- public static class NoAppsAvailableEmptyState implements EmptyState {
-
- @NonNull
- private String mTitle;
-
- @NonNull
- private String mMetricsCategory;
-
- private boolean mIsPersonalProfile;
-
- public NoAppsAvailableEmptyState(@NonNull String title,
- @NonNull String metricsCategory,
- boolean isPersonalProfile) {
- mTitle = title;
- mMetricsCategory = metricsCategory;
- mIsPersonalProfile = isPersonalProfile;
- }
-
- @Nullable
- @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/emptystate/NoCrossProfileEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java
index ce7bd8d9..fa33928b 100644
--- a/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java
+++ b/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java
@@ -16,124 +16,74 @@
package com.android.intentresolver.emptystate;
-import android.app.admin.DevicePolicyEventLogger;
-import android.app.admin.DevicePolicyManager;
-import android.content.Context;
+import android.content.Intent;
import android.os.UserHandle;
-import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.StringRes;
+import com.android.intentresolver.ProfileHelper;
import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.shared.model.Profile;
+import com.android.intentresolver.shared.model.User;
+
+import java.util.List;
/**
- * 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.
+ * Empty state provider that informs about a lack of cross profile sharing. It will return
+ * an empty state in case there are no intents which can be forwarded to another profile.
*/
public class NoCrossProfileEmptyStateProvider implements EmptyStateProvider {
- private final UserHandle mPersonalProfileUserHandle;
+ private final ProfileHelper mProfileHelper;
private final EmptyState mNoWorkToPersonalEmptyState;
private final EmptyState mNoPersonalToWorkEmptyState;
private final CrossProfileIntentsChecker mCrossProfileIntentsChecker;
- private final UserHandle mTabOwnerUserHandleForLaunch;
- public NoCrossProfileEmptyStateProvider(UserHandle personalUserHandle,
+ public NoCrossProfileEmptyStateProvider(
+ ProfileHelper profileHelper,
EmptyState noWorkToPersonalEmptyState,
EmptyState noPersonalToWorkEmptyState,
- CrossProfileIntentsChecker crossProfileIntentsChecker,
- UserHandle tabOwnerUserHandleForLaunch) {
- mPersonalProfileUserHandle = personalUserHandle;
+ CrossProfileIntentsChecker crossProfileIntentsChecker) {
+ mProfileHelper = profileHelper;
mNoWorkToPersonalEmptyState = noWorkToPersonalEmptyState;
mNoPersonalToWorkEmptyState = noPersonalToWorkEmptyState;
mCrossProfileIntentsChecker = crossProfileIntentsChecker;
- mTabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch;
+ }
+
+ private boolean anyCrossProfileAllowedIntents(ResolverListAdapter selected, UserHandle source) {
+ List<Intent> intents = selected.getIntents();
+ UserHandle target = selected.getUserHandle();
+ return mCrossProfileIntentsChecker.hasCrossProfileIntents(intents,
+ source.getIdentifier(), target.getIdentifier());
}
@Nullable
@Override
- public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
- boolean shouldShowBlocker =
- !mTabOwnerUserHandleForLaunch.equals(resolverListAdapter.getUserHandle())
- && !mCrossProfileIntentsChecker
- .hasCrossProfileIntents(resolverListAdapter.getIntents(),
- mTabOwnerUserHandleForLaunch.getIdentifier(),
- resolverListAdapter.getUserHandle().getIdentifier());
-
- if (!shouldShowBlocker) {
+ public EmptyState getEmptyState(ResolverListAdapter adapter) {
+ Profile launchedAsProfile = mProfileHelper.getLaunchedAsProfile();
+ User launchedAs = mProfileHelper.getLaunchedAsProfile().getPrimary();
+ UserHandle tabOwnerHandle = adapter.getUserHandle();
+ boolean launchedAsSameUser = launchedAs.getHandle().equals(tabOwnerHandle);
+ Profile.Type tabOwnerType = mProfileHelper.findProfileType(tabOwnerHandle);
+
+ // Not applicable for private profile.
+ if (launchedAsProfile.getType() == Profile.Type.PRIVATE
+ || tabOwnerType == Profile.Type.PRIVATE) {
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();
+ // Allow access to the tab when launched by the same user as the tab owner
+ // or when there is at least one target which is permitted for cross-profile.
+ if (launchedAsSameUser || anyCrossProfileAllowedIntents(adapter,
+ /* source = */ launchedAs.getHandle())) {
+ return null;
}
- @Override
- public boolean shouldSkipDataRebuild() {
- return true;
+ switch (launchedAsProfile.getType()) {
+ case WORK: return mNoWorkToPersonalEmptyState;
+ case PERSONAL: return mNoPersonalToWorkEmptyState;
}
+ return null;
}
+
}
diff --git a/java/src/com/android/intentresolver/emptystate/WorkProfileOffEmptyState.java b/java/src/com/android/intentresolver/emptystate/WorkProfileOffEmptyState.java
new file mode 100644
index 00000000..e9de3221
--- /dev/null
+++ b/java/src/com/android/intentresolver/emptystate/WorkProfileOffEmptyState.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.emptystate;
+
+import android.app.admin.DevicePolicyEventLogger;
+import android.stats.devicepolicy.nano.DevicePolicyEnums;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+public 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/emptystate/WorkProfilePausedEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java
index 612828e0..f78d1ca2 100644
--- a/java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java
+++ b/java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java
@@ -18,19 +18,21 @@ package com.android.intentresolver.emptystate;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE;
-import android.app.admin.DevicePolicyEventLogger;
+import static java.util.Objects.requireNonNull;
+
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.ProfileAvailability;
+import com.android.intentresolver.ProfileHelper;
import com.android.intentresolver.R;
import com.android.intentresolver.ResolverListAdapter;
-import com.android.intentresolver.WorkProfileAvailabilityManager;
+import com.android.intentresolver.profiles.OnSwitchOnWorkSelectedListener;
+import com.android.intentresolver.shared.model.Profile;
/**
* Chooser/ResolverActivity empty state provider that returns empty state which is shown when
@@ -38,20 +40,20 @@ import com.android.intentresolver.WorkProfileAvailabilityManager;
*/
public class WorkProfilePausedEmptyStateProvider implements EmptyStateProvider {
- private final UserHandle mWorkProfileUserHandle;
- private final WorkProfileAvailabilityManager mWorkProfileAvailability;
+ private final ProfileHelper mProfileHelper;
+ private final ProfileAvailability mProfileAvailability;
private final String mMetricsCategory;
private final OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
private final Context mContext;
public WorkProfilePausedEmptyStateProvider(@NonNull Context context,
- @Nullable UserHandle workProfileUserHandle,
- @NonNull WorkProfileAvailabilityManager workProfileAvailability,
+ ProfileHelper profileHelper,
+ ProfileAvailability profileAvailability,
@Nullable OnSwitchOnWorkSelectedListener onSwitchOnWorkSelectedListener,
@NonNull String metricsCategory) {
mContext = context;
- mWorkProfileUserHandle = workProfileUserHandle;
- mWorkProfileAvailability = workProfileAvailability;
+ mProfileHelper = profileHelper;
+ mProfileAvailability = profileAvailability;
mMetricsCategory = metricsCategory;
mOnSwitchOnWorkSelectedListener = onSwitchOnWorkSelectedListener;
}
@@ -59,56 +61,34 @@ public class WorkProfilePausedEmptyStateProvider implements EmptyStateProvider {
@Nullable
@Override
public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
- if (!resolverListAdapter.getUserHandle().equals(mWorkProfileUserHandle)
- || !mWorkProfileAvailability.isQuietModeEnabled()
- || resolverListAdapter.getCount() == 0) {
+ UserHandle userHandle = resolverListAdapter.getUserHandle();
+ if (!mProfileHelper.getWorkProfilePresent()) {
+ return null;
+ }
+ Profile workProfile = requireNonNull(mProfileHelper.getWorkProfile());
+
+ // Policy: only show the "Work profile paused" state when:
+ // * provided list adapter is from the work profile
+ // * the list adapter is not empty
+ // * work profile quiet mode is _enabled_ (unavailable)
+
+ if (!userHandle.equals(workProfile.getPrimary().getHandle())
+ || resolverListAdapter.getCount() == 0
+ || mProfileAvailability.isAvailable(workProfile)) {
return null;
}
- final String title = mContext.getSystemService(DevicePolicyManager.class)
+ 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) -> {
+ return new WorkProfileOffEmptyState(title, /* EmptyState.ClickListener */ (tab) -> {
tab.showSpinner();
if (mOnSwitchOnWorkSelectedListener != null) {
mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected();
}
- mWorkProfileAvailability.requestQuietModeEnabled(false);
+ mProfileAvailability.requestQuietModeState(workProfile, 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/ext/CreationExtrasExt.kt b/java/src/com/android/intentresolver/ext/CreationExtrasExt.kt
new file mode 100644
index 00000000..2ba08c90
--- /dev/null
+++ b/java/src/com/android/intentresolver/ext/CreationExtrasExt.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.ext
+
+import android.os.Bundle
+import android.os.Parcelable
+import androidx.core.os.bundleOf
+import androidx.lifecycle.DEFAULT_ARGS_KEY
+import androidx.lifecycle.viewmodel.CreationExtras
+import androidx.lifecycle.viewmodel.MutableCreationExtras
+
+/**
+ * Returns a new instance with additional [values] added to the existing default args Bundle (if
+ * present), otherwise adds a new entry with a copy of this bundle.
+ */
+fun CreationExtras.addDefaultArgs(vararg values: Pair<String, Parcelable>): CreationExtras {
+ val defaultArgs: Bundle = get(DEFAULT_ARGS_KEY) ?: Bundle()
+ defaultArgs.putAll(bundleOf(*values))
+ return MutableCreationExtras(this).apply { set(DEFAULT_ARGS_KEY, defaultArgs) }
+}
diff --git a/java/src/com/android/intentresolver/ext/IntentExt.kt b/java/src/com/android/intentresolver/ext/IntentExt.kt
new file mode 100644
index 00000000..127dbf86
--- /dev/null
+++ b/java/src/com/android/intentresolver/ext/IntentExt.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.ext
+
+import android.content.Intent
+import java.util.function.Predicate
+
+/** Applies an operation on this Intent if matches the given filter. */
+inline fun Intent.ifMatch(
+ predicate: Predicate<Intent>,
+ crossinline block: Intent.() -> Unit
+): Intent {
+ if (predicate.test(this)) {
+ apply(block)
+ }
+ return this
+}
+
+/** True if the Intent has one of the specified actions. */
+fun Intent.hasAction(vararg actions: String): Boolean = action in actions
+
+/** True if the Intent has a specific component target */
+fun Intent.hasComponent(): Boolean = (component != null)
+
+/** True if the Intent has a single matching category. */
+fun Intent.hasSingleCategory(category: String) = categories.singleOrNull() == category
+
+/** True if the Intent is a SEND or SEND_MULTIPLE action. */
+fun Intent.hasSendAction() = hasAction(Intent.ACTION_SEND, Intent.ACTION_SEND_MULTIPLE)
+
+/** True if the Intent resolves to the special Home (Launcher) component */
+fun Intent.isHomeIntent() = hasAction(Intent.ACTION_MAIN) && hasSingleCategory(Intent.CATEGORY_HOME)
diff --git a/java/src/com/android/intentresolver/ext/ParcelExt.kt b/java/src/com/android/intentresolver/ext/ParcelExt.kt
new file mode 100644
index 00000000..68ea600f
--- /dev/null
+++ b/java/src/com/android/intentresolver/ext/ParcelExt.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.ext
+
+import android.os.Parcel
+
+inline fun <reified T> Parcel.requireParcelable(): T {
+ return requireNotNull(readParcelable<T>()) { "A non-value required from this parcel was null!" }
+}
+
+inline fun <reified T> Parcel.readParcelable(): T? {
+ return readParcelable(T::class.java.classLoader, T::class.java)
+}
diff --git a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
index 51d4e677..7cf9d2e9 100644
--- a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
+++ b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
@@ -40,7 +40,6 @@ 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;
import com.google.android.collect.Lists;
@@ -50,7 +49,6 @@ import com.google.android.collect.Lists;
* row level by this adapter but not on the item level. Individual targets within the row are
* handled by {@link ChooserListAdapter}
*/
-@VisibleForTesting
public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
/**
@@ -68,15 +66,6 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
* out of `ChooserGridAdapter` altogether.
*/
public interface ChooserActivityDelegate {
- /** @return whether we're showing a tabbed (multi-profile) UI. */
- boolean shouldShowTabs();
-
- /**
- * @return a content preview {@link View} that's appropriate for the caller's share
- * content, constructed for display in the provided {@code parent} group.
- */
- View buildContentPreview(ViewGroup parent);
-
/** Notify the client that the item with the selected {@code itemIndex} was selected. */
void onTargetSelected(int itemIndex);
@@ -85,19 +74,10 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
* long-pressed.
*/
void onTargetLongPressed(int itemIndex);
-
- /**
- * Notify the client that the provided {@code View} should be configured as the new
- * "profile view" button. Callers should attach their own click listeners to implement
- * behaviors on this view.
- */
- void updateProfileViewButton(View newButtonFromProfileRow);
}
private static final int VIEW_TYPE_DIRECT_SHARE = 0;
private static final int VIEW_TYPE_NORMAL = 1;
- private static final int VIEW_TYPE_CONTENT_PREVIEW = 2;
- private static final int VIEW_TYPE_PROFILE = 3;
private static final int VIEW_TYPE_AZ_LABEL = 4;
private static final int VIEW_TYPE_CALLER_AND_RANK = 5;
private static final int VIEW_TYPE_FOOTER = 6;
@@ -159,9 +139,7 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
@Override
public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
- if (mFeatureFlags.scrollablePreview()) {
- mRecyclerView = recyclerView;
- }
+ mRecyclerView = recyclerView;
}
@Override
@@ -170,7 +148,14 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
}
public void setFooterHeight(int height) {
- mFooterHeight = height;
+ if (mFooterHeight != height) {
+ mFooterHeight = height;
+ if (mFeatureFlags.fixTargetListFooter()) {
+ // we always have at least one view, the footer, see getItemCount() and
+ // getFooterRowCount()
+ notifyItemChanged(getItemCount() - 1);
+ }
+ }
}
/**
@@ -200,9 +185,7 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
public int getRowCount() {
return (int) (
- getSystemRowCount()
- + getProfileRowCount()
- + getServiceTargetRowCount()
+ getServiceTargetRowCount()
+ getCallerAndRankedTargetRowCount()
+ getAzLabelRowCount()
+ Math.ceil(
@@ -211,36 +194,6 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
);
}
- /**
- * Whether the "system" row of targets is displayed.
- * This area includes the content preview (if present) and action row.
- */
- public int getSystemRowCount() {
- // For the tabbed case we show the sticky content preview above the tabs,
- // please refer to shouldShowStickyContentPreview
- if (mChooserActivityDelegate.shouldShowTabs()
- || mFeatureFlags.scrollablePreview()) {
- return 0;
- }
-
- if (!mShouldShowContentPreview) {
- return 0;
- }
-
- if (mChooserListAdapter == null || mChooserListAdapter.getCount() == 0) {
- return 0;
- }
-
- return 1;
- }
-
- public int getProfileRowCount() {
- if (mChooserActivityDelegate.shouldShowTabs()) {
- return 0;
- }
- return mChooserListAdapter.getOtherProfile() == null ? 0 : 1;
- }
-
public int getFooterRowCount() {
return 1;
}
@@ -271,17 +224,13 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
return -1;
}
- return getSystemRowCount()
- + getProfileRowCount()
- + getServiceTargetRowCount()
+ return getServiceTargetRowCount()
+ getCallerAndRankedTargetRowCount();
}
@Override
public int getItemCount() {
- return getSystemRowCount()
- + getProfileRowCount()
- + getServiceTargetRowCount()
+ return getServiceTargetRowCount()
+ getCallerAndRankedTargetRowCount()
+ getAzLabelRowCount()
+ mChooserListAdapter.getAlphaTargetCount()
@@ -292,18 +241,6 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
switch (viewType) {
- case VIEW_TYPE_CONTENT_PREVIEW:
- return new ItemViewHolder(
- mChooserActivityDelegate.buildContentPreview(parent),
- viewType,
- null,
- null);
- case VIEW_TYPE_PROFILE:
- return new ItemViewHolder(
- createProfileView(parent),
- viewType,
- null,
- null);
case VIEW_TYPE_AZ_LABEL:
return new ItemViewHolder(
createAzLabelView(parent),
@@ -374,13 +311,8 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
@Override
public int getItemViewType(int position) {
- int count;
-
- int countSum = (count = getSystemRowCount());
- if (count > 0 && position < countSum) return VIEW_TYPE_CONTENT_PREVIEW;
-
- countSum += (count = getProfileRowCount());
- if (count > 0 && position < countSum) return VIEW_TYPE_PROFILE;
+ int count = 0;
+ int countSum = count;
countSum += (count = getServiceTargetRowCount());
if (count > 0 && position < countSum) return VIEW_TYPE_DIRECT_SHARE;
@@ -400,12 +332,6 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
return mChooserListAdapter.getPositionTargetType(getListPosition(position));
}
- private View createProfileView(ViewGroup parent) {
- View profileRow = mLayoutInflater.inflate(R.layout.chooser_profile_row, parent, false);
- mChooserActivityDelegate.updateProfileViewButton(profileRow);
- return profileRow;
- }
-
private View createAzLabelView(ViewGroup parent) {
return mLayoutInflater.inflate(R.layout.chooser_az_label_row, parent, false);
}
@@ -583,8 +509,6 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
}
int getListPosition(int position) {
- position -= getSystemRowCount() + getProfileRowCount();
-
final int serviceCount = mChooserListAdapter.getServiceTargetCount();
final int serviceRows = (int) Math.ceil((float) serviceCount / mMaxTargetsPerRow);
if (position < serviceRows) {
diff --git a/java/src/com/android/intentresolver/icon/ComposeIcon.kt b/java/src/com/android/intentresolver/icon/ComposeIcon.kt
new file mode 100644
index 00000000..dbea1e55
--- /dev/null
+++ b/java/src/com/android/intentresolver/icon/ComposeIcon.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.icon
+
+import android.content.ContentResolver
+import android.content.pm.PackageManager
+import android.content.res.Resources
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.drawable.Icon
+import java.io.File
+import java.io.FileInputStream
+
+sealed interface ComposeIcon
+
+data class BitmapIcon(val bitmap: Bitmap) : ComposeIcon
+
+data class ResourceIcon(val resId: Int, val res: Resources) : ComposeIcon
+
+@JvmInline value class AdaptiveIcon(val wrapped: ComposeIcon) : ComposeIcon
+
+fun Icon.toComposeIcon(pm: PackageManager, resolver: ContentResolver): ComposeIcon? {
+ return when (type) {
+ Icon.TYPE_BITMAP -> BitmapIcon(bitmap)
+ Icon.TYPE_RESOURCE -> pm.resourcesForPackage(resPackage)?.let { ResourceIcon(resId, it) }
+ Icon.TYPE_DATA ->
+ BitmapIcon(BitmapFactory.decodeByteArray(dataBytes, dataOffset, dataLength))
+ Icon.TYPE_URI -> uriIcon(resolver)
+ Icon.TYPE_ADAPTIVE_BITMAP -> AdaptiveIcon(BitmapIcon(bitmap))
+ Icon.TYPE_URI_ADAPTIVE_BITMAP -> uriIcon(resolver)?.let { AdaptiveIcon(it) }
+ else -> error("unexpected icon type: $type")
+ }
+}
+
+fun Icon.toComposeIcon(resources: Resources?, resolver: ContentResolver): ComposeIcon? {
+ return when (type) {
+ Icon.TYPE_BITMAP -> BitmapIcon(bitmap)
+ Icon.TYPE_RESOURCE -> resources?.let { ResourceIcon(resId, resources) }
+ Icon.TYPE_DATA ->
+ BitmapIcon(BitmapFactory.decodeByteArray(dataBytes, dataOffset, dataLength))
+ Icon.TYPE_URI -> uriIcon(resolver)
+ Icon.TYPE_ADAPTIVE_BITMAP -> AdaptiveIcon(BitmapIcon(bitmap))
+ Icon.TYPE_URI_ADAPTIVE_BITMAP -> uriIcon(resolver)?.let { AdaptiveIcon(it) }
+ else -> error("unexpected icon type: $type")
+ }
+}
+
+// TODO: this is probably constant and doesn't need to be re-queried for each icon
+fun PackageManager.resourcesForPackage(pkgName: String): Resources? {
+ return if (pkgName == "android") {
+ Resources.getSystem()
+ } else {
+ runCatching {
+ this@resourcesForPackage.getApplicationInfo(
+ pkgName,
+ PackageManager.MATCH_UNINSTALLED_PACKAGES or
+ PackageManager.GET_SHARED_LIBRARY_FILES
+ )
+ }
+ .getOrNull()
+ ?.let { ai -> getResourcesForApplication(ai) }
+ }
+}
+
+private fun Icon.uriIcon(resolver: ContentResolver): BitmapIcon? {
+ return runCatching {
+ when (uri.scheme) {
+ ContentResolver.SCHEME_CONTENT,
+ ContentResolver.SCHEME_FILE -> resolver.openInputStream(uri)
+ else -> FileInputStream(File(uriString))
+ }
+ }
+ .getOrNull()
+ ?.let { inStream -> BitmapIcon(BitmapFactory.decodeStream(inStream)) }
+}
diff --git a/java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt b/java/src/com/android/intentresolver/icons/TargetDataLoaderModule.kt
index 4e8783f8..32c040b8 100644
--- a/java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt
+++ b/java/src/com/android/intentresolver/icons/TargetDataLoaderModule.kt
@@ -14,12 +14,10 @@
* limitations under the License.
*/
-package com.android.intentresolver.v2.icons
+package com.android.intentresolver.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
diff --git a/java/src/com/android/intentresolver/inject/ActivityModelModule.kt b/java/src/com/android/intentresolver/inject/ActivityModelModule.kt
new file mode 100644
index 00000000..bbd25eb7
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/ActivityModelModule.kt
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.inject
+
+import android.content.Intent
+import android.net.Uri
+import android.service.chooser.ChooserAction
+import androidx.lifecycle.SavedStateHandle
+import com.android.intentresolver.data.model.ChooserRequest
+import com.android.intentresolver.ui.model.ActivityModel
+import com.android.intentresolver.ui.viewmodel.readChooserRequest
+import com.android.intentresolver.util.ownedByCurrentUser
+import com.android.intentresolver.validation.Valid
+import com.android.intentresolver.validation.ValidationResult
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.android.scopes.ViewModelScoped
+import javax.inject.Qualifier
+
+@Module
+@InstallIn(ViewModelComponent::class)
+object ActivityModelModule {
+ @Provides
+ fun provideActivityModel(savedStateHandle: SavedStateHandle): ActivityModel =
+ requireNotNull(savedStateHandle[ActivityModel.ACTIVITY_MODEL_KEY]) {
+ "ActivityModel missing in SavedStateHandle! (${ActivityModel.ACTIVITY_MODEL_KEY})"
+ }
+
+ @Provides
+ @ChooserIntent
+ fun chooserIntent(activityModel: ActivityModel): Intent = activityModel.intent
+
+ @Provides
+ @ViewModelScoped
+ fun provideInitialRequest(
+ activityModel: ActivityModel,
+ flags: ChooserServiceFlags,
+ ): ValidationResult<ChooserRequest> = readChooserRequest(activityModel, flags)
+
+ @Provides
+ fun provideChooserRequest(
+ initialRequest: ValidationResult<ChooserRequest>,
+ ): ChooserRequest =
+ requireNotNull((initialRequest as? Valid)?.value) {
+ "initialRequest is Invalid, no chooser request available"
+ }
+
+ @Provides
+ @TargetIntent
+ fun targetIntent(chooserReq: ValidationResult<ChooserRequest>): Intent =
+ requireNotNull((chooserReq as? Valid)?.value?.targetIntent) { "no target intent available" }
+
+ @Provides
+ fun customActions(chooserReq: ValidationResult<ChooserRequest>): List<ChooserAction> =
+ requireNotNull((chooserReq as? Valid)?.value?.chooserActions) {
+ "no chooser actions available"
+ }
+
+ @Provides
+ @ViewModelScoped
+ @ContentUris
+ fun selectedUris(chooserRequest: ValidationResult<ChooserRequest>): List<Uri> =
+ requireNotNull((chooserRequest as? Valid)?.value?.targetIntent?.contentUris?.toList()) {
+ "no selected uris available"
+ }
+
+ @Provides
+ @FocusedItemIndex
+ fun focusedItemIndex(chooserReq: ValidationResult<ChooserRequest>): Int =
+ requireNotNull((chooserReq as? Valid)?.value?.focusedItemPosition) {
+ "no focused item position available"
+ }
+
+ @Provides
+ @AdditionalContent
+ fun additionalContentUri(chooserReq: ValidationResult<ChooserRequest>): Uri =
+ requireNotNull((chooserReq as? Valid)?.value?.additionalContentUri) {
+ "no additional content uri available"
+ }
+}
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class FocusedItemIndex
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class AdditionalContent
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ChooserIntent
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ContentUris
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class TargetIntent
+
+private val Intent.contentUris: Sequence<Uri>
+ get() = sequence {
+ if (Intent.ACTION_SEND == action) {
+ getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
+ ?.takeIf { it.ownedByCurrentUser }
+ ?.let { yield(it) }
+ } else {
+ getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java)?.forEach { uri ->
+ if (uri.ownedByCurrentUser) {
+ yield(uri)
+ }
+ }
+ }
+ }
diff --git a/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt b/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt
index e0f8e88b..5fbdf090 100644
--- a/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt
+++ b/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt
@@ -16,6 +16,10 @@
package com.android.intentresolver.inject
+import android.os.Handler
+import android.os.HandlerThread
+import android.os.Looper
+import android.os.Process
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
@@ -26,6 +30,10 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
+// thread
+private const val BROADCAST_SLOW_DISPATCH_THRESHOLD = 1000L
+private const val BROADCAST_SLOW_DELIVERY_THRESHOLD = 1000L
+
@Module
@InstallIn(SingletonComponent::class)
object ConcurrencyModule {
@@ -40,4 +48,25 @@ object ConcurrencyModule {
CoroutineScope(SupervisorJob() + mainDispatcher)
@Provides @Background fun backgroundDispatcher(): CoroutineDispatcher = Dispatchers.IO
+
+ @Provides
+ @Singleton
+ @Broadcast
+ fun provideBroadcastLooper(): Looper {
+ val thread = HandlerThread("BroadcastReceiver", Process.THREAD_PRIORITY_BACKGROUND)
+ thread.start()
+ thread.looper.setSlowLogThresholdMs(
+ BROADCAST_SLOW_DISPATCH_THRESHOLD,
+ BROADCAST_SLOW_DELIVERY_THRESHOLD
+ )
+ return thread.looper
+ }
+
+ /** Provide a BroadcastReceiver Executor (for sending and receiving broadcasts). */
+ @Provides
+ @Singleton
+ @Broadcast
+ fun provideBroadcastHandler(@Broadcast looper: Looper): Handler {
+ return Handler(looper)
+ }
}
diff --git a/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt b/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt
index 05cf2104..d7be67db 100644
--- a/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt
+++ b/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt
@@ -1,15 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.intentresolver.inject
-import com.android.intentresolver.FeatureFlags
-import com.android.intentresolver.FeatureFlagsImpl
+import android.service.chooser.FeatureFlagsImpl as ChooserServiceFlagsImpl
+import com.android.intentresolver.FeatureFlagsImpl as IntentResolverFlagsImpl
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
+typealias IntentResolverFlags = com.android.intentresolver.FeatureFlags
+
+typealias FakeIntentResolverFlags = com.android.intentresolver.FakeFeatureFlagsImpl
+
+typealias ChooserServiceFlags = android.service.chooser.FeatureFlags
+
+typealias FakeChooserServiceFlags = android.service.chooser.FakeFeatureFlagsImpl
+
@Module
@InstallIn(SingletonComponent::class)
object FeatureFlagsModule {
- @Provides fun featureFlags(): FeatureFlags = FeatureFlagsImpl()
+ @Provides fun intentResolverFlags(): IntentResolverFlags = IntentResolverFlagsImpl()
+
+ @Provides fun chooserServiceFlags(): ChooserServiceFlags = ChooserServiceFlagsImpl()
}
diff --git a/java/src/com/android/intentresolver/inject/FrameworkModule.kt b/java/src/com/android/intentresolver/inject/FrameworkModule.kt
deleted file mode 100644
index 2f6cc6a0..00000000
--- a/java/src/com/android/intentresolver/inject/FrameworkModule.kt
+++ /dev/null
@@ -1,76 +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.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
index 157e8f76..77315cac 100644
--- a/java/src/com/android/intentresolver/inject/Qualifiers.kt
+++ b/java/src/com/android/intentresolver/inject/Qualifiers.kt
@@ -23,6 +23,11 @@ import javax.inject.Qualifier
@Qualifier
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
+annotation class ViewModelOwned
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
annotation class ApplicationOwned
@Qualifier
@@ -30,6 +35,8 @@ annotation class ApplicationOwned
@Retention(AnnotationRetention.RUNTIME)
annotation class ApplicationUser
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Broadcast
+
@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ProfileParent
@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Background
diff --git a/java/src/com/android/intentresolver/inject/SingletonModule.kt b/java/src/com/android/intentresolver/inject/SingletonModule.kt
index e517800d..af054625 100644
--- a/java/src/com/android/intentresolver/inject/SingletonModule.kt
+++ b/java/src/com/android/intentresolver/inject/SingletonModule.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.intentresolver.inject
import android.content.Context
diff --git a/java/src/com/android/intentresolver/inject/SystemServices.kt b/java/src/com/android/intentresolver/inject/SystemServices.kt
new file mode 100644
index 00000000..2a123dc7
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/SystemServices.kt
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.inject
+
+import android.app.ActivityManager
+import android.app.admin.DevicePolicyManager
+import android.app.prediction.AppPredictionManager
+import android.content.ClipboardManager
+import android.content.ContentInterface
+import android.content.ContentResolver
+import android.content.Context
+import android.content.pm.LauncherApps
+import android.content.pm.ShortcutManager
+import android.os.UserManager
+import android.view.WindowManager
+import androidx.core.content.getSystemService
+import com.android.intentresolver.data.repository.UserScopedService
+import com.android.intentresolver.data.repository.UserScopedServiceImpl
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+
+inline fun <reified T> Context.requireSystemService(): T {
+ return checkNotNull(getSystemService())
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+class ActivityManagerModule {
+ @Provides
+ fun activityManager(@ApplicationContext ctx: Context): ActivityManager =
+ ctx.requireSystemService()
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+class ClipboardManagerModule {
+ @Provides
+ fun clipboardManager(@ApplicationContext ctx: Context): ClipboardManager =
+ ctx.requireSystemService()
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+interface ContentResolverModule {
+ @Binds fun bindContentInterface(cr: ContentResolver): ContentInterface
+
+ companion object {
+ @Provides
+ fun contentResolver(@ApplicationContext ctx: Context) = requireNotNull(ctx.contentResolver)
+ }
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+class DevicePolicyManagerModule {
+ @Provides
+ fun devicePolicyManager(@ApplicationContext ctx: Context): DevicePolicyManager =
+ ctx.requireSystemService()
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+class LauncherAppsModule {
+ @Provides
+ fun launcherApps(@ApplicationContext ctx: Context): LauncherApps = ctx.requireSystemService()
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+class PackageManagerModule {
+ @Provides
+ fun packageManager(@ApplicationContext ctx: Context) = requireNotNull(ctx.packageManager)
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+class PredictionManagerModule {
+ @Provides
+ fun scopedPredictionManager(
+ @ApplicationContext ctx: Context,
+ ): UserScopedService<AppPredictionManager> {
+ return UserScopedServiceImpl(ctx, AppPredictionManager::class)
+ }
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+class ShortcutManagerModule {
+ @Provides
+ fun shortcutManager(@ApplicationContext ctx: Context): ShortcutManager {
+ return ctx.requireSystemService()
+ }
+
+ @Provides
+ fun scopedShortcutManager(
+ @ApplicationContext ctx: Context,
+ ): UserScopedService<ShortcutManager> {
+ return UserScopedServiceImpl(ctx, ShortcutManager::class)
+ }
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+class UserManagerModule {
+ @Provides
+ fun userManager(@ApplicationContext ctx: Context): UserManager = ctx.requireSystemService()
+
+ @Provides
+ fun scopedUserManager(@ApplicationContext ctx: Context): UserScopedService<UserManager> {
+ return UserScopedServiceImpl(ctx, UserManager::class)
+ }
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+class WindowManagerModule {
+ @Provides
+ fun windowManager(@ApplicationContext ctx: Context): WindowManager = ctx.requireSystemService()
+}
diff --git a/java/src/com/android/intentresolver/inject/ViewModelCoroutineScopeModule.kt b/java/src/com/android/intentresolver/inject/ViewModelCoroutineScopeModule.kt
new file mode 100644
index 00000000..4dda2653
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/ViewModelCoroutineScopeModule.kt
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.inject
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.ViewModelLifecycle
+import dagger.hilt.android.components.ViewModelComponent
+import dagger.hilt.android.scopes.ViewModelScoped
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.cancel
+
+@Module
+@InstallIn(ViewModelComponent::class)
+object ViewModelCoroutineScopeModule {
+ @Provides
+ @ViewModelScoped
+ @ViewModelOwned
+ fun viewModelScope(@Main dispatcher: CoroutineDispatcher, lifecycle: ViewModelLifecycle) =
+ lifecycle.asCoroutineScope(dispatcher)
+}
+
+fun ViewModelLifecycle.asCoroutineScope(context: CoroutineContext = EmptyCoroutineContext) =
+ CoroutineScope(context).also { addOnClearedListener { it.cancel() } }
diff --git a/java/src/com/android/intentresolver/logging/EventLogImpl.java b/java/src/com/android/intentresolver/logging/EventLogImpl.java
index 84029e76..39d23865 100644
--- a/java/src/com/android/intentresolver/logging/EventLogImpl.java
+++ b/java/src/com/android/intentresolver/logging/EventLogImpl.java
@@ -379,7 +379,9 @@ public class EventLogImpl implements EventLog {
@UiEvent(doc = "Sharesheet app share ranking timed out.")
SHARESHEET_APP_SHARE_RANKING_TIMEOUT(831),
@UiEvent(doc = "Sharesheet empty direct share row.")
- SHARESHEET_EMPTY_DIRECT_SHARE_ROW(828);
+ SHARESHEET_EMPTY_DIRECT_SHARE_ROW(828),
+ @UiEvent(doc = "Shareousel payload item toggled")
+ SHARESHEET_PAYLOAD_TOGGLED(1662);
private final int mId;
SharesheetStandardEvent(int id) {
diff --git a/java/src/com/android/intentresolver/logging/EventLogModule.kt b/java/src/com/android/intentresolver/logging/EventLogModule.kt
index eba8ecc8..73af7d37 100644
--- a/java/src/com/android/intentresolver/logging/EventLogModule.kt
+++ b/java/src/com/android/intentresolver/logging/EventLogModule.kt
@@ -24,14 +24,14 @@ 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
+import dagger.hilt.android.components.ActivityRetainedComponent
+import dagger.hilt.android.scopes.ActivityRetainedScoped
@Module
-@InstallIn(ActivityComponent::class)
+@InstallIn(ActivityRetainedComponent::class)
interface EventLogModule {
- @Binds @ActivityScoped fun eventLog(value: EventLogImpl): EventLog
+ @Binds @ActivityRetainedScoped fun eventLog(value: EventLogImpl): EventLog
companion object {
@Provides
diff --git a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
index 724fa849..4871ef4d 100644
--- a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
@@ -20,6 +20,7 @@ import android.app.usage.UsageStatsManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.BadParcelableException;
@@ -37,7 +38,6 @@ 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;
import java.util.Comparator;
import java.util.HashMap;
@@ -135,7 +135,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
user,
(UsageStatsManager) userContext.getSystemService(Context.USAGE_STATS_SERVICE));
}
- mAzComparator = new AzInfoComparator(launchedFromContext);
+ mAzComparator = new ResolveInfoAzInfoComparator(launchedFromContext);
mPromoteToFirst = promoteToFirst;
}
@@ -203,8 +203,8 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
}
if (mHttp) {
- final boolean lhsSpecific = ResolverActivity.isSpecificUriMatch(lhs.match);
- final boolean rhsSpecific = ResolverActivity.isSpecificUriMatch(rhs.match);
+ final boolean lhsSpecific = isSpecificUriMatch(lhs.match);
+ final boolean rhsSpecific = isSpecificUriMatch(rhs.match);
if (lhsSpecific != rhsSpecific) {
return lhsSpecific ? -1 : 1;
}
@@ -226,6 +226,13 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
return compare(lhs, rhs);
}
+ /** 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;
+ }
+
/**
* Delegated to when used as a {@link Comparator<ResolvedComponentInfo>} if there is not a
* special case. The {@link ResolveInfo ResolveInfos} are the first {@link ResolveInfo} in
@@ -306,24 +313,4 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
mAfterCompute = null;
}
- /**
- * Sort intents alphabetically based on package name.
- */
- class AzInfoComparator implements Comparator<ResolveInfo> {
- Collator mCollator;
- AzInfoComparator(Context context) {
- mCollator = Collator.getInstance(context.getResources().getConfiguration().locale);
- }
-
- @Override
- public int compare(ResolveInfo lhsp, ResolveInfo rhsp) {
- if (lhsp == null) {
- return -1;
- } else if (rhsp == null) {
- return 1;
- }
- return mCollator.compare(lhsp.activityInfo.packageName, rhsp.activityInfo.packageName);
- }
- }
-
}
diff --git a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
index 0651d26c..c6de3260 100644
--- a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
@@ -107,16 +107,20 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp
.setClassName(target.name.getClassName())
.build());
}
- mAppPredictor.sortTargets(
- appTargets,
- Executors.newSingleThreadExecutor(),
- new ScopedAppTargetListCallback(
- mContext,
- sortedAppTargets -> {
- onAppTargetsSorted(targets, sortedAppTargets);
- return kotlin.Unit.INSTANCE;
- }).toConsumer()
- );
+ try {
+ mAppPredictor.sortTargets(
+ appTargets,
+ Executors.newSingleThreadExecutor(),
+ new ScopedAppTargetListCallback(
+ mContext,
+ sortedAppTargets -> {
+ onAppTargetsSorted(targets, sortedAppTargets);
+ return kotlin.Unit.INSTANCE;
+ }).toConsumer()
+ );
+ } catch (IllegalStateException e) {
+ Log.w(TAG, "Couldn't sort targets with AppPredictionService", e);
+ }
}
private void onAppTargetsSorted(
@@ -292,8 +296,12 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp
new AppTarget.Builder(targetId, targetComponent.getPackageName(), mUser)
.setClassName(targetComponent.getClassName())
.build();
- mAppPredictor.notifyAppTargetEvent(
- new AppTargetEvent.Builder(appTarget, ACTION_LAUNCH).build());
+ try {
+ mAppPredictor.notifyAppTargetEvent(
+ new AppTargetEvent.Builder(appTarget, ACTION_LAUNCH).build());
+ } catch (IllegalStateException e) {
+ Log.w(TAG, "Couldn't send feedback to AppPredictionService", e);
+ }
}
}
}
diff --git a/java/src/com/android/intentresolver/model/ResolveInfoAzInfoComparator.java b/java/src/com/android/intentresolver/model/ResolveInfoAzInfoComparator.java
new file mode 100644
index 00000000..411d0c6e
--- /dev/null
+++ b/java/src/com/android/intentresolver/model/ResolveInfoAzInfoComparator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.model;
+
+import android.content.Context;
+import android.content.pm.ResolveInfo;
+
+import java.text.Collator;
+import java.util.Comparator;
+
+/**
+ * Sort intents alphabetically based on package name.
+ */
+public class ResolveInfoAzInfoComparator<T extends ResolveInfo> implements Comparator<T> {
+ Collator mCollator;
+
+ public ResolveInfoAzInfoComparator(Context context) {
+ mCollator = Collator.getInstance(context.getResources().getConfiguration().locale);
+ }
+
+ @Override
+ public int compare(ResolveInfo lhsp, ResolveInfo rhsp) {
+ if (lhsp == null) {
+ return -1;
+ } else if (rhsp == null) {
+ return 1;
+ }
+ return mCollator.compare(lhsp.activityInfo.packageName, rhsp.activityInfo.packageName);
+ }
+}
diff --git a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
index f3804154..963091b5 100644
--- a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
@@ -28,6 +28,7 @@ import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.metrics.LogMaker;
+import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
@@ -48,6 +49,7 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.google.android.collect.Lists;
+import java.lang.ref.WeakReference;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Comparator;
@@ -392,20 +394,7 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom
}
public final IResolverRankerResult resolverRankerResult =
- new IResolverRankerResult.Stub() {
- @Override
- public void sendResult(List<ResolverTarget> targets) throws RemoteException {
- if (DEBUG) {
- Log.d(TAG, "Sending Result back to Resolver: " + targets);
- }
- synchronized (mLock) {
- final Message msg = Message.obtain();
- msg.what = RANKER_SERVICE_RESULT;
- msg.obj = targets;
- mHandler.sendMessage(msg);
- }
- }
- };
+ new ResolverRankerResultCallback(mLock, mHandler);
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
@@ -437,6 +426,32 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom
}
}
+ private static class ResolverRankerResultCallback extends IResolverRankerResult.Stub {
+ private final Object mLock;
+ private final WeakReference<Handler> mHandlerRef;
+
+ private ResolverRankerResultCallback(Object lock, Handler handler) {
+ mLock = lock;
+ mHandlerRef = new WeakReference<>(handler);
+ }
+
+ @Override
+ public void sendResult(List<ResolverTarget> targets) throws RemoteException {
+ if (DEBUG) {
+ Log.d(TAG, "Sending Result back to Resolver: " + targets);
+ }
+ synchronized (mLock) {
+ final Message msg = Message.obtain();
+ msg.what = RANKER_SERVICE_RESULT;
+ msg.obj = targets;
+ Handler handler = mHandlerRef.get();
+ if (handler != null) {
+ handler.sendMessage(msg);
+ }
+ }
+ }
+ }
+
@Override
void beforeCompute() {
super.beforeCompute();
diff --git a/java/src/com/android/intentresolver/platform/AppPredictionModule.kt b/java/src/com/android/intentresolver/platform/AppPredictionModule.kt
new file mode 100644
index 00000000..415d5f7d
--- /dev/null
+++ b/java/src/com/android/intentresolver/platform/AppPredictionModule.kt
@@ -0,0 +1,42 @@
+/*
+ * 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.platform
+
+import android.content.pm.PackageManager
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Qualifier
+import javax.inject.Singleton
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class AppPredictionAvailable
+
+@Module
+@InstallIn(SingletonComponent::class)
+object AppPredictionModule {
+
+ /** Eventually replaced with: Optional<AppPredictionRepository>, etc. */
+ @Provides
+ @Singleton
+ @AppPredictionAvailable
+ fun isAppPredictionAvailable(packageManager: PackageManager): Boolean {
+ return packageManager.appPredictionServicePackageName != null
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt b/java/src/com/android/intentresolver/platform/ImageEditorModule.kt
index efbf053e..24257968 100644
--- a/java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt
+++ b/java/src/com/android/intentresolver/platform/ImageEditorModule.kt
@@ -1,4 +1,20 @@
-package com.android.intentresolver.v2.platform
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.platform
import android.content.ComponentName
import android.content.res.Resources
diff --git a/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt b/java/src/com/android/intentresolver/platform/NearbyShareModule.kt
index 25ee9198..6cb30b41 100644
--- a/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt
+++ b/java/src/com/android/intentresolver/platform/NearbyShareModule.kt
@@ -1,4 +1,20 @@
-package com.android.intentresolver.v2.platform
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.platform
import android.content.ComponentName
import android.content.res.Resources
diff --git a/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt b/java/src/com/android/intentresolver/platform/PlatformSecureSettings.kt
index 531152ba..0c802c97 100644
--- a/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt
+++ b/java/src/com/android/intentresolver/platform/PlatformSecureSettings.kt
@@ -1,4 +1,20 @@
-package com.android.intentresolver.v2.platform
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.platform
import android.content.ContentResolver
import android.provider.Settings
diff --git a/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt b/java/src/com/android/intentresolver/platform/SecureSettings.kt
index 62ee8ae9..8a1dc531 100644
--- a/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt
+++ b/java/src/com/android/intentresolver/platform/SecureSettings.kt
@@ -1,4 +1,20 @@
-package com.android.intentresolver.v2.platform
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.platform
import android.provider.Settings.SettingNotFoundException
diff --git a/java/src/com/android/intentresolver/platform/SecureSettingsModule.kt b/java/src/com/android/intentresolver/platform/SecureSettingsModule.kt
new file mode 100644
index 00000000..fa3ee4fe
--- /dev/null
+++ b/java/src/com/android/intentresolver/platform/SecureSettingsModule.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.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/profiles/AdapterBinder.java b/java/src/com/android/intentresolver/profiles/AdapterBinder.java
new file mode 100644
index 00000000..f92a140f
--- /dev/null
+++ b/java/src/com/android/intentresolver/profiles/AdapterBinder.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.profiles;
+
+/**
+ * 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);
+}
diff --git a/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/profiles/ChooserMultiProfilePagerAdapter.java
index de0a9426..8aee0da1 100644
--- a/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/profiles/ChooserMultiProfilePagerAdapter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.intentresolver.v2;
+package com.android.intentresolver.profiles;
import android.content.Context;
import android.os.UserHandle;
@@ -27,12 +27,10 @@ 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;
@@ -42,7 +40,6 @@ 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;
@@ -52,71 +49,45 @@ public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<
public ChooserMultiProfilePagerAdapter(
Context context,
- ChooserGridAdapter adapter,
+ ImmutableList<TabConfig<ChooserGridAdapter>> tabs,
EmptyStateProvider emptyStateProvider,
Supplier<Boolean> workProfileQuietModeChecker,
+ @ProfileType int defaultProfile,
UserHandle workProfileUserHandle,
UserHandle cloneProfileUserHandle,
- int maxTargetsPerRow,
- FeatureFlags featureFlags) {
+ int maxTargetsPerRow) {
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),
+ tabs,
emptyStateProvider,
workProfileQuietModeChecker,
defaultProfile,
workProfileUserHandle,
cloneProfileUserHandle,
- new BottomPaddingOverrideSupplier(context),
- featureFlags);
+ new BottomPaddingOverrideSupplier(context));
}
private ChooserMultiProfilePagerAdapter(
Context context,
ChooserProfileAdapterBinder adapterBinder,
- ImmutableList<ChooserGridAdapter> gridAdapters,
+ ImmutableList<TabConfig<ChooserGridAdapter>> tabs,
EmptyStateProvider emptyStateProvider,
Supplier<Boolean> workProfileQuietModeChecker,
- @Profile int defaultProfile,
+ @ProfileType int defaultProfile,
UserHandle workProfileUserHandle,
UserHandle cloneProfileUserHandle,
- BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier,
- FeatureFlags featureFlags) {
+ BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {
super(
- gridAdapter -> gridAdapter.getListAdapter(),
+ gridAdapter -> gridAdapter.getListAdapter(),
adapterBinder,
- gridAdapters,
+ tabs,
emptyStateProvider,
workProfileQuietModeChecker,
defaultProfile,
workProfileUserHandle,
cloneProfileUserHandle,
- () -> makeProfileView(context, featureFlags),
+ () -> makeProfileView(context),
bottomPaddingOverrideSupplier);
mAdapterBinder = adapterBinder;
mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier;
@@ -137,16 +108,14 @@ public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<
*/
public void setIsCollapsed(boolean isCollapsed) {
for (int i = 0, size = getItemCount(); i < size; i++) {
- getAdapterForIndex(i).setAzLabelVisibility(!isCollapsed);
+ getPageAdapterForIndex(i).setAzLabelVisibility(!isCollapsed);
}
}
- private static ViewGroup makeProfileView(
- Context context, FeatureFlags featureFlags) {
+ private static ViewGroup makeProfileView(Context context) {
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);
+ ViewGroup rootView =
+ (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile_wrap, null, false);
RecyclerView recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list);
recyclerView.setAccessibilityDelegateCompat(
new ChooserRecyclerViewAccessibilityDelegate(recyclerView));
@@ -172,7 +141,17 @@ public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<
/** 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);
+ getPageAdapterForIndex(i).setFooterHeight(height);
+ }
+ }
+
+ /** Cleanup system resources */
+ public void destroy() {
+ for (int i = 0, count = getItemCount(); i < count; i++) {
+ ChooserGridAdapter adapter = getPageAdapterForIndex(i);
+ if (adapter != null) {
+ adapter.getListAdapter().onDestroy();
+ }
}
}
diff --git a/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/profiles/MultiProfilePagerAdapter.java
index 2d9be816..11a6caca 100644
--- a/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/profiles/MultiProfilePagerAdapter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -13,14 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.intentresolver.v2;
+package com.android.intentresolver.profiles;
-import android.annotation.IntDef;
import android.annotation.Nullable;
import android.os.Trace;
import android.os.UserHandle;
+import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TabHost;
+import android.widget.TextView;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
@@ -28,33 +31,22 @@ 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.intentresolver.shared.model.Profile;
import com.android.internal.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import java.util.HashSet;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.function.Consumer;
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
@@ -69,24 +61,12 @@ public class MultiProfilePagerAdapter<
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 = Profile.Type.PERSONAL.ordinal();
+ public static final int PROFILE_WORK = Profile.Type.WORK.ordinal();
- public static final int PROFILE_PERSONAL = 0;
- public static final int PROFILE_WORK = 1;
-
- @IntDef({PROFILE_PERSONAL, PROFILE_WORK})
- public @interface Profile {}
+ // Removed, must be constants. This is only used for linting anyway.
+ // @IntDef({PROFILE_PERSONAL, PROFILE_WORK})
+ public @interface ProfileType {}
private final Function<SinglePageAdapterT, ListAdapterT> mListAdapterExtractor;
private final AdapterBinder<PageViewT, SinglePageAdapterT> mAdapterBinder;
@@ -99,22 +79,21 @@ public class MultiProfilePagerAdapter<
private final UserHandle mCloneProfileUserHandle;
private final Supplier<Boolean> mWorkProfileQuietModeChecker; // True when work is quiet.
- private Set<Integer> mLoadedPages;
+ private final Set<Integer> mLoadedPages;
private int mCurrentPage;
private OnProfileSelectedListener mOnProfileSelectedListener;
protected MultiProfilePagerAdapter(
Function<SinglePageAdapterT, ListAdapterT> listAdapterExtractor,
AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder,
- ImmutableList<SinglePageAdapterT> adapters,
+ ImmutableList<TabConfig<SinglePageAdapterT>> tabs,
EmptyStateProvider emptyStateProvider,
Supplier<Boolean> workProfileQuietModeChecker,
- @Profile int defaultProfile,
+ @ProfileType int defaultProfile,
UserHandle workProfileUserHandle,
UserHandle cloneProfileUserHandle,
Supplier<ViewGroup> pageViewInflater,
Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
- mCurrentPage = defaultProfile;
mLoadedPages = new HashSet<>();
mWorkProfileUserHandle = workProfileUserHandle;
mCloneProfileUserHandle = cloneProfileUserHandle;
@@ -127,21 +106,191 @@ public class MultiProfilePagerAdapter<
ImmutableList.Builder<ProfileDescriptor<PageViewT, SinglePageAdapterT>> items =
new ImmutableList.Builder<>();
- for (SinglePageAdapterT adapter : adapters) {
- items.add(createProfileDescriptor(adapter, containerBottomPaddingOverrideSupplier));
+ for (TabConfig<SinglePageAdapterT> tab : tabs) {
+ // TODO: consider representing tabConfig in a different data structure that can ensure
+ // uniqueness of their profile assignments (while still respecting the client's
+ // requested tab order).
+ items.add(
+ createProfileDescriptor(
+ tab.mProfile,
+ tab.mTabLabel,
+ tab.mTabAccessibilityLabel,
+ tab.mTabTag,
+ tab.mPageAdapter,
+ containerBottomPaddingOverrideSupplier));
}
mItems = items.build();
+
+ mCurrentPage =
+ hasPageForProfile(defaultProfile) ? getPageNumberForProfile(defaultProfile) : 0;
}
private ProfileDescriptor<PageViewT, SinglePageAdapterT> createProfileDescriptor(
+ @ProfileType int profile,
+ String tabLabel,
+ String tabAccessibilityLabel,
+ String tabTag,
SinglePageAdapterT adapter,
Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
return new ProfileDescriptor<>(
- mPageViewInflater.get(), adapter, containerBottomPaddingOverrideSupplier);
+ profile,
+ tabLabel,
+ tabAccessibilityLabel,
+ tabTag,
+ mPageViewInflater.get(),
+ adapter,
+ containerBottomPaddingOverrideSupplier);
}
- public void setOnProfileSelectedListener(OnProfileSelectedListener listener) {
- mOnProfileSelectedListener = listener;
+ private boolean hasPageForIndex(int pageIndex) {
+ return (pageIndex >= 0) && (pageIndex < getCount());
+ }
+
+ public final boolean hasPageForProfile(@ProfileType int profile) {
+ return hasPageForIndex(getPageNumberForProfile(profile));
+ }
+
+ private @ProfileType int getProfileForPageNumber(int position) {
+ if (hasPageForIndex(position)) {
+ return mItems.get(position).mProfile;
+ }
+ return -1;
+ }
+
+ public int getPageNumberForProfile(@ProfileType int profile) {
+ for (int i = 0; i < mItems.size(); ++i) {
+ if (profile == mItems.get(i).mProfile) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private ListAdapterT getListAdapterForPageNumber(int pageNumber) {
+ SinglePageAdapterT pageAdapter = getPageAdapterForIndex(pageNumber);
+ if (pageAdapter == null) {
+ return null;
+ }
+ return mListAdapterExtractor.apply(pageAdapter);
+ }
+
+ private @ProfileType int getProfileForUserHandle(UserHandle userHandle) {
+ if (userHandle.equals(getCloneUserHandle())) {
+ // TODO: can we push this special case elsewhere -- e.g., when we check against each
+ // list adapter's user handle in the loop below, could we instead ask the list adapter
+ // whether it "represents" the queried user handle, and have the personal list adapter
+ // return true because it knows it's also associated with the clone profile? Or if we
+ // don't want to make modifications to the list adapter, maybe we could at least specify
+ // it in our per-page configuration data that we use to build our tabs/pages, and then
+ // maintain the relevant bookkeeping in our own ProfileDescriptor?
+ return PROFILE_PERSONAL;
+ }
+ for (int i = 0; i < mItems.size(); ++i) {
+ ListAdapterT listAdapter = getListAdapterForPageNumber(i);
+ if (listAdapter.getUserHandle().equals(userHandle)) {
+ return mItems.get(i).mProfile;
+ }
+ }
+ return -1;
+ }
+
+ private int getPageNumberForUserHandle(UserHandle userHandle) {
+ return getPageNumberForProfile(getProfileForUserHandle(userHandle));
+ }
+
+ /**
+ * 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) {
+ return getListAdapterForPageNumber(getPageNumberForUserHandle(userHandle));
+ }
+
+ @Nullable
+ private ProfileDescriptor<PageViewT, SinglePageAdapterT> getDescriptorForUserHandle(
+ UserHandle userHandle) {
+ return getItem(getPageNumberForUserHandle(userHandle));
+ }
+
+ private int getPageNumberForTabTag(String tag) {
+ for (int i = 0; i < mItems.size(); ++i) {
+ if (Objects.equals(mItems.get(i).mTabTag, tag)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ private void updateActiveTabStyle(TabHost tabHost) {
+ int currentTab = tabHost.getCurrentTab();
+
+ for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) {
+ // TODO: can we avoid this downcast by pushing our knowledge of the intended view type
+ // somewhere else?
+ TextView tabText = (TextView) tabHost.getTabWidget().getChildAt(pageNumber);
+ tabText.setSelected(currentTab == pageNumber);
+ }
+ }
+
+ public void setupProfileTabs(
+ LayoutInflater layoutInflater,
+ TabHost tabHost,
+ ViewPager viewPager,
+ int tabButtonLayoutResId,
+ int tabPageContentViewId,
+ Runnable onTabChangeListener,
+ OnProfileSelectedListener clientOnProfileSelectedListener) {
+ tabHost.setup();
+ tabHost.getTabWidget().removeAllViews();
+ viewPager.setSaveEnabled(false);
+
+ for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) {
+ ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = mItems.get(pageNumber);
+ Button profileButton = (Button) layoutInflater.inflate(
+ tabButtonLayoutResId, tabHost.getTabWidget(), false);
+ profileButton.setText(descriptor.mTabLabel);
+ profileButton.setContentDescription(descriptor.mTabAccessibilityLabel);
+
+ TabHost.TabSpec profileTabSpec = tabHost.newTabSpec(descriptor.mTabTag)
+ .setContent(tabPageContentViewId)
+ .setIndicator(profileButton);
+ tabHost.addTab(profileTabSpec);
+ }
+
+ tabHost.getTabWidget().setVisibility(View.VISIBLE);
+
+ updateActiveTabStyle(tabHost);
+
+ tabHost.setOnTabChangedListener(tabTag -> {
+ updateActiveTabStyle(tabHost);
+
+ int pageNumber = getPageNumberForTabTag(tabTag);
+ if (pageNumber >= 0) {
+ viewPager.setCurrentItem(pageNumber);
+ }
+ onTabChangeListener.run();
+ });
+
+ viewPager.setVisibility(View.VISIBLE);
+ tabHost.setCurrentTab(getCurrentPage());
+ mOnProfileSelectedListener =
+ new OnProfileSelectedListener() {
+ @Override
+ public void onProfilePageSelected(@ProfileType int profileId, int pageNumber) {
+ tabHost.setCurrentTab(pageNumber);
+ clientOnProfileSelectedListener.onProfilePageSelected(
+ profileId, pageNumber);
+ }
+
+ @Override
+ public void onProfilePageStateChanged(int state) {
+ clientOnProfileSelectedListener.onProfilePageStateChanged(state);
+ }
+ };
}
/**
@@ -153,14 +302,7 @@ public class MultiProfilePagerAdapter<
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);
- }
+ MultiProfilePagerAdapter.this.onPageSelected(position);
}
@Override
@@ -175,11 +317,20 @@ public class MultiProfilePagerAdapter<
mLoadedPages.add(mCurrentPage);
}
- public void clearInactiveProfileCache() {
- if (mLoadedPages.size() == 1) {
- return;
+ private void onPageSelected(int position) {
+ mCurrentPage = position;
+ if (!mLoadedPages.contains(position)) {
+ rebuildActiveTab(true);
+ mLoadedPages.add(position);
+ }
+ if (mOnProfileSelectedListener != null) {
+ mOnProfileSelectedListener.onProfilePageSelected(
+ getProfileForPageNumber(position), position);
}
- mLoadedPages.remove(1 - mCurrentPage);
+ }
+
+ public void clearInactiveProfileCache() {
+ forEachInactivePage(pageNumber -> mLoadedPages.remove(pageNumber));
}
@Override
@@ -204,12 +355,15 @@ public class MultiProfilePagerAdapter<
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();
+ /**
+ * Set active adapter page. A support method for the poayload reselection logic.
+ */
+ public void setCurrentPage(int page) {
+ onPageSelected(page);
+ }
+
+ public final @ProfileType int getActiveProfile() {
+ return getProfileForPageNumber(getCurrentPage());
}
@VisibleForTesting
@@ -241,7 +395,11 @@ public class MultiProfilePagerAdapter<
* <code>1</code> would return the work profile {@link ProfileDescriptor}.</li>
* </ul>
*/
+ @Nullable
private ProfileDescriptor<PageViewT, SinglePageAdapterT> getItem(int pageIndex) {
+ if (!hasPageForIndex(pageIndex)) {
+ return null;
+ }
return mItems.get(pageIndex);
}
@@ -263,7 +421,7 @@ public class MultiProfilePagerAdapter<
}
public final PageViewT getListViewForIndex(int index) {
- return getItem(index).mView;
+ return getItem(index).getView();
}
/**
@@ -273,8 +431,11 @@ public class MultiProfilePagerAdapter<
* depending on the adapter type.
*/
@VisibleForTesting
- public final SinglePageAdapterT getAdapterForIndex(int index) {
- return getItem(index).mAdapter;
+ public final SinglePageAdapterT getPageAdapterForIndex(int index) {
+ if (!hasPageForIndex(index)) {
+ return null;
+ }
+ return getItem(index).getAdapter();
}
/**
@@ -282,26 +443,7 @@ public class MultiProfilePagerAdapter<
* 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;
+ mAdapterBinder.bind(getListViewForIndex(pageIndex), getPageAdapterForIndex(pageIndex));
}
/**
@@ -309,70 +451,35 @@ public class MultiProfilePagerAdapter<
* 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()));
+ return getListAdapterForPageNumber(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);
+ return getListAdapterForPageNumber(getPageNumberForProfile(PROFILE_PERSONAL));
}
@Nullable
public final ListAdapterT getWorkListAdapter() {
- if (!hasAdapterForIndex(PROFILE_WORK)) {
+ if (!hasPageForProfile(PROFILE_WORK)) {
return null;
}
- return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_WORK));
+ return getListAdapterForPageNumber(getPageNumberForProfile(PROFILE_WORK));
}
public final SinglePageAdapterT getCurrentRootAdapter() {
- return getAdapterForIndex(getCurrentPage());
+ return getPageAdapterForIndex(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));
+ ListAdapterT listAdapter = getListAdapterForPageNumber(i);
if (listAdapter.getCount() > 0) {
return true;
}
@@ -381,13 +488,10 @@ public class MultiProfilePagerAdapter<
}
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.
+ // TODO: 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();
- }
+ forEachInactivePage(page -> getListAdapterForPageNumber(page).handlePackagesChanged());
}
/**
@@ -445,9 +549,10 @@ public class MultiProfilePagerAdapter<
// autolaunch conditions).
boolean rebuildCompleted = rebuildActiveTab(true) || getActiveListAdapter().isTabLoaded();
if (includePartialRebuildOfInactiveTabs) {
- boolean rebuildInactiveCompleted =
- rebuildInactiveTab(false) || getInactiveListAdapter().isTabLoaded();
- rebuildCompleted = rebuildCompleted && rebuildInactiveCompleted;
+ // Per legacy logic, avoid short-circuiting (TODO: why? possibly so that we *start*
+ // loading the inactive tabs even if we're still waiting on the active tab to finish?).
+ boolean completedRebuildingInactiveTabs = rebuildInactiveTabs(false);
+ rebuildCompleted = rebuildCompleted && completedRebuildingInactiveTabs;
}
return rebuildCompleted;
}
@@ -464,28 +569,43 @@ public class MultiProfilePagerAdapter<
}
/**
- * Rebuilds the tab that is not currently visible to the user, if such one exists.
- * <p>Returns {@code true} if rebuild has completed.
+ * Rebuilds any tabs that are not currently visible to the user.
+ * <p>Returns {@code true} if rebuild has completed in all inactive tabs.
*/
- private boolean rebuildInactiveTab(boolean doPostProcessing) {
+ private boolean rebuildInactiveTabs(boolean doPostProcessing) {
Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab");
- if (getItemCount() == 1) {
- Trace.endSection();
- return false;
- }
- boolean result = rebuildTab(getInactiveListAdapter(), doPostProcessing);
+ AtomicBoolean allRebuildsComplete = new AtomicBoolean(true);
+ forEachInactivePage(pageNumber -> {
+ // Evaluate the rebuild for every inactive page, even if we've already seen some adapter
+ // return an "incomplete" status (i.e., even if `allRebuildsComplete` is already false)
+ // and so we already know we'll end up returning false for the batch.
+ // TODO: any particular reason the per-page legacy logic was set up in this order, or
+ // could we possibly short-circuit the rebuild if the tab is already "loaded"?
+ ListAdapterT inactiveAdapter = getListAdapterForPageNumber(pageNumber);
+ boolean rebuildInactivePageCompleted =
+ rebuildTab(inactiveAdapter, doPostProcessing) || inactiveAdapter.isTabLoaded();
+ if (!rebuildInactivePageCompleted) {
+ allRebuildsComplete.set(false);
+ }
+ });
Trace.endSection();
- return result;
+ return allRebuildsComplete.get();
}
- private int userHandleToPageIndex(UserHandle userHandle) {
- if (userHandle.equals(getPersonalListAdapter().getUserHandle())) {
- return PROFILE_PERSONAL;
- } else {
- return PROFILE_WORK;
+ protected void forEachPage(Consumer<Integer> pageNumberHandler) {
+ for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) {
+ pageNumberHandler.accept(pageNumber);
}
}
+ protected void forEachInactivePage(Consumer<Integer> inactivePageNumberHandler) {
+ forEachPage(pageNumber -> {
+ if (pageNumber != getCurrentPage()) {
+ inactivePageNumberHandler.accept(pageNumber);
+ }
+ });
+ }
+
protected boolean rebuildTab(ListAdapterT activeListAdapter, boolean doPostProcessing) {
if (shouldSkipRebuild(activeListAdapter)) {
activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true);
@@ -499,10 +619,6 @@ public class MultiProfilePagerAdapter<
return emptyState != null && emptyState.shouldSkipDataRebuild();
}
- private boolean hasAdapterForIndex(int pageIndex) {
- return (pageIndex < getCount());
- }
-
/**
* The empty state screens are shown according to their priority:
* <ol>
@@ -531,8 +647,8 @@ public class MultiProfilePagerAdapter<
if (emptyState.getButtonClickListener() != null) {
clickListener = v -> emptyState.getButtonClickListener().onClick(() -> {
- ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
- userHandleToPageIndex(listAdapter.getUserHandle()));
+ ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor =
+ getDescriptorForUserHandle(listAdapter.getUserHandle());
descriptor.mEmptyStateUi.showSpinner();
});
}
@@ -540,24 +656,12 @@ public class MultiProfilePagerAdapter<
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()));
+ ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor =
+ getDescriptorForUserHandle(activeListAdapter.getUserHandle());
descriptor.mEmptyStateUi.showEmptyState(emptyState, buttonOnClick);
activeListAdapter.markTabLoaded();
}
@@ -571,8 +675,8 @@ public class MultiProfilePagerAdapter<
}
public void showListView(ListAdapterT activeListAdapter) {
- ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
- userHandleToPageIndex(activeListAdapter.getUserHandle()));
+ ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor =
+ getDescriptorForUserHandle(activeListAdapter.getUserHandle());
descriptor.mEmptyStateUi.hide();
}
@@ -581,11 +685,14 @@ public class MultiProfilePagerAdapter<
* 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());
+ AtomicBoolean anyEmpty = new AtomicBoolean(false);
+ // TODO: The "inactive" condition is legacy logic. Could we simplify and ask "any"?
+ forEachInactivePage(pageNumber -> {
+ if (shouldShowEmptyStateScreen(getListAdapterForPageNumber(pageNumber))) {
+ anyEmpty.set(true);
+ }
+ });
+ return anyEmpty.get();
}
public boolean shouldShowEmptyStateScreen(ListAdapterT listAdapter) {
@@ -595,72 +702,4 @@ public class MultiProfilePagerAdapter<
&& 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/profiles/OnProfileSelectedListener.java b/java/src/com/android/intentresolver/profiles/OnProfileSelectedListener.java
new file mode 100644
index 00000000..e6299954
--- /dev/null
+++ b/java/src/com/android/intentresolver/profiles/OnProfileSelectedListener.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.profiles;
+
+import androidx.viewpager.widget.ViewPager;
+
+/** Listener interface for changes between the per-profile UI tabs. */
+public interface OnProfileSelectedListener {
+ /**
+ * Callback for when the user changes the active tab.
+ * <p>This callback is only called when the intent resolver or share sheet shows
+ * more than one profile.
+ *
+ * @param profileId the ID of the newly-selected profile, e.g. {@link #PROFILE_PERSONAL}
+ * if the personal profile tab was selected or {@link #PROFILE_WORK} if the
+ * work profile tab
+ * was selected.
+ */
+ void onProfilePageSelected(@MultiProfilePagerAdapter.ProfileType int profileId, int pageNumber);
+
+
+ /**
+ * 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);
+}
diff --git a/java/src/com/android/intentresolver/profiles/OnSwitchOnWorkSelectedListener.java b/java/src/com/android/intentresolver/profiles/OnSwitchOnWorkSelectedListener.java
new file mode 100644
index 00000000..7989551a
--- /dev/null
+++ b/java/src/com/android/intentresolver/profiles/OnSwitchOnWorkSelectedListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.profiles;
+
+/**
+ * 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/profiles/ProfileDescriptor.java b/java/src/com/android/intentresolver/profiles/ProfileDescriptor.java
new file mode 100644
index 00000000..61c7c670
--- /dev/null
+++ b/java/src/com/android/intentresolver/profiles/ProfileDescriptor.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.profiles;
+
+import android.view.ViewGroup;
+
+import com.android.intentresolver.emptystate.EmptyStateUiHelper;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+
+// 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)?
+class ProfileDescriptor<PageViewT, SinglePageAdapterT> {
+ final @MultiProfilePagerAdapter.ProfileType int mProfile;
+ final String mTabLabel;
+ final String mTabAccessibilityLabel;
+ final String mTabTag;
+
+ 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;
+
+ public SinglePageAdapterT getAdapter() {
+ return mAdapter;
+ }
+
+ public PageViewT getView() {
+ return mView;
+ }
+
+ private final PageViewT mView;
+
+ ProfileDescriptor(
+ @MultiProfilePagerAdapter.ProfileType int forProfile,
+ String tabLabel,
+ String tabAccessibilityLabel,
+ String tabTag,
+ ViewGroup rootView,
+ SinglePageAdapterT adapter,
+ Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
+ mProfile = forProfile;
+ mTabLabel = tabLabel;
+ mTabAccessibilityLabel = tabAccessibilityLabel;
+ mTabTag = tabTag;
+ 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;
+ }
+
+ public void setupContainerPadding() {
+ mEmptyStateUi.setupContainerPadding();
+ }
+}
diff --git a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/profiles/ResolverMultiProfilePagerAdapter.java
index 591c23b7..0c669510 100644
--- a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/profiles/ResolverMultiProfilePagerAdapter.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.intentresolver;
+package com.android.intentresolver.profiles;
import android.content.Context;
import android.os.UserHandle;
@@ -24,8 +24,9 @@ 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;
@@ -35,40 +36,20 @@ 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,
+ ImmutableList<TabConfig<ResolverListAdapter>> tabs,
EmptyStateProvider emptyStateProvider,
Supplier<Boolean> workProfileQuietModeChecker,
- @Profile int defaultProfile,
+ @ProfileType int defaultProfile,
UserHandle workProfileUserHandle,
UserHandle cloneProfileUserHandle) {
this(
context,
- ImmutableList.of(personalAdapter, workAdapter),
+ tabs,
emptyStateProvider,
workProfileQuietModeChecker,
defaultProfile,
@@ -79,17 +60,17 @@ public class ResolverMultiProfilePagerAdapter extends
private ResolverMultiProfilePagerAdapter(
Context context,
- ImmutableList<ResolverListAdapter> listAdapters,
+ ImmutableList<TabConfig<ResolverListAdapter>> tabs,
EmptyStateProvider emptyStateProvider,
Supplier<Boolean> workProfileQuietModeChecker,
- @Profile int defaultProfile,
+ @ProfileType int defaultProfile,
UserHandle workProfileUserHandle,
UserHandle cloneProfileUserHandle,
BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {
super(
listAdapter -> listAdapter,
(listView, bindAdapter) -> listView.setAdapter(bindAdapter),
- listAdapters,
+ tabs,
emptyStateProvider,
workProfileQuietModeChecker,
defaultProfile,
@@ -105,6 +86,17 @@ public class ResolverMultiProfilePagerAdapter extends
mBottomPaddingOverrideSupplier.setUseLayoutWithDefault(useLayoutWithDefault);
}
+ /** Un-check any item(s) that may be checked in any of our inactive adapter(s). */
+ public void clearCheckedItemsInInactiveProfiles() {
+ // TODO: The "inactive" condition is legacy logic. Could we simplify and clear-all?
+ forEachInactivePage(pageNumber -> {
+ ListView inactiveListView = getListViewForIndex(pageNumber);
+ if (inactiveListView.getCheckedItemCount() > 0) {
+ inactiveListView.setItemChecked(inactiveListView.getCheckedItemPosition(), false);
+ }
+ });
+ }
+
private static class BottomPaddingOverrideSupplier implements Supplier<Optional<Integer>> {
private boolean mUseLayoutWithDefault;
diff --git a/java/src/com/android/intentresolver/profiles/TabConfig.java b/java/src/com/android/intentresolver/profiles/TabConfig.java
new file mode 100644
index 00000000..320f069a
--- /dev/null
+++ b/java/src/com/android/intentresolver/profiles/TabConfig.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.profiles;
+
+public class TabConfig<PageAdapterT> {
+ final @MultiProfilePagerAdapter.ProfileType int mProfile;
+ final String mTabLabel;
+ final String mTabAccessibilityLabel;
+ final String mTabTag;
+ final PageAdapterT mPageAdapter;
+
+ public TabConfig(
+ @MultiProfilePagerAdapter.ProfileType int profile,
+ String tabLabel,
+ String tabAccessibilityLabel,
+ String tabTag,
+ PageAdapterT pageAdapter) {
+ mProfile = profile;
+ mTabLabel = tabLabel;
+ mTabAccessibilityLabel = tabAccessibilityLabel;
+ mTabTag = tabTag;
+ mPageAdapter = pageAdapter;
+ }
+}
diff --git a/java/src/com/android/intentresolver/shared/model/Profile.kt b/java/src/com/android/intentresolver/shared/model/Profile.kt
new file mode 100644
index 00000000..c557c151
--- /dev/null
+++ b/java/src/com/android/intentresolver/shared/model/Profile.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.shared.model
+
+import com.android.intentresolver.shared.model.Profile.Type
+
+/**
+ * Associates [users][User] into a [Type] instance.
+ *
+ * This is a simple abstraction which combines a primary [user][User] with an optional
+ * [cloned apps][User.Role.CLONE] user. This encapsulates the cloned app user id, while still being
+ * available where needed.
+ */
+data class Profile(
+ val type: Type,
+ val primary: User,
+ /**
+ * An optional [User] of which contains second instances of some applications installed for the
+ * personal user. This value may only be supplied when creating the PERSONAL profile.
+ */
+ val clone: User? = null
+) {
+
+ init {
+ clone?.apply {
+ require(primary.role == User.Role.PERSONAL) {
+ "clone is not supported for profile=${this@Profile.type} / primary=$primary"
+ }
+ require(role == User.Role.CLONE) { "clone is not a clone user ($this)" }
+ }
+ }
+
+ enum class Type {
+ PERSONAL,
+ WORK,
+ PRIVATE
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/data/model/User.kt b/java/src/com/android/intentresolver/shared/model/User.kt
index 504b04c8..b544a390 100644
--- a/java/src/com/android/intentresolver/v2/data/model/User.kt
+++ b/java/src/com/android/intentresolver/shared/model/User.kt
@@ -1,10 +1,23 @@
-package com.android.intentresolver.v2.data.model
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.shared.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.
@@ -30,21 +43,10 @@ data class User(
) {
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)
+ enum class Role {
+ PERSONAL,
+ PRIVATE,
+ WORK,
+ CLONE
}
}
diff --git a/java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt b/java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt
index 82f40b91..c7bd0336 100644
--- a/java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt
+++ b/java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt
@@ -31,37 +31,39 @@ private const val SHARED_TEXT_KEY = "shared_text"
/**
* A factory to create an AppPredictor instance for a profile, if available.
+ *
* @param context, application context
- * @param sharedText, a shared text associated with the Chooser's target intent
- * (see [android.content.Intent.EXTRA_TEXT]).
- * Will be mapped to app predictor's "shared_text" parameter.
- * @param targetIntentFilter, an IntentFilter to match direct share targets against.
- * Will be mapped app predictor's "intent_filter" parameter.
+ * @param sharedText, a shared text associated with the Chooser's target intent (see
+ * [android.content.Intent.EXTRA_TEXT]). Will be mapped to app predictor's "shared_text"
+ * parameter.
+ * @param targetIntentFilter, an IntentFilter to match direct share targets against. Will be mapped
+ * app predictor's "intent_filter" parameter.
*/
class AppPredictorFactory(
private val context: Context,
private val sharedText: String?,
- private val targetIntentFilter: IntentFilter?
+ private val targetIntentFilter: IntentFilter?,
+ private val appPredictionAvailable: Boolean,
) {
- private val mIsComponentAvailable =
- context.packageManager.appPredictionServicePackageName != null
-
/**
* Creates an AppPredictor instance for a profile or `null` if app predictor is not available.
*/
fun create(userHandle: UserHandle): AppPredictor? {
- if (!mIsComponentAvailable) return null
+ if (!appPredictionAvailable) return null
val contextAsUser = context.createContextAsUser(userHandle, 0 /* flags */)
- val extras = Bundle().apply {
- putParcelable(APP_PREDICTION_INTENT_FILTER_KEY, targetIntentFilter)
- putString(SHARED_TEXT_KEY, sharedText)
- }
- val appPredictionContext = AppPredictionContext.Builder(contextAsUser)
- .setUiSurface(APP_PREDICTION_SHARE_UI_SURFACE)
- .setPredictedTargetCount(APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT)
- .setExtras(extras)
- .build()
- return contextAsUser.getSystemService(AppPredictionManager::class.java)
+ val extras =
+ Bundle().apply {
+ putParcelable(APP_PREDICTION_INTENT_FILTER_KEY, targetIntentFilter)
+ putString(SHARED_TEXT_KEY, sharedText)
+ }
+ val appPredictionContext =
+ AppPredictionContext.Builder(contextAsUser)
+ .setUiSurface(APP_PREDICTION_SHARE_UI_SURFACE)
+ .setPredictedTargetCount(APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT)
+ .setExtras(extras)
+ .build()
+ return contextAsUser
+ .getSystemService(AppPredictionManager::class.java)
?.createAppPredictionSession(appPredictionContext)
}
}
diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
index a8b59fb0..08230d90 100644
--- a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
+++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
@@ -186,6 +186,11 @@ constructor(
// Default to just querying ShortcutManager if AppPredictor not present.
if (targetIntentFilter == null) {
Log.d(TAG, "skip querying ShortcutManager for $userHandle")
+ sendShareShortcutInfoList(
+ emptyList(),
+ isFromAppPredictor = false,
+ appPredictorTargets = null
+ )
return
}
Log.d(TAG, "query ShortcutManager for user $userHandle")
diff --git a/java/src/com/android/intentresolver/v2/ui/ActionTitle.java b/java/src/com/android/intentresolver/ui/ActionTitle.java
index 271c6f38..1cc96fa9 100644
--- a/java/src/com/android/intentresolver/v2/ui/ActionTitle.java
+++ b/java/src/com/android/intentresolver/ui/ActionTitle.java
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.intentresolver.v2.ui;
+package com.android.intentresolver.ui;
import android.content.Intent;
import android.provider.MediaStore;
@@ -21,7 +21,6 @@ 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.
diff --git a/java/src/com/android/intentresolver/ui/ProfilePagerResources.kt b/java/src/com/android/intentresolver/ui/ProfilePagerResources.kt
new file mode 100644
index 00000000..0d07af8f
--- /dev/null
+++ b/java/src/com/android/intentresolver/ui/ProfilePagerResources.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.ui
+
+import android.content.res.Resources
+import com.android.intentresolver.R
+import com.android.intentresolver.data.repository.DevicePolicyResources
+import com.android.intentresolver.inject.ApplicationOwned
+import com.android.intentresolver.shared.model.Profile
+import javax.inject.Inject
+
+class ProfilePagerResources
+@Inject
+constructor(
+ @ApplicationOwned private val resources: Resources,
+ private val devicePolicyResources: DevicePolicyResources
+) {
+ private val privateTabLabel by lazy { resources.getString(R.string.resolver_private_tab) }
+
+ private val privateTabAccessibilityLabel by lazy {
+ resources.getString(R.string.resolver_private_tab_accessibility)
+ }
+
+ fun profileTabLabel(profile: Profile.Type): String {
+ return when (profile) {
+ Profile.Type.PERSONAL -> devicePolicyResources.personalTabLabel
+ Profile.Type.WORK -> devicePolicyResources.workTabLabel
+ Profile.Type.PRIVATE -> privateTabLabel
+ }
+ }
+
+ fun profileTabAccessibilityLabel(type: Profile.Type): String {
+ return when (type) {
+ Profile.Type.PERSONAL -> devicePolicyResources.personalTabAccessibilityLabel
+ Profile.Type.WORK -> devicePolicyResources.workTabAccessibilityLabel
+ Profile.Type.PRIVATE -> privateTabAccessibilityLabel
+ }
+ }
+
+ fun noAppsMessage(type: Profile.Type): String {
+ return when (type) {
+ Profile.Type.PERSONAL -> devicePolicyResources.noPersonalApps
+ Profile.Type.WORK -> devicePolicyResources.noWorkApps
+ Profile.Type.PRIVATE -> resources.getString(R.string.resolver_no_private_apps_available)
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ui/ShareResultSender.kt b/java/src/com/android/intentresolver/ui/ShareResultSender.kt
new file mode 100644
index 00000000..7be2076e
--- /dev/null
+++ b/java/src/com/android/intentresolver/ui/ShareResultSender.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.ui
+
+import android.app.Activity
+import android.app.compat.CompatChanges
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.IntentSender
+import android.service.chooser.ChooserResult
+import android.service.chooser.ChooserResult.CHOOSER_RESULT_COPY
+import android.service.chooser.ChooserResult.CHOOSER_RESULT_EDIT
+import android.service.chooser.ChooserResult.CHOOSER_RESULT_SELECTED_COMPONENT
+import android.service.chooser.ChooserResult.CHOOSER_RESULT_UNKNOWN
+import android.service.chooser.ChooserResult.ResultType
+import android.util.Log
+import com.android.intentresolver.inject.Background
+import com.android.intentresolver.inject.ChooserServiceFlags
+import com.android.intentresolver.inject.Main
+import com.android.intentresolver.ui.model.ShareAction
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import dagger.hilt.android.qualifiers.ActivityContext
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+
+private const val TAG = "ShareResultSender"
+
+/** Reports the result of a share to another process across binder, via an [IntentSender] */
+interface ShareResultSender {
+ /** Reports user selection of an activity to launch from the provided choices. */
+ fun onComponentSelected(component: ComponentName, directShare: Boolean)
+
+ /** Reports user invocation of a built-in system action. See [ShareAction]. */
+ fun onActionSelected(action: ShareAction)
+}
+
+@AssistedFactory
+interface ShareResultSenderFactory {
+ fun create(callerUid: Int, chosenComponentSender: IntentSender): ShareResultSenderImpl
+}
+
+/** Dispatches Intents via IntentSender */
+fun interface IntentSenderDispatcher {
+ fun dispatchIntent(intentSender: IntentSender, intent: Intent)
+}
+
+class ShareResultSenderImpl(
+ private val flags: ChooserServiceFlags,
+ @Main private val scope: CoroutineScope,
+ @Background val backgroundDispatcher: CoroutineDispatcher,
+ private val callerUid: Int,
+ private val resultSender: IntentSender,
+ private val intentDispatcher: IntentSenderDispatcher
+) : ShareResultSender {
+ @AssistedInject
+ constructor(
+ @ActivityContext context: Context,
+ flags: ChooserServiceFlags,
+ @Main scope: CoroutineScope,
+ @Background backgroundDispatcher: CoroutineDispatcher,
+ @Assisted callerUid: Int,
+ @Assisted chosenComponentSender: IntentSender,
+ ) : this(
+ flags,
+ scope,
+ backgroundDispatcher,
+ callerUid,
+ chosenComponentSender,
+ IntentSenderDispatcher { sender, intent -> sender.dispatchIntent(context, intent) }
+ )
+
+ override fun onComponentSelected(component: ComponentName, directShare: Boolean) {
+ Log.i(TAG, "onComponentSelected: $component directShare=$directShare")
+ scope.launch {
+ val intent = createChosenComponentIntent(component, directShare)
+ intentDispatcher.dispatchIntent(resultSender, intent)
+ }
+ }
+
+ override fun onActionSelected(action: ShareAction) {
+ Log.i(TAG, "onActionSelected: $action")
+ scope.launch {
+ if (flags.enableChooserResult() && chooserResultSupported(callerUid)) {
+ @ResultType val chosenAction = shareActionToChooserResult(action)
+ val intent: Intent = createSelectedActionIntent(chosenAction)
+ intentDispatcher.dispatchIntent(resultSender, intent)
+ } else {
+ Log.i(TAG, "Not sending SelectedAction")
+ }
+ }
+ }
+
+ private suspend fun createChosenComponentIntent(
+ component: ComponentName,
+ direct: Boolean,
+ ): Intent {
+ // Add extra with component name for backwards compatibility.
+ val intent: Intent = Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, component)
+
+ // Add ChooserResult value for Android V+
+ if (flags.enableChooserResult() && chooserResultSupported(callerUid)) {
+ intent.putExtra(
+ Intent.EXTRA_CHOOSER_RESULT,
+ ChooserResult(CHOOSER_RESULT_SELECTED_COMPONENT, component, direct)
+ )
+ } else {
+ Log.i(TAG, "Not including ${Intent.EXTRA_CHOOSER_RESULT}")
+ }
+ return intent
+ }
+
+ @ResultType
+ private fun shareActionToChooserResult(action: ShareAction) =
+ when (action) {
+ ShareAction.SYSTEM_COPY -> CHOOSER_RESULT_COPY
+ ShareAction.SYSTEM_EDIT -> CHOOSER_RESULT_EDIT
+ ShareAction.APPLICATION_DEFINED -> CHOOSER_RESULT_UNKNOWN
+ }
+
+ private fun createSelectedActionIntent(@ResultType result: Int): Intent {
+ return Intent().putExtra(Intent.EXTRA_CHOOSER_RESULT, ChooserResult(result, null, false))
+ }
+
+ private suspend fun chooserResultSupported(uid: Int): Boolean {
+ return withContext(backgroundDispatcher) {
+ // background -> Binder call to system_server
+ CompatChanges.isChangeEnabled(ChooserResult.SEND_CHOOSER_RESULT, uid)
+ }
+ }
+}
+
+private fun IntentSender.dispatchIntent(context: Context, intent: Intent) {
+ try {
+ sendIntent(
+ /* context = */ context,
+ /* code = */ Activity.RESULT_OK,
+ /* intent = */ intent,
+ /* onFinished = */ null,
+ /* handler = */ null
+ )
+ } catch (e: IntentSender.SendIntentException) {
+ Log.e(TAG, "Failed to send intent to IntentSender", e)
+ }
+}
diff --git a/java/src/com/android/intentresolver/ui/ShortcutPolicyModule.kt b/java/src/com/android/intentresolver/ui/ShortcutPolicyModule.kt
new file mode 100644
index 00000000..7239198e
--- /dev/null
+++ b/java/src/com/android/intentresolver/ui/ShortcutPolicyModule.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.ui
+
+import android.content.res.Resources
+import android.provider.DeviceConfig
+import com.android.intentresolver.R
+import com.android.intentresolver.inject.ApplicationOwned
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Qualifier
+import javax.inject.Singleton
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class AppShortcutLimit
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class EnforceShortcutLimit
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ShortcutRowLimit
+
+@Module
+@InstallIn(SingletonComponent::class)
+object ShortcutPolicyModule {
+ /**
+ * Defines the limit for the number of shortcut targets provided for any single app.
+ *
+ * This value applies to both results from Shortcut-service and app-provided targets on a
+ * per-package basis.
+ */
+ @Provides
+ @Singleton
+ @AppShortcutLimit
+ fun appShortcutLimit(@ApplicationOwned resources: Resources): Int {
+ return resources.getInteger(R.integer.config_maxShortcutTargetsPerApp)
+ }
+
+ /**
+ * Once this value is no longer necessary it should be replaced in tests with simply replacing
+ * [AppShortcutLimit]:
+ * ```
+ * @BindValue
+ * @AppShortcutLimit
+ * var shortcutLimit = Int.MAX_VALUE
+ * ```
+ */
+ @Provides
+ @Singleton
+ @EnforceShortcutLimit
+ fun applyShortcutLimit(): Boolean {
+ return DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
+ true
+ )
+ }
+
+ /**
+ * Defines the limit for the number of shortcuts presented within the direct share row.
+ *
+ * This value applies to all displayed direct share targets, including those from Shortcut
+ * service as well as app-provided targets.
+ */
+ @Provides
+ @Singleton
+ @ShortcutRowLimit
+ fun shortcutRowLimit(@ApplicationOwned resources: Resources): Int {
+ return resources.getInteger(R.integer.config_chooser_max_targets_per_row)
+ }
+}
diff --git a/java/src/com/android/intentresolver/ui/model/ActivityModel.kt b/java/src/com/android/intentresolver/ui/model/ActivityModel.kt
new file mode 100644
index 00000000..4bcdd69b
--- /dev/null
+++ b/java/src/com/android/intentresolver/ui/model/ActivityModel.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.ui.model
+
+import android.app.Activity
+import android.content.Intent
+import android.net.Uri
+import android.os.Parcel
+import android.os.Parcelable
+import com.android.intentresolver.data.model.ANDROID_APP_SCHEME
+import com.android.intentresolver.ext.readParcelable
+import com.android.intentresolver.ext.requireParcelable
+import java.util.Objects
+
+/** Contains Activity-scope information about the state when started. */
+data class ActivityModel(
+ /** The [Intent] received by the app */
+ val intent: Intent,
+ /** The identifier for the sending app and user */
+ val launchedFromUid: Int,
+ /** The package of the sending app */
+ val launchedFromPackage: String,
+ /** The referrer as supplied to the activity. */
+ val referrer: Uri?
+) : Parcelable {
+ constructor(
+ source: Parcel
+ ) : this(
+ intent = source.requireParcelable(),
+ launchedFromUid = source.readInt(),
+ launchedFromPackage = requireNotNull(source.readString()),
+ referrer = source.readParcelable()
+ )
+
+ /** A package name from referrer, if it is an android-app URI */
+ val referrerPackage = referrer?.takeIf { it.scheme == ANDROID_APP_SCHEME }?.authority
+
+ override fun describeContents() = 0 /* flags */
+
+ override fun writeToParcel(dest: Parcel, flags: Int) {
+ dest.writeParcelable(intent, flags)
+ dest.writeInt(launchedFromUid)
+ dest.writeString(launchedFromPackage)
+ dest.writeParcelable(referrer, flags)
+ }
+
+ companion object {
+ const val ACTIVITY_MODEL_KEY = "com.android.intentresolver.ACTIVITY_MODEL"
+
+ @JvmField
+ @Suppress("unused")
+ val CREATOR =
+ object : Parcelable.Creator<ActivityModel> {
+ override fun newArray(size: Int) = arrayOfNulls<ActivityModel>(size)
+ override fun createFromParcel(source: Parcel) = ActivityModel(source)
+ }
+
+ @JvmStatic
+ fun createFrom(activity: Activity): ActivityModel {
+ return ActivityModel(
+ activity.intent,
+ activity.launchedFromUid,
+ Objects.requireNonNull<String>(activity.launchedFromPackage),
+ activity.referrer
+ )
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ui/model/ResolverRequest.kt b/java/src/com/android/intentresolver/ui/model/ResolverRequest.kt
new file mode 100644
index 00000000..363c413d
--- /dev/null
+++ b/java/src/com/android/intentresolver/ui/model/ResolverRequest.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.ui.model
+
+import android.content.Intent
+import android.content.pm.ResolveInfo
+import android.os.UserHandle
+import com.android.intentresolver.ext.isHomeIntent
+import com.android.intentresolver.shared.model.Profile
+
+/** All of the things that are consumed from an incoming Intent Resolution request (+Extras). */
+data class ResolverRequest(
+ /** The intent to be resolved to a target. */
+ val intent: Intent,
+
+ /**
+ * Supplied by the system to indicate which profile should be selected by default. This is
+ * required since ResolverActivity may be launched as either the originating OR target user when
+ * resolving a cross profile intent.
+ *
+ * Valid values are: [PERSONAL][Profile.Type.PERSONAL] and [WORK][Profile.Type.WORK] and null
+ * when the intent is not a forwarded cross-profile intent.
+ */
+ val selectedProfile: Profile.Type?,
+
+ /**
+ * When handing a cross profile forwarded intent, this is the user which started the original
+ * intent. This is required to allow ResolverActivity to be launched as the target user under
+ * some conditions.
+ */
+ val callingUser: UserHandle?,
+
+ /**
+ * Indicates if resolving actions for a connected device which has audio capture capability
+ * (e.g. is a USB Microphone).
+ *
+ * When used to handle a connected device, ResolverActivity uses this signal to present a
+ * warning when a resolved application does not hold the RECORD_AUDIO permission. (If selected
+ * the app would be able to capture audio directly via the device, bypassing audio API
+ * permissions.)
+ */
+ val isAudioCaptureDevice: Boolean = false,
+
+ /** A list of a resolved activity targets. This list overrides normal intent resolution. */
+ val resolutionList: List<ResolveInfo>? = null,
+
+ /** A customized title for the resolver interface. */
+ val title: String? = null,
+) {
+ val isResolvingHome = intent.isHomeIntent()
+
+ /** For compatibility with existing code shared between chooser/resolver. */
+ val payloadIntents: List<Intent> = listOf(intent)
+}
diff --git a/java/src/com/android/intentresolver/ui/model/ShareAction.kt b/java/src/com/android/intentresolver/ui/model/ShareAction.kt
new file mode 100644
index 00000000..4d727b9a
--- /dev/null
+++ b/java/src/com/android/intentresolver/ui/model/ShareAction.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.ui.model
+
+enum class ShareAction {
+ SYSTEM_COPY,
+ SYSTEM_EDIT,
+ APPLICATION_DEFINED
+}
diff --git a/java/src/com/android/intentresolver/ui/viewmodel/ChooserRequestReader.kt b/java/src/com/android/intentresolver/ui/viewmodel/ChooserRequestReader.kt
new file mode 100644
index 00000000..a9b6de7e
--- /dev/null
+++ b/java/src/com/android/intentresolver/ui/viewmodel/ChooserRequestReader.kt
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.ui.viewmodel
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.Intent.EXTRA_ALTERNATE_INTENTS
+import android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS
+import android.content.Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION
+import android.content.Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER
+import android.content.Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER
+import android.content.Intent.EXTRA_CHOOSER_TARGETS
+import android.content.Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER
+import android.content.Intent.EXTRA_EXCLUDE_COMPONENTS
+import android.content.Intent.EXTRA_INITIAL_INTENTS
+import android.content.Intent.EXTRA_INTENT
+import android.content.Intent.EXTRA_METADATA_TEXT
+import android.content.Intent.EXTRA_REPLACEMENT_EXTRAS
+import android.content.Intent.EXTRA_TEXT
+import android.content.Intent.EXTRA_TITLE
+import android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK
+import android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+import android.content.IntentFilter
+import android.content.IntentSender
+import android.net.Uri
+import android.os.Bundle
+import android.service.chooser.ChooserAction
+import android.service.chooser.ChooserTarget
+import com.android.intentresolver.ChooserActivity
+import com.android.intentresolver.ContentTypeHint
+import com.android.intentresolver.R
+import com.android.intentresolver.data.model.ChooserRequest
+import com.android.intentresolver.ext.hasSendAction
+import com.android.intentresolver.ext.ifMatch
+import com.android.intentresolver.inject.ChooserServiceFlags
+import com.android.intentresolver.ui.model.ActivityModel
+import com.android.intentresolver.util.hasValidIcon
+import com.android.intentresolver.validation.Validation
+import com.android.intentresolver.validation.ValidationResult
+import com.android.intentresolver.validation.types.IntentOrUri
+import com.android.intentresolver.validation.types.array
+import com.android.intentresolver.validation.types.value
+import com.android.intentresolver.validation.validateFrom
+
+private const val MAX_CHOOSER_ACTIONS = 5
+private const val MAX_INITIAL_INTENTS = 2
+
+internal fun Intent.maybeAddSendActionFlags() =
+ ifMatch(Intent::hasSendAction) {
+ addFlags(FLAG_ACTIVITY_NEW_DOCUMENT)
+ addFlags(FLAG_ACTIVITY_MULTIPLE_TASK)
+ }
+
+fun readChooserRequest(
+ model: ActivityModel,
+ flags: ChooserServiceFlags
+): ValidationResult<ChooserRequest> {
+ val extras = model.intent.extras ?: Bundle()
+ @Suppress("DEPRECATION")
+ return validateFrom(extras::get) {
+ val targetIntent = required(IntentOrUri(EXTRA_INTENT)).maybeAddSendActionFlags()
+
+ val isSendAction = targetIntent.hasSendAction()
+
+ val additionalTargets = readAlternateIntents() ?: emptyList()
+
+ val replacementExtras = optional(value<Bundle>(EXTRA_REPLACEMENT_EXTRAS))
+
+ val (customTitle, defaultTitleResource) =
+ if (isSendAction) {
+ ignored(
+ value<CharSequence>(EXTRA_TITLE),
+ "deprecated in P. You may wish to set a preview title by using EXTRA_TITLE " +
+ "property of the wrapped EXTRA_INTENT."
+ )
+ null to R.string.chooseActivity
+ } else {
+ val custom = optional(value<CharSequence>(EXTRA_TITLE))
+ custom to (custom?.let { 0 } ?: R.string.chooseActivity)
+ }
+
+ val initialIntents =
+ optional(array<Intent>(EXTRA_INITIAL_INTENTS))?.take(MAX_INITIAL_INTENTS)?.map {
+ it.maybeAddSendActionFlags()
+ }
+ ?: emptyList()
+
+ val chosenComponentSender =
+ optional(value<IntentSender>(EXTRA_CHOOSER_RESULT_INTENT_SENDER))
+ ?: optional(value<IntentSender>(EXTRA_CHOSEN_COMPONENT_INTENT_SENDER))
+
+ val refinementIntentSender =
+ optional(value<IntentSender>(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER))
+
+ val filteredComponents =
+ optional(array<ComponentName>(EXTRA_EXCLUDE_COMPONENTS)) ?: emptyList()
+
+ @Suppress("DEPRECATION")
+ val callerChooserTargets =
+ optional(array<ChooserTarget>(EXTRA_CHOOSER_TARGETS)) ?: emptyList()
+
+ val retainInOnStop =
+ optional(value<Boolean>(ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP)) ?: false
+
+ val sharedText = optional(value<CharSequence>(EXTRA_TEXT))
+
+ val chooserActions = readChooserActions() ?: emptyList()
+
+ val modifyShareAction = optional(value<ChooserAction>(EXTRA_CHOOSER_MODIFY_SHARE_ACTION))
+
+ val additionalContentUri: Uri?
+ val focusedItemPos: Int
+ if (isSendAction && flags.chooserPayloadToggling()) {
+ additionalContentUri = optional(value<Uri>(Intent.EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI))
+ focusedItemPos = optional(value<Int>(Intent.EXTRA_CHOOSER_FOCUSED_ITEM_POSITION)) ?: 0
+ } else {
+ additionalContentUri = null
+ focusedItemPos = 0
+ }
+
+ val contentTypeHint =
+ if (flags.chooserAlbumText()) {
+ when (optional(value<Int>(Intent.EXTRA_CHOOSER_CONTENT_TYPE_HINT))) {
+ Intent.CHOOSER_CONTENT_TYPE_ALBUM -> ContentTypeHint.ALBUM
+ else -> ContentTypeHint.NONE
+ }
+ } else {
+ ContentTypeHint.NONE
+ }
+
+ val metadataText =
+ if (flags.enableSharesheetMetadataExtra()) {
+ optional(value<CharSequence>(EXTRA_METADATA_TEXT))
+ } else {
+ null
+ }
+
+ ChooserRequest(
+ targetIntent = targetIntent,
+ targetAction = targetIntent.action,
+ isSendActionTarget = isSendAction,
+ targetType = targetIntent.type,
+ launchedFromPackage =
+ requireNotNull(model.launchedFromPackage) {
+ "launch.fromPackage was null, See Activity.getLaunchedFromPackage()"
+ },
+ title = customTitle,
+ defaultTitleResource = defaultTitleResource,
+ referrer = model.referrer,
+ filteredComponentNames = filteredComponents,
+ callerChooserTargets = callerChooserTargets,
+ chooserActions = chooserActions,
+ modifyShareAction = modifyShareAction,
+ shouldRetainInOnStop = retainInOnStop,
+ additionalTargets = additionalTargets,
+ replacementExtras = replacementExtras,
+ initialIntents = initialIntents,
+ chosenComponentSender = chosenComponentSender,
+ refinementIntentSender = refinementIntentSender,
+ sharedText = sharedText,
+ shareTargetFilter = targetIntent.toShareTargetFilter(),
+ additionalContentUri = additionalContentUri,
+ focusedItemPosition = focusedItemPos,
+ contentTypeHint = contentTypeHint,
+ metadataText = metadataText,
+ )
+ }
+}
+
+fun Validation.readAlternateIntents(): List<Intent>? =
+ optional(array<Intent>(EXTRA_ALTERNATE_INTENTS))?.map { it.maybeAddSendActionFlags() }
+
+fun Validation.readChooserActions(): List<ChooserAction>? =
+ optional(array<ChooserAction>(EXTRA_CHOOSER_CUSTOM_ACTIONS))
+ ?.filter { hasValidIcon(it) }
+ ?.take(MAX_CHOOSER_ACTIONS)
+
+private fun Intent.toShareTargetFilter(): IntentFilter? {
+ return type?.let {
+ IntentFilter().apply {
+ action?.also { addAction(it) }
+ addDataType(it)
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt b/java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt
new file mode 100644
index 00000000..c9cae3db
--- /dev/null
+++ b/java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt
@@ -0,0 +1,94 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.ui.viewmodel
+
+import android.util.Log
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.viewModelScope
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.FetchPreviewsInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.ProcessTargetIntentUpdatesInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel.ShareouselViewModel
+import com.android.intentresolver.data.model.ChooserRequest
+import com.android.intentresolver.data.repository.ChooserRequestRepository
+import com.android.intentresolver.inject.Background
+import com.android.intentresolver.inject.ChooserServiceFlags
+import com.android.intentresolver.ui.model.ActivityModel
+import com.android.intentresolver.ui.model.ActivityModel.Companion.ACTIVITY_MODEL_KEY
+import com.android.intentresolver.validation.Invalid
+import com.android.intentresolver.validation.Valid
+import com.android.intentresolver.validation.ValidationResult
+import dagger.Lazy
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.launch
+
+private const val TAG = "ChooserViewModel"
+
+@HiltViewModel
+class ChooserViewModel
+@Inject
+constructor(
+ args: SavedStateHandle,
+ private val shareouselViewModelProvider: Lazy<ShareouselViewModel>,
+ private val processUpdatesInteractor: Lazy<ProcessTargetIntentUpdatesInteractor>,
+ private val fetchPreviewsInteractor: Lazy<FetchPreviewsInteractor>,
+ @Background private val bgDispatcher: CoroutineDispatcher,
+ private val flags: ChooserServiceFlags,
+ /**
+ * Provided only for the express purpose of early exit in the event of an invalid request.
+ *
+ * Note: [request] can only be safely accessed after checking if this value is [Valid].
+ */
+ val initialRequest: ValidationResult<ChooserRequest>,
+ private val chooserRequestRepository: Lazy<ChooserRequestRepository>,
+) : ViewModel() {
+
+ /** Parcelable-only references provided from the creating Activity */
+ val activityModel: ActivityModel =
+ requireNotNull(args[ACTIVITY_MODEL_KEY]) {
+ "ActivityModel missing in SavedStateHandle! ($ACTIVITY_MODEL_KEY)"
+ }
+
+ val shareouselViewModel: ShareouselViewModel by lazy {
+ // TODO: consolidate this logic, this would require a consolidated preview view model but
+ // for now just postpone starting the payload selection preview machinery until it's needed
+ assert(flags.chooserPayloadToggling()) {
+ "An attempt to use payload selection preview with the disabled flag"
+ }
+
+ viewModelScope.launch(bgDispatcher) { processUpdatesInteractor.get().activate() }
+ viewModelScope.launch(bgDispatcher) { fetchPreviewsInteractor.get().activate() }
+ shareouselViewModelProvider.get()
+ }
+
+ /**
+ * A [StateFlow] of [ChooserRequest].
+ *
+ * Note: Only safe to access after checking if [initialRequest] is [Valid].
+ */
+ val request: StateFlow<ChooserRequest>
+ get() = chooserRequestRepository.get().chooserRequest.asStateFlow()
+
+ init {
+ if (initialRequest is Invalid) {
+ Log.w(TAG, "initialRequest is Invalid, initialization failed")
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ui/viewmodel/ResolverRequestReader.kt b/java/src/com/android/intentresolver/ui/viewmodel/ResolverRequestReader.kt
new file mode 100644
index 00000000..856d9fdd
--- /dev/null
+++ b/java/src/com/android/intentresolver/ui/viewmodel/ResolverRequestReader.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.ui.viewmodel
+
+import android.os.Bundle
+import android.os.UserHandle
+import com.android.intentresolver.ResolverActivity.PROFILE_PERSONAL
+import com.android.intentresolver.ResolverActivity.PROFILE_WORK
+import com.android.intentresolver.shared.model.Profile
+import com.android.intentresolver.ui.model.ActivityModel
+import com.android.intentresolver.ui.model.ResolverRequest
+import com.android.intentresolver.validation.Validation
+import com.android.intentresolver.validation.ValidationResult
+import com.android.intentresolver.validation.types.value
+import com.android.intentresolver.validation.validateFrom
+
+const val EXTRA_CALLING_USER = "com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER"
+const val EXTRA_SELECTED_PROFILE =
+ "com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE"
+const val EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device"
+
+fun readResolverRequest(launch: ActivityModel): ValidationResult<ResolverRequest> {
+ @Suppress("DEPRECATION")
+ return validateFrom((launch.intent.extras ?: Bundle())::get) {
+ val callingUser = optional(value<UserHandle>(EXTRA_CALLING_USER))
+ val selectedProfile = checkSelectedProfile()
+ val audioDevice = optional(value<Boolean>(EXTRA_IS_AUDIO_CAPTURE_DEVICE)) ?: false
+ ResolverRequest(launch.intent, selectedProfile, callingUser, audioDevice)
+ }
+}
+
+private fun Validation.checkSelectedProfile(): Profile.Type? {
+ return when (val selected = optional(value<Int>(EXTRA_SELECTED_PROFILE))) {
+ null -> null
+ PROFILE_PERSONAL -> Profile.Type.PERSONAL
+ PROFILE_WORK -> Profile.Type.WORK
+ else ->
+ error(
+ EXTRA_SELECTED_PROFILE +
+ " has invalid value ($selected)." +
+ " Must be either ResolverActivity.PROFILE_PERSONAL ($PROFILE_PERSONAL)" +
+ " or ResolverActivity.PROFILE_WORK ($PROFILE_WORK)."
+ )
+ }
+}
diff --git a/java/src/com/android/intentresolver/ui/viewmodel/ResolverViewModel.kt b/java/src/com/android/intentresolver/ui/viewmodel/ResolverViewModel.kt
new file mode 100644
index 00000000..a3dc58a6
--- /dev/null
+++ b/java/src/com/android/intentresolver/ui/viewmodel/ResolverViewModel.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.ui.viewmodel
+
+import android.util.Log
+import androidx.lifecycle.SavedStateHandle
+import androidx.lifecycle.ViewModel
+import com.android.intentresolver.ui.model.ActivityModel
+import com.android.intentresolver.ui.model.ActivityModel.Companion.ACTIVITY_MODEL_KEY
+import com.android.intentresolver.ui.model.ResolverRequest
+import com.android.intentresolver.validation.Invalid
+import com.android.intentresolver.validation.Valid
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+
+private const val TAG = "ResolverViewModel"
+
+@HiltViewModel
+class ResolverViewModel @Inject constructor(args: SavedStateHandle) : ViewModel() {
+
+ /** Parcelable-only references provided from the creating Activity */
+ val activityModel: ActivityModel =
+ requireNotNull(args[ACTIVITY_MODEL_KEY]) {
+ "ActivityModel missing in SavedStateHandle! ($ACTIVITY_MODEL_KEY)"
+ }
+
+ /**
+ * Provided only for the express purpose of early exit in the event of an invalid request.
+ *
+ * Note: [request] can only be safely accessed after checking if this value is [Valid].
+ */
+ internal val initialRequest = readResolverRequest(activityModel)
+
+ private lateinit var _request: MutableStateFlow<ResolverRequest>
+
+ /**
+ * A [StateFlow] of [ResolverRequest].
+ *
+ * Note: Only safe to access after checking if [initialRequest] is [Valid].
+ */
+ lateinit var request: StateFlow<ResolverRequest>
+ private set
+
+ init {
+ when (initialRequest) {
+ is Valid -> {
+ _request = MutableStateFlow(initialRequest.value)
+ request = _request.asStateFlow()
+ }
+ is Invalid -> Log.w(TAG, "initialRequest is Invalid, initialization failed")
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/util/CancellationSignalUtils.kt b/java/src/com/android/intentresolver/util/CancellationSignalUtils.kt
new file mode 100644
index 00000000..e89cb5ca
--- /dev/null
+++ b/java/src/com/android/intentresolver/util/CancellationSignalUtils.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.util
+
+import android.os.CancellationSignal
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * Invokes [block] with a [CancellationSignal] that is bound to this coroutine's lifetime; if this
+ * coroutine is cancelled, then [CancellationSignal.cancel] is promptly invoked.
+ */
+suspend fun <R> withCancellationSignal(block: suspend (signal: CancellationSignal) -> R): R =
+ coroutineScope {
+ val signal = CancellationSignal()
+ val signalJob =
+ launch(start = CoroutineStart.UNDISPATCHED) {
+ try {
+ awaitCancellation()
+ } finally {
+ signal.cancel()
+ }
+ }
+ block(signal).also { signalJob.cancel() }
+ }
diff --git a/java/src/com/android/intentresolver/util/Flow.kt b/java/src/com/android/intentresolver/util/Flow.kt
index 1155b9fe..598379f3 100644
--- a/java/src/com/android/intentresolver/util/Flow.kt
+++ b/java/src/com/android/intentresolver/util/Flow.kt
@@ -31,7 +31,6 @@ import kotlinx.coroutines.launch
* latest value is emitted.
*
* Example:
- *
* ```kotlin
* flow {
* emit(1) // t=0ms
@@ -70,10 +69,11 @@ fun <T> Flow<T>.throttle(periodMs: Long): Flow<T> = channelFlow {
// We create delayJob to allow cancellation during the delay period
delayJob = launch {
delay(timeUntilNextEmit)
- sendJob = outerScope.launch(start = CoroutineStart.UNDISPATCHED) {
- send(it)
- previousEmitTimeMs = SystemClock.elapsedRealtime()
- }
+ sendJob =
+ outerScope.launch(start = CoroutineStart.UNDISPATCHED) {
+ send(it)
+ previousEmitTimeMs = SystemClock.elapsedRealtime()
+ }
}
} else {
send(it)
diff --git a/java/src/com/android/intentresolver/util/ParallelIteration.kt b/java/src/com/android/intentresolver/util/ParallelIteration.kt
new file mode 100644
index 00000000..70c46c47
--- /dev/null
+++ b/java/src/com/android/intentresolver/util/ParallelIteration.kt
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.util
+
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.sync.withPermit
+import kotlinx.coroutines.yield
+
+/** Like [Iterable.map] but executes each [block] invocation in a separate coroutine. */
+suspend fun <A, B> Iterable<A>.mapParallel(
+ parallelism: Int? = null,
+ block: suspend (A) -> B,
+): List<B> =
+ parallelism?.let { permits ->
+ withSemaphore(permits = permits) { mapParallel { withPermit { block(it) } } }
+ }
+ ?: mapParallel(block)
+
+/** Like [Iterable.map] but executes each [block] invocation in a separate coroutine. */
+suspend fun <A, B> Sequence<A>.mapParallel(
+ parallelism: Int? = null,
+ block: suspend (A) -> B,
+): List<B> = asIterable().mapParallel(parallelism, block)
+
+private suspend fun <A, B> Iterable<A>.mapParallel(block: suspend (A) -> B): List<B> =
+ coroutineScope {
+ map {
+ async {
+ yield()
+ block(it)
+ }
+ }
+ .awaitAll()
+ }
diff --git a/java/src/com/android/intentresolver/util/SyncUtils.kt b/java/src/com/android/intentresolver/util/SyncUtils.kt
new file mode 100644
index 00000000..eaebc6ea
--- /dev/null
+++ b/java/src/com/android/intentresolver/util/SyncUtils.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.util
+
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.Semaphore
+
+/**
+ * Constructs a [Semaphore] for usage within [block], useful for launching a lot of work in parallel
+ * that needs some synchronization.
+ */
+inline fun <R> withSemaphore(permits: Int, block: Semaphore.() -> R): R =
+ Semaphore(permits).run(block)
+
+/**
+ * Constructs a [Mutex] for usage within [block], useful for launching a lot of work in parallel
+ * that needs some synchronization.
+ */
+inline fun <R> withMutex(block: Mutex.() -> R): R = Mutex().run(block)
diff --git a/java/src/com/android/intentresolver/util/cursor/CursorView.kt b/java/src/com/android/intentresolver/util/cursor/CursorView.kt
new file mode 100644
index 00000000..eca7d335
--- /dev/null
+++ b/java/src/com/android/intentresolver/util/cursor/CursorView.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.util.cursor
+
+import android.database.Cursor
+
+/** A [Cursor] that holds values of [E] for each row. */
+interface CursorView<out E> : Cursor {
+ /**
+ * Reads the current row from this [CursorView]. A result of `null` indicates that the row could
+ * not be read / value could not be produced.
+ */
+ fun readRow(): E?
+}
+
+/**
+ * Returns a [CursorView] from the given [Cursor], and a function [readRow] used to produce the
+ * value for a single row.
+ */
+fun <E> Cursor.viewBy(readRow: Cursor.() -> E): CursorView<E> =
+ object : CursorView<E>, Cursor by this@viewBy {
+ override fun readRow(): E? = immobilized().readRow()
+ }
+
+/** Returns a [CursorView] that begins (index 0) at [newStartIndex] of the given cursor. */
+fun <E> CursorView<E>.startAt(newStartIndex: Int): CursorView<E> =
+ object : CursorView<E>, Cursor by (this@startAt as Cursor).startAt(newStartIndex) {
+ override fun readRow(): E? = this@startAt.readRow()
+ }
+
+/** Returns a [CursorView] that is truncated to contain only [count] elements. */
+fun <E> CursorView<E>.limit(count: Int): CursorView<E> =
+ object : CursorView<E>, Cursor by (this@limit as Cursor).limit(count) {
+ override fun readRow(): E? = this@limit.readRow()
+ }
+
+/** Retrieves a single row at index [idx] from the [CursorView]. */
+operator fun <E> CursorView<E>.get(idx: Int): E? = if (moveToPosition(idx)) readRow() else null
+
+/** Returns a [Sequence] that iterates over the [CursorView] returning each row. */
+fun <E> CursorView<E>.asSequence(): Sequence<E?> = sequence {
+ for (i in 0 until count) {
+ yield(get(i))
+ }
+}
diff --git a/java/src/com/android/intentresolver/util/cursor/Cursors.kt b/java/src/com/android/intentresolver/util/cursor/Cursors.kt
new file mode 100644
index 00000000..ce768f3b
--- /dev/null
+++ b/java/src/com/android/intentresolver/util/cursor/Cursors.kt
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.util.cursor
+
+import android.database.Cursor
+import android.database.CursorWrapper
+
+/** Returns a Cursor that is truncated to contain only [count] elements. */
+fun Cursor.limit(count: Int): Cursor =
+ object : CursorWrapper(this) {
+ override fun getCount(): Int = minOf(count, super.getCount())
+
+ override fun getPosition(): Int = super.getPosition().coerceAtMost(count)
+
+ override fun moveToLast(): Boolean = super.moveToPosition(getCount() - 1)
+
+ override fun isFirst(): Boolean = getCount() != 0 && super.isFirst()
+
+ override fun isLast(): Boolean = getCount() != 0 && super.getPosition() == getCount() - 1
+
+ override fun isAfterLast(): Boolean = getCount() == 0 || super.getPosition() >= getCount()
+
+ override fun isBeforeFirst(): Boolean = getCount() == 0 || super.isBeforeFirst()
+
+ override fun moveToNext(): Boolean = super.moveToNext() && position < getCount()
+
+ override fun moveToPosition(position: Int): Boolean =
+ super.moveToPosition(position) && position < getCount()
+ }
+
+/** Returns a Cursor that begins (index 0) at [newStartIndex] of the given Cursor. */
+fun Cursor.startAt(newStartIndex: Int): Cursor =
+ object : CursorWrapper(this) {
+ override fun getCount(): Int = (super.getCount() - newStartIndex).coerceAtLeast(0)
+
+ override fun getPosition(): Int = (super.getPosition() - newStartIndex).coerceAtLeast(-1)
+
+ override fun moveToFirst(): Boolean = super.moveToPosition(newStartIndex)
+
+ override fun moveToNext(): Boolean = super.moveToNext() && position < count
+
+ override fun moveToPrevious(): Boolean = super.moveToPrevious() && position >= 0
+
+ override fun moveToPosition(position: Int): Boolean =
+ super.moveToPosition(position + newStartIndex) && position >= 0
+
+ override fun isFirst(): Boolean = count != 0 && super.getPosition() == newStartIndex
+
+ override fun isLast(): Boolean = count != 0 && super.isLast()
+
+ override fun isBeforeFirst(): Boolean = count == 0 || super.getPosition() < newStartIndex
+
+ override fun isAfterLast(): Boolean = count == 0 || super.isAfterLast()
+ }
+
+/** Returns a read-only non-movable view into the given Cursor. */
+fun Cursor.immobilized(): Cursor =
+ object : CursorWrapper(this) {
+ private val unsupported: Nothing
+ get() = error("unsupported")
+
+ override fun moveToFirst(): Boolean = unsupported
+
+ override fun moveToLast(): Boolean = unsupported
+
+ override fun move(offset: Int): Boolean = unsupported
+
+ override fun moveToPosition(position: Int): Boolean = unsupported
+
+ override fun moveToNext(): Boolean = unsupported
+
+ override fun moveToPrevious(): Boolean = unsupported
+ }
diff --git a/java/src/com/android/intentresolver/util/cursor/PagedCursor.kt b/java/src/com/android/intentresolver/util/cursor/PagedCursor.kt
new file mode 100644
index 00000000..6e4318dc
--- /dev/null
+++ b/java/src/com/android/intentresolver/util/cursor/PagedCursor.kt
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.util.cursor
+
+import android.database.Cursor
+
+/** A [CursorView] that produces chunks/pages from an underlying cursor. */
+interface PagedCursor<out E> : CursorView<Sequence<E?>> {
+ /** The configured size of each page produced by this cursor. */
+ val pageSize: Int
+}
+
+/** Returns a [PagedCursor] that produces pages of data from the given [CursorView]. */
+fun <E> CursorView<E>.paged(pageSize: Int): PagedCursor<E> =
+ object : PagedCursor<E>, Cursor by this@paged {
+
+ init {
+ check(pageSize > 0) { "pageSize must be greater than 0" }
+ }
+
+ override val pageSize: Int = pageSize
+
+ override fun getCount(): Int =
+ this@paged.count.let { it / pageSize + minOf(1, it % pageSize) }
+
+ override fun getPosition(): Int =
+ (this@paged.position / pageSize).let { if (this@paged.position < 0) it - 1 else it }
+
+ override fun moveToNext(): Boolean = moveToPosition(position + 1)
+
+ override fun moveToPrevious(): Boolean = moveToPosition(position - 1)
+
+ override fun moveToPosition(position: Int): Boolean =
+ this@paged.moveToPosition(position * pageSize)
+
+ override fun readRow(): Sequence<E?> =
+ this@paged.startAt(position * pageSize).limit(pageSize).asSequence()
+ }
diff --git a/java/src/com/android/intentresolver/v2/ActivityLogic.kt b/java/src/com/android/intentresolver/v2/ActivityLogic.kt
deleted file mode 100644
index c81bed09..00000000
--- a/java/src/com/android/intentresolver/v2/ActivityLogic.kt
+++ /dev/null
@@ -1,156 +0,0 @@
-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
deleted file mode 100644
index db840387..00000000
--- a/java/src/com/android/intentresolver/v2/ChooserActionFactory.java
+++ /dev/null
@@ -1,395 +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.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
deleted file mode 100644
index 70812642..00000000
--- a/java/src/com/android/intentresolver/v2/ChooserActivity.java
+++ /dev/null
@@ -1,1845 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 7bc39a24..00000000
--- a/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt
+++ /dev/null
@@ -1,87 +0,0 @@
-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/ResolverActivity.java b/java/src/com/android/intentresolver/v2/ResolverActivity.java
deleted file mode 100644
index 2ba50ec3..00000000
--- a/java/src/com/android/intentresolver/v2/ResolverActivity.java
+++ /dev/null
@@ -1,2181 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 0e2b25ec..00000000
--- a/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt
+++ /dev/null
@@ -1,81 +0,0 @@
-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
deleted file mode 100644
index d96fd15a..00000000
--- a/java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 1a58afcb..00000000
--- a/java/src/com/android/intentresolver/v2/data/BroadcastFlow.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-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/repository/DevicePolicyResources.kt b/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt
deleted file mode 100644
index 7debdf07..00000000
--- a/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt
+++ /dev/null
@@ -1,68 +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.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
deleted file mode 100644
index fc82efee..00000000
--- a/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-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
deleted file mode 100644
index dc809b46..00000000
--- a/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt
+++ /dev/null
@@ -1,261 +0,0 @@
-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
deleted file mode 100644
index 94f985e7..00000000
--- a/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-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
deleted file mode 100644
index 7ee78d91..00000000
--- a/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-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
deleted file mode 100644
index 2f1e1b59..00000000
--- a/java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java
+++ /dev/null
@@ -1,141 +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.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
deleted file mode 100644
index e9d1bb34..00000000
--- a/java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java
+++ /dev/null
@@ -1,157 +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.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
deleted file mode 100644
index b744c589..00000000
--- a/java/src/com/android/intentresolver/v2/emptystate/NoCrossProfileEmptyStateProvider.java
+++ /dev/null
@@ -1,138 +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.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
deleted file mode 100644
index a6fee3ec..00000000
--- a/java/src/com/android/intentresolver/v2/emptystate/WorkProfilePausedEmptyStateProvider.java
+++ /dev/null
@@ -1,116 +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.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/listcontroller/FilterableComponents.kt b/java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt
deleted file mode 100644
index 5855e2fc..00000000
--- a/java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt
+++ /dev/null
@@ -1,39 +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.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
deleted file mode 100644
index bb9394b4..00000000
--- a/java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt
+++ /dev/null
@@ -1,70 +0,0 @@
-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
deleted file mode 100644
index b2856526..00000000
--- a/java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt
+++ /dev/null
@@ -1,77 +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.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/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt b/java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt
deleted file mode 100644
index cae2af95..00000000
--- a/java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-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
deleted file mode 100644
index 8be45ba2..00000000
--- a/java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt
+++ /dev/null
@@ -1,39 +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.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
deleted file mode 100644
index f0b4bf3f..00000000
--- a/java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-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
deleted file mode 100644
index e78bff00..00000000
--- a/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt
+++ /dev/null
@@ -1,121 +0,0 @@
-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
deleted file mode 100644
index 8ab41ef0..00000000
--- a/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt
+++ /dev/null
@@ -1,108 +0,0 @@
-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/SecureSettingsModule.kt b/java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt
deleted file mode 100644
index 18f47023..00000000
--- a/java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-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/util/MutableLazy.kt b/java/src/com/android/intentresolver/v2/util/MutableLazy.kt
deleted file mode 100644
index 4ce9b7fd..00000000
--- a/java/src/com/android/intentresolver/v2/util/MutableLazy.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-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/ValidationResult.kt b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt
deleted file mode 100644
index 092cabe8..00000000
--- a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt
+++ /dev/null
@@ -1,39 +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.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/Validators.kt b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt
deleted file mode 100644
index 4e6e5dff..00000000
--- a/java/src/com/android/intentresolver/v2/validation/types/Validators.kt
+++ /dev/null
@@ -1,45 +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.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/v2/validation/Findings.kt b/java/src/com/android/intentresolver/validation/Findings.kt
index 9a3cc9c7..0d62017f 100644
--- a/java/src/com/android/intentresolver/v2/validation/Findings.kt
+++ b/java/src/com/android/intentresolver/validation/Findings.kt
@@ -13,11 +13,11 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.intentresolver.v2.validation
+package com.android.intentresolver.validation
import android.util.Log
-import com.android.intentresolver.v2.validation.Importance.CRITICAL
-import com.android.intentresolver.v2.validation.Importance.WARNING
+import com.android.intentresolver.validation.Importance.CRITICAL
+import com.android.intentresolver.validation.Importance.WARNING
import kotlin.reflect.KClass
sealed interface Finding {
@@ -34,9 +34,13 @@ val Finding.logcatPriority
get() =
when (importance) {
CRITICAL -> Log.ERROR
- else -> Log.WARN
+ WARNING -> Log.WARN
}
+fun Finding.log(tag: String) {
+ Log.println(logcatPriority, tag, message)
+}
+
private fun formatMessage(key: String? = null, msg: String) = buildString {
key?.also { append("['$key']: ") }
append(msg)
@@ -52,18 +56,21 @@ data class IgnoredValue(
get() = formatMessage(key, "Ignored. $reason")
}
-data class RequiredValueMissing(
+data class NoValue(
val key: String,
+ override val importance: Importance,
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"
+ if (importance == CRITICAL) {
+ "expected value of ${allowedType.simpleName}, " + "but no value was present"
+ } else {
+ "no ${allowedType.simpleName} value present"
+ }
)
}
diff --git a/java/src/com/android/intentresolver/v2/validation/Validation.kt b/java/src/com/android/intentresolver/validation/Validation.kt
index 46939602..6ba62e57 100644
--- a/java/src/com/android/intentresolver/v2/validation/Validation.kt
+++ b/java/src/com/android/intentresolver/validation/Validation.kt
@@ -13,10 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.intentresolver.v2.validation
+package com.android.intentresolver.validation
-import com.android.intentresolver.v2.validation.Importance.CRITICAL
-import com.android.intentresolver.v2.validation.Importance.WARNING
+import com.android.intentresolver.validation.Importance.CRITICAL
+import com.android.intentresolver.validation.Importance.WARNING
/**
* Provides a mechanism for validating a result from a set of properties.
@@ -90,7 +90,7 @@ fun <T> validateFrom(source: (String) -> Any?, validate: Validation.() -> T): Va
is InvalidResultError -> Invalid(validation.findings)
// Some other exception was thrown from [validate],
- else -> Invalid(findings = listOf(UncaughtException(it)))
+ else -> Invalid(error = UncaughtException(it))
}
}
)
@@ -107,8 +107,8 @@ private class ValidationImpl(val source: (String) -> Any?) : Validation {
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.
+ if (result is Valid) {
+ // Note: Any warnings about the value itself (result.findings) are ignored.
findings += IgnoredValue(property.key, reason)
}
}
@@ -117,8 +117,16 @@ private class ValidationImpl(val source: (String) -> Any?) : Validation {
return runCatching { property.validate(source, importance) }
.fold(
onSuccess = { result ->
- findings += result.findings
- result.value
+ return when (result) {
+ is Valid -> {
+ findings += result.warnings
+ result.value
+ }
+ is Invalid -> {
+ findings += result.errors
+ null
+ }
+ }
},
onFailure = {
findings += UncaughtException(it, property.key)
diff --git a/java/src/com/android/intentresolver/validation/ValidationResult.kt b/java/src/com/android/intentresolver/validation/ValidationResult.kt
new file mode 100644
index 00000000..9685c70d
--- /dev/null
+++ b/java/src/com/android/intentresolver/validation/ValidationResult.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.validation
+
+sealed interface ValidationResult<T>
+
+data class Valid<T>(val value: T, val warnings: List<Finding> = emptyList()) : ValidationResult<T> {
+ constructor(value: T, warning: Finding) : this(value, listOf(warning))
+}
+
+data class Invalid<T>(val errors: List<Finding> = emptyList()) : ValidationResult<T> {
+ constructor(error: Finding) : this(listOf(error))
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt b/java/src/com/android/intentresolver/validation/types/IntentOrUri.kt
index 3cefeb15..74c48a23 100644
--- a/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt
+++ b/java/src/com/android/intentresolver/validation/types/IntentOrUri.kt
@@ -13,16 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.intentresolver.v2.validation.types
+package com.android.intentresolver.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
+import com.android.intentresolver.validation.Importance
+import com.android.intentresolver.validation.Invalid
+import com.android.intentresolver.validation.NoValue
+import com.android.intentresolver.validation.Valid
+import com.android.intentresolver.validation.ValidationResult
+import com.android.intentresolver.validation.Validator
+import com.android.intentresolver.validation.ValueIsWrongType
class IntentOrUri(override val key: String) : Validator<Intent> {
@@ -30,7 +31,6 @@ class IntentOrUri(override val key: String) : Validator<Intent> {
source: (String) -> Any?,
importance: Importance
): ValidationResult<Intent> {
-
return when (val value = source(key)) {
// An intent, return it.
is Intent -> Valid(value)
@@ -40,12 +40,15 @@ class IntentOrUri(override val key: String) : Validator<Intent> {
is Uri -> Valid(Intent.parseUri(value.toString(), Intent.URI_INTENT_SCHEME))
// No value present.
- null -> createResult(importance, RequiredValueMissing(key, Intent::class))
+ null ->
+ when (importance) {
+ Importance.WARNING -> Invalid() // No warnings if optional, but missing
+ Importance.CRITICAL -> Invalid(NoValue(key, importance, Intent::class))
+ }
// Some other type.
else -> {
- return createResult(
- importance,
+ return Invalid(
ValueIsWrongType(
key,
importance,
diff --git a/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt b/java/src/com/android/intentresolver/validation/types/ParceledArray.kt
index c6c4abba..5150ec5e 100644
--- a/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt
+++ b/java/src/com/android/intentresolver/validation/types/ParceledArray.kt
@@ -13,15 +13,16 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.intentresolver.v2.validation.types
+package com.android.intentresolver.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 com.android.intentresolver.validation.Importance
+import com.android.intentresolver.validation.Invalid
+import com.android.intentresolver.validation.NoValue
+import com.android.intentresolver.validation.Valid
+import com.android.intentresolver.validation.ValidationResult
+import com.android.intentresolver.validation.Validator
+import com.android.intentresolver.validation.ValueIsWrongType
+import com.android.intentresolver.validation.WrongElementType
import kotlin.reflect.KClass
import kotlin.reflect.cast
@@ -34,11 +35,13 @@ class ParceledArray<T : Any>(
source: (String) -> Any?,
importance: Importance
): ValidationResult<List<T>> {
-
return when (val value: Any? = source(key)) {
// No value present.
- null -> createResult(importance, RequiredValueMissing(key, elementType))
-
+ null ->
+ when (importance) {
+ Importance.WARNING -> Invalid() // No warnings if optional, but missing
+ Importance.CRITICAL -> Invalid(NoValue(key, importance, 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>.
@@ -54,8 +57,7 @@ class ParceledArray<T : Any>(
// At least one incorrect element type found.
else ->
- createResult(
- importance,
+ Invalid(
WrongElementType(
key,
importance,
@@ -69,8 +71,7 @@ class ParceledArray<T : Any>(
// The value is not an Array at all.
else ->
- createResult(
- importance,
+ Invalid(
ValueIsWrongType(
key,
importance,
diff --git a/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt b/java/src/com/android/intentresolver/validation/types/SimpleValue.kt
index 3287b84b..64299e11 100644
--- a/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt
+++ b/java/src/com/android/intentresolver/validation/types/SimpleValue.kt
@@ -13,14 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package com.android.intentresolver.v2.validation.types
+package com.android.intentresolver.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.validation.Importance
+import com.android.intentresolver.validation.Invalid
+import com.android.intentresolver.validation.NoValue
+import com.android.intentresolver.validation.Valid
+import com.android.intentresolver.validation.ValidationResult
+import com.android.intentresolver.validation.Validator
+import com.android.intentresolver.validation.ValueIsWrongType
import kotlin.reflect.KClass
import kotlin.reflect.cast
@@ -36,17 +37,22 @@ class SimpleValue<T : Any>(
expected.isInstance(value) -> return Valid(expected.cast(value))
// No value is present.
- value == null -> createResult(importance, RequiredValueMissing(key, expected))
+ value == null ->
+ when (importance) {
+ Importance.WARNING -> Invalid() // No warnings if optional, but missing
+ Importance.CRITICAL -> Invalid(NoValue(key, importance, expected))
+ }
// The value is some other type.
else ->
- createResult(
- importance,
- ValueIsWrongType(
- key,
- importance,
- actualType = value::class,
- allowedTypes = listOf(expected)
+ Invalid(
+ listOf(
+ ValueIsWrongType(
+ key,
+ importance,
+ actualType = value::class,
+ allowedTypes = listOf(expected)
+ )
)
)
}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/ListController.kt b/java/src/com/android/intentresolver/validation/types/Validators.kt
index 4ddab755..1049f045 100644
--- a/java/src/com/android/intentresolver/v2/listcontroller/ListController.kt
+++ b/java/src/com/android/intentresolver/validation/types/Validators.kt
@@ -13,9 +13,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+package com.android.intentresolver.validation.types
-package com.android.intentresolver.v2.listcontroller
+import com.android.intentresolver.validation.Validator
-/** Controller for managing lists of [com.android.intentresolver.ResolvedComponentInfo]s. */
-interface ListController :
- LastChosenManager, IntentResolver, ResolvedComponentFiltering, ResolvedComponentSorting
+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)
+}
diff --git a/java/src/com/android/intentresolver/widget/ActionRow.kt b/java/src/com/android/intentresolver/widget/ActionRow.kt
index 6764d3ae..c1f03751 100644
--- a/java/src/com/android/intentresolver/widget/ActionRow.kt
+++ b/java/src/com/android/intentresolver/widget/ActionRow.kt
@@ -22,7 +22,9 @@ import android.graphics.drawable.Drawable
interface ActionRow {
fun setActions(actions: List<Action>)
- class Action @JvmOverloads constructor(
+ class Action
+ @JvmOverloads
+ constructor(
// TODO: apparently, IDs set to this field are used in unit tests only; evaluate whether we
// get rid of them
val id: Int = ID_NULL,
diff --git a/java/src/com/android/intentresolver/widget/BadgeTextView.kt b/java/src/com/android/intentresolver/widget/BadgeTextView.kt
new file mode 100644
index 00000000..6674d92d
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/BadgeTextView.kt
@@ -0,0 +1,104 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.widget
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.Gravity
+import android.widget.TextView
+
+/**
+ * A TextView that supports a badge at the end of the text. If the text, when centered in the view,
+ * leaves enough room for the badge, the badge is just displayed at the end of the view. Otherwise,
+ * the necessary amount of space for the badge is reserved and the text gets centered in the
+ * remaining free space.
+ */
+class BadgeTextView : TextView {
+ constructor(context: Context) : this(context, null)
+
+ constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
+
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int
+ ) : this(context, attrs, defStyleAttr, 0)
+
+ constructor(
+ context: Context?,
+ attrs: AttributeSet?,
+ defStyleAttr: Int,
+ defStyleRes: Int
+ ) : super(context, attrs, defStyleAttr, defStyleRes) {
+ super.setGravity(Gravity.CENTER)
+ defaultPaddingLeft = paddingLeft
+ defaultPaddingRight = paddingRight
+ }
+
+ private var defaultPaddingLeft = 0
+ private var defaultPaddingRight = 0
+
+ override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
+ super.setPadding(left, top, right, bottom)
+ defaultPaddingLeft = paddingLeft
+ defaultPaddingRight = paddingRight
+ }
+
+ override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
+ super.setPaddingRelative(start, top, end, bottom)
+ defaultPaddingLeft = paddingLeft
+ defaultPaddingRight = paddingRight
+ }
+
+ /** Sets end-sided badge. */
+ var badgeDrawable: Drawable? = null
+ set(value) {
+ if (field !== value) {
+ field = value
+ super.setBackground(value)
+ }
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.setPadding(defaultPaddingLeft, paddingTop, defaultPaddingRight, paddingBottom)
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ val badge = badgeDrawable ?: return
+ if (badge.intrinsicWidth <= paddingEnd) return
+ var maxLineWidth = 0f
+ for (i in 0 until layout.lineCount) {
+ maxLineWidth = maxOf(maxLineWidth, layout.getLineWidth(i))
+ }
+ val sideSpace = (measuredWidth - maxLineWidth) / 2
+ if (sideSpace < badge.intrinsicWidth) {
+ super.setPaddingRelative(
+ paddingStart,
+ paddingTop,
+ paddingEnd + badge.intrinsicWidth - sideSpace.toInt(),
+ paddingBottom
+ )
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+ }
+
+ override fun setBackground(background: Drawable?) {
+ badgeDrawable = null
+ super.setBackground(background)
+ }
+
+ override fun setGravity(gravity: Int): Unit = error("Not supported")
+}
diff --git a/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt b/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt
index 26464ca1..e86de888 100644
--- a/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt
+++ b/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.intentresolver.widget
import android.content.Context
diff --git a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt
index 3f0458ee..55418c49 100644
--- a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt
+++ b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt
@@ -24,15 +24,16 @@ interface ImagePreviewView {
/**
* [ImagePreviewView] progressively prepares views for shared element transition and reports
- * each successful preparation with [onTransitionElementReady] call followed by
- * closing [onAllTransitionElementsReady] invocation. Thus the overall invocation pattern is
- * zero or more [onTransitionElementReady] calls followed by the final
- * [onAllTransitionElementsReady] call.
+ * each successful preparation with [onTransitionElementReady] call followed by closing
+ * [onAllTransitionElementsReady] invocation. Thus the overall invocation pattern is zero or
+ * more [onTransitionElementReady] calls followed by the final [onAllTransitionElementsReady]
+ * call.
*/
interface TransitionElementStatusCallback {
/**
- * Invoked when a view for a shared transition animation element is ready i.e. the image
- * is loaded and the view is laid out.
+ * Invoked when a view for a shared transition animation element is ready i.e. the image is
+ * loaded and the view is laid out.
+ *
* @param name shared element name.
*/
fun onTransitionElementReady(name: String)
diff --git a/java/src/com/android/intentresolver/widget/RecyclerViewExtensions.kt b/java/src/com/android/intentresolver/widget/RecyclerViewExtensions.kt
index a7906001..a8aa633b 100644
--- a/java/src/com/android/intentresolver/widget/RecyclerViewExtensions.kt
+++ b/java/src/com/android/intentresolver/widget/RecyclerViewExtensions.kt
@@ -26,10 +26,10 @@ internal val RecyclerView.areAllChildrenVisible: Boolean
val first = getChildAt(0)
val last = getChildAt(count - 1)
val itemCount = adapter?.itemCount ?: 0
- return getChildAdapterPosition(first) == 0
- && getChildAdapterPosition(last) == itemCount - 1
- && isFullyVisible(first)
- && isFullyVisible(last)
+ return getChildAdapterPosition(first) == 0 &&
+ getChildAdapterPosition(last) == itemCount - 1 &&
+ isFullyVisible(first) &&
+ isFullyVisible(last)
}
private fun RecyclerView.isFullyVisible(view: View): Boolean =
diff --git a/java/src/com/android/intentresolver/widget/ViewExtensions.kt b/java/src/com/android/intentresolver/widget/ViewExtensions.kt
index 11b7c146..d19933f5 100644
--- a/java/src/com/android/intentresolver/widget/ViewExtensions.kt
+++ b/java/src/com/android/intentresolver/widget/ViewExtensions.kt
@@ -19,21 +19,26 @@ package com.android.intentresolver.widget
import android.util.Log
import android.view.View
import androidx.core.view.OneShotPreDrawListener
-import kotlinx.coroutines.suspendCancellableCoroutine
import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.suspendCancellableCoroutine
internal suspend fun View.waitForPreDraw(): Unit = suspendCancellableCoroutine { continuation ->
val isResumed = AtomicBoolean(false)
- val callback = OneShotPreDrawListener.add(
- this,
- Runnable {
- if (isResumed.compareAndSet(false, true)) {
- continuation.resumeWith(Result.success(Unit))
- } else {
- // it's not really expected but in some unknown corner-case let's not crash
- Log.e("waitForPreDraw", "An attempt to resume a completed coroutine", Exception())
+ val callback =
+ OneShotPreDrawListener.add(
+ this,
+ Runnable {
+ if (isResumed.compareAndSet(false, true)) {
+ continuation.resumeWith(Result.success(Unit))
+ } else {
+ // it's not really expected but in some unknown corner-case let's not crash
+ Log.e(
+ "waitForPreDraw",
+ "An attempt to resume a completed coroutine",
+ Exception()
+ )
+ }
}
- }
- )
+ )
continuation.invokeOnCancellation { callback.removeListener() }
}
diff --git a/lint-baseline.xml b/lint-baseline.xml
new file mode 100644
index 00000000..c970b7a7
--- /dev/null
+++ b/lint-baseline.xml
@@ -0,0 +1,2425 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<issues format="6" by="lint 8.4.0-alpha08" type="baseline" client="" dependencies="true" name="" variant="all" version="8.4.0-alpha08">
+
+ <issue
+ id="NonInjectedMainThread"
+ message="Replace with injected `@Main Executor`."
+ errorLine1=" getMainLooper(),"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="421"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NonInjectedMainThread"
+ message="Replace with injected `@Main Executor`."
+ errorLine1=" getMainLooper(),"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="431"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NonInjectedMainThread"
+ message="Replace with injected `@Main Executor`."
+ errorLine1=" getMainLooper(),"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="517"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NonInjectedMainThread"
+ message="Replace with injected `@Main Executor`."
+ errorLine1=" getMainLooper(),"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="526"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NonInjectedMainThread"
+ message="Replace with injected `@Main Executor`."
+ errorLine1=" getMainLooper(),"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="722"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="NonInjectedMainThread"
+ message="Replace with injected `@Main Executor`."
+ errorLine1=" getMainLooper(),"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="733"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NonInjectedMainThread"
+ message="Replace with injected `@Main Executor`."
+ errorLine1=" getMainThreadHandler())) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1684"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="NonInjectedMainThread"
+ message="Replace with injected `@Main Executor`."
+ errorLine1=" getMainThreadHandler().post(() -> {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="2199"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="NonInjectedMainThread"
+ message="Replace with injected `@Main Executor`."
+ errorLine1=" context.getMainExecutor(),"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="192"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NonInjectedMainThread"
+ message="Replace with injected `@Main Executor`."
+ errorLine1=" }, getApplicationContext().getMainExecutor());"
+ errorLine2=" ~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/IntentForwarderActivity.java"
+ line="161"
+ column="44"/>
+ </issue>
+
+ <issue
+ id="NonInjectedMainThread"
+ message="Replace with injected `@Main Executor`."
+ errorLine1=" getMainLooper(),"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="297"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NonInjectedMainThread"
+ message="Replace with injected `@Main Executor`."
+ errorLine1=" getMainLooper(),"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="307"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="NonInjectedMainThread"
+ message="Replace with injected `@Main Executor`."
+ errorLine1=" getMainLooper(),"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="374"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="NonInjectedMainThread"
+ message="Replace with injected `@Main Executor`."
+ errorLine1=" getMainLooper(),"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="383"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NonInjectedMainThread"
+ message="Replace with injected `@Main Executor`."
+ errorLine1=" runnable -> context.getMainThreadHandler().post(runnable));"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverListAdapter.java"
+ line="127"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="WrongCommentType"
+ message="This block comment looks like it was intended to be a javadoc comment"
+ errorLine1=" * {@link MultiProfilePagerAdapter.OnProfileSelectedListener}. The only apparent distinctions"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="821"
+ column="8"/>
+ </issue>
+
+ <issue
+ id="CleanArchitectureDependencyViolation"
+ message="The ui layer may not depend on the data layer."
+ errorLine1="import com.android.intentresolver.data.model.ANDROID_APP_SCHEME"
+ errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ui/model/ActivityModel.kt"
+ line="23"
+ column="1"/>
+ </issue>
+
+ <issue
+ id="CleanArchitectureDependencyViolation"
+ message="The ui layer may not depend on the data layer."
+ errorLine1="import com.android.intentresolver.data.model.ChooserRequest"
+ errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ui/viewmodel/ChooserRequestReader.kt"
+ line="45"
+ column="1"/>
+ </issue>
+
+ <issue
+ id="CleanArchitectureDependencyViolation"
+ message="The ui layer may not depend on the data layer."
+ errorLine1="import com.android.intentresolver.data.model.ChooserRequest"
+ errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt"
+ line="25"
+ column="1"/>
+ </issue>
+
+ <issue
+ id="CleanArchitectureDependencyViolation"
+ message="The ui layer may not depend on the data layer."
+ errorLine1="import com.android.intentresolver.data.repository.ChooserRequestRepository"
+ errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt"
+ line="26"
+ column="1"/>
+ </issue>
+
+ <issue
+ id="CleanArchitectureDependencyViolation"
+ message="The ui layer may not depend on the data layer."
+ errorLine1="import com.android.intentresolver.data.repository.DevicePolicyResources"
+ errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ui/ProfilePagerResources.kt"
+ line="21"
+ column="1"/>
+ </issue>
+
+ <issue
+ id="CleanArchitectureDependencyViolation"
+ message="The domain layer may not depend on the ui layer."
+ errorLine1="import com.android.intentresolver.ui.viewmodel.readAlternateIntents"
+ errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallback.kt"
+ line="40"
+ column="1"/>
+ </issue>
+
+ <issue
+ id="CleanArchitectureDependencyViolation"
+ message="The domain layer may not depend on the ui layer."
+ errorLine1="import com.android.intentresolver.ui.viewmodel.readChooserActions"
+ errorLine2="~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallback.kt"
+ line="41"
+ column="1"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" (UsageStatsManager) userContext.getSystemService(Context.USAGE_STATS_SERVICE));"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/model/AbstractResolverComparator.java"
+ line="136"
+ column="53"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" .getSystemService(AppPredictionManager::class.java)"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt"
+ line="66"
+ column="14"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" (UserManager) context.getSystemService(Context.USER_SERVICE);"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="289"
+ column="47"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" getContext().getSystemService(LauncherApps.class).pinShortcuts("
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java"
+ line="226"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" List&lt;ShortcutManager.ShareShortcutInfo> targets = contextAsUser.getSystemService("
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java"
+ line="233"
+ column="73"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" .getSystemService(ACTIVITY_SERVICE);"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java"
+ line="279"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" context.getSystemService(ActivityManager::class.java)?.launcherLargeIconDensity"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt"
+ line="47"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" return getSystemService(DevicePolicyManager.class).getResources().getString("
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/IntentForwarderActivity.java"
+ line="165"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" return getSystemService(DevicePolicyManager.class).getResources().getString("
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/IntentForwarderActivity.java"
+ line="171"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" return getSystemService(UserManager.class);"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/IntentForwarderActivity.java"
+ line="402"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" LauncherApps launcherApps = context.getSystemService(LauncherApps.class);"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java"
+ line="100"
+ column="49"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" return mContext.getSystemService(DevicePolicyManager.class).getResources().getString("
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java"
+ line="127"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" return mContext.getSystemService(DevicePolicyManager.class).getResources().getString("
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java"
+ line="135"
+ column="29"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" (UserManager) mContext.getSystemService(Context.USER_SERVICE);"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverListAdapter.java"
+ line="503"
+ column="52"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" private val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt"
+ line="77"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" selectedProfileContext.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager?"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt"
+ line="209"
+ column="36"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" final ActivityManager am = (ActivityManager) ctx.getSystemService(ACTIVITY_SERVICE);"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/SimpleIconFactory.java"
+ line="98"
+ column="62"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" return requireNotNull(context.getSystemService(serviceType.java))"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/data/repository/UserScopedService.kt"
+ line="65"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="NonInjectedService"
+ message="Use `@Inject` to get system-level service handles instead of `Context.getSystemService()`"
+ errorLine1=" String title = mContext.getSystemService(DevicePolicyManager.class)"
+ errorLine2=" ~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java"
+ line="83"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="StaticSettingsProvider"
+ message="`@Inject` a GlobalSettings instead"
+ errorLine1=" return Settings.Global.getInt(getContentResolver(),"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/IntentForwarderActivity.java"
+ line="280"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="StaticSettingsProvider"
+ message="`@Inject` a SecureSettings instead"
+ errorLine1=" return Settings.Secure.getString(resolver, name)"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/platform/PlatformSecureSettings.kt"
+ line="32"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="StaticSettingsProvider"
+ message="`@Inject` a SecureSettings instead"
+ errorLine1=" return runCatching { Settings.Secure.getInt(resolver, name) }.getOrNull()"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/platform/PlatformSecureSettings.kt"
+ line="36"
+ column="46"/>
+ </issue>
+
+ <issue
+ id="StaticSettingsProvider"
+ message="`@Inject` a SecureSettings instead"
+ errorLine1=" return runCatching { Settings.Secure.getLong(resolver, name) }.getOrNull()"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/platform/PlatformSecureSettings.kt"
+ line="40"
+ column="46"/>
+ </issue>
+
+ <issue
+ id="StaticSettingsProvider"
+ message="`@Inject` a SecureSettings instead"
+ errorLine1=" return runCatching { Settings.Secure.getFloat(resolver, name) }.getOrNull()"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/platform/PlatformSecureSettings.kt"
+ line="44"
+ column="46"/>
+ </issue>
+
+ <issue
+ id="StaticSettingsProvider"
+ message="`@Inject` a SecureSettings instead"
+ errorLine1=" return Settings.Secure.getString(resolver, name)"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/SecureSettings.kt"
+ line="25"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="CanvasSize"
+ message="Calling `Canvas.getWidth()` is usually wrong; you should be calling `getWidth()` instead"
+ errorLine1=" int xPos = canvas.getWidth() / 2;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/widget/RoundedRectImageView.java"
+ line="134"
+ column="24"/>
+ </issue>
+
+ <issue
+ id="CanvasSize"
+ message="Calling `Canvas.getHeight()` is usually wrong; you should be calling `getHeight()` instead"
+ errorLine1=" int yPos = (int) ((canvas.getHeight() / 2.0f)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/widget/RoundedRectImageView.java"
+ line="135"
+ column="32"/>
+ </issue>
+
+ <issue
+ id="CustomViewStyleable"
+ message="By convention, the declare-styleable (`ResolverDrawerLayout_LayoutParams`) for a layout parameter class (`LayoutParams`) is expected to be the surrounding class (`ResolverDrawerLayout`) plus &quot;`_Layout`&quot;, e.g. `ResolverDrawerLayout_Layout`. (Various editor features rely on this convention.)"
+ errorLine1=" R.styleable.ResolverDrawerLayout_LayoutParams);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java"
+ line="1222"
+ column="21"/>
+ </issue>
+
+ <issue
+ id="InconsistentLayout"
+ message="The id &quot;edit&quot; in layout &quot;image_preview_image_item&quot; is missing from the following layout configurations: layout (present in layout-h480dp)"
+ errorLine1=" android:id=&quot;@+id/edit&quot;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout-h480dp/image_preview_image_item.xml"
+ line="58"
+ column="9"
+ message="Occurrence in layout-h480dp"/>
+ </issue>
+
+ <issue
+ id="MissingConstraints"
+ message="This view is not constrained vertically: at runtime it will jump to the top unless you add a vertical constraint"
+ errorLine1=" &lt;TextView"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/chooser_headline_row.xml"
+ line="27"
+ column="6"/>
+ </issue>
+
+ <issue
+ id="MissingConstraints"
+ message="This view is not constrained horizontally: at runtime it will jump to the left unless you add a horizontal constraint"
+ errorLine1=" &lt;com.android.intentresolver.widget.RoundedRectImageView"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout-h480dp/image_preview_image_item.xml"
+ line="24"
+ column="6"/>
+ </issue>
+
+ <issue
+ id="InflateParams"
+ message="Avoid passing `null` as the view root (needed to resolve layout parameters on the inflated layout&apos;s root element)"
+ errorLine1=" R.layout.resolver_different_item_header, null, false);"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1197"
+ column="62"/>
+ </issue>
+
+ <issue
+ id="InflateParams"
+ message="Avoid passing `null` as the view root (needed to resolve layout parameters on the inflated layout&apos;s root element)"
+ errorLine1=" ? (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile_wrap, null, false)"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/profiles/ChooserMultiProfilePagerAdapter.java"
+ line="123"
+ column="88"/>
+ </issue>
+
+ <issue
+ id="InflateParams"
+ message="Avoid passing `null` as the view root (needed to resolve layout parameters on the inflated layout&apos;s root element)"
+ errorLine1=" : (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile, null, false);"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/profiles/ChooserMultiProfilePagerAdapter.java"
+ line="124"
+ column="83"/>
+ </issue>
+
+ <issue
+ id="InflateParams"
+ message="Avoid passing `null` as the view root (needed to resolve layout parameters on the inflated layout&apos;s root element)"
+ errorLine1=" R.layout.resolver_different_item_header, null, false);"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="853"
+ column="62"/>
+ </issue>
+
+ <issue
+ id="InflateParams"
+ message="Avoid passing `null` as the view root (needed to resolve layout parameters on the inflated layout&apos;s root element)"
+ errorLine1=" R.layout.resolver_list_per_profile, null, false),"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/profiles/ResolverMultiProfilePagerAdapter.java"
+ line="80"
+ column="69"/>
+ </issue>
+
+ <issue
+ id="ManifestOrder"
+ message="`&lt;uses-sdk>` tag appears after `&lt;application>` tag"
+ errorLine1=" &lt;uses-sdk android:minSdkVersion=&quot;VanillaIceCream&quot; android:targetSdkVersion=&quot;16&quot;/>"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="./out/soong/.intermediates/packages/modules/IntentResolver/IntentResolver-core/android_common/e18b8e8d84cb9f664aa09a397b08c165/manifest_fixer/AndroidManifest.xml"
+ line="22"
+ column="6"/>
+ </issue>
+
+ <issue
+ id="MissingInflatedId"
+ message="`@layout/chooser_dialog` does not contain a declaration with id `title`"
+ errorLine1=" TextView title = v.findViewById(com.android.internal.R.id.title);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java"
+ line="133"
+ column="41"/>
+ </issue>
+
+ <issue
+ id="MissingInflatedId"
+ message="`@layout/chooser_dialog` does not contain a declaration with id `icon`"
+ errorLine1=" ImageView icon = v.findViewById(com.android.internal.R.id.icon);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java"
+ line="134"
+ column="41"/>
+ </issue>
+
+ <issue
+ id="MissingInflatedId"
+ message="`@layout/chooser_dialog` does not contain a declaration with id `listContainer`"
+ errorLine1=" RecyclerView rv = v.findViewById(com.android.internal.R.id.listContainer);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java"
+ line="135"
+ column="42"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="437"
+ column="42"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mChooserMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="559"
+ column="54"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" int count = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="761"
+ column="54"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" if (mChooserMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="766"
+ column="46"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" final TargetInfo target = mChooserMultiProfilePagerAdapter.getActiveListAdapter()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="771"
+ column="68"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mChooserMultiProfilePagerAdapter.getActiveListAdapter().getFilteredPosition() >= 0;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="900"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" .getActiveListAdapter().getFilteredItem()))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="909"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" int count = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getCount();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1133"
+ column="54"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" TargetInfo target = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getItem(i);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1136"
+ column="66"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" if (mChooserMultiProfilePagerAdapter.getActiveListAdapter() == null) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1155"
+ column="46"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mChooserMultiProfilePagerAdapter.getActiveListAdapter();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1206"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" int count = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1217"
+ column="54"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1483"
+ column="42"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1623"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mChooserMultiProfilePagerAdapter.getActiveListAdapter();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1699"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" TargetInfo target = mChooserMultiProfilePagerAdapter.getActiveListAdapter()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1724"
+ column="62"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mChooserMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1731"
+ column="58"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mChooserMultiProfilePagerAdapter.getActiveListAdapter();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1801"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mChooserMultiProfilePagerAdapter.getActiveListAdapter();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1838"
+ column="58"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mChooserMultiProfilePagerAdapter.getCurrentUserHandle());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1886"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mChooserMultiProfilePagerAdapter.getCurrentUserHandle());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1914"
+ column="50"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" .getActiveListAdapter()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="1981"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" if (mChooserMultiProfilePagerAdapter.getCurrentUserHandle().equals("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="2041"
+ column="46"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" if (listProfileUserHandle.equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="2288"
+ column="75"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" if (mChooserMultiProfilePagerAdapter.getActiveListAdapter() == adapter) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserActivity.java"
+ line="2351"
+ column="46"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This class should only be accessed from tests or within private scope"
+ errorLine1=" final ViewHolder vh = (ViewHolder) v.getTag();"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java"
+ line="414"
+ column="23"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This class should only be accessed from tests or within private scope"
+ errorLine1=" final ViewHolder vh = (ViewHolder) v.getTag();"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java"
+ line="414"
+ column="40"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" vh.text.setLines(2);"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java"
+ line="415"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" vh.text.setHorizontallyScrolling(false);"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java"
+ line="416"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" vh.text2.setVisibility(View.GONE);"
+ errorLine2=" ~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java"
+ line="417"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This class should only be accessed from tests or within private scope"
+ errorLine1=" private void resetViewHolder(ViewHolder holder) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="432"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" holder.reset();"
+ errorLine2=" ~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="433"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" holder.itemView.setBackground(holder.defaultItemViewBackground);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="434"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" holder.itemView.setBackground(holder.defaultItemViewBackground);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="434"
+ column="46"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" ((BadgeTextView) holder.text).setBadgeDrawable(null);"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="437"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" holder.text.setBackground(null);"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="439"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" holder.text.setPaddingRelative(0, 0, 0, 0);"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="440"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This class should only be accessed from tests or within private scope"
+ errorLine1=" private void updateContentDescription(ViewHolder holder, String description) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="443"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" holder.itemView.setContentDescription(description);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="444"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This class should only be accessed from tests or within private scope"
+ errorLine1=" private void bindPlaceholder(ViewHolder holder) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="447"
+ column="34"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" holder.itemView.setBackground(null);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="448"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This class should only be accessed from tests or within private scope"
+ errorLine1=" private void bindGroupIndicator(ViewHolder holder, Drawable indicator) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="451"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" ((BadgeTextView) holder.text).setBadgeDrawable(indicator);"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="453"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" holder.text.setPaddingRelative(0, 0, /*end = */indicator.getIntrinsicWidth(), 0);"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="455"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" holder.text.setBackground(indicator);"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="456"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This class should only be accessed from tests or within private scope"
+ errorLine1=" private void bindPinnedIndicator(ViewHolder holder, Drawable indicator) {"
+ errorLine2=" ~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="460"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" holder.text.setPaddingRelative(/*start = */indicator.getIntrinsicWidth(), 0, 0, 0);"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="461"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" holder.text.setBackground(indicator);"
+ errorLine2=" ~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="462"
+ column="16"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" getPageAdapterForIndex(i).setAzLabelVisibility(!isCollapsed);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/profiles/ChooserMultiProfilePagerAdapter.java"
+ line="115"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" getActiveListAdapter().notifyDataSetChanged();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/profiles/ChooserMultiProfilePagerAdapter.java"
+ line="135"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" getPageAdapterForIndex(i).setFooterHeight(height);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/profiles/ChooserMultiProfilePagerAdapter.java"
+ line="150"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" ChooserGridAdapter adapter = getPageAdapterForIndex(i);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/profiles/ChooserMultiProfilePagerAdapter.java"
+ line="157"
+ column="42"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" /* instance_id = 3 */ mInstanceId.getId(),"
+ errorLine2=" ~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/logging/EventLogImpl.java"
+ line="96"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" /* instance_id = 3 */ mInstanceId.getId(),"
+ errorLine2=" ~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/logging/EventLogImpl.java"
+ line="118"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" /* instance_id = 3 */ mInstanceId.getId(),"
+ errorLine2=" ~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/logging/EventLogImpl.java"
+ line="142"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" /* instance_id = 3 */ mInstanceId.getId(),"
+ errorLine2=" ~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/logging/EventLogImpl.java"
+ line="200"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This class should only be accessed from tests or within private scope"
+ errorLine1=" private final ResolverListAdapter.ViewHolder mWrappedViewHolder;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/grid/ItemViewHolder.java"
+ line="36"
+ column="19"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mWrappedViewHolder = new ResolverListAdapter.ViewHolder(itemView);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/grid/ItemViewHolder.java"
+ line="46"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="313"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" &amp;&amp; mMultiProfilePagerAdapter.getActiveListAdapter() != null) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="323"
+ column="46"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mMultiProfilePagerAdapter.getActiveListAdapter().onDestroy();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="324"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" MetricsLogger.action(this, mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="415"
+ column="62"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="540"
+ column="35"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" ResolverListAdapter currentListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="560"
+ column="76"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="572"
+ column="52"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="582"
+ column="55"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="596"
+ column="47"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" &amp;&amp; mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredResolveList()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="627"
+ column="46"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" final int N = mMultiProfilePagerAdapter.getActiveListAdapter()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="713"
+ column="57"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="720"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" ResolveInfo r = mMultiProfilePagerAdapter.getActiveListAdapter()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="729"
+ column="63"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" set[N] = mMultiProfilePagerAdapter.getActiveListAdapter()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="737"
+ column="56"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" final int otherProfileMatch = mMultiProfilePagerAdapter.getActiveListAdapter()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="739"
+ column="77"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mMultiProfilePagerAdapter.getActiveListAdapter()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="761"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" .mResolverListController.setLastChosen(intent, filter, bestMatch);"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="762"
+ column="58"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mMultiProfilePagerAdapter.getActiveListAdapter();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="868"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" int count = mMultiProfilePagerAdapter.getActiveListAdapter().getCount();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1143"
+ column="47"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter().getItem(i);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1146"
+ column="59"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mMultiProfilePagerAdapter.getActiveListAdapter().getFilteredPosition() >= 0;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1172"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" .getActiveListAdapter().getFilteredItem()))"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1181"
+ column="42"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" if (!mMultiProfilePagerAdapter.getCurrentUserHandle().equals(getUser())) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1217"
+ column="40"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" ri = mMultiProfilePagerAdapter.getActiveListAdapter()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1233"
+ column="44"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" startActivityAsUser(in, mMultiProfilePagerAdapter.getCurrentUserHandle());"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1326"
+ column="59"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" if (mMultiProfilePagerAdapter.getActiveListAdapter() == null) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1334"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1399"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1399"
+ column="66"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" int count = mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1472"
+ column="47"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" int count = mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1534"
+ column="47"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" if (mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null) {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1539"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" final TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1544"
+ column="61"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" ResolverListAdapter activeListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1687"
+ column="75"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" int filteredPosition = mMultiProfilePagerAdapter.getActiveListAdapter()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1754"
+ column="58"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" if (mMultiProfilePagerAdapter.getActiveListAdapter()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1795"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1828"
+ column="56"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" final TargetInfo ti = ra.mMultiProfilePagerAdapter.getActiveListAdapter()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverActivity.java"
+ line="1896"
+ column="68"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" return mResolverListController.getScore(target);"
+ errorLine2=" ~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverListAdapter.java"
+ line="212"
+ column="40"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mResolverListController.addResolveListDedupe(currentResolveList,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverListAdapter.java"
+ line="329"
+ column="37"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mResolverListController.filterIneligibleActivities(currentResolveList, true);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverListAdapter.java"
+ line="362"
+ column="41"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" return mResolverListController.filterLowPriority("
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverListAdapter.java"
+ line="384"
+ column="40"/>
+ </issue>
+
+ <issue
+ id="VisibleForTests"
+ message="This method should only be accessed from tests or within private scope"
+ errorLine1=" mLastChosen = mResolverListController.getLastChosen();"
+ errorLine2=" ~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ResolverListAdapter.java"
+ line="410"
+ column="55"/>
+ </issue>
+
+ <issue
+ id="SupportAnnotationUsage"
+ message="This annotation does not apply for type java.lang.Object; expected int"
+ errorLine1=" @ContentPreviewType"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt"
+ line="230"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="ExpiredTargetSdkVersion"
+ message="Google Play requires that apps target API level 33 or higher."
+ errorLine1=" &lt;uses-sdk android:minSdkVersion=&quot;VanillaIceCream&quot; android:targetSdkVersion=&quot;16&quot;/>"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="./out/soong/.intermediates/packages/modules/IntentResolver/IntentResolver-core/android_common/e18b8e8d84cb9f664aa09a397b08c165/manifest_fixer/AndroidManifest.xml"
+ line="22"
+ column="55"/>
+ </issue>
+
+ <issue
+ id="BindServiceOnMainThread"
+ message="This method should be annotated with `@WorkerThread` because it calls unbindService"
+ errorLine1=" mContext.unbindService(mConnection);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java"
+ line="308"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="BindServiceOnMainThread"
+ message="This method should be annotated with `@WorkerThread` because it calls bindServiceAsUser"
+ errorLine1=" context.bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE, UserHandle.SYSTEM);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java"
+ line="333"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="NotifyDataSetChanged"
+ message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort."
+ errorLine1=" notifyDataSetChanged();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java"
+ line="139"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="NotifyDataSetChanged"
+ message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort."
+ errorLine1=" notifyDataSetChanged();"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java"
+ line="145"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="NotifyDataSetChanged"
+ message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort."
+ errorLine1=" notifyDataSetChanged()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt"
+ line="94"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="NotifyDataSetChanged"
+ message="It will always be more efficient to use more specific change events if you can. Rely on `notifyDataSetChanged` as a last resort."
+ errorLine1=" notifyDataSetChanged()"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt"
+ line="316"
+ column="13"/>
+ </issue>
+
+ <issue
+ id="RegisterReceiverViaContext"
+ message="Register `BroadcastReceiver` using `BroadcastDispatcher` instead of `Context`"
+ errorLine1=" context.registerReceiverAsUser("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/data/BroadcastSubscriber.kt"
+ line="63"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="RegisterReceiverViaContext"
+ message="Register `BroadcastReceiver` using `BroadcastDispatcher` instead of `Context`"
+ errorLine1=" context.registerReceiverAsUser("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/WorkProfileAvailabilityManager.java"
+ line="74"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="SharedFlowCreation"
+ message="`MutableSharedFlow()` creates a new shared flow, which has poor performance characteristics"
+ errorLine1=" MutableSharedFlow&lt;FileInfo>(replay = records.size).apply {"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt"
+ line="91"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="SharedFlowCreation"
+ message="`MutableSharedFlow()` creates a new shared flow, which has poor performance characteristics"
+ errorLine1=" val reportFlow = MutableSharedFlow&lt;Any>(replay = 2)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt"
+ line="660"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="SharedFlowCreation"
+ message="`MutableSharedFlow()` creates a new shared flow, which has poor performance characteristics"
+ errorLine1=" MutableSharedFlow&lt;Array&lt;DisplayResolveInfo>?>("
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt"
+ line="82"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="SharedFlowCreation"
+ message="`MutableSharedFlow()` creates a new shared flow, which has poor performance characteristics"
+ errorLine1=" MutableSharedFlow&lt;ShortcutData?>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt"
+ line="87"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="SlowUserIdQuery"
+ message="Use `UserTracker.getUserId()` instead of `ActivityManager.getCurrentUser()`"
+ errorLine1=" userHandle == UserHandle.of(ActivityManager.getCurrentUser()),"
+ errorLine2=" ~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt"
+ line="104"
+ column="53"/>
+ </issue>
+
+ <issue
+ id="SlowUserInfoQuery"
+ message="Use `UserTracker.getUserInfo()` instead of `UserManager.getUserInfo()`"
+ errorLine1=" val originUserInfo = userManager.getUserInfo(contentUserHint)"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/IntentForwarding.kt"
+ line="51"
+ column="46"/>
+ </issue>
+
+ <issue
+ id="SlowUserInfoQuery"
+ message="Use `UserTracker.getUserInfo()` instead of `UserManager.getUserInfo()`"
+ errorLine1=" withContext(backgroundDispatcher) { userManager.getUserInfo(user.identifier) }"
+ errorLine2=" ~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/data/repository/UserRepository.kt"
+ line="267"
+ column="61"/>
+ </issue>
+
+ <issue
+ id="SoftwareBitmap"
+ message="Replace software bitmap with `Config.HARDWARE`"
+ errorLine1=" mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8);"
+ errorLine2=" ~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/SimpleIconFactory.java"
+ line="172"
+ column="73"/>
+ </issue>
+
+ <issue
+ id="SoftwareBitmap"
+ message="Replace software bitmap with `Config.HARDWARE`"
+ errorLine1=" bitmap.getHeight(), Bitmap.Config.ARGB_8888);"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/SimpleIconFactory.java"
+ line="297"
+ column="51"/>
+ </issue>
+
+ <issue
+ id="SoftwareBitmap"
+ message="Replace software bitmap with `Config.HARDWARE`"
+ errorLine1=" Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/SimpleIconFactory.java"
+ line="343"
+ column="71"/>
+ </issue>
+
+ <issue
+ id="ObsoleteLayoutParam"
+ message="Invalid layout param in a `LinearLayout`: `layout_alignParentTop`"
+ errorLine1=" android:layout_alignParentTop=&quot;true&quot;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/chooser_grid_scrollable_preview.xml"
+ line="99"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="ObsoleteLayoutParam"
+ message="Invalid layout param in a `LinearLayout`: `layout_centerHorizontal`"
+ errorLine1=" android:layout_centerHorizontal=&quot;true&quot;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/chooser_grid_scrollable_preview.xml"
+ line="100"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="StaticFieldLeak"
+ message="This field leaks a context object"
+ errorLine1=" protected final Context mContext;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/icons/BaseLoadIconTask.java"
+ line="29"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="StaticFieldLeak"
+ message="This `AsyncTask` class should be static or leaks might occur (anonymous android.os.AsyncTask)"
+ errorLine1=" new AsyncTask&lt;Void, Void, List&lt;DisplayResolveInfo>>() {"
+ errorLine2=" ^">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/ChooserListAdapter.java"
+ line="488"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="StaticFieldLeak"
+ message="This field leaks a context object"
+ errorLine1=" private final Context mContext;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/icons/LoadLabelTask.java"
+ line="32"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="UseCompoundDrawables"
+ message="This tag and its children can be replaced by one `&lt;TextView/>` and a compound drawable"
+ errorLine1=" &lt;LinearLayout"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/chooser_dialog.xml"
+ line="29"
+ column="6"/>
+ </issue>
+
+ <issue
+ id="Overdraw"
+ message="Possible overdraw: Root element paints background `?androidprv:attr/materialColorSurfaceContainer` with a theme that also paints a background (inferred theme is `@android:style/Theme.Holo`)"
+ errorLine1=" android:background=&quot;?androidprv:attr/materialColorSurfaceContainer&quot;>"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/chooser_grid_preview_file.xml"
+ line="27"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="Overdraw"
+ message="Possible overdraw: Root element paints background `?androidprv:attr/materialColorSurfaceContainer` with a theme that also paints a background (inferred theme is `@android:style/Theme.Holo`)"
+ errorLine1=" android:background=&quot;?androidprv:attr/materialColorSurfaceContainer&quot;>"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/chooser_grid_preview_files_text.xml"
+ line="26"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="Overdraw"
+ message="Possible overdraw: Root element paints background `?androidprv:attr/materialColorSurfaceContainer` with a theme that also paints a background (inferred theme is `@android:style/Theme.Holo`)"
+ errorLine1=" android:background=&quot;?androidprv:attr/materialColorSurfaceContainer&quot;>"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/chooser_grid_preview_image.xml"
+ line="27"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="Overdraw"
+ message="Possible overdraw: Root element paints background `?androidprv:attr/materialColorSurfaceContainer` with a theme that also paints a background (inferred theme is `@android:style/Theme.Holo`)"
+ errorLine1=" android:background=&quot;?androidprv:attr/materialColorSurfaceContainer&quot;>"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/chooser_grid_preview_text.xml"
+ line="28"
+ column="5"/>
+ </issue>
+
+ <issue
+ id="RedundantNamespace"
+ message="This namespace declaration is redundant"
+ errorLine1=" &lt;vector xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/drawable/chooser_direct_share_icon_placeholder.xml"
+ line="20"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="RedundantNamespace"
+ message="This namespace declaration is redundant"
+ errorLine1=" xmlns:aapt=&quot;http://schemas.android.com/aapt&quot;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/drawable/chooser_direct_share_icon_placeholder.xml"
+ line="21"
+ column="17"/>
+ </issue>
+
+ <issue
+ id="RedundantNamespace"
+ message="This namespace declaration is redundant"
+ errorLine1=" &lt;LinearLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/resolve_list_item.xml"
+ line="40"
+ column="19"/>
+ </issue>
+
+ <issue
+ id="RedundantNamespace"
+ message="This namespace declaration is redundant"
+ errorLine1=" xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/resolver_list_per_profile.xml"
+ line="23"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="UnusedNamespace"
+ message="Unused namespace declaration xmlns:android; already declared on the root element"
+ errorLine1=" &lt;LinearLayout xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/resolve_list_item.xml"
+ line="40"
+ column="19"/>
+ </issue>
+
+ <issue
+ id="UnusedNamespace"
+ message="Unused namespace declaration xmlns:android; already declared on the root element"
+ errorLine1=" xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/resolver_list_per_profile.xml"
+ line="23"
+ column="9"/>
+ </issue>
+
+ <issue
+ id="TypographyEllipsis"
+ message="Replace &quot;...&quot; with ellipsis character (…, &amp;#8230;) ?"
+ errorLine1=" &lt;string name=&quot;whichApplication&quot; msgid=&quot;2309561338625872614&quot;>&quot;... በመጠቀም ድርጊቱን አጠናቅ&quot;&lt;/string>"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/values-am/strings.xml"
+ line="19"
+ column="65"/>
+ </issue>
+
+ <issue
+ id="TypographyEllipsis"
+ message="Replace &quot;...&quot; with ellipsis character (…, &amp;#8230;) ?"
+ errorLine1=" &lt;string name=&quot;whichApplication&quot; msgid=&quot;2309561338625872614&quot;>&quot;Wykonaj czynność przez...&quot;&lt;/string>"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/values-pl/strings.xml"
+ line="19"
+ column="65"/>
+ </issue>
+
+ <issue
+ id="TypographyEllipsis"
+ message="Replace &quot;...&quot; with ellipsis character (…, &amp;#8230;) ?"
+ errorLine1=" &lt;string name=&quot;whichViewApplication&quot; msgid=&quot;7660051361612888119&quot;>&quot;...ဖြင့် ဖွင့်မည်&quot;&lt;/string>"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/values-my/strings.xml"
+ line="22"
+ column="69"/>
+ </issue>
+
+ <issue
+ id="TypographyEllipsis"
+ message="Replace &quot;...&quot; with ellipsis character (…, &amp;#8230;) ?"
+ errorLine1=" &lt;string name=&quot;whichEditApplication&quot; msgid=&quot;5097563012157950614&quot;>&quot;...နှင့် တည်းဖြတ်ရန်&quot;&lt;/string>"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/values-my/strings.xml"
+ line="30"
+ column="69"/>
+ </issue>
+
+ <issue
+ id="TypographyEllipsis"
+ message="Replace &quot;...&quot; with ellipsis character (…, &amp;#8230;) ?"
+ errorLine1=" &lt;string name=&quot;whichSendToApplication&quot; msgid=&quot;2724450540348806267&quot;>&quot;Sūtīšana, izmantojot...&quot;&lt;/string>"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/values-lv/strings.xml"
+ line="36"
+ column="71"/>
+ </issue>
+
+ <issue
+ id="ClickableViewAccessibility"
+ message="Custom view `ResolverDrawerLayout` overrides `onTouchEvent` but not `performClick`"
+ errorLine1=" public boolean onTouchEvent(MotionEvent ev) {"
+ errorLine2=" ~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java"
+ line="403"
+ column="20"/>
+ </issue>
+
+ <issue
+ id="ContentDescription"
+ message="Missing `contentDescription` attribute on image"
+ errorLine1=" &lt;ImageView android:id=&quot;@android:id/icon&quot;"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/chooser_dialog.xml"
+ line="37"
+ column="10"/>
+ </issue>
+
+ <issue
+ id="ContentDescription"
+ message="Missing `contentDescription` attribute on image"
+ errorLine1=" &lt;ImageView android:id=&quot;@android:id/icon&quot;"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/chooser_dialog_item.xml"
+ line="30"
+ column="6"/>
+ </issue>
+
+ <issue
+ id="ContentDescription"
+ message="Missing `contentDescription` attribute on image"
+ errorLine1=" &lt;ImageView android:id=&quot;@android:id/icon&quot;"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/chooser_grid_item.xml"
+ line="32"
+ column="6"/>
+ </issue>
+
+ <issue
+ id="ContentDescription"
+ message="Missing `contentDescription` attribute on image"
+ errorLine1=" &lt;ImageView"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/chooser_grid_preview_file.xml"
+ line="47"
+ column="10"/>
+ </issue>
+
+ <issue
+ id="ContentDescription"
+ message="Missing `contentDescription` attribute on image"
+ errorLine1=" &lt;ImageView"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/chooser_grid_preview_text.xml"
+ line="112"
+ column="8"/>
+ </issue>
+
+ <issue
+ id="ContentDescription"
+ message="Missing `contentDescription` attribute on image"
+ errorLine1=" &lt;ImageView"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/image_preview_image_item.xml"
+ line="43"
+ column="10"/>
+ </issue>
+
+ <issue
+ id="ContentDescription"
+ message="Missing `contentDescription` attribute on image"
+ errorLine1=" &lt;ImageView"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout-h480dp/image_preview_image_item.xml"
+ line="46"
+ column="10"/>
+ </issue>
+
+ <issue
+ id="ContentDescription"
+ message="Missing `contentDescription` attribute on image"
+ errorLine1=" &lt;ImageView"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout-h480dp/image_preview_image_item.xml"
+ line="68"
+ column="10"/>
+ </issue>
+
+ <issue
+ id="ContentDescription"
+ message="Missing `contentDescription` attribute on image"
+ errorLine1=" &lt;ImageView"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/miniresolver.xml"
+ line="39"
+ column="10"/>
+ </issue>
+
+ <issue
+ id="ContentDescription"
+ message="Missing `contentDescription` attribute on image"
+ errorLine1=" &lt;ImageView android:id=&quot;@android:id/icon&quot;"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/resolve_grid_item.xml"
+ line="32"
+ column="6"/>
+ </issue>
+
+ <issue
+ id="ContentDescription"
+ message="Missing `contentDescription` attribute on image"
+ errorLine1=" &lt;ImageView android:id=&quot;@android:id/icon&quot;"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/resolve_list_item.xml"
+ line="30"
+ column="6"/>
+ </issue>
+
+ <issue
+ id="ContentDescription"
+ message="Missing `contentDescription` attribute on image"
+ errorLine1=" &lt;ImageView"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/resolver_list_with_default.xml"
+ line="44"
+ column="14"/>
+ </issue>
+
+ <issue
+ id="ContentDescription"
+ message="Missing `contentDescription` attribute on image"
+ errorLine1=" &lt;ImageView"
+ errorLine2=" ~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/resolver_list_with_default.xml"
+ line="79"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="HardcodedText"
+ message="Hardcoded string &quot;App name&quot;, should use `@string` resource"
+ errorLine1=" android:text=&quot;App name&quot;"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/chooser_dialog.xml"
+ line="46"
+ column="19"/>
+ </issue>
+
+ <issue
+ id="RelativeOverlap"
+ message="`@androidprv:id/button_open` can overlap `@androidprv:id/use_same_profile_browser` if @string/activity_resolver_use_once, @string/whichViewApplicationLabel grow due to localized text expansion"
+ errorLine1=" &lt;Button"
+ errorLine2=" ~~~~~~">
+ <location
+ file="packages/modules/IntentResolver/java/res/layout/miniresolver.xml"
+ line="100"
+ column="14"/>
+ </issue>
+
+</issues>
diff --git a/tests/activity/Android.bp b/tests/activity/Android.bp
index f69caf0e..32077f98 100644
--- a/tests/activity/Android.bp
+++ b/tests/activity/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_capture_and_share",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -53,6 +54,7 @@ android_test {
"junit",
"kotlinx_coroutines_test",
"mockito-target-minus-junit4",
+ "mockito-kotlin2",
"testables",
"truth",
"truth-java8-extension",
diff --git a/tests/activity/AndroidManifest.xml b/tests/activity/AndroidManifest.xml
index be05e99e..00dbd78d 100644
--- a/tests/activity/AndroidManifest.xml
+++ b/tests/activity/AndroidManifest.xml
@@ -26,8 +26,8 @@
<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" />
+ <activity android:name="com.android.intentresolver.ChooserWrapperActivity" />
+ <activity android:name="com.android.intentresolver.ResolverWrapperActivity" />
<provider
android:authorities="com.android.intentresolver.tests"
android:name="com.android.intentresolver.TestContentProvider"
diff --git a/tests/activity/AndroidTest.xml b/tests/activity/AndroidTest.xml
index 6c9d4953..04e4e69f 100644
--- a/tests/activity/AndroidTest.xml
+++ b/tests/activity/AndroidTest.xml
@@ -27,6 +27,7 @@
<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="instrumentation-arg" key="thisisignored" value="thisisignored --no-window-animation" />
<option name="hidden-api-checks" value="false"/>
</test>
</configuration>
diff --git a/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java b/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java
index 3ee80c14..507ce3d7 100644
--- a/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java
+++ b/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.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.content.pm.PackageManager;
import android.content.res.Resources;
import android.database.Cursor;
import android.os.UserHandle;
@@ -31,11 +30,11 @@ import com.android.intentresolver.contentpreview.ImageLoader;
import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
import com.android.intentresolver.shortcuts.ShortcutLoader;
+import kotlin.jvm.functions.Function2;
+
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
@@ -50,75 +49,35 @@ public class 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 ChooserListController resolverListController;
+ public 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;
+ resolverListController = mock(ChooserListController.class);
+ workResolverListController = mock(ChooserListController.class);
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);
diff --git a/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/tests/activity/src/com/android/intentresolver/ChooserActivityTest.java
index f597d7f2..66f7650d 100644
--- a/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityTest.java
+++ b/tests/activity/src/com/android/intentresolver/ChooserActivityTest.java
@@ -88,7 +88,6 @@ 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;
@@ -119,14 +118,29 @@ import androidx.test.rule.ActivityTestRule;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.contentpreview.ImageLoader;
+import com.android.intentresolver.contentpreview.ImageLoaderModule;
+import com.android.intentresolver.data.repository.FakeUserRepository;
+import com.android.intentresolver.data.repository.UserRepository;
+import com.android.intentresolver.data.repository.UserRepositoryModule;
+import com.android.intentresolver.ext.RecyclerViewExt;
+import com.android.intentresolver.inject.ApplicationUser;
+import com.android.intentresolver.inject.PackageManagerModule;
+import com.android.intentresolver.inject.ProfileParent;
import com.android.intentresolver.logging.EventLog;
import com.android.intentresolver.logging.FakeEventLog;
+import com.android.intentresolver.platform.AppPredictionAvailable;
+import com.android.intentresolver.platform.AppPredictionModule;
+import com.android.intentresolver.platform.ImageEditor;
+import com.android.intentresolver.platform.ImageEditorModule;
+import com.android.intentresolver.shared.model.User;
import com.android.intentresolver.shortcuts.ShortcutLoader;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
-import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import dagger.hilt.android.qualifiers.ApplicationContext;
+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;
@@ -137,31 +151,39 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
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;
+
+import javax.inject.Inject;
/**
* Instrumentation tests for ChooserActivity.
* <p>
* Legacy test suite migrated from framework CoreTests.
- * <p>
*/
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
@RunWith(Parameterized.class)
@HiltAndroidTest
-public class UnbundledChooserActivityTest {
+@UninstallModules({
+ AppPredictionModule.class,
+ ImageEditorModule.class,
+ PackageManagerModule.class,
+ ImageLoaderModule.class,
+ UserRepositoryModule.class,
+})
+public class ChooserActivityTest {
private static FakeEventLog getEventLog(ChooserWrapperActivity activity) {
return (FakeEventLog) activity.mEventLog;
@@ -169,25 +191,26 @@ public class UnbundledChooserActivityTest {
private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry
.getInstrumentation().getTargetContext().getUser();
+
+ private static final User PERSONAL_USER =
+ new User(PERSONAL_USER_HANDLE.getIdentifier(), User.Role.PERSONAL);
+
private static final UserHandle WORK_PROFILE_USER_HANDLE = UserHandle.of(10);
+
+ private static final User WORK_USER =
+ new User(WORK_PROFILE_USER_HANDLE.getIdentifier(), User.Role.WORK);
+
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;
- };
+ private static final User CLONE_USER =
+ new User(CLONE_PROFILE_USER_HANDLE.getIdentifier(), User.Role.CLONE);
- @Parameterized.Parameters
- public static Collection packageManagers() {
- return Arrays.asList(new Object[][] {
- // Default PackageManager
- { DEFAULT_PM },
- // No App Prediction Service
- { NO_APP_PREDICTION_SERVICE_PM}
- });
+ @Parameters(name = "appPrediction={0}")
+ public static Iterable<?> parameters() {
+ return Arrays.asList(
+ /* appPredictionAvailable = */ true,
+ /* appPredictionAvailable = */ false
+ );
}
private static final String TEST_MIME_TYPE = "application/TestType";
@@ -206,6 +229,44 @@ public class UnbundledChooserActivityTest {
public ActivityTestRule<ChooserWrapperActivity> mActivityRule =
new ActivityTestRule<>(ChooserWrapperActivity.class, false, false);
+ @Inject
+ @ApplicationContext
+ Context mContext;
+
+ /** 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"));
+
+ /** Whether an AppPredictionService is available for use. */
+ @BindValue
+ @AppPredictionAvailable
+ final boolean mAppPredictionAvailable;
+
+ @BindValue
+ PackageManager mPackageManager;
+
+ /** "launchedAs" */
+ @BindValue
+ @ApplicationUser
+ UserHandle mApplicationUser = PERSONAL_USER_HANDLE;
+
+ @BindValue
+ @ProfileParent
+ UserHandle mProfileParent = PERSONAL_USER_HANDLE;
+
+ private final FakeUserRepository mFakeUserRepo = new FakeUserRepository(List.of(PERSONAL_USER));
+
+ @BindValue
+ final UserRepository mUserRepository = mFakeUserRepo;
+
+ private final FakeImageLoader mFakeImageLoader = new FakeImageLoader();
+
+ @BindValue
+ final ImageLoader mImageLoader = mFakeImageLoader;
+
@Before
public void setUp() {
// TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the
@@ -216,14 +277,21 @@ public class UnbundledChooserActivityTest {
.adoptShellPermissionIdentity();
cleanOverrideData();
+
+ // Assign @Inject fields
mHiltAndroidRule.inject();
- }
- private final Function<PackageManager, PackageManager> mPackageManagerOverride;
+ // Populate @BindValue dependencies using injected values. These fields contribute
+ // values to the dependency graph at activity launch time. This allows replacing
+ // arbitrary bindings per-test case if needed.
+ mPackageManager = mContext.getPackageManager();
+
+ // TODO: inject image loader in the prod code and remove this override
+ ChooserActivityOverrideData.getInstance().imageLoader = mFakeImageLoader;
+ }
- public UnbundledChooserActivityTest(
- Function<PackageManager, PackageManager> packageManagerOverride) {
- mPackageManagerOverride = packageManagerOverride;
+ public ChooserActivityTest(boolean appPredictionAvailable) {
+ mAppPredictionAvailable = appPredictionAvailable;
}
private void setDeviceConfigProperty(
@@ -246,13 +314,18 @@ public class UnbundledChooserActivityTest {
public void cleanOverrideData() {
ChooserActivityOverrideData.getInstance().reset();
- ChooserActivityOverrideData.getInstance().createPackageManager = mPackageManagerOverride;
setDeviceConfigProperty(
SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
Boolean.toString(true));
}
+ private static PackageManager createFakePackageManager(ResolveInfo resolveInfo) {
+ PackageManager packageManager = mock(PackageManager.class);
+ when(packageManager.resolveActivity(any(Intent.class), any())).thenReturn(resolveInfo);
+ return packageManager;
+ }
+
@Test
public void customTitle() throws InterruptedException {
Intent viewIntent = createViewTextIntent();
@@ -392,14 +465,13 @@ public class UnbundledChooserActivityTest {
}
@Test
- public void visiblePreviewTitleAndThumbnail() throws InterruptedException {
+ public void visiblePreviewTitleAndThumbnail() {
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());
+ mFakeImageLoader.setBitmap(uri, createBitmap());
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
setupResolverControllers(resolvedComponentInfos);
@@ -665,8 +737,7 @@ public class UnbundledChooserActivityTest {
public void testFilePlusTextSharing_ExcludeText() {
Uri uri = createTestContentProviderUri(null, "image/png");
Intent sendIntent = createSendImageIntent(uri);
- ChooserActivityOverrideData.getInstance().imageLoader =
- createImageLoader(uri, createBitmap());
+ mFakeImageLoader.setBitmap(uri, createBitmap());
sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google");
List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList(
@@ -707,8 +778,7 @@ public class UnbundledChooserActivityTest {
public void testFilePlusTextSharing_RemoveAndAddBackText() {
Uri uri = createTestContentProviderUri("application/pdf", "image/png");
Intent sendIntent = createSendImageIntent(uri);
- ChooserActivityOverrideData.getInstance().imageLoader =
- createImageLoader(uri, createBitmap());
+ mFakeImageLoader.setBitmap(uri, createBitmap());
final String text = "https://google.com/search?q=google";
sendIntent.putExtra(Intent.EXTRA_TEXT, text);
@@ -755,8 +825,7 @@ public class UnbundledChooserActivityTest {
public void testFilePlusTextSharing_TextExclusionDoesNotAffectAlternativeIntent() {
Uri uri = createTestContentProviderUri("image/png", null);
Intent sendIntent = createSendImageIntent(uri);
- ChooserActivityOverrideData.getInstance().imageLoader =
- createImageLoader(uri, createBitmap());
+ mFakeImageLoader.setBitmap(uri, createBitmap());
sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google");
Intent alternativeIntent = createSendTextIntent();
@@ -799,8 +868,6 @@ public class UnbundledChooserActivityTest {
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(
@@ -890,14 +957,12 @@ public class UnbundledChooserActivityTest {
// 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());
+ mFakeImageLoader.setBitmap(uri, createBitmap());
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -921,8 +986,7 @@ public class UnbundledChooserActivityTest {
uris.add(uri);
Intent sendIntent = createSendUriIntentWithPreview(uris);
- ChooserActivityOverrideData.getInstance().imageLoader =
- createImageLoader(uri, createWideBitmap());
+ mFakeImageLoader.setBitmap(uri, createWideBitmap());
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -935,8 +999,11 @@ public class UnbundledChooserActivityTest {
throw exception;
}
RecyclerView recyclerView = (RecyclerView) view;
- assertThat(recyclerView.getAdapter().getItemCount(), is(1));
- assertThat(recyclerView.getChildCount(), is(1));
+ RecyclerViewExt.endAnimations(recyclerView);
+ assertThat("recyclerView adapter item count",
+ recyclerView.getAdapter().getItemCount(), is(1));
+ assertThat("recyclerView child view count",
+ recyclerView.getChildCount(), is(1));
View imageView = recyclerView.getChildAt(0);
Rect rect = new Rect();
boolean isPartiallyVisible = imageView.getGlobalVisibleRect(rect);
@@ -957,8 +1024,6 @@ public class UnbundledChooserActivityTest {
uris.add(uri);
Intent sendIntent = createSendUriIntentWithPreview(uris);
- ChooserActivityOverrideData.getInstance().imageLoader =
- new TestPreviewImageLoader(Collections.emptyMap());
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -976,8 +1041,7 @@ public class UnbundledChooserActivityTest {
ArrayList<Uri> uris = new ArrayList<>(1);
uris.add(uri);
Intent sendIntent = createSendUriIntentWithPreview(uris);
- ChooserActivityOverrideData.getInstance().imageLoader =
- createImageLoader(uri, createBitmap());
+ mFakeImageLoader.setBitmap(uri, createBitmap());
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -1003,8 +1067,7 @@ public class UnbundledChooserActivityTest {
}
uris.add(imageUri);
Intent sendIntent = createSendUriIntentWithPreview(uris);
- ChooserActivityOverrideData.getInstance().imageLoader =
- createImageLoader(imageUri, createBitmap());
+ mFakeImageLoader.setBitmap(imageUri, createBitmap());
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
setupResolverControllers(resolvedComponentInfos);
@@ -1036,8 +1099,7 @@ public class UnbundledChooserActivityTest {
uris.add(uri);
Intent sendIntent = createSendUriIntentWithPreview(uris);
- ChooserActivityOverrideData.getInstance().imageLoader =
- createImageLoader(uri, createBitmap());
+ mFakeImageLoader.setBitmap(uri, createBitmap());
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -1071,12 +1133,9 @@ public class UnbundledChooserActivityTest {
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);
+ mFakeImageLoader.setBitmap(imgOneUri, createWideBitmap(Color.RED));
+ mFakeImageLoader.setBitmap(imgTwoUri, createWideBitmap(Color.GREEN));
+ mFakeImageLoader.setBitmap(docUri, createWideBitmap(Color.BLUE));
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
setupResolverControllers(resolvedComponentInfos);
@@ -1094,6 +1153,7 @@ public class UnbundledChooserActivityTest {
throw exception;
}
RecyclerView recyclerView = (RecyclerView) view;
+ RecyclerViewExt.endAnimations(recyclerView);
assertThat(recyclerView.getChildCount()).isAtLeast(1);
// the first view is a preview
View imageView = recyclerView.getChildAt(0).findViewById(R.id.image);
@@ -1124,8 +1184,7 @@ public class UnbundledChooserActivityTest {
Intent sendIntent = createSendUriIntentWithPreview(uris);
sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText);
- ChooserActivityOverrideData.getInstance().imageLoader =
- createImageLoader(uri, createBitmap());
+ mFakeImageLoader.setBitmap(uri, createBitmap());
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -1154,8 +1213,7 @@ public class UnbundledChooserActivityTest {
Intent sendIntent = createSendUriIntentWithPreview(uris);
sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText);
- ChooserActivityOverrideData.getInstance().imageLoader =
- createImageLoader(uri, createBitmap());
+ mFakeImageLoader.setBitmap(uri, createBitmap());
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -1191,8 +1249,7 @@ public class UnbundledChooserActivityTest {
Intent sendIntent = createSendUriIntentWithPreview(uris);
sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText);
- ChooserActivityOverrideData.getInstance().imageLoader =
- createImageLoader(uri, createBitmap());
+ mFakeImageLoader.setBitmap(uri, createBitmap());
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -1232,8 +1289,10 @@ public class UnbundledChooserActivityTest {
public void testOnCreateLoggingFromWorkProfile() {
Intent sendIntent = createSendTextIntent();
sendIntent.setType(TEST_MIME_TYPE);
- ChooserActivityOverrideData.getInstance().alternateProfileSetting =
- MetricsEvent.MANAGED_PROFILE;
+
+ // Launch as work user.
+ mFakeUserRepo.addUser(WORK_USER, true);
+ mApplicationUser = WORK_PROFILE_USER_HANDLE;
ChooserWrapperActivity activity =
mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
@@ -1288,8 +1347,7 @@ public class UnbundledChooserActivityTest {
uris.add(uri);
Intent sendIntent = createSendUriIntentWithPreview(uris);
- ChooserActivityOverrideData.getInstance().imageLoader =
- createImageLoader(uri, createBitmap());
+ mFakeImageLoader.setBitmap(uri, createBitmap());
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -2131,7 +2189,7 @@ public class UnbundledChooserActivityTest {
createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
List<ResolvedComponentInfo> workResolvedComponentInfos =
createResolvedComponentsForTest(workProfileTargets);
- ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true;
+ mFakeUserRepo.updateState(WORK_USER, false);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendTextIntent();
sendIntent.setType(TEST_MIME_TYPE);
@@ -2170,7 +2228,6 @@ public class UnbundledChooserActivityTest {
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_SCROLLABLE_PREVIEW)
public void testWorkTab_previewIsScrollable() {
markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
@@ -2185,8 +2242,7 @@ public class UnbundledChooserActivityTest {
uris.add(uri);
Intent sendIntent = createSendUriIntentWithPreview(uris);
- ChooserActivityOverrideData.getInstance().imageLoader =
- createImageLoader(uri, createWideBitmap());
+ mFakeImageLoader.setBitmap(uri, createWideBitmap());
mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Scrollable preview test"));
waitForIdle();
@@ -2214,7 +2270,7 @@ public class UnbundledChooserActivityTest {
List<ResolvedComponentInfo> workResolvedComponentInfos =
createResolvedComponentsForTest(0);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
- ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true;
+ mFakeUserRepo.updateState(WORK_USER, false);
ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false;
Intent sendIntent = createSendTextIntent();
sendIntent.setType(TEST_MIME_TYPE);
@@ -2238,7 +2294,7 @@ public class UnbundledChooserActivityTest {
List<ResolvedComponentInfo> workResolvedComponentInfos =
createResolvedComponentsForTest(0);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
- ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true;
+ mFakeUserRepo.updateState(WORK_USER, false);
Intent sendIntent = createSendTextIntent();
sendIntent.setType(TEST_MIME_TYPE);
@@ -2514,13 +2570,7 @@ public class UnbundledChooserActivityTest {
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);
+ mPackageManager = createFakePackageManager(createFakeResolveInfo());
waitForIdle();
IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent);
@@ -2545,13 +2595,7 @@ public class UnbundledChooserActivityTest {
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());
+ mPackageManager = createFakePackageManager(createFakeResolveInfo());
waitForIdle();
IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent);
@@ -2576,13 +2620,8 @@ public class UnbundledChooserActivityTest {
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());
+ mPackageManager = createFakePackageManager(createFakeResolveInfo());
+
mActivityRule.launchActivity(chooserIntent);
waitForIdle();
@@ -2608,13 +2647,8 @@ public class UnbundledChooserActivityTest {
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());
+ mPackageManager = createFakePackageManager(createFakeResolveInfo());
+
mActivityRule.launchActivity(chooserIntent);
waitForIdle();
@@ -2636,15 +2670,8 @@ public class UnbundledChooserActivityTest {
// 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);
+ mPackageManager = createFakePackageManager(ResolverDataProvider.createResolveInfo(0,
+ UserHandle.USER_CURRENT, PERSONAL_USER_HANDLE));
waitForIdle();
IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent);
@@ -3003,18 +3030,12 @@ public class UnbundledChooserActivityTest {
}
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);
+ mFakeUserRepo.addUser(WORK_USER, /* available= */ true);
}
if (cloneAvailable) {
- handles.setCloneProfileUserHandle(CLONE_PROFILE_USER_HANDLE);
+ mFakeUserRepo.addUser(CLONE_USER, /* available= */ true);
}
- ChooserWrapperActivity.sOverrides.annotatedUserHandles = handles.build();
}
private void setupResolverControllers(
@@ -3034,8 +3055,8 @@ public class UnbundledChooserActivityTest {
Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class),
- eq(UserHandle.SYSTEM)))
- .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ eq(PERSONAL_USER_HANDLE)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
when(
ChooserActivityOverrideData
.getInstance()
@@ -3045,19 +3066,8 @@ public class UnbundledChooserActivityTest {
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));
+ eq(WORK_PROFILE_USER_HANDLE)))
+ .thenReturn(new ArrayList<>(workResolvedComponentInfos));
}
private static GridRecyclerSpanCountMatcher withGridColumnCount(int columnCount) {
@@ -3120,8 +3130,4 @@ public class UnbundledChooserActivityTest {
};
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/UnbundledChooserActivityWorkProfileTest.java b/tests/activity/src/com/android/intentresolver/ChooserActivityWorkProfileTest.java
index da879f74..022ae2e1 100644
--- a/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java
+++ b/tests/activity/src/com/android/intentresolver/ChooserActivityWorkProfileTest.java
@@ -27,14 +27,14 @@ 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.ChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.NO_BLOCKER;
+import static com.android.intentresolver.ChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_ACCESS_BLOCKER;
+import static com.android.intentresolver.ChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_SHARE_BLOCKER;
+import static com.android.intentresolver.ChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_ACCESS_BLOCKER;
+import static com.android.intentresolver.ChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_SHARE_BLOCKER;
+import static com.android.intentresolver.ChooserActivityWorkProfileTest.TestCase.Tab.PERSONAL;
+import static com.android.intentresolver.ChooserActivityWorkProfileTest.TestCase.Tab.WORK;
import static com.android.intentresolver.ChooserWrapperActivity.sOverrides;
-import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.NO_BLOCKER;
-import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_ACCESS_BLOCKER;
-import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_SHARE_BLOCKER;
-import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_ACCESS_BLOCKER;
-import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_SHARE_BLOCKER;
-import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab.PERSONAL;
-import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab.WORK;
import static org.hamcrest.CoreMatchers.not;
import static org.mockito.ArgumentMatchers.eq;
@@ -44,11 +44,22 @@ import android.companion.DeviceFilter;
import android.content.Intent;
import android.os.UserHandle;
-import androidx.test.InstrumentationRegistry;
import androidx.test.espresso.NoMatchingViewException;
+import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
-import com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab;
+import com.android.intentresolver.ChooserActivityWorkProfileTest.TestCase.Tab;
+import com.android.intentresolver.data.repository.FakeUserRepository;
+import com.android.intentresolver.data.repository.UserRepository;
+import com.android.intentresolver.data.repository.UserRepositoryModule;
+import com.android.intentresolver.inject.ApplicationUser;
+import com.android.intentresolver.inject.ProfileParent;
+import com.android.intentresolver.shared.model.User;
+
+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 junit.framework.AssertionFailedError;
@@ -64,13 +75,11 @@ 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 {
+@UninstallModules(UserRepositoryModule.class)
+public class ChooserActivityWorkProfileTest {
private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry
.getInstrumentation().getTargetContext().getUser();
@@ -83,10 +92,31 @@ public class UnbundledChooserActivityWorkProfileTest {
public ActivityTestRule<ChooserWrapperActivity> mActivityRule =
new ActivityTestRule<>(ChooserWrapperActivity.class, false,
false);
+
+ @BindValue
+ @ApplicationUser
+ public final UserHandle mApplicationUser;
+
+ @BindValue
+ @ProfileParent
+ public final UserHandle mProfileParent;
+
+ /** For setup of test state, a mutable reference of mUserRepository */
+ private final FakeUserRepository mFakeUserRepo = new FakeUserRepository(
+ List.of(new User(PERSONAL_USER_HANDLE.getIdentifier(), User.Role.PERSONAL)));
+
+ @BindValue
+ public final UserRepository mUserRepository;
+
private final TestCase mTestCase;
- public UnbundledChooserActivityWorkProfileTest(TestCase testCase) {
+ public ChooserActivityWorkProfileTest(TestCase testCase) {
mTestCase = testCase;
+ mApplicationUser = mTestCase.getMyUserHandle();
+ mProfileParent = PERSONAL_USER_HANDLE;
+ mUserRepository = new FakeUserRepository(List.of(
+ new User(PERSONAL_USER_HANDLE.getIdentifier(), User.Role.PERSONAL),
+ new User(WORK_USER_HANDLE.getIdentifier(), User.Role.WORK)));
}
@Before
@@ -267,12 +297,6 @@ public class UnbundledChooserActivityWorkProfileTest {
}
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,
diff --git a/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java b/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java
index 4ea0681d..4b71aa29 100644
--- a/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java
+++ b/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java
@@ -16,14 +16,13 @@
package com.android.intentresolver;
+import android.annotation.Nullable;
import android.app.prediction.AppPredictor;
import android.app.usage.UsageStatsManager;
-import android.content.ComponentName;
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;
@@ -31,17 +30,12 @@ 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.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;
@@ -54,15 +48,8 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW
static final ChooserActivityOverrideData sOverrides = ChooserActivityOverrideData.getInstance();
private UsageStatsManager mUsm;
- // 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(
+ public final ChooserListAdapter createChooserListAdapter(
Context context,
List<Intent> payloadIntents,
Intent[] initialIntents,
@@ -71,12 +58,9 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW
ResolverListController resolverListController,
UserHandle userHandle,
Intent targetIntent,
- Intent referrrerFillInIntent,
- int maxTargetsPerRow,
- TargetDataLoader targetDataLoader) {
- PackageManager packageManager =
- sOverrides.packageManager == null ? context.getPackageManager()
- : sOverrides.packageManager;
+ Intent referrerFillInIntent,
+ int maxTargetsPerRow) {
+
return new ChooserListAdapter(
context,
payloadIntents,
@@ -86,14 +70,15 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW
createListController(userHandle),
userHandle,
targetIntent,
- referrrerFillInIntent,
+ referrerFillInIntent,
this,
- packageManager,
+ mPackageManager,
getEventLog(),
maxTargetsPerRow,
userHandle,
- targetDataLoader,
- null);
+ mTargetDataLoader,
+ null,
+ mFeatureFlags);
}
@Override
@@ -103,17 +88,12 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW
@Override
public ChooserListAdapter getPersonalListAdapter() {
- return ((ChooserGridAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(0))
- .getListAdapter();
+ return mChooserMultiProfilePagerAdapter.getPersonalListAdapter();
}
@Override
public ChooserListAdapter getWorkListAdapter() {
- if (mMultiProfilePagerAdapter.getInactiveListAdapter() == null) {
- return null;
- }
- return ((ChooserGridAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(1))
- .getListAdapter();
+ return mChooserMultiProfilePagerAdapter.getWorkListAdapter();
}
@Override
@@ -122,16 +102,6 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW
}
@Override
- protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() {
- return new ChooserIntegratedDeviceComponents(
- /* editSharingComponent=*/ null,
- // An arbitrary pre-installed activity that handles this type of intent:
- /* nearbySharingComponent=*/ new ComponentName(
- "com.google.android.apps.messaging",
- ".ui.conversationlist.ShareIntentActivity"));
- }
-
- @Override
public UsageStatsManager getUsageStatsManager() {
if (mUsm == null) {
mUsm = getSystemService(UsageStatsManager.class);
@@ -156,14 +126,6 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW
}
@Override
- protected WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() {
- if (sOverrides.mWorkProfileAvailability != null) {
- return sOverrides.mWorkProfileAvailability;
- }
- return super.createWorkProfileAvailabilityManager();
- }
-
- @Override
public void safelyStartActivityInternal(TargetInfo cti, UserHandle user,
@Nullable Bundle options) {
if (sOverrides.onSafelyStartInternalCallback != null
@@ -174,7 +136,7 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW
}
@Override
- protected ChooserListController createListController(UserHandle userHandle) {
+ public final ChooserListController createListController(UserHandle userHandle) {
if (userHandle == UserHandle.SYSTEM) {
return sOverrides.resolverListController;
}
@@ -182,14 +144,6 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW
}
@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;
@@ -218,14 +172,6 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW
}
@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,
@@ -241,16 +187,10 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW
}
@Override
- protected AnnotatedUserHandles computeAnnotatedUserHandles() {
- return sOverrides.annotatedUserHandles;
- }
-
- @Override
public UserHandle getCurrentUserHandle() {
- return mMultiProfilePagerAdapter.getCurrentUserHandle();
+ return mChooserMultiProfilePagerAdapter.getCurrentUserHandle();
}
- @NonNull
@Override
public Context createContextAsUser(UserHandle user, int flags) {
// return the current context as a work profile doesn't really exist in these tests
diff --git a/tests/activity/src/com/android/intentresolver/ResolverActivityTest.java b/tests/activity/src/com/android/intentresolver/ResolverActivityTest.java
index dde2f980..b44f4f91 100644
--- a/tests/activity/src/com/android/intentresolver/ResolverActivityTest.java
+++ b/tests/activity/src/com/android/intentresolver/ResolverActivityTest.java
@@ -25,6 +25,7 @@ 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 androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
import static com.android.intentresolver.MatcherUtils.first;
import static com.android.intentresolver.ResolverWrapperActivity.sOverrides;
@@ -49,16 +50,27 @@ 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.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
+import com.android.intentresolver.data.repository.FakeUserRepository;
+import com.android.intentresolver.data.repository.UserRepository;
+import com.android.intentresolver.data.repository.UserRepositoryModule;
+import com.android.intentresolver.inject.ApplicationUser;
+import com.android.intentresolver.inject.ProfileParent;
+import com.android.intentresolver.shared.model.User;
import com.android.intentresolver.widget.ResolverDrawerLayout;
import com.google.android.collect.Lists;
+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.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
@@ -73,14 +85,21 @@ import java.util.List;
* Resolver activity instrumentation tests
*/
@RunWith(AndroidJUnit4.class)
+@HiltAndroidTest
+@UninstallModules(UserRepositoryModule.class)
public class ResolverActivityTest {
- private static final UserHandle PERSONAL_USER_HANDLE = androidx.test.platform.app
- .InstrumentationRegistry.getInstrumentation().getTargetContext().getUser();
+ private static final UserHandle PERSONAL_USER_HANDLE =
+ 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 User WORK_PROFILE_USER =
+ new User(WORK_PROFILE_USER_HANDLE.getIdentifier(), User.Role.WORK);
+
+ @Rule(order = 0)
+ public HiltAndroidRule mHiltAndroidRule = new HiltAndroidRule(this);
- @Rule
+ @Rule(order = 1)
public ActivityTestRule<ResolverWrapperActivity> mActivityRule =
new ActivityTestRule<>(ResolverWrapperActivity.class, false, false);
@@ -88,14 +107,30 @@ public class ResolverActivityTest {
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()
+ getInstrumentation()
.getUiAutomation()
.adoptShellPermissionIdentity();
sOverrides.reset();
}
+ @BindValue
+ @ApplicationUser
+ public final UserHandle mApplicationUser = PERSONAL_USER_HANDLE;
+
+ @BindValue
+ @ProfileParent
+ public final UserHandle mProfileParent = PERSONAL_USER_HANDLE;
+
+ /** For setup of test state, a mutable reference of mUserRepository */
+ private final FakeUserRepository mFakeUserRepo =
+ new FakeUserRepository(List.of(
+ new User(PERSONAL_USER_HANDLE.getIdentifier(), User.Role.PERSONAL)
+ ));
+
+ @BindValue
+ public final UserRepository mUserRepository = mFakeUserRepo;
+
@Test
public void twoOptionsAndUserSelectsOne() throws InterruptedException {
Intent sendIntent = createSendImageIntent();
@@ -386,15 +421,14 @@ public class ResolverActivityTest {
@Test
public void testWorkTab_workTabUsesExpectedAdapter() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
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();
@@ -406,9 +440,9 @@ public class ResolverActivityTest {
@Test
public void testWorkTab_personalTabUsesExpectedAdapter() {
+ markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
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);
@@ -446,7 +480,8 @@ public class ResolverActivityTest {
public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() throws InterruptedException {
markOtherProfileAvailability(/* workAvailable= */ true, /* cloneAvailable= */ false);
List<ResolvedComponentInfo> personalResolvedComponentInfos =
- createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10,
+ createResolvedComponentsForTestWithOtherProfile(3,
+ /* userId */ WORK_PROFILE_USER_HANDLE.getIdentifier(),
PERSONAL_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
WORK_PROFILE_USER_HANDLE);
@@ -604,7 +639,7 @@ public class ResolverActivityTest {
PERSONAL_USER_HANDLE);
List<ResolvedComponentInfo> workResolvedComponentInfos =
createResolvedComponentsForTest(workProfileTargets, WORK_PROFILE_USER_HANDLE);
- sOverrides.isQuietModeEnabled = true;
+ mFakeUserRepo.updateState(WORK_PROFILE_USER, false);
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
sendIntent.setType("TestType");
@@ -652,7 +687,7 @@ public class ResolverActivityTest {
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
sendIntent.setType("TestType");
- sOverrides.isQuietModeEnabled = true;
+ mFakeUserRepo.updateState(WORK_PROFILE_USER, false);
sOverrides.hasCrossProfileIntents = false;
mActivityRule.launchActivity(sendIntent);
@@ -722,7 +757,7 @@ public class ResolverActivityTest {
setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
Intent sendIntent = createSendImageIntent();
sendIntent.setType("TestType");
- sOverrides.isQuietModeEnabled = true;
+ mFakeUserRepo.updateState(WORK_PROFILE_USER, false);
mActivityRule.launchActivity(sendIntent);
waitForIdle();
@@ -1050,18 +1085,14 @@ public class ResolverActivityTest {
}
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);
+ mFakeUserRepo.addUser(
+ new User(WORK_PROFILE_USER_HANDLE.getIdentifier(), User.Role.WORK), true);
}
if (cloneAvailable) {
- handles.setCloneProfileUserHandle(CLONE_PROFILE_USER_HANDLE);
+ mFakeUserRepo.addUser(
+ new User(CLONE_PROFILE_USER_HANDLE.getIdentifier(), User.Role.CLONE), true);
}
- sOverrides.annotatedUserHandles = handles.build();
}
private void setupResolverControllers(
@@ -1077,21 +1108,14 @@ public class ResolverActivityTest {
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)))
+ eq(PERSONAL_USER_HANDLE)))
.thenReturn(new ArrayList<>(personalResolvedComponentInfos));
when(sOverrides.workResolverListController.getResolversForIntentAsUser(
Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class),
- eq(UserHandle.of(10))))
+ eq(WORK_PROFILE_USER_HANDLE)))
.thenReturn(new ArrayList<>(workResolvedComponentInfos));
}
}
diff --git a/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java b/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java
index d1adfba9..30858c8e 100644
--- a/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java
+++ b/tests/activity/src/com/android/intentresolver/ResolverWrapperActivity.java
@@ -21,9 +21,9 @@ 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;
@@ -31,7 +31,6 @@ 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.chooser.DisplayResolveInfo;
@@ -54,10 +53,6 @@ public class ResolverWrapperActivity extends ResolverActivity {
private final CountingIdlingResource mLabelIdlingResource =
new CountingIdlingResource("LoadLabelTask");
- public ResolverWrapperActivity() {
- super(/* isIntentPicker= */ true);
- }
-
public CountingIdlingResource getLabelIdlingResource() {
return mLabelIdlingResource;
}
@@ -69,8 +64,7 @@ public class ResolverWrapperActivity extends ResolverActivity {
Intent[] initialIntents,
List<ResolveInfo> rList,
boolean filterLastUsed,
- UserHandle userHandle,
- TargetDataLoader targetDataLoader) {
+ UserHandle userHandle) {
return new ResolverListAdapter(
context,
payloadIntents,
@@ -82,7 +76,7 @@ public class ResolverWrapperActivity extends ResolverActivity {
payloadIntents.get(0), // TODO: extract upstream
this,
userHandle,
- new TargetDataLoaderWrapper(targetDataLoader, mLabelIdlingResource));
+ new TargetDataLoaderWrapper(mTargetDataLoader, mLabelIdlingResource));
}
@Override
@@ -93,27 +87,16 @@ public class ResolverWrapperActivity extends ResolverActivity {
return super.createCrossProfileIntentsChecker();
}
- @Override
- protected WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() {
- if (sOverrides.mWorkProfileAvailability != null) {
- return sOverrides.mWorkProfileAvailability;
- }
- return super.createWorkProfileAvailabilityManager();
- }
-
ResolverListAdapter getAdapter() {
return mMultiProfilePagerAdapter.getActiveListAdapter();
}
ResolverListAdapter getPersonalListAdapter() {
- return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(0));
+ return mMultiProfilePagerAdapter.getPersonalListAdapter();
}
ResolverListAdapter getWorkListAdapter() {
- if (mMultiProfilePagerAdapter.getInactiveListAdapter() == null) {
- return null;
- }
- return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(1));
+ return mMultiProfilePagerAdapter.getWorkListAdapter();
}
@Override
@@ -142,96 +125,35 @@ public class ResolverWrapperActivity extends ResolverActivity {
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
- protected AnnotatedUserHandles computeAnnotatedUserHandles() {
- return sOverrides.annotatedUserHandles;
- }
- @Override
- public void startActivityAsUser(
- @NonNull Intent intent,
- Bundle options,
- @NonNull UserHandle user
- ) {
+ 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.
*/
- static class OverrideData {
+ 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);
diff --git a/tests/activity/src/com/android/intentresolver/ext/RecyclerViewExt.kt b/tests/activity/src/com/android/intentresolver/ext/RecyclerViewExt.kt
new file mode 100644
index 00000000..90acaa60
--- /dev/null
+++ b/tests/activity/src/com/android/intentresolver/ext/RecyclerViewExt.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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("RecyclerViewExt")
+
+package com.android.intentresolver.ext
+
+import androidx.recyclerview.widget.RecyclerView
+
+/** Ends active RecyclerView animations, if any */
+fun RecyclerView.endAnimations() {
+ if (isAnimating) {
+ itemAnimator?.endAnimations()
+ }
+}
diff --git a/tests/activity/src/com/android/intentresolver/logging/TestEventLogModule.kt b/tests/activity/src/com/android/intentresolver/logging/TestEventLogModule.kt
index cd808af4..d1dea7c3 100644
--- a/tests/activity/src/com/android/intentresolver/logging/TestEventLogModule.kt
+++ b/tests/activity/src/com/android/intentresolver/logging/TestEventLogModule.kt
@@ -21,16 +21,16 @@ 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.android.components.ActivityRetainedComponent
+import dagger.hilt.android.scopes.ActivityRetainedScoped
import dagger.hilt.testing.TestInstallIn
/** Binds a [FakeEventLog] as [EventLog] in tests. */
@Module
-@TestInstallIn(components = [ActivityComponent::class], replaces = [EventLogModule::class])
+@TestInstallIn(components = [ActivityRetainedComponent::class], replaces = [EventLogModule::class])
interface TestEventLogModule {
- @Binds @ActivityScoped fun fakeEventLog(impl: FakeEventLog): EventLog
+ @Binds @ActivityRetainedScoped fun fakeEventLog(impl: FakeEventLog): EventLog
companion object {
@Provides
diff --git a/tests/activity/src/com/android/intentresolver/v2/ChooserActivityOverrideData.java b/tests/activity/src/com/android/intentresolver/v2/ChooserActivityOverrideData.java
deleted file mode 100644
index 32eabbed..00000000
--- a/tests/activity/src/com/android/intentresolver/v2/ChooserActivityOverrideData.java
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.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
deleted file mode 100644
index a7930f8a..00000000
--- a/tests/activity/src/com/android/intentresolver/v2/ChooserWrapperActivity.java
+++ /dev/null
@@ -1,265 +0,0 @@
-/*
- * 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
deleted file mode 100644
index f0911833..00000000
--- a/tests/activity/src/com/android/intentresolver/v2/ResolverActivityTest.java
+++ /dev/null
@@ -1,1105 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 7ae58254..00000000
--- a/tests/activity/src/com/android/intentresolver/v2/ResolverWrapperActivity.java
+++ /dev/null
@@ -1,289 +0,0 @@
-/*
- * 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
deleted file mode 100644
index 198b9236..00000000
--- a/tests/activity/src/com/android/intentresolver/v2/TestChooserActivityLogic.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-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
deleted file mode 100644
index 7581043e..00000000
--- a/tests/activity/src/com/android/intentresolver/v2/TestResolverActivityLogic.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-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
deleted file mode 100644
index 5245f655..00000000
--- a/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityTest.java
+++ /dev/null
@@ -1,3147 +0,0 @@
-/*
- * 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
deleted file mode 100644
index e4ec1776..00000000
--- a/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityWorkProfileTest.java
+++ /dev/null
@@ -1,481 +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.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
index f17df160..4c8fc37a 100644
--- a/tests/integration/Android.bp
+++ b/tests/integration/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_capture_and_share",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -40,5 +41,5 @@ android_test {
"truth",
"truth-java8-extension",
],
- test_suites: ["general-tests"]
+ test_suites: ["general-tests"],
}
diff --git a/tests/shared/Android.bp b/tests/shared/Android.bp
index 55188ee3..7f5d605a 100644
--- a/tests/shared/Android.bp
+++ b/tests/shared/Android.bp
@@ -31,7 +31,9 @@ java_library {
static_libs: [
"hamcrest",
"IntentResolver-core",
+ "kosmos",
+ "mockito-kotlin2",
"mockito-target-minus-junit4",
- "truth"
+ "truth",
],
}
diff --git a/tests/shared/src/com/android/intentresolver/CoroutinesKosmos.kt b/tests/shared/src/com/android/intentresolver/CoroutinesKosmos.kt
new file mode 100644
index 00000000..eacefdc0
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/CoroutinesKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import com.android.systemui.kosmos.Kosmos
+import kotlinx.coroutines.CoroutineDispatcher
+
+var Kosmos.backgroundDispatcher: CoroutineDispatcher by Kosmos.Fixture()
diff --git a/tests/shared/src/com/android/intentresolver/TestPreviewImageLoader.kt b/tests/shared/src/com/android/intentresolver/FakeImageLoader.kt
index f0203bb6..c57ea78b 100644
--- a/tests/shared/src/com/android/intentresolver/TestPreviewImageLoader.kt
+++ b/tests/shared/src/com/android/intentresolver/FakeImageLoader.kt
@@ -22,7 +22,9 @@ import com.android.intentresolver.contentpreview.ImageLoader
import java.util.function.Consumer
import kotlinx.coroutines.CoroutineScope
-class TestPreviewImageLoader(private val bitmaps: Map<Uri, Bitmap>) : ImageLoader {
+class FakeImageLoader(initialBitmaps: Map<Uri, Bitmap> = emptyMap()) : ImageLoader {
+ private val bitmaps = HashMap<Uri, Bitmap>().apply { putAll(initialBitmaps) }
+
override fun loadImage(callerScope: CoroutineScope, uri: Uri, callback: Consumer<Bitmap?>) {
callback.accept(bitmaps[uri])
}
@@ -30,4 +32,8 @@ class TestPreviewImageLoader(private val bitmaps: Map<Uri, Bitmap>) : ImageLoade
override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = bitmaps[uri]
override fun prePopulate(uris: List<Uri>) = Unit
+
+ fun setBitmap(uri: Uri, bitmap: Bitmap) {
+ bitmaps[uri] = bitmap
+ }
}
diff --git a/tests/shared/src/com/android/intentresolver/FrameworkMocksKosmos.kt b/tests/shared/src/com/android/intentresolver/FrameworkMocksKosmos.kt
new file mode 100644
index 00000000..df3931c6
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/FrameworkMocksKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.content.ContentResolver
+import android.content.pm.PackageManager
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.contentResolver by Kosmos.Fixture { org.mockito.kotlin.mock<ContentResolver> {} }
+var Kosmos.contentInterface by Kosmos.Fixture { contentResolver }
+var Kosmos.packageManager by Kosmos.Fixture { org.mockito.kotlin.mock<PackageManager> {} }
diff --git a/tests/shared/src/com/android/intentresolver/MockitoKotlinHelpers.kt b/tests/shared/src/com/android/intentresolver/MockitoKotlinHelpers.kt
index db9fbd93..40ee6325 100644
--- a/tests/shared/src/com/android/intentresolver/MockitoKotlinHelpers.kt
+++ b/tests/shared/src/com/android/intentresolver/MockitoKotlinHelpers.kt
@@ -14,15 +14,11 @@
* limitations under the License.
*/
+@file:Suppress("NOTHING_TO_INLINE")
+
package com.android.intentresolver
-/**
- * Kotlin versions of popular mockito methods that can return null in situations when Kotlin expects
- * a non-null value. Kotlin will throw an IllegalStateException when this takes place ("x must not
- * be null"). To fix this, we can use methods that modify the return type to be nullable. This
- * causes Kotlin to skip the null checks.
- * Cloned from frameworks/base/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
- */
+import kotlin.DeprecationLevel.WARNING
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatcher
import org.mockito.ArgumentMatchers
@@ -32,49 +28,112 @@ import org.mockito.stubbing.Answer
import org.mockito.stubbing.OngoingStubbing
import org.mockito.stubbing.Stubber
+/*
+ * Kotlin versions of popular mockito methods that can return null in situations when Kotlin expects
+ * a non-null value. Kotlin will throw an IllegalStateException when this takes place ("x must not
+ * be null"). To fix this, we can use methods that modify the return type to be nullable. This
+ * causes Kotlin to skip the null checks. Cloned from
+ * frameworks/base/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
+ */
+
/**
- * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when
- * null is returned.
+ * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when null is
+ * returned.
*
* Generic T is nullable because implicitly bounded by Any?.
*/
-fun <T> eq(obj: T): T = Mockito.eq<T>(obj)
+@Deprecated(
+ "Replace with mockito-kotlin. See http://go/mockito-kotlin",
+ ReplaceWith(expression = "eq", imports = ["org.mockito.kotlin.eq"]),
+ level = WARNING
+)
+inline fun <T> eq(obj: T): T = Mockito.eq<T>(obj) ?: obj
/**
- * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when
- * null is returned.
+ * Returns Mockito.same() as nullable type to avoid java.lang.IllegalStateException when null is
+ * returned.
*
* Generic T is nullable because implicitly bounded by Any?.
*/
-fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
+@Deprecated(
+ "Replace with mockito-kotlin. See http://go/mockito-kotlin",
+ ReplaceWith(expression = "same(obj)", imports = ["org.mockito.kotlin.same"]),
+ level = WARNING
+)
+inline fun <T> same(obj: T): T = Mockito.same<T>(obj) ?: obj
+
+/**
+ * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when null is
+ * returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+@Deprecated(
+ "Replace with mockito-kotlin. See http://go/mockito-kotlin",
+ ReplaceWith(expression = "any(type)", imports = ["org.mockito.kotlin.any"]),
+ level = WARNING
+)
+inline fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
+
+@Deprecated(
+ "Replace with mockito-kotlin. See http://go/mockito-kotlin",
+ ReplaceWith(expression = "any()", imports = ["org.mockito.kotlin.any"]),
+ level = WARNING
+)
inline fun <reified T> any(): T = any(T::class.java)
/**
- * Returns Mockito.argThat() as nullable type to avoid java.lang.IllegalStateException when
- * null is returned.
+ * Returns Mockito.argThat() as nullable type to avoid java.lang.IllegalStateException when null is
+ * returned.
*
* Generic T is nullable because implicitly bounded by Any?.
*/
-fun <T> argThat(matcher: ArgumentMatcher<T>): T = Mockito.argThat(matcher)
+@Deprecated(
+ "Replace with mockito-kotlin. See http://go/mockito-kotlin",
+ ReplaceWith(expression = "argThat(matcher)", imports = ["org.mockito.kotlin.argThat"]),
+ level = WARNING
+)
+inline fun <T> argThat(matcher: ArgumentMatcher<T>): T = Mockito.argThat(matcher)
/**
* Kotlin type-inferred version of Mockito.nullable()
+ *
+ * @see org.mockito.kotlin.anyOrNull
*/
+@Deprecated(
+ "Replace with mockito-kotlin. See http://go/mockito-kotlin",
+ ReplaceWith(expression = "anyOrNull()", imports = ["org.mockito.kotlin.anyOrNull"]),
+ level = WARNING
+)
inline fun <reified T> nullable(): T? = Mockito.nullable(T::class.java)
/**
- * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException
- * when null is returned.
+ * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException when
+ * null is returned.
*
* Generic T is nullable because implicitly bounded by Any?.
+ *
+ * @see org.mockito.kotlin.capture
*/
-fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+@Deprecated(
+ "Replace with mockito-kotlin. See http://go/mockito-kotlin",
+ ReplaceWith(expression = "capture(argumentCaptor)", imports = ["org.mockito.kotlin.capture"]),
+ level = WARNING
+)
+inline fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
/**
* Helper function for creating an argumentCaptor in kotlin.
*
* Generic T is nullable because implicitly bounded by Any?.
+ *
+ * @see org.mockito.kotlin.argumentCaptor
*/
+@Deprecated(
+ "Replace with mockito-kotlin. See http://go/mockito-kotlin",
+ ReplaceWith(expression = "argumentCaptor()", imports = ["org.mockito.kotlin.argumentCaptor"]),
+ level = WARNING
+)
inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> =
ArgumentCaptor.forClass(T::class.java)
@@ -83,24 +142,69 @@ inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> =
*
* Generic T is nullable because implicitly bounded by Any?.
*
- * @param apply builder function to simplify stub configuration by improving type inference.
+ * Updated kotlin-mockito usage:
+ * ```
+ * val value: Widget = mock<> {
+ * on { status } doReturn "OK"
+ * on { buttonPress } doNothing
+ * on { destroy } doAnswer error("Boom!")
+ * }
+ * ```
+ *
+ * __Deprecation note__
+ *
+ * Automatic replacement is not possible due to a change in lambda receiver type to KStubbing<T>
+ *
+ * @see org.mockito.kotlin.mock
+ * @see org.mockito.kotlin.KStubbing.on
*/
+@Suppress("DeprecatedCallableAddReplaceWith")
+@Deprecated("Replace with mockito-kotlin. See http://go/mockito-kotlin", level = WARNING)
inline fun <reified T : Any> mock(
mockSettings: MockSettings = Mockito.withSettings(),
apply: T.() -> Unit = {}
): T = Mockito.mock(T::class.java, mockSettings).apply(apply)
+/** Matches any array of type T. */
+@Deprecated(
+ "Replace with mockito-kotlin. See http://go/mockito-kotlin",
+ ReplaceWith(expression = "anyArray()", imports = ["org.mockito.kotlin.anyArray"]),
+ level = WARNING
+)
+inline fun <reified T : Any?> anyArray(): Array<T> = Mockito.any(Array<T>::class.java) ?: arrayOf()
+
/**
* Helper function for stubbing methods without the need to use backticks.
*
- * @see Mockito.when
+ * Avoid. It is preferable to provide stubbing at creation time using the [mock] lambda argument.
+ *
+ * @see org.mockito.kotlin.whenever
*/
-fun <T> whenever(methodCall: T): OngoingStubbing<T> = Mockito.`when`(methodCall)
+@Deprecated(
+ "Replace with mockito-kotlin. See http://go/mockito-kotlin",
+ ReplaceWith(expression = "whenever(methodCall)", imports = ["org.mockito.kotlin.whenever"]),
+ level = WARNING
+)
+inline fun <T> whenever(methodCall: T): OngoingStubbing<T> = Mockito.`when`(methodCall)
/**
* Helper function for stubbing methods without the need to use backticks.
+ *
+ * Avoid. It is preferable to provide stubbing at creation time using the [mock] lambda argument.
+ *
+ * __Deprecation note__
+ *
+ * Replace with KStubber<T>.on within [org.mockito.kotlin.mock] { stubbing }
+ *
+ * @see org.mockito.kotlin.mock
+ * @see org.mockito.kotlin.KStubbing.on
*/
-fun <T> Stubber.whenever(mock: T): T = `when`(mock)
+@Deprecated(
+ "Replace with mockito-kotlin. See http://go/mockito-kotlin",
+ ReplaceWith(expression = "whenever(mock)", imports = ["org.mockito.kotlin.whenever"]),
+ level = WARNING
+)
+inline fun <T> Stubber.whenever(mock: T): T = `when`(mock)
/**
* A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when
@@ -108,6 +212,7 @@ fun <T> Stubber.whenever(mock: T): T = `when`(mock)
*
* java.lang.NullPointerException: capture() must not be null
*/
+@Deprecated("Replace with mockito-kotlin. See http://go/mockito-kotlin", level = WARNING)
class KotlinArgumentCaptor<T> constructor(clazz: Class<T>) {
private val wrapped: ArgumentCaptor<T> = ArgumentCaptor.forClass(clazz)
fun capture(): T = wrapped.capture()
@@ -121,57 +226,67 @@ class KotlinArgumentCaptor<T> constructor(clazz: Class<T>) {
* Helper function for creating an argumentCaptor in kotlin.
*
* Generic T is nullable because implicitly bounded by Any?.
+ *
+ * @see org.mockito.kotlin.argumentCaptor
*/
+@Deprecated(
+ "Replace with mockito-kotlin. See http://go/mockito-kotlin",
+ ReplaceWith(expression = "argumentCaptor()", imports = ["org.mockito.kotlin.argumentCaptor"]),
+ level = WARNING
+)
inline fun <reified T : Any> kotlinArgumentCaptor(): KotlinArgumentCaptor<T> =
KotlinArgumentCaptor(T::class.java)
/**
* Helper function for creating and using a single-use ArgumentCaptor in kotlin.
*
- * val captor = argumentCaptor<Foo>()
- * verify(...).someMethod(captor.capture())
- * val captured = captor.value
+ * val captor = argumentCaptor<Foo>() verify(...).someMethod(captor.capture()) val captured =
+ * captor.value
*
* becomes:
*
- * val captured = withArgCaptor<Foo> { verify(...).someMethod(capture()) }
+ * val captured = withArgCaptor<Foo> { verify(...).someMethod(capture()) }
*
* NOTE: this uses the KotlinArgumentCaptor to avoid the NullPointerException.
+ *
+ * @see org.mockito.kotlin.verify
*/
+@Suppress("DeprecatedCallableAddReplaceWith")
+@Deprecated("Replace with mockito-kotlin. See http://go/mockito-kotlin", level = WARNING)
inline fun <reified T : Any> withArgCaptor(block: KotlinArgumentCaptor<T>.() -> Unit): T =
kotlinArgumentCaptor<T>().apply { block() }.value
/**
* Variant of [withArgCaptor] for capturing multiple arguments.
*
- * val captor = argumentCaptor<Foo>()
- * verify(...).someMethod(captor.capture())
- * val captured: List<Foo> = captor.allValues
+ * val captor = argumentCaptor<Foo>() verify(...).someMethod(captor.capture()) val captured:
+ * List<Foo> = captor.allValues
*
* becomes:
*
- * val capturedList = captureMany<Foo> { verify(...).someMethod(capture()) }
+ * val capturedList = captureMany<Foo> { verify(...).someMethod(capture()) }
+ *
+ * @see org.mockito.kotlin.verify
*/
+@Deprecated(
+ "Replace with mockito-kotlin. See http://go/mockito-kotlin",
+ ReplaceWith(expression = "capture()", imports = ["org.mockito.kotlin.capture"]),
+ level = WARNING
+)
inline fun <reified T : Any> captureMany(block: KotlinArgumentCaptor<T>.() -> Unit): List<T> =
kotlinArgumentCaptor<T>().apply { block() }.allValues
+/** @see org.mockito.kotlin.anyOrNull */
+@Deprecated(
+ "Replace with mockito-kotlin. See http://go/mockito-kotlin",
+ ReplaceWith(expression = "anyOrNull()", imports = ["org.mockito.kotlin.anyOrNull"]),
+ level = WARNING
+)
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]
+ * @see org.mockito.kotlin.mock
+ * @see org.mockito.kotlin.doThrow
*/
+@Deprecated("Replace with mockito-kotlin. See http://go/mockito-kotlin", level = WARNING)
val THROWS_EXCEPTION = Answer { error("Unstubbed behavior was accessed.") }
diff --git a/tests/shared/src/com/android/intentresolver/TestContentPreviewViewModel.kt b/tests/shared/src/com/android/intentresolver/TestContentPreviewViewModel.kt
index 888fc161..8f246424 100644
--- a/tests/shared/src/com/android/intentresolver/TestContentPreviewViewModel.kt
+++ b/tests/shared/src/com/android/intentresolver/TestContentPreviewViewModel.kt
@@ -17,24 +17,29 @@
package com.android.intentresolver
import android.content.Intent
+import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewmodel.CreationExtras
import com.android.intentresolver.contentpreview.BasePreviewViewModel
import com.android.intentresolver.contentpreview.ImageLoader
-import com.android.intentresolver.contentpreview.PreviewDataProvider
/** A test content preview model that supports image loader override. */
class TestContentPreviewViewModel(
private val viewModel: BasePreviewViewModel,
- private val imageLoader: ImageLoader? = null,
+ override val imageLoader: ImageLoader,
) : BasePreviewViewModel() {
- override fun createOrReuseProvider(
- targetIntent: Intent
- ): PreviewDataProvider = viewModel.createOrReuseProvider(targetIntent)
- override fun createOrReuseImageLoader(): ImageLoader =
- imageLoader ?: viewModel.createOrReuseImageLoader()
+ override val previewDataProvider
+ get() = viewModel.previewDataProvider
+
+ override fun init(
+ targetIntent: Intent,
+ additionalContentUri: Uri?,
+ isPayloadTogglingEnabled: Boolean,
+ ) {
+ viewModel.init(targetIntent, additionalContentUri, isPayloadTogglingEnabled)
+ }
companion object {
fun wrap(
@@ -47,10 +52,12 @@ class TestContentPreviewViewModel(
modelClass: Class<T>,
extras: CreationExtras
): T {
+ val wrapped = factory.create(modelClass, extras) as BasePreviewViewModel
return TestContentPreviewViewModel(
- factory.create(modelClass, extras) as BasePreviewViewModel,
- imageLoader,
- ) as T
+ wrapped,
+ imageLoader ?: wrapped.imageLoader,
+ )
+ as T
}
}
}
diff --git a/tests/shared/src/com/android/intentresolver/contentpreview/MimetypeClassifierKosmos.kt b/tests/shared/src/com/android/intentresolver/contentpreview/MimetypeClassifierKosmos.kt
new file mode 100644
index 00000000..4f979f54
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/contentpreview/MimetypeClassifierKosmos.kt
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.mimetypeClassifier: MimeTypeClassifier by Kosmos.Fixture { DefaultMimeTypeClassifier }
diff --git a/tests/shared/src/com/android/intentresolver/contentpreview/UriMetadataReaderKosmos.kt b/tests/shared/src/com/android/intentresolver/contentpreview/UriMetadataReaderKosmos.kt
new file mode 100644
index 00000000..bdee477d
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/contentpreview/UriMetadataReaderKosmos.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview
+
+import com.android.intentresolver.contentInterface
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.uriMetadataReader: UriMetadataReader by Kosmos.Fixture { uriMetadataReaderImpl }
+val Kosmos.uriMetadataReaderImpl
+ get() =
+ UriMetadataReaderImpl(
+ contentInterface,
+ mimetypeClassifier,
+ )
diff --git a/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PayloadToggleRepoKosmos.kt b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PayloadToggleRepoKosmos.kt
new file mode 100644
index 00000000..894ef163
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/data/repository/PayloadToggleRepoKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+val Kosmos.activityResultRepository by Fixture { ActivityResultRepository() }
+val Kosmos.cursorPreviewsRepository by Fixture { CursorPreviewsRepository() }
+val Kosmos.pendingSelectionCallbackRepository by Fixture { PendingSelectionCallbackRepository() }
+val Kosmos.previewSelectionsRepository by Fixture { PreviewSelectionsRepository() }
diff --git a/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/CursorResolverKosmos.kt b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/CursorResolverKosmos.kt
new file mode 100644
index 00000000..10b89c71
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/CursorResolverKosmos.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor
+
+import android.net.Uri
+import com.android.intentresolver.contentResolver
+import com.android.intentresolver.inject.additionalContentUri
+import com.android.intentresolver.inject.chooserIntent
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.payloadToggleCursorResolver: CursorResolver<Uri?> by
+ Kosmos.Fixture { payloadToggleCursorResolverImpl }
+val Kosmos.payloadToggleCursorResolverImpl
+ get() =
+ PayloadToggleCursorResolver(
+ contentResolver,
+ additionalContentUri,
+ chooserIntent,
+ )
diff --git a/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/PendingIntentSenderKosmos.kt b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/PendingIntentSenderKosmos.kt
new file mode 100644
index 00000000..1b4c0c8f
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/PendingIntentSenderKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.intent
+
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.kotlin.mock
+
+var Kosmos.pendingIntentSender by Kosmos.Fixture { mock<PendingIntentSender> {} }
diff --git a/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifierKosmos.kt b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifierKosmos.kt
new file mode 100644
index 00000000..29e11a15
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifierKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.intent
+
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.targetIntentModifier: TargetIntentModifier<PreviewModel> by Kosmos.Fixture()
diff --git a/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/PayloadToggleInteractorKosmos.kt b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/PayloadToggleInteractorKosmos.kt
new file mode 100644
index 00000000..659c178c
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/PayloadToggleInteractorKosmos.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import com.android.intentresolver.backgroundDispatcher
+import com.android.intentresolver.contentResolver
+import com.android.intentresolver.contentpreview.HeadlineGenerator
+import com.android.intentresolver.contentpreview.ImageLoader
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.activityResultRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.cursorPreviewsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.pendingSelectionCallbackRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.previewSelectionsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.payloadToggleCursorResolver
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.pendingIntentSender
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.targetIntentModifier
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.update.selectionChangeCallback
+import com.android.intentresolver.contentpreview.uriMetadataReader
+import com.android.intentresolver.data.repository.chooserRequestRepository
+import com.android.intentresolver.inject.contentUris
+import com.android.intentresolver.logging.eventLog
+import com.android.intentresolver.packageManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+var Kosmos.focusedItemIndex: Int by Fixture { 0 }
+var Kosmos.pageSize: Int by Fixture { 16 }
+var Kosmos.maxLoadedPages: Int by Fixture { 3 }
+
+val Kosmos.chooserRequestInteractor
+ get() = ChooserRequestInteractor(chooserRequestRepository)
+
+val Kosmos.cursorPreviewsInteractor
+ get() =
+ CursorPreviewsInteractor(
+ interactor = setCursorPreviewsInteractor,
+ focusedItemIdx = focusedItemIndex,
+ uriMetadataReader = uriMetadataReader,
+ pageSize = pageSize,
+ maxLoadedPages = maxLoadedPages,
+ )
+
+val Kosmos.customActionsInteractor
+ get() =
+ CustomActionsInteractor(
+ activityResultRepo = activityResultRepository,
+ bgDispatcher = backgroundDispatcher,
+ contentResolver = contentResolver,
+ eventLog = eventLog,
+ packageManager = packageManager,
+ chooserRequestInteractor = chooserRequestInteractor,
+ )
+
+val Kosmos.fetchPreviewsInteractor
+ get() =
+ FetchPreviewsInteractor(
+ setCursorPreviews = setCursorPreviewsInteractor,
+ selectionRepository = previewSelectionsRepository,
+ cursorInteractor = cursorPreviewsInteractor,
+ focusedItemIdx = focusedItemIndex,
+ selectedItems = contentUris,
+ uriMetadataReader = uriMetadataReader,
+ cursorResolver = payloadToggleCursorResolver,
+ )
+
+val Kosmos.processTargetIntentUpdatesInteractor
+ get() =
+ ProcessTargetIntentUpdatesInteractor(
+ selectionCallback = selectionChangeCallback,
+ repository = pendingSelectionCallbackRepository,
+ chooserRequestInteractor = updateChooserRequestInteractor,
+ )
+
+val Kosmos.selectablePreviewsInteractor
+ get() =
+ SelectablePreviewsInteractor(
+ previewsRepo = cursorPreviewsRepository,
+ selectionInteractor = selectionInteractor,
+ )
+
+val Kosmos.selectionInteractor
+ get() =
+ SelectionInteractor(
+ selectionsRepo = previewSelectionsRepository,
+ targetIntentModifier = targetIntentModifier,
+ updateTargetIntentInteractor = updateTargetIntentInteractor,
+ )
+
+val Kosmos.setCursorPreviewsInteractor
+ get() = SetCursorPreviewsInteractor(previewsRepo = cursorPreviewsRepository)
+
+val Kosmos.updateChooserRequestInteractor
+ get() =
+ UpdateChooserRequestInteractor(
+ chooserRequestRepository,
+ pendingIntentSender,
+ )
+
+val Kosmos.updateTargetIntentInteractor
+ get() =
+ UpdateTargetIntentInteractor(
+ repository = pendingSelectionCallbackRepository,
+ chooserRequestInteractor = updateChooserRequestInteractor,
+ )
+
+var Kosmos.payloadToggleImageLoader: ImageLoader by Fixture()
+var Kosmos.headlineGenerator: HeadlineGenerator by Fixture()
diff --git a/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallbackKosmos.kt b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallbackKosmos.kt
new file mode 100644
index 00000000..548b1f37
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallbackKosmos.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.update
+
+import com.android.intentresolver.contentInterface
+import com.android.intentresolver.inject.additionalContentUri
+import com.android.intentresolver.inject.chooserIntent
+import com.android.intentresolver.inject.chooserServiceFlags
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.selectionChangeCallbackImpl by
+ Kosmos.Fixture {
+ SelectionChangeCallbackImpl(
+ additionalContentUri,
+ chooserIntent,
+ contentInterface,
+ chooserServiceFlags,
+ )
+ }
+var Kosmos.selectionChangeCallback: SelectionChangeCallback by
+ Kosmos.Fixture { selectionChangeCallbackImpl }
diff --git a/tests/shared/src/com/android/intentresolver/data/repository/FakeUserRepository.kt b/tests/shared/src/com/android/intentresolver/data/repository/FakeUserRepository.kt
new file mode 100644
index 00000000..fb8fbd3f
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/data/repository/FakeUserRepository.kt
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.data.repository
+
+import com.android.intentresolver.shared.model.User
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.update
+
+/** A simple repository which can be initialized from a list and updated. */
+class FakeUserRepository(userList: List<User>) : UserRepository {
+ internal data class UserState(val user: User, val available: Boolean)
+
+ private val userState = MutableStateFlow(userList.map { UserState(it, available = true) })
+
+ // Expose a List<User> from List<UserState>
+ override val users = userState.map { userList -> userList.map { it.user } }
+
+ fun addUser(user: User, available: Boolean) {
+ require(userState.value.none { it.user.id == user.id }) {
+ "A User with ${user.id} already exists!"
+ }
+ userState.update { it + UserState(user, available) }
+ }
+
+ fun removeUser(user: User) {
+ require(userState.value.any { it.user.id == user.id }) {
+ "A User with ${user.id} does not exist!"
+ }
+ userState.update { it.filterNot { state -> state.user.id == user.id } }
+ }
+
+ override val availability =
+ userState.map { userStateList -> userStateList.associate { it.user to it.available } }
+
+ fun updateState(user: User, available: Boolean) {
+ userState.update { userStateList ->
+ userStateList.map { userState ->
+ if (userState.user.id == user.id) {
+ UserState(user, available)
+ } else {
+ userState
+ }
+ }
+ }
+ }
+
+ override suspend fun requestState(user: User, available: Boolean) {
+ updateState(user, available)
+ }
+}
diff --git a/tests/shared/src/com/android/intentresolver/data/repository/V2RepositoryKosmos.kt b/tests/shared/src/com/android/intentresolver/data/repository/V2RepositoryKosmos.kt
new file mode 100644
index 00000000..0b2d3eb4
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/data/repository/V2RepositoryKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.data.repository
+
+import android.content.Intent
+import com.android.intentresolver.data.model.ChooserRequest
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+
+var Kosmos.chooserRequestRepository by Fixture {
+ ChooserRequestRepository(
+ initialRequest = ChooserRequest(targetIntent = Intent(), launchedFromPackage = "pkg"),
+ initialActions = emptyList()
+ )
+}
diff --git a/tests/shared/src/com/android/intentresolver/ext/ParcelableExt.kt b/tests/shared/src/com/android/intentresolver/ext/ParcelableExt.kt
new file mode 100644
index 00000000..0b9caa32
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/ext/ParcelableExt.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.ext
+
+import android.os.Parcel
+import android.os.Parcelable
+import java.lang.reflect.Field
+
+inline fun <reified T : Parcelable> T.toParcelAndBack(): T {
+ val creator: Parcelable.Creator<out T> = getCreator()
+ val parcel = Parcel.obtain()
+ writeToParcel(parcel, 0)
+ parcel.setDataPosition(0)
+ return creator.createFromParcel(parcel)
+}
+
+inline fun <reified T : Parcelable> getCreator(): Parcelable.Creator<out T> {
+ return getCreator(T::class.java)
+}
+
+inline fun <reified T : Parcelable> getCreator(clazz: Class<out T>): Parcelable.Creator<out T> {
+ return try {
+ val field: Field = clazz.getDeclaredField("CREATOR")
+ @Suppress("UNCHECKED_CAST")
+ field.get(null) as Parcelable.Creator<T>
+ } catch (e: NoSuchFieldException) {
+ error("$clazz is a Parcelable without CREATOR")
+ } catch (e: IllegalAccessException) {
+ error("CREATOR in $clazz::class is not accessible")
+ }
+}
diff --git a/tests/shared/src/com/android/intentresolver/inject/ActivityModelKosmos.kt b/tests/shared/src/com/android/intentresolver/inject/ActivityModelKosmos.kt
new file mode 100644
index 00000000..9944163b
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/inject/ActivityModelKosmos.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.inject
+
+import android.content.Intent
+import android.net.Uri
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.contentUris: List<Uri> by Kosmos.Fixture { emptyList() }
+var Kosmos.additionalContentUri: Uri by
+ Kosmos.Fixture { Uri.fromParts("scheme", "ssp", "fragment") }
+var Kosmos.chooserIntent: Intent by Kosmos.Fixture { Intent() }
diff --git a/tests/shared/src/com/android/intentresolver/inject/ChooserServiceFlagsKosmos.kt b/tests/shared/src/com/android/intentresolver/inject/ChooserServiceFlagsKosmos.kt
new file mode 100644
index 00000000..51dad82a
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/inject/ChooserServiceFlagsKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.inject
+
+import android.service.chooser.FeatureFlagsImpl
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.chooserServiceFlags: ChooserServiceFlags by Kosmos.Fixture { chooserServiceFlagsImpl }
+val chooserServiceFlagsImpl: FeatureFlagsImpl
+ get() = FeatureFlagsImpl()
diff --git a/tests/shared/src/com/android/intentresolver/logging/EventLogKosmos.kt b/tests/shared/src/com/android/intentresolver/logging/EventLogKosmos.kt
new file mode 100644
index 00000000..5bf3ddee
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/logging/EventLogKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.logging
+
+import com.android.internal.logging.InstanceId
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.eventLog by Kosmos.Fixture { fakeEventLog }
+var Kosmos.fakeEventLog by Kosmos.Fixture { FakeEventLog(InstanceId.fakeInstanceId(0)) }
diff --git a/tests/shared/src/com/android/intentresolver/v2/platform/FakeSecureSettings.kt b/tests/shared/src/com/android/intentresolver/platform/FakeSecureSettings.kt
index 4e279623..862be76f 100644
--- a/tests/shared/src/com/android/intentresolver/v2/platform/FakeSecureSettings.kt
+++ b/tests/shared/src/com/android/intentresolver/platform/FakeSecureSettings.kt
@@ -1,4 +1,20 @@
-package com.android.intentresolver.v2.platform
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.platform
/**
* Creates a SecureSettings instance with predefined values:
diff --git a/tests/shared/src/com/android/intentresolver/v2/platform/FakeUserManager.kt b/tests/shared/src/com/android/intentresolver/platform/FakeUserManager.kt
index 370e5a00..32cb9062 100644
--- a/tests/shared/src/com/android/intentresolver/v2/platform/FakeUserManager.kt
+++ b/tests/shared/src/com/android/intentresolver/platform/FakeUserManager.kt
@@ -1,12 +1,22 @@
-package com.android.intentresolver.v2.platform
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.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
@@ -16,19 +26,18 @@ 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 com.android.intentresolver.data.repository.AvailabilityChange
+import com.android.intentresolver.data.repository.ProfileAdded
+import com.android.intentresolver.data.repository.ProfileRemoved
+import com.android.intentresolver.data.repository.UserEvent
+import com.android.intentresolver.platform.FakeUserManager.State
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
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
/**
* A stand-in for [UserManager] to support testing of data layer components which depend on it.
@@ -155,21 +164,7 @@ class FakeUserManager(val state: State = State()) :
} 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))
- }
+ eventChannel.trySend(AvailabilityChange(user, quietMode))
}
}
@@ -187,7 +182,7 @@ class FakeUserManager(val state: State = State()) :
profileGroupId = parentUser.profileGroupId
}
userInfoMap[userInfo.userHandle] = userInfo
- eventChannel.trySend(UserEvent(ACTION_PROFILE_ADDED, userInfo.userHandle))
+ eventChannel.trySend(ProfileAdded(userInfo.userHandle))
return userInfo.userHandle
}
@@ -195,7 +190,7 @@ class FakeUserManager(val state: State = State()) :
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))
+ eventChannel.trySend(ProfileRemoved(user.userHandle))
return true
}
?: false
@@ -212,11 +207,16 @@ class FakeUserManager(val state: State = State()) :
}
/** 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 mockContext(userHandle: UserHandle = UserHandle.SYSTEM): Context {
+ return mock<Context>(
+ defaultAnswer = {
+ error("Unstubbed behavior invoked! (${it.method}(${it.arguments.asList()})")
+ }
+ ) {
+ // Careful! Specify behaviors *first* to avoid throwing while stubbing!
+ doReturn(mock).whenever(mock).applicationContext
+ doReturn(userHandle).whenever(mock).user
+ doReturn(userHandle.identifier).whenever(mock).userId
}
}
@@ -230,7 +230,11 @@ private fun FakeUserManager.ProfileType.toUserType(): String {
/** A safe mock of [IUserManager] which throws on any unstubbed method call. */
fun mockService(): IUserManager {
- return mock<IUserManager>(withSettings().defaultAnswer(THROWS_EXCEPTION))
+ return mock<IUserManager>(
+ defaultAnswer = {
+ error("Unstubbed behavior invoked! ${it.method}(${it.arguments.asList()}")
+ }
+ )
}
val UserInfo.debugString: String
diff --git a/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt b/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt
deleted file mode 100644
index 1ff0ce8e..00000000
--- a/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-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
index a07af1a4..2c3c7910 100644
--- a/tests/unit/Android.bp
+++ b/tests/unit/Android.bp
@@ -15,6 +15,7 @@
//
package {
+ default_team: "trendy_team_capture_and_share",
default_applicable_licenses: ["Android-Apache-2.0"],
}
@@ -50,8 +51,11 @@ android_test {
"IntentResolver-core",
"IntentResolver-tests-shared",
"junit",
+ "kosmos",
"kotlinx_coroutines_test",
"mockito-target-minus-junit4",
+ "mockito-kotlin2",
+ "platform-compat-test-rules", // PlatformCompatChangeRule
"testables", // TestableContext/TestableResources
"truth",
"truth-java8-extension",
diff --git a/tests/unit/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt b/tests/unit/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt
deleted file mode 100644
index cd2fbc7a..00000000
--- a/tests/unit/src/com/android/intentresolver/AnnotatedUserHandlesTest.kt
+++ /dev/null
@@ -1,79 +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 android.os.UserHandle
-
-import com.google.common.truth.Truth.assertThat
-
-import org.junit.Test
-
-class AnnotatedUserHandlesTest {
-
- @Test
- fun testBasicProperties() { // Fields that are reflected back w/o logic.
- val info = AnnotatedUserHandles.newBuilder()
- .setUserIdOfCallingApp(42)
- .setUserHandleSharesheetLaunchedAs(UserHandle.of(116))
- .setPersonalProfileUserHandle(UserHandle.of(117))
- .setWorkProfileUserHandle(UserHandle.of(118))
- .setCloneProfileUserHandle(UserHandle.of(119))
- .build()
-
- assertThat(info.userIdOfCallingApp).isEqualTo(42)
- assertThat(info.userHandleSharesheetLaunchedAs.identifier).isEqualTo(116)
- assertThat(info.personalProfileUserHandle.identifier).isEqualTo(117)
- assertThat(info.workProfileUserHandle?.identifier).isEqualTo(118)
- assertThat(info.cloneProfileUserHandle?.identifier).isEqualTo(119)
- }
-
- @Test
- fun testWorkTabInitiallySelectedWhenLaunchedFromWorkProfile() {
- val info = AnnotatedUserHandles.newBuilder()
- .setUserIdOfCallingApp(42)
- .setPersonalProfileUserHandle(UserHandle.of(101))
- .setWorkProfileUserHandle(UserHandle.of(202))
- .setUserHandleSharesheetLaunchedAs(UserHandle.of(202))
- .build()
-
- assertThat(info.tabOwnerUserHandleForLaunch.identifier).isEqualTo(202)
- }
-
- @Test
- fun testPersonalTabInitiallySelectedWhenLaunchedFromPersonalProfile() {
- val info = AnnotatedUserHandles.newBuilder()
- .setUserIdOfCallingApp(42)
- .setPersonalProfileUserHandle(UserHandle.of(101))
- .setWorkProfileUserHandle(UserHandle.of(202))
- .setUserHandleSharesheetLaunchedAs(UserHandle.of(101))
- .build()
-
- assertThat(info.tabOwnerUserHandleForLaunch.identifier).isEqualTo(101)
- }
-
- @Test
- fun testPersonalTabInitiallySelectedWhenLaunchedFromOtherProfile() {
- val info = AnnotatedUserHandles.newBuilder()
- .setUserIdOfCallingApp(42)
- .setPersonalProfileUserHandle(UserHandle.of(101))
- .setWorkProfileUserHandle(UserHandle.of(202))
- .setUserHandleSharesheetLaunchedAs(UserHandle.of(303))
- .build()
-
- assertThat(info.tabOwnerUserHandleForLaunch.identifier).isEqualTo(101)
- }
-}
diff --git a/tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt b/tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt
index 55a94ebd..0c2ae800 100644
--- a/tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt
+++ b/tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt
@@ -29,8 +29,10 @@ import android.service.chooser.ChooserAction
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.android.intentresolver.logging.EventLog
-import com.google.common.collect.ImmutableList
+import com.android.intentresolver.ui.ShareResultSender
+import com.android.intentresolver.ui.model.ShareAction
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
@@ -40,15 +42,15 @@ import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mockito
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.verify
@RunWith(AndroidJUnit4::class)
class ChooserActionFactoryTest {
- private val context = InstrumentationRegistry.getInstrumentation().getContext()
+ 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 =
@@ -89,27 +91,7 @@ class ChooserActionFactoryTest {
// 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))
+ 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))
@@ -122,21 +104,20 @@ class ChooserActionFactoryTest {
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,
- mock(),
- logger,
- {},
- { null },
- mock(),
- {},
+ /* context = */ context,
+ /* targetIntent = */ targetIntent,
+ /* referrerPackageName = */ null,
+ /* chooserActions = */ emptyList(),
+ /* imageEditor = */ Optional.empty(),
+ /* log = */ logger,
+ /* onUpdateSharedTextIsExcluded = */ {},
+ /* firstVisibleImageQuery = */ { null },
+ /* activityStarter = */ mock(),
+ /* shareResultSender = */ null,
+ /* finishCallback = */ {},
+ /* clipboardManager = */ mock(),
)
assertThat(testSubject.copyButtonRunnable).isNull()
}
@@ -144,50 +125,51 @@ class ChooserActionFactoryTest {
@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,
- mock(),
- logger,
- {},
- { null },
- mock(),
- {},
+ /* context = */ context,
+ /* targetIntent = */ targetIntent,
+ /* referrerPackageName = */ "com.example",
+ /* chooserActions = */ emptyList(),
+ /* imageEditor = */ Optional.empty(),
+ /* log = */ logger,
+ /* onUpdateSharedTextIsExcluded = */ {},
+ /* firstVisibleImageQuery = */ { null },
+ /* activityStarter = */ mock(),
+ /* shareResultSender = */ null,
+ /* finishCallback = */ {},
+ /* clipboardManager = */ mock(),
)
assertThat(testSubject.copyButtonRunnable).isNull()
}
@Test
- fun sendActionWithText_nonNullCopyRunnable() {
+ fun sendActionWithTextCopyRunnable() {
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 resultSender = mock<ShareResultSender>()
val testSubject =
ChooserActionFactory(
- context,
- chooserRequest,
- mock(),
- logger,
- {},
- { null },
- mock(),
- {},
+ /* context = */ context,
+ /* targetIntent = */ targetIntent,
+ /* referrerPackageName = */ "com.example",
+ /* chooserActions = */ emptyList(),
+ /* imageEditor = */ Optional.empty(),
+ /* log = */ logger,
+ /* onUpdateSharedTextIsExcluded = */ {},
+ /* firstVisibleImageQuery = */ { null },
+ /* activityStarter = */ mock(),
+ /* shareResultSender = */ resultSender,
+ /* finishCallback = */ {},
+ /* clipboardManager = */ mock(),
)
assertThat(testSubject.copyButtonRunnable).isNotNull()
+
+ testSubject.copyButtonRunnable?.run()
+
+ verify(resultSender) { 1 * { onActionSelected(ShareAction.SYSTEM_COPY) } }
}
- private fun createFactory(includeModifyShare: Boolean = false): ChooserActionFactory {
+ private fun createFactory(): ChooserActionFactory {
val testPendingIntent =
PendingIntent.getBroadcast(context, 0, Intent(testAction), PendingIntent.FLAG_IMMUTABLE)
val targetIntent = Intent()
@@ -198,30 +180,19 @@ class ChooserActionFactoryTest {
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,
- mock(),
- logger,
- {},
- { null },
- mock(),
- resultConsumer
+ /* context = */ context,
+ /* targetIntent = */ targetIntent,
+ /* referrerPackageName = */ "com.example",
+ /* chooserActions = */ listOf(action),
+ /* imageEditor = */ Optional.empty(),
+ /* log = */ logger,
+ /* onUpdateSharedTextIsExcluded = */ {},
+ /* firstVisibleImageQuery = */ { null },
+ /* activityStarter = */ mock(),
+ /* shareResultSender = */ null,
+ /* finishCallback = */ resultConsumer,
+ /* clipboardManager = */ mock(),
)
}
}
diff --git a/tests/unit/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt b/tests/unit/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt
deleted file mode 100644
index 9a5dabdb..00000000
--- a/tests/unit/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt
+++ /dev/null
@@ -1,71 +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 android.content.ComponentName
-import android.provider.Settings
-import android.testing.TestableContext
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class ChooserIntegratedDeviceComponentsTest {
- private val secureSettings = mock<SecureSettings>()
- private val testableContext =
- TestableContext(InstrumentationRegistry.getInstrumentation().getContext())
-
- @Test
- fun testEditorAndNearby() {
- val resources = testableContext.getOrCreateTestableResources()
-
- resources.addOverride(R.string.config_systemImageEditor, "")
- resources.addOverride(R.string.config_defaultNearbySharingComponent, "")
-
- var components = ChooserIntegratedDeviceComponents.get(testableContext, secureSettings)
-
- assertThat(components.editSharingComponent).isNull()
- assertThat(components.nearbySharingComponent).isNull()
-
- val editor = ComponentName.unflattenFromString("com.android/com.android.Editor")
- val nearby = ComponentName.unflattenFromString("com.android/com.android.nearby")
-
- resources.addOverride(R.string.config_systemImageEditor, editor?.flattenToString())
- resources.addOverride(
- R.string.config_defaultNearbySharingComponent, nearby?.flattenToString())
-
- components = ChooserIntegratedDeviceComponents.get(testableContext, secureSettings)
-
- assertThat(components.editSharingComponent).isEqualTo(editor)
- assertThat(components.nearbySharingComponent).isEqualTo(nearby)
-
- val anotherNearby =
- ComponentName.unflattenFromString("com.android/com.android.another_nearby")
- whenever(
- secureSettings.getString(
- any(),
- eq(Settings.Secure.NEARBY_SHARING_COMPONENT)
- )
- ).thenReturn(anotherNearby?.flattenToString())
-
- components = ChooserIntegratedDeviceComponents.get(testableContext, secureSettings)
-
- assertThat(components.nearbySharingComponent).isEqualTo(anotherNearby)
- }
-}
diff --git a/tests/unit/src/com/android/intentresolver/ChooserListAdapterDataTest.kt b/tests/unit/src/com/android/intentresolver/ChooserListAdapterDataTest.kt
index 98c5e008..e974cb7d 100644
--- a/tests/unit/src/com/android/intentresolver/ChooserListAdapterDataTest.kt
+++ b/tests/unit/src/com/android/intentresolver/ChooserListAdapterDataTest.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.intentresolver
import android.content.Context
@@ -46,6 +62,8 @@ class ChooserListAdapterDataTest {
private val immediateExecutor = TestExecutor(immediate = true)
private val referrerFillInIntent =
Intent().putExtra(Intent.EXTRA_REFERRER, "org.referrer.package")
+ private val featureFlags =
+ FakeFeatureFlagsImpl().apply { setFlag(Flags.FLAG_BESPOKE_LABEL_VIEW, false) }
@Test
fun test_twoTargetsWithNonOverlappingInitialIntent_threeTargetsInResolverAdapter() {
@@ -97,6 +115,7 @@ class ChooserListAdapterDataTest {
null,
backgroundExecutor,
immediateExecutor,
+ featureFlags,
)
val doPostProcessing = true
@@ -160,6 +179,7 @@ class ChooserListAdapterDataTest {
null,
backgroundExecutor,
immediateExecutor,
+ featureFlags,
)
val doPostProcessing = true
diff --git a/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt b/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt
index cb043943..3c23ff26 100644
--- a/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt
+++ b/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt
@@ -33,6 +33,7 @@ import com.android.intentresolver.chooser.SelectableTargetInfo
import com.android.intentresolver.chooser.TargetInfo
import com.android.intentresolver.icons.TargetDataLoader
import com.android.intentresolver.logging.EventLogImpl
+import com.android.intentresolver.widget.BadgeTextView
import com.android.internal.R
import com.google.common.truth.Truth.assertThat
import org.junit.Before
@@ -57,6 +58,7 @@ class ChooserListAdapterTest {
private val mEventLog = mock<EventLogImpl>()
private val mTargetDataLoader = mock<TargetDataLoader>()
private val mPackageChangeCallback = mock<ChooserListAdapter.PackageChangeCallback>()
+ private val featureFlags = FeatureFlagsImpl()
private val testSubject by lazy {
ChooserListAdapter(
@@ -75,7 +77,8 @@ class ChooserListAdapterTest {
0,
null,
mTargetDataLoader,
- mPackageChangeCallback
+ mPackageChangeCallback,
+ featureFlags,
)
}
@@ -216,10 +219,15 @@ class ChooserListAdapterTest {
private fun createView(): View {
val view = FrameLayout(context)
- TextView(context).apply {
- id = R.id.text1
- view.addView(this)
- }
+ if (featureFlags.bespokeLabelView()) {
+ BadgeTextView(context)
+ } else {
+ TextView(context)
+ }
+ .apply {
+ id = R.id.text1
+ view.addView(this)
+ }
TextView(context).apply {
id = R.id.text2
view.addView(this)
diff --git a/tests/unit/src/com/android/intentresolver/ChooserRefinementManagerTest.kt b/tests/unit/src/com/android/intentresolver/ChooserRefinementManagerTest.kt
index 61ac0c21..16c917b0 100644
--- a/tests/unit/src/com/android/intentresolver/ChooserRefinementManagerTest.kt
+++ b/tests/unit/src/com/android/intentresolver/ChooserRefinementManagerTest.kt
@@ -29,8 +29,8 @@ import androidx.lifecycle.Observer
import androidx.test.annotation.UiThreadTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.intentresolver.ChooserRefinementManager.RefinementCompletion
+import com.android.intentresolver.ChooserRefinementManager.RefinementType
import com.android.intentresolver.chooser.ImmutableTargetInfo
-import com.android.intentresolver.chooser.TargetInfo
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
@@ -55,15 +55,15 @@ class ChooserRefinementManagerTest {
object : Observer<RefinementCompletion> {
val failureCountDown = CountDownLatch(1)
val successCountDown = CountDownLatch(1)
- var latestTargetInfo: TargetInfo? = null
+ var latestRefinedIntent: Intent? = null
override fun onChanged(completion: RefinementCompletion) {
if (completion.consume()) {
- val targetInfo = completion.targetInfo
- if (targetInfo == null) {
+ val refinedIntent = completion.refinedIntent
+ if (refinedIntent == null) {
failureCountDown.countDown()
} else {
- latestTargetInfo = targetInfo
+ latestRefinedIntent = refinedIntent
successCountDown.countDown()
}
}
@@ -115,8 +115,7 @@ class ChooserRefinementManagerTest {
receiver?.send(Activity.RESULT_OK, bundle)
assertThat(completionObserver.successCountDown.await(1000, TimeUnit.MILLISECONDS)).isTrue()
- assertThat(completionObserver.latestTargetInfo?.resolvedIntent?.action)
- .isEqualTo(Intent.ACTION_VIEW)
+ assertThat(completionObserver.latestRefinedIntent?.action).isEqualTo(Intent.ACTION_VIEW)
}
@Test
@@ -231,10 +230,11 @@ class ChooserRefinementManagerTest {
@Test
fun testRefinementCompletion() {
- val refinementCompletion = RefinementCompletion(exampleTargetInfo)
- assertThat(refinementCompletion.targetInfo).isEqualTo(exampleTargetInfo)
+ val refinementCompletion =
+ RefinementCompletion(RefinementType.TARGET_INFO, exampleTargetInfo, null)
+ assertThat(refinementCompletion.originalTargetInfo).isEqualTo(exampleTargetInfo)
assertThat(refinementCompletion.consume()).isTrue()
- assertThat(refinementCompletion.targetInfo).isEqualTo(exampleTargetInfo)
+ assertThat(refinementCompletion.originalTargetInfo).isEqualTo(exampleTargetInfo)
// can only consume once.
assertThat(refinementCompletion.consume()).isFalse()
diff --git a/tests/unit/src/com/android/intentresolver/ChooserRequestParametersTest.kt b/tests/unit/src/com/android/intentresolver/ChooserRequestParametersTest.kt
deleted file mode 100644
index 90f6cf93..00000000
--- a/tests/unit/src/com/android/intentresolver/ChooserRequestParametersTest.kt
+++ /dev/null
@@ -1,87 +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 android.app.PendingIntent
-import android.content.Intent
-import android.graphics.drawable.Icon
-import android.net.Uri
-import android.service.chooser.ChooserAction
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.platform.app.InstrumentationRegistry
-import com.google.common.truth.Truth.assertThat
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@RunWith(AndroidJUnit4::class)
-class ChooserRequestParametersTest {
-
- @Test
- fun testChooserActions() {
- val actionCount = 3
- val intent = Intent(Intent.ACTION_SEND)
- val actions = createChooserActions(actionCount)
- val chooserIntent =
- Intent(Intent.ACTION_CHOOSER).apply {
- putExtra(Intent.EXTRA_INTENT, intent)
- putExtra(Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, actions)
- }
- val request = ChooserRequestParameters(chooserIntent, "", Uri.EMPTY)
- assertThat(request.chooserActions).containsExactlyElementsIn(actions).inOrder()
- }
-
- @Test
- fun testChooserActions_empty() {
- val intent = Intent(Intent.ACTION_SEND)
- val chooserIntent =
- Intent(Intent.ACTION_CHOOSER).apply { putExtra(Intent.EXTRA_INTENT, intent) }
- val request = ChooserRequestParameters(chooserIntent, "", Uri.EMPTY)
- assertThat(request.chooserActions).isEmpty()
- }
-
- @Test
- fun testChooserActions_tooMany() {
- val intent = Intent(Intent.ACTION_SEND)
- val chooserActions = createChooserActions(10)
- val chooserIntent =
- Intent(Intent.ACTION_CHOOSER).apply {
- putExtra(Intent.EXTRA_INTENT, intent)
- putExtra(Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS, chooserActions)
- }
-
- val request = ChooserRequestParameters(chooserIntent, "", Uri.EMPTY)
-
- val expectedActions = chooserActions.sliceArray(0 until 5)
- assertThat(request.chooserActions).containsExactlyElementsIn(expectedActions).inOrder()
- }
-
- private fun createChooserActions(count: Int): Array<ChooserAction> {
- return Array(count) { i -> createChooserAction("$i") }
- }
-
- private fun createChooserAction(label: CharSequence): ChooserAction {
- val icon = Icon.createWithContentUri("content://org.package.app/image")
- val pendingIntent =
- PendingIntent.getBroadcast(
- InstrumentationRegistry.getInstrumentation().getTargetContext(),
- 0,
- Intent("TESTACTION"),
- PendingIntent.FLAG_IMMUTABLE
- )
- return ChooserAction.Builder(icon, label, pendingIntent).build()
- }
-}
diff --git a/tests/unit/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt b/tests/unit/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt
index c7d20000..2b7d6ff9 100644
--- a/tests/unit/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt
+++ b/tests/unit/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt
@@ -31,10 +31,11 @@ import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Test
-import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.never
-import org.mockito.Mockito.times
-import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
private const val TIMEOUT_MS = 200
@@ -48,18 +49,18 @@ class EnterTransitionAnimationDelegateTest {
private val transitionTargetView =
mock<View> {
// avoid the request-layout path in the delegate
- whenever(isInLayout).thenReturn(true)
+ on { isInLayout } doReturn true
}
private val windowMock = mock<Window>()
private val resourcesMock =
- mock<Resources> { whenever(getInteger(anyInt())).thenReturn(TIMEOUT_MS) }
+ mock<Resources> { on { getInteger(any<Int>()) } doReturn TIMEOUT_MS }
private val activity =
mock<ComponentActivity> {
- whenever(lifecycle).thenReturn(lifecycleOwner.lifecycle)
- whenever(resources).thenReturn(resourcesMock)
- whenever(isActivityTransitionRunning).thenReturn(true)
- whenever(window).thenReturn(windowMock)
+ on { lifecycle } doReturn lifecycleOwner.lifecycle
+ on { resources } doReturn resourcesMock
+ on { isActivityTransitionRunning } doReturn true
+ on { window } doReturn windowMock
}
private val testSubject = EnterTransitionAnimationDelegate(activity) { transitionTargetView }
@@ -82,8 +83,8 @@ class EnterTransitionAnimationDelegateTest {
testSubject.markOffsetCalculated()
scheduler.advanceTimeBy(TIMEOUT_MS + 1L)
- verify(activity, times(1)).startPostponedEnterTransition()
- verify(windowMock, never()).setWindowAnimations(anyInt())
+ verify(activity) { 1 * { mock.startPostponedEnterTransition() } }
+ verify(windowMock) { 0 * { setWindowAnimations(any<Int>()) } }
}
@Test
@@ -101,12 +102,12 @@ class EnterTransitionAnimationDelegateTest {
@Test
fun test_postponeTransition_resume_animation_conditions() {
testSubject.postponeTransition()
- verify(activity, never()).startPostponedEnterTransition()
+ verify(activity) { 0 * { startPostponedEnterTransition() } }
testSubject.markOffsetCalculated()
- verify(activity, never()).startPostponedEnterTransition()
+ verify(activity) { 0 * { startPostponedEnterTransition() } }
testSubject.onAllTransitionElementsReady()
- verify(activity, times(1)).startPostponedEnterTransition()
+ verify(activity) { 1 * { startPostponedEnterTransition() } }
}
}
diff --git a/tests/unit/src/com/android/intentresolver/FakeResolverListCommunicator.kt b/tests/unit/src/com/android/intentresolver/FakeResolverListCommunicator.kt
index 5e9cd98f..b25f4036 100644
--- a/tests/unit/src/com/android/intentresolver/FakeResolverListCommunicator.kt
+++ b/tests/unit/src/com/android/intentresolver/FakeResolverListCommunicator.kt
@@ -27,8 +27,6 @@ class FakeResolverListCommunicator(private val layoutWithDefaults: Boolean = tru
val sendVoiceCommandCount
get() = sendVoiceCounter.get()
- val updateProfileViewButtonCount
- get() = updateProfileViewButtonCounter.get()
override fun getReplacementIntent(activityInfo: ActivityInfo?, defIntent: Intent): Intent {
return defIntent
@@ -44,10 +42,6 @@ class FakeResolverListCommunicator(private val layoutWithDefaults: Boolean = tru
sendVoiceCounter.incrementAndGet()
}
- override fun updateProfileViewButton() {
- updateProfileViewButtonCounter.incrementAndGet()
- }
-
override fun useLayoutWithDefault(): Boolean = layoutWithDefaults
override fun shouldGetActivityMetadata(): Boolean = true
diff --git a/tests/unit/src/com/android/intentresolver/MultiProfilePagerAdapterTest.kt b/tests/unit/src/com/android/intentresolver/MultiProfilePagerAdapterTest.kt
deleted file mode 100644
index ed06f7d1..00000000
--- a/tests/unit/src/com/android/intentresolver/MultiProfilePagerAdapterTest.kt
+++ /dev/null
@@ -1,277 +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 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/ProfileAvailabilityTest.kt b/tests/unit/src/com/android/intentresolver/ProfileAvailabilityTest.kt
new file mode 100644
index 00000000..47db0cf5
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/ProfileAvailabilityTest.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import com.android.intentresolver.annotation.JavaInterop
+import com.android.intentresolver.data.repository.FakeUserRepository
+import com.android.intentresolver.domain.interactor.UserInteractor
+import com.android.intentresolver.shared.model.Profile
+import com.android.intentresolver.shared.model.User
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+@OptIn(ExperimentalCoroutinesApi::class, JavaInterop::class)
+class ProfileAvailabilityTest {
+ private val personalUser = User(0, User.Role.PERSONAL)
+ private val workUser = User(10, User.Role.WORK)
+
+ private val personalProfile = Profile(Profile.Type.PERSONAL, personalUser)
+ private val workProfile = Profile(Profile.Type.WORK, workUser)
+
+ private val repository = FakeUserRepository(listOf(personalUser, workUser))
+ private val interactor = UserInteractor(repository, launchedAs = personalUser.handle)
+
+ @Test
+ fun testProfileAvailable() = runTest {
+ val availability = ProfileAvailability(interactor, this, Dispatchers.IO)
+
+ assertThat(availability.isAvailable(personalProfile)).isTrue()
+ assertThat(availability.isAvailable(workProfile)).isTrue()
+
+ availability.requestQuietModeState(workProfile, true)
+ runCurrent()
+
+ assertThat(availability.isAvailable(workProfile)).isFalse()
+
+ availability.requestQuietModeState(workProfile, false)
+ runCurrent()
+
+ assertThat(availability.isAvailable(workProfile)).isTrue()
+ }
+
+ @Test
+ fun waitingToEnableProfile() = runTest {
+ val availability = ProfileAvailability(interactor, this, Dispatchers.IO)
+
+ availability.requestQuietModeState(workProfile, true)
+ assertThat(availability.waitingToEnableProfile).isFalse()
+ runCurrent()
+
+ availability.requestQuietModeState(workProfile, false)
+ assertThat(availability.waitingToEnableProfile).isTrue()
+ runCurrent()
+
+ assertThat(availability.waitingToEnableProfile).isFalse()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/ProfileHelperTest.kt b/tests/unit/src/com/android/intentresolver/ProfileHelperTest.kt
new file mode 100644
index 00000000..05d642f7
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/ProfileHelperTest.kt
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import com.android.intentresolver.Flags.FLAG_ENABLE_PRIVATE_PROFILE
+import com.android.intentresolver.annotation.JavaInterop
+import com.android.intentresolver.data.repository.FakeUserRepository
+import com.android.intentresolver.domain.interactor.UserInteractor
+import com.android.intentresolver.inject.FakeIntentResolverFlags
+import com.android.intentresolver.shared.model.Profile
+import com.android.intentresolver.shared.model.User
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+@OptIn(JavaInterop::class)
+class ProfileHelperTest {
+
+ private val personalUser = User(0, User.Role.PERSONAL)
+ private val cloneUser = User(10, User.Role.CLONE)
+
+ private val personalProfile = Profile(Profile.Type.PERSONAL, personalUser)
+ private val personalWithCloneProfile = Profile(Profile.Type.PERSONAL, personalUser, cloneUser)
+
+ private val workUser = User(11, User.Role.WORK)
+ private val workProfile = Profile(Profile.Type.WORK, workUser)
+
+ private val privateUser = User(12, User.Role.PRIVATE)
+ private val privateProfile = Profile(Profile.Type.PRIVATE, privateUser)
+
+ private val flags =
+ FakeIntentResolverFlags().apply { setFlag(FLAG_ENABLE_PRIVATE_PROFILE, true) }
+
+ private fun assertProfiles(
+ helper: ProfileHelper,
+ personalProfile: Profile,
+ workProfile: Profile? = null,
+ privateProfile: Profile? = null
+ ) {
+ assertThat(helper.personalProfile).isEqualTo(personalProfile)
+ assertThat(helper.personalHandle).isEqualTo(personalProfile.primary.handle)
+
+ personalProfile.clone?.also {
+ assertThat(helper.cloneUserPresent).isTrue()
+ assertThat(helper.cloneHandle).isEqualTo(it.handle)
+ }
+ ?: {
+ assertThat(helper.cloneUserPresent).isFalse()
+ assertThat(helper.cloneHandle).isNull()
+ }
+
+ workProfile?.also {
+ assertThat(helper.workProfilePresent).isTrue()
+ assertThat(helper.workProfile).isEqualTo(it)
+ assertThat(helper.workHandle).isEqualTo(it.primary.handle)
+ }
+ ?: {
+ assertThat(helper.workProfilePresent).isFalse()
+ assertThat(helper.workProfile).isNull()
+ assertThat(helper.workHandle).isNull()
+ }
+
+ privateProfile?.also {
+ assertThat(helper.privateProfilePresent).isTrue()
+ assertThat(helper.privateProfile).isEqualTo(it)
+ assertThat(helper.privateHandle).isEqualTo(it.primary.handle)
+ }
+ ?: {
+ assertThat(helper.privateProfilePresent).isFalse()
+ assertThat(helper.privateProfile).isNull()
+ assertThat(helper.privateHandle).isNull()
+ }
+ }
+
+ @Test
+ fun launchedByPersonal() = runTest {
+ val repository = FakeUserRepository(listOf(personalUser))
+ val interactor = UserInteractor(repository, launchedAs = personalUser.handle)
+
+ val helper =
+ ProfileHelper(
+ interactor = interactor,
+ scope = this,
+ background = Dispatchers.Unconfined,
+ flags = flags
+ )
+
+ assertProfiles(helper, personalProfile)
+
+ assertThat(helper.isLaunchedAsCloneProfile).isFalse()
+ assertThat(helper.launchedAsProfileType).isEqualTo(Profile.Type.PERSONAL)
+ assertThat(helper.getQueryIntentsHandle(personalUser.handle))
+ .isEqualTo(personalProfile.primary.handle)
+ assertThat(helper.tabOwnerUserHandleForLaunch).isEqualTo(personalProfile.primary.handle)
+ }
+
+ @Test
+ fun launchedByPersonal_withClone() = runTest {
+ val repository = FakeUserRepository(listOf(personalUser, cloneUser))
+ val interactor = UserInteractor(repository, launchedAs = personalUser.handle)
+
+ val helper =
+ ProfileHelper(
+ interactor = interactor,
+ scope = this,
+ background = Dispatchers.Unconfined,
+ flags = flags
+ )
+
+ assertProfiles(helper, personalWithCloneProfile)
+
+ assertThat(helper.isLaunchedAsCloneProfile).isFalse()
+ assertThat(helper.launchedAsProfileType).isEqualTo(Profile.Type.PERSONAL)
+ assertThat(helper.getQueryIntentsHandle(personalUser.handle)).isEqualTo(personalUser.handle)
+ assertThat(helper.tabOwnerUserHandleForLaunch).isEqualTo(personalProfile.primary.handle)
+ }
+
+ @Test
+ fun launchedByClone() = runTest {
+ val repository = FakeUserRepository(listOf(personalUser, cloneUser))
+ val interactor = UserInteractor(repository, launchedAs = cloneUser.handle)
+
+ val helper =
+ ProfileHelper(
+ interactor = interactor,
+ scope = this,
+ background = Dispatchers.Unconfined,
+ flags = flags
+ )
+
+ assertProfiles(helper, personalWithCloneProfile)
+
+ assertThat(helper.isLaunchedAsCloneProfile).isTrue()
+ assertThat(helper.launchedAsProfileType).isEqualTo(Profile.Type.PERSONAL)
+ assertThat(helper.getQueryIntentsHandle(personalWithCloneProfile.primary.handle))
+ .isEqualTo(personalWithCloneProfile.clone?.handle)
+ assertThat(helper.tabOwnerUserHandleForLaunch)
+ .isEqualTo(personalWithCloneProfile.primary.handle)
+ }
+
+ @Test
+ fun launchedByPersonal_withWork() = runTest {
+ val repository = FakeUserRepository(listOf(personalUser, workUser))
+ val interactor = UserInteractor(repository, launchedAs = personalUser.handle)
+
+ val helper =
+ ProfileHelper(
+ interactor = interactor,
+ scope = this,
+ background = Dispatchers.Unconfined,
+ flags = flags
+ )
+
+ assertProfiles(helper, personalProfile = personalProfile, workProfile = workProfile)
+
+ assertThat(helper.launchedAsProfileType).isEqualTo(Profile.Type.PERSONAL)
+ assertThat(helper.isLaunchedAsCloneProfile).isFalse()
+ assertThat(helper.getQueryIntentsHandle(personalUser.handle))
+ .isEqualTo(personalProfile.primary.handle)
+ assertThat(helper.getQueryIntentsHandle(workUser.handle))
+ .isEqualTo(workProfile.primary.handle)
+ assertThat(helper.tabOwnerUserHandleForLaunch).isEqualTo(personalProfile.primary.handle)
+ }
+
+ @Test
+ fun launchedByWork() = runTest {
+ val repository = FakeUserRepository(listOf(personalUser, workUser))
+ val interactor = UserInteractor(repository, launchedAs = workUser.handle)
+
+ val helper =
+ ProfileHelper(
+ interactor = interactor,
+ scope = this,
+ background = Dispatchers.Unconfined,
+ flags = flags
+ )
+
+ assertProfiles(helper, personalProfile = personalProfile, workProfile = workProfile)
+
+ assertThat(helper.isLaunchedAsCloneProfile).isFalse()
+ assertThat(helper.launchedAsProfileType).isEqualTo(Profile.Type.WORK)
+ assertThat(helper.getQueryIntentsHandle(personalProfile.primary.handle))
+ .isEqualTo(personalProfile.primary.handle)
+ assertThat(helper.getQueryIntentsHandle(workProfile.primary.handle))
+ .isEqualTo(workProfile.primary.handle)
+ assertThat(helper.tabOwnerUserHandleForLaunch).isEqualTo(workProfile.primary.handle)
+ }
+
+ @Test
+ fun launchedByPersonal_withPrivate() = runTest {
+ val repository = FakeUserRepository(listOf(personalUser, privateUser))
+ val interactor = UserInteractor(repository, launchedAs = personalUser.handle)
+
+ val helper =
+ ProfileHelper(
+ interactor = interactor,
+ scope = this,
+ background = Dispatchers.Unconfined,
+ flags = flags
+ )
+
+ assertProfiles(helper, personalProfile = personalProfile, privateProfile = privateProfile)
+
+ assertThat(helper.isLaunchedAsCloneProfile).isFalse()
+ assertThat(helper.launchedAsProfileType).isEqualTo(Profile.Type.PERSONAL)
+ assertThat(helper.getQueryIntentsHandle(personalProfile.primary.handle))
+ .isEqualTo(personalProfile.primary.handle)
+ assertThat(helper.getQueryIntentsHandle(privateProfile.primary.handle))
+ .isEqualTo(privateProfile.primary.handle)
+ assertThat(helper.tabOwnerUserHandleForLaunch).isEqualTo(personalProfile.primary.handle)
+ }
+
+ @Test
+ fun launchedByPrivate() = runTest {
+ val repository = FakeUserRepository(listOf(personalUser, privateUser))
+ val interactor = UserInteractor(repository, launchedAs = privateUser.handle)
+
+ val helper =
+ ProfileHelper(
+ interactor = interactor,
+ scope = this,
+ background = Dispatchers.Unconfined,
+ flags = flags
+ )
+
+ assertProfiles(helper, personalProfile = personalProfile, privateProfile = privateProfile)
+
+ assertThat(helper.isLaunchedAsCloneProfile).isFalse()
+ assertThat(helper.launchedAsProfileType).isEqualTo(Profile.Type.PRIVATE)
+ assertThat(helper.getQueryIntentsHandle(personalProfile.primary.handle))
+ .isEqualTo(personalProfile.primary.handle)
+ assertThat(helper.getQueryIntentsHandle(privateProfile.primary.handle))
+ .isEqualTo(privateProfile.primary.handle)
+ assertThat(helper.tabOwnerUserHandleForLaunch).isEqualTo(privateProfile.primary.handle)
+ }
+
+ @Test
+ fun launchedByPersonal_withPrivate_privateDisabled() = runTest {
+ flags.setFlag(FLAG_ENABLE_PRIVATE_PROFILE, false)
+
+ val repository = FakeUserRepository(listOf(personalUser, privateUser))
+ val interactor = UserInteractor(repository, launchedAs = personalUser.handle)
+
+ val helper =
+ ProfileHelper(
+ interactor = interactor,
+ scope = this,
+ background = Dispatchers.Unconfined,
+ flags = flags
+ )
+
+ assertProfiles(helper, personalProfile = personalProfile, privateProfile = null)
+
+ assertThat(helper.isLaunchedAsCloneProfile).isFalse()
+ assertThat(helper.launchedAsProfileType).isEqualTo(Profile.Type.PERSONAL)
+ assertThat(helper.getQueryIntentsHandle(personalProfile.primary.handle))
+ .isEqualTo(personalProfile.primary.handle)
+ assertThat(helper.tabOwnerUserHandleForLaunch).isEqualTo(personalProfile.primary.handle)
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/ResolverListAdapterTest.kt b/tests/unit/src/com/android/intentresolver/ResolverListAdapterTest.kt
index 61b9fd9c..d8cb7adc 100644
--- a/tests/unit/src/com/android/intentresolver/ResolverListAdapterTest.kt
+++ b/tests/unit/src/com/android/intentresolver/ResolverListAdapterTest.kt
@@ -29,11 +29,17 @@ 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 com.google.common.truth.Truth.assertWithMessage
import org.junit.Test
-import org.mockito.Mockito.anyBoolean
-import org.mockito.Mockito.inOrder
-import org.mockito.Mockito.never
-import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doAnswer
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.inOrder
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
private const val PKG_NAME = "org.pkg.app"
private const val PKG_NAME_TWO = "org.pkg.two.app"
@@ -43,20 +49,15 @@ 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 userManager = mock<UserManager> { on { isManagedProfile } doReturn (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)
+ on { getSystemService(Context.LAYOUT_INFLATER_SERVICE) } doReturn layoutInflater
+ on { getSystemService(Context.USER_SERVICE) } doReturn userManager
+ on { packageManager } doReturn 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>()
@@ -66,16 +67,20 @@ class ResolverListAdapterTest {
@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 resolverListController =
+ mock<ResolverListController> {
+ on { filterIneligibleActivities(any(), any()) } doReturn null
+ on { filterLowPriority(any(), any()) } doReturn null
+ on {
+ getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ } doReturn resolvedTargets
+ }
val testSubject =
ResolverListAdapter(
context,
@@ -105,25 +110,27 @@ class ResolverListAdapterTest {
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 resolverListController =
+ mock<ResolverListController> {
+ on { filterIneligibleActivities(any(), any()) } doReturn null
+ on { filterLowPriority(any(), any()) } doReturn null
+ on {
+ getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ } doReturn resolvedTargets
+ on { lastChosen } doReturn resolvedTargets[0].getResolveInfoAt(0)
+ }
val testSubject =
ResolverListAdapter(
context,
@@ -158,18 +165,21 @@ class ResolverListAdapterTest {
@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 resolverListController =
+ mock<ResolverListController> {
+ on { filterIneligibleActivities(any(), any()) } doReturn null
+ on { filterLowPriority(any(), any()) } doReturn null
+ on {
+ getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ } doReturn resolvedTargets
+ on { lastChosen } doReturn createResolveInfo(PKG_NAME_TWO, CLASS_NAME, userHandle)
+ }
val testSubject =
ResolverListAdapter(
context,
@@ -196,7 +206,9 @@ class ResolverListAdapterTest {
assertThat(testSubject.hasFilteredItem()).isTrue()
assertThat(testSubject.filteredItem).isNull()
assertThat(testSubject.filteredPosition).isLessThan(0)
- assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertWithMessage("unfilteredResolveList")
+ .that(testSubject.unfilteredResolveList)
+ .containsExactlyElementsIn(resolvedTargets)
assertThat(testSubject.isTabLoaded).isTrue()
assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(0)
}
@@ -204,18 +216,21 @@ class ResolverListAdapterTest {
@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 resolverListController =
+ mock<ResolverListController> {
+ on { filterIneligibleActivities(any(), any()) } doReturn null
+ on { filterLowPriority(any(), any()) } doReturn null
+ on {
+ getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ } doReturn resolvedTargets
+ on { lastChosen } doReturn resolvedTargets[0].getResolveInfoAt(0)
+ }
val testSubject =
ResolverListAdapter(
context,
@@ -243,7 +258,9 @@ class ResolverListAdapterTest {
assertThat(testSubject.hasFilteredItem()).isFalse()
assertThat(testSubject.filteredItem).isNull()
assertThat(testSubject.filteredPosition).isLessThan(0)
- assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
+ assertWithMessage("unfilteredResolveList")
+ .that(testSubject.unfilteredResolveList)
+ .containsExactlyElementsIn(resolvedTargets)
assertThat(testSubject.isTabLoaded).isTrue()
}
@@ -273,20 +290,23 @@ class ResolverListAdapterTest {
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 resolverListController =
+ mock<ResolverListController> {
+ on { filterIneligibleActivities(any(), any()) } doReturn null
+ on { filterLowPriority(any(), any()) } doReturn null
+ on {
+ getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ } doReturn resolvedTargets
+ if (hasLastChosen) {
+ on { lastChosen } doReturn resolvedTargets[0].getResolveInfoAt(0)
+ }
+ }
val resolverListCommunicator = FakeResolverListCommunicator(useLayoutWithDefaults)
val testSubject =
ResolverListAdapter(
@@ -318,7 +338,6 @@ class ResolverListAdapterTest {
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()
@@ -337,7 +356,6 @@ class ResolverListAdapterTest {
}
assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
assertThat(testSubject.isTabLoaded).isTrue()
- assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(1)
assertThat(resolverListCommunicator.sendVoiceCommandCount).isEqualTo(1)
assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(0)
}
@@ -349,18 +367,21 @@ class ResolverListAdapterTest {
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 resolverListController =
+ mock<ResolverListController> {
+ on { filterIneligibleActivities(any(), any()) } doReturn null
+ on { filterLowPriority(any(), any()) } doReturn null
+ on {
+ getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ } doReturn resolvedTargets
+ on { lastChosen } doReturn createResolveInfo(PKG_NAME, CLASS_NAME + "2", userHandle)
+ }
val testSubject =
ResolverListAdapter(
context,
@@ -391,7 +412,6 @@ class ResolverListAdapterTest {
assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
assertThat(testSubject.isTabLoaded).isFalse()
assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(1)
- assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(0)
backgroundExecutor.runUntilIdle()
@@ -403,7 +423,6 @@ class ResolverListAdapterTest {
assertThat(testSubject.filteredPosition).isLessThan(0)
assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets)
assertThat(testSubject.isTabLoaded).isTrue()
- assertThat(resolverListCommunicator.updateProfileViewButtonCount).isEqualTo(0)
assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(0)
}
@@ -415,19 +434,22 @@ class ResolverListAdapterTest {
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)
+ // whenever(resolvedTargets[1].getResolveInfoAt(0).loadLabel(any())).thenReturn("Label")
+ val resolverListController =
+ mock<ResolverListController> {
+ on { filterIneligibleActivities(any(), any()) } doReturn null
+ on { filterLowPriority(any(), any()) } doReturn null
+ on {
+ getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ } doReturn resolvedTargets
+ on { lastChosen } doReturn resolvedTargets[0].getResolveInfoAt(0)
+ }
val testSubject =
ResolverListAdapter(
context,
@@ -468,21 +490,26 @@ class ResolverListAdapterTest {
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 resolverListController =
+ mock<ResolverListController> {
+ on { filterIneligibleActivities(any(), any()) } doReturn null
+ on { filterLowPriority(any(), any()) } doReturn null
+ on {
+ getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ } doReturn resolvedTargets
+ on { sort(any()) } doAnswer
+ {
+ val components = it.arguments[0] as MutableList<ResolvedComponentInfo>
+ components[0] = components[1].also { components[1] = components[0] }
+ null
+ }
+ }
val testSubject =
ResolverListAdapter(
context,
@@ -521,22 +548,26 @@ class ResolverListAdapterTest {
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 resolverListController =
+ mock<ResolverListController> {
+ on { filterIneligibleActivities(any(), any()) } doReturn null
+ on { filterLowPriority(any(), any()) } doReturn null
+ on {
+ getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ } doReturn resolvedTargets
+ on { filterIneligibleActivities(any(), any()) } doAnswer
+ {
+ val components = it.arguments[0] as MutableList<ResolvedComponentInfo>
+ val original = ArrayList(components)
+ components.removeAt(1)
+ original
+ }
}
val testSubject =
ResolverListAdapter(
@@ -570,24 +601,28 @@ class ResolverListAdapterTest {
@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 rList = listOf(createResolveInfo(PKG_NAME, CLASS_NAME, userHandle))
+ val resolverListController =
+ mock<ResolverListController> {
+ on { filterLowPriority(any(), any()) } doReturn null
+ on { addResolveListDedupe(any(), eq(targetIntent), eq(rList)) } doAnswer
+ {
+ val result = it.arguments[0] as MutableList<ResolvedComponentInfo>
+ result.addAll(
+ createResolvedComponents(
+ ComponentName(PKG_NAME, CLASS_NAME),
+ ComponentName(PKG_NAME_TWO, CLASS_NAME),
+ )
+ )
+ null
+ }
+ on { filterIneligibleActivities(any(), any()) } doAnswer
+ {
+ val components = it.arguments[0] as MutableList<ResolvedComponentInfo>
+ val original = ArrayList(components)
+ components.clear()
+ original
+ }
}
val testSubject =
ResolverListAdapter(
@@ -624,23 +659,26 @@ class ResolverListAdapterTest {
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 resolverListController =
+ mock<ResolverListController> {
+ on { filterIneligibleActivities(any(), any()) } doReturn null
+ on {
+ getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ } doReturn resolvedTargets
+ on { filterLowPriority(any(), any()) } doAnswer
+ {
+ val components = it.arguments[0] as MutableList<ResolvedComponentInfo>
+ val original = ArrayList(components)
+ components.removeAt(1)
+ original
+ }
+ }
val testSubject =
ResolverListAdapter(
context,
@@ -677,19 +715,23 @@ class ResolverListAdapterTest {
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 })
+ val resolverListController =
+ mock<ResolverListController> {
+ on { filterIneligibleActivities(any(), any()) } doReturn null
+ on { filterLowPriority(any(), any()) } doReturn null
+ on {
+ getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ } doReturn resolvedTargets
+ }
whenever(packageManager.getActivityInfo(eq(initialComponent), eq(0)))
.thenReturn(createActivityInfo(initialComponent))
val testSubject =
@@ -722,7 +764,6 @@ class ResolverListAdapterTest {
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()
@@ -737,7 +778,6 @@ class ResolverListAdapterTest {
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)
}
@@ -749,16 +789,20 @@ class ResolverListAdapterTest {
ComponentName(PKG_NAME, CLASS_NAME),
ComponentName(PKG_NAME_TWO, CLASS_NAME),
)
- whenever(
- resolverListController.getResolversForIntentAsUser(
- true,
- resolverListCommunicator.shouldGetActivityMetadata(),
- resolverListCommunicator.shouldGetOnlyDefaultActivities(),
- payloadIntents,
- userHandle
- )
- )
- .thenReturn(resolvedTargets)
+ val resolverListController =
+ mock<ResolverListController> {
+ on { filterIneligibleActivities(any(), any()) } doReturn null
+ on { filterLowPriority(any(), any()) } doReturn null
+ on {
+ getResolversForIntentAsUser(
+ true,
+ resolverListCommunicator.shouldGetActivityMetadata(),
+ resolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ payloadIntents,
+ userHandle
+ )
+ } doReturn resolvedTargets
+ }
val initialComponent = ComponentName(PKG_NAME_TWO, CLASS_NAME)
val initialIntents =
arrayOf(Intent(Intent.ACTION_SEND).apply { component = initialComponent })
@@ -794,7 +838,6 @@ class ResolverListAdapterTest {
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()
@@ -809,7 +852,6 @@ class ResolverListAdapterTest {
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)
}
@@ -817,6 +859,7 @@ class ResolverListAdapterTest {
@Test
fun testPostListReadyAtEndOfRebuild_synchronous() {
val communicator = mock<ResolverListCommunicator> {}
+ val resolverListController = mock<ResolverListController>()
val testSubject =
ResolverListAdapter(
context,
@@ -848,26 +891,16 @@ class ResolverListAdapterTest {
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 resolverListController =
+ mock<ResolverListController> {
+ on { filterIneligibleActivities(any(), any()) } doReturn null
+ on { filterLowPriority(any(), any()) } doReturn null
+ on { getResolversForIntentAsUser(any(), any(), any(), any(), any()) } doReturn
+ resolvedTargets
+ }
val communicator =
mock<ResolverListCommunicator> {
- whenever(getReplacementIntent(any(), any())).thenAnswer { invocation ->
- invocation.arguments[1]
- }
+ on { getReplacementIntent(any(), any()) } doAnswer { it.arguments[1] as Intent }
}
val testSubject =
ResolverListAdapter(
@@ -906,26 +939,16 @@ class ResolverListAdapterTest {
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 resolverListController =
+ mock<ResolverListController> {
+ on { filterIneligibleActivities(any(), any()) } doReturn null
+ on { filterLowPriority(any(), any()) } doReturn null
+ on { getResolversForIntentAsUser(any(), any(), any(), any(), any()) } doReturn
+ resolvedTargets
+ }
val communicator =
mock<ResolverListCommunicator> {
- whenever(getReplacementIntent(any(), any())).thenAnswer { invocation ->
- invocation.arguments[1]
- }
+ on { getReplacementIntent(any(), any()) } doAnswer { it.arguments[1] as Intent }
}
val testSubject =
ResolverListAdapter(
@@ -971,26 +994,16 @@ class ResolverListAdapterTest {
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 resolverListController =
+ mock<ResolverListController> {
+ on { filterIneligibleActivities(any(), any()) } doReturn null
+ on { filterLowPriority(any(), any()) } doReturn null
+ on { getResolversForIntentAsUser(any(), any(), any(), any(), any()) } doReturn
+ resolvedTargets
+ }
val communicator =
mock<ResolverListCommunicator> {
- whenever(getReplacementIntent(any(), any())).thenAnswer { invocation ->
- invocation.arguments[1]
- }
+ on { getReplacementIntent(any(), any()) } doAnswer { it.arguments[1] as Intent }
}
val testSubject =
ResolverListAdapter(
@@ -1032,17 +1045,23 @@ class ResolverListAdapterTest {
ResolvedComponentInfo(
ComponentName(PKG_NAME, CLASS_NAME),
targetIntent,
- createResolveInfo(component.packageName, component.className)
+ createResolveInfo(component.packageName, component.className, userHandle)
)
result.add(resolvedComponentInfo)
}
return result
}
- private fun createResolveInfo(packageName: String, className: String): ResolveInfo =
- mock<ResolveInfo> {
+ private fun createResolveInfo(
+ packageName: String,
+ className: String,
+ handle: UserHandle,
+ label: String? = null
+ ): ResolveInfo =
+ ResolveInfo().apply {
activityInfo = createActivityInfo(ComponentName(packageName, className))
- targetUserId = this@ResolverListAdapterTest.userHandle.identifier
- userHandle = this@ResolverListAdapterTest.userHandle
+ targetUserId = handle.identifier
+ userHandle = handle
+ nonLocalizedLabel = label
}
}
diff --git a/tests/unit/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt b/tests/unit/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt
index 2346d98b..e26dffb8 100644
--- a/tests/unit/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt
+++ b/tests/unit/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt
@@ -22,13 +22,15 @@ import android.content.Intent
import android.content.pm.ShortcutInfo
import android.os.UserHandle
import android.service.chooser.ChooserTarget
-import com.android.intentresolver.chooser.DisplayResolveInfo
-import com.android.intentresolver.chooser.TargetInfo
import androidx.test.filters.SmallTest
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.intentresolver.chooser.DisplayResolveInfo
+import com.android.intentresolver.chooser.TargetInfo
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
private const val PACKAGE_A = "package.a"
private const val PACKAGE_B = "package.b"
@@ -36,39 +38,43 @@ private const val CLASS_NAME = "./MainActivity"
@SmallTest
class ShortcutSelectionLogicTest {
- private val PERSONAL_USER_HANDLE: UserHandle = InstrumentationRegistry
- .getInstrumentation().getTargetContext().getUser()
+ private val PERSONAL_USER_HANDLE: UserHandle =
+ InstrumentationRegistry.getInstrumentation().getTargetContext().getUser()
- private val packageTargets = HashMap<String, Array<ChooserTarget>>().apply {
- arrayOf(PACKAGE_A, PACKAGE_B).forEach { pkg ->
- // shortcuts in reverse priority order
- val targets = Array(3) { i ->
- createChooserTarget(
- "Shortcut $i",
- (i + 1).toFloat() / 10f,
- ComponentName(pkg, CLASS_NAME),
- pkg.shortcutId(i),
- )
+ private val packageTargets =
+ HashMap<String, Array<ChooserTarget>>().apply {
+ arrayOf(PACKAGE_A, PACKAGE_B).forEach { pkg ->
+ // shortcuts in reverse priority order
+ val targets =
+ Array(3) { i ->
+ createChooserTarget(
+ "Shortcut $i",
+ (i + 1).toFloat() / 10f,
+ ComponentName(pkg, CLASS_NAME),
+ pkg.shortcutId(i),
+ )
+ }
+ this[pkg] = targets
}
- this[pkg] = targets
}
- }
- private val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo(
+ private val baseDisplayInfo =
+ DisplayResolveInfo.newDisplayResolveInfo(
Intent(),
ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE),
"label",
"extended info",
Intent()
- )
+ )
- private val otherBaseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo(
+ private val otherBaseDisplayInfo =
+ DisplayResolveInfo.newDisplayResolveInfo(
Intent(),
ResolverDataProvider.createResolveInfo(4, 0, PERSONAL_USER_HANDLE),
"label 2",
"extended info 2",
Intent()
- )
+ )
private operator fun Map<String, Array<ChooserTarget>>.get(pkg: String, idx: Int) =
this[pkg]?.get(idx) ?: error("missing package $pkg")
@@ -78,24 +84,26 @@ class ShortcutSelectionLogicTest {
val serviceResults = ArrayList<TargetInfo>()
val sc1 = packageTargets[PACKAGE_A, 0]
val sc2 = packageTargets[PACKAGE_A, 1]
- val testSubject = ShortcutSelectionLogic(
- /* maxShortcutTargetsPerApp = */ 1,
- /* applySharingAppLimits = */ false
- )
+ val testSubject =
+ ShortcutSelectionLogic(
+ /* maxShortcutTargetsPerApp = */ 1,
+ /* applySharingAppLimits = */ false
+ )
- val isUpdated = testSubject.addServiceResults(
- /* origTarget = */ baseDisplayInfo,
- /* origTargetScore = */ 0.1f,
- /* targets = */ listOf(sc1, sc2),
- /* isShortcutResult = */ true,
- /* directShareToShortcutInfos = */ emptyMap(),
- /* directShareToAppTargets = */ emptyMap(),
- /* userContext = */ mock(),
- /* targetIntent = */ mock(),
- /* refererFillInIntent = */ mock(),
- /* maxRankedTargets = */ 4,
- /* serviceTargets = */ serviceResults
- )
+ val isUpdated =
+ testSubject.addServiceResults(
+ /* origTarget = */ baseDisplayInfo,
+ /* origTargetScore = */ 0.1f,
+ /* targets = */ listOf(sc1, sc2),
+ /* isShortcutResult = */ true,
+ /* directShareToShortcutInfos = */ emptyMap(),
+ /* directShareToAppTargets = */ emptyMap(),
+ /* userContext = */ mock(),
+ /* targetIntent = */ mock(),
+ /* refererFillInIntent = */ mock(),
+ /* maxRankedTargets = */ 4,
+ /* serviceTargets = */ serviceResults
+ )
assertTrue("Updates are expected", isUpdated)
assertShortcutsInOrder(
@@ -110,24 +118,26 @@ class ShortcutSelectionLogicTest {
val serviceResults = ArrayList<TargetInfo>()
val sc1 = packageTargets[PACKAGE_A, 0]
val sc2 = packageTargets[PACKAGE_A, 1]
- val testSubject = ShortcutSelectionLogic(
- /* maxShortcutTargetsPerApp = */ 1,
- /* applySharingAppLimits = */ true
- )
+ val testSubject =
+ ShortcutSelectionLogic(
+ /* maxShortcutTargetsPerApp = */ 1,
+ /* applySharingAppLimits = */ true
+ )
- val isUpdated = testSubject.addServiceResults(
- /* origTarget = */ baseDisplayInfo,
- /* origTargetScore = */ 0.1f,
- /* targets = */ listOf(sc1, sc2),
- /* isShortcutResult = */ true,
- /* directShareToShortcutInfos = */ emptyMap(),
- /* directShareToAppTargets = */ emptyMap(),
- /* userContext = */ mock(),
- /* targetIntent = */ mock(),
- /* refererFillInIntent = */ mock(),
- /* maxRankedTargets = */ 4,
- /* serviceTargets = */ serviceResults
- )
+ val isUpdated =
+ testSubject.addServiceResults(
+ /* origTarget = */ baseDisplayInfo,
+ /* origTargetScore = */ 0.1f,
+ /* targets = */ listOf(sc1, sc2),
+ /* isShortcutResult = */ true,
+ /* directShareToShortcutInfos = */ emptyMap(),
+ /* directShareToAppTargets = */ emptyMap(),
+ /* userContext = */ mock(),
+ /* targetIntent = */ mock(),
+ /* refererFillInIntent = */ mock(),
+ /* maxRankedTargets = */ 4,
+ /* serviceTargets = */ serviceResults
+ )
assertTrue("Updates are expected", isUpdated)
assertShortcutsInOrder(
@@ -142,24 +152,26 @@ class ShortcutSelectionLogicTest {
val serviceResults = ArrayList<TargetInfo>()
val sc1 = packageTargets[PACKAGE_A, 0]
val sc2 = packageTargets[PACKAGE_A, 1]
- val testSubject = ShortcutSelectionLogic(
- /* maxShortcutTargetsPerApp = */ 1,
- /* applySharingAppLimits = */ false
- )
+ val testSubject =
+ ShortcutSelectionLogic(
+ /* maxShortcutTargetsPerApp = */ 1,
+ /* applySharingAppLimits = */ false
+ )
- val isUpdated = testSubject.addServiceResults(
- /* origTarget = */ baseDisplayInfo,
- /* origTargetScore = */ 0.1f,
- /* targets = */ listOf(sc1, sc2),
- /* isShortcutResult = */ true,
- /* directShareToShortcutInfos = */ emptyMap(),
- /* directShareToAppTargets = */ emptyMap(),
- /* userContext = */ mock(),
- /* targetIntent = */ mock(),
- /* refererFillInIntent = */ mock(),
- /* maxRankedTargets = */ 1,
- /* serviceTargets = */ serviceResults
- )
+ val isUpdated =
+ testSubject.addServiceResults(
+ /* origTarget = */ baseDisplayInfo,
+ /* origTargetScore = */ 0.1f,
+ /* targets = */ listOf(sc1, sc2),
+ /* isShortcutResult = */ true,
+ /* directShareToShortcutInfos = */ emptyMap(),
+ /* directShareToAppTargets = */ emptyMap(),
+ /* userContext = */ mock(),
+ /* targetIntent = */ mock(),
+ /* refererFillInIntent = */ mock(),
+ /* maxRankedTargets = */ 1,
+ /* serviceTargets = */ serviceResults
+ )
assertTrue("Updates are expected", isUpdated)
assertShortcutsInOrder(
@@ -176,10 +188,11 @@ class ShortcutSelectionLogicTest {
val pkgAsc2 = packageTargets[PACKAGE_A, 1]
val pkgBsc1 = packageTargets[PACKAGE_B, 0]
val pkgBsc2 = packageTargets[PACKAGE_B, 1]
- val testSubject = ShortcutSelectionLogic(
- /* maxShortcutTargetsPerApp = */ 1,
- /* applySharingAppLimits = */ true
- )
+ val testSubject =
+ ShortcutSelectionLogic(
+ /* maxShortcutTargetsPerApp = */ 1,
+ /* applySharingAppLimits = */ true
+ )
testSubject.addServiceResults(
/* origTarget = */ baseDisplayInfo,
@@ -220,30 +233,31 @@ class ShortcutSelectionLogicTest {
val serviceResults = ArrayList<TargetInfo>()
val sc1 = packageTargets[PACKAGE_A, 0]
val sc2 = packageTargets[PACKAGE_A, 1]
- val testSubject = ShortcutSelectionLogic(
- /* maxShortcutTargetsPerApp = */ 1,
- /* applySharingAppLimits = */ false
- )
+ val testSubject =
+ ShortcutSelectionLogic(
+ /* maxShortcutTargetsPerApp = */ 1,
+ /* applySharingAppLimits = */ false
+ )
- val isUpdated = testSubject.addServiceResults(
- /* origTarget = */ baseDisplayInfo,
- /* origTargetScore = */ 0.1f,
- /* targets = */ listOf(sc1, sc2),
- /* isShortcutResult = */ true,
- /* directShareToShortcutInfos = */ mapOf(
- sc1 to createShortcutInfo(
- PACKAGE_A.shortcutId(1),
- sc1.componentName, 1).apply {
- addFlags(ShortcutInfo.FLAG_PINNED)
- }
- ),
- /* directShareToAppTargets = */ emptyMap(),
- /* userContext = */ mock(),
- /* targetIntent = */ mock(),
- /* refererFillInIntent = */ mock(),
- /* maxRankedTargets = */ 4,
- /* serviceTargets = */ serviceResults
- )
+ val isUpdated =
+ testSubject.addServiceResults(
+ /* origTarget = */ baseDisplayInfo,
+ /* origTargetScore = */ 0.1f,
+ /* targets = */ listOf(sc1, sc2),
+ /* isShortcutResult = */ true,
+ /* directShareToShortcutInfos = */ mapOf(
+ sc1 to
+ createShortcutInfo(PACKAGE_A.shortcutId(1), sc1.componentName, 1).apply {
+ addFlags(ShortcutInfo.FLAG_PINNED)
+ }
+ ),
+ /* directShareToAppTargets = */ emptyMap(),
+ /* userContext = */ mock(),
+ /* targetIntent = */ mock(),
+ /* refererFillInIntent = */ mock(),
+ /* maxRankedTargets = */ 4,
+ /* serviceTargets = */ serviceResults
+ )
assertTrue("Updates are expected", isUpdated)
assertShortcutsInOrder(
@@ -259,13 +273,12 @@ class ShortcutSelectionLogicTest {
val sc1 = packageTargets[PACKAGE_A, 0]
val sc2 = packageTargets[PACKAGE_A, 1]
val sc3 = packageTargets[PACKAGE_A, 2]
- val testSubject = ShortcutSelectionLogic(
- /* maxShortcutTargetsPerApp = */ 1,
- /* applySharingAppLimits = */ true
- )
- val context = mock<Context> {
- whenever(packageManager).thenReturn(mock())
- }
+ val testSubject =
+ ShortcutSelectionLogic(
+ /* maxShortcutTargetsPerApp = */ 1,
+ /* applySharingAppLimits = */ true
+ )
+ val context = mock<Context> { on { packageManager } doReturn (mock()) }
testSubject.addServiceResults(
/* origTarget = */ baseDisplayInfo,
@@ -291,7 +304,9 @@ class ShortcutSelectionLogicTest {
// TODO: consider renaming. Not all `ChooserTarget`s are "shortcuts" and many of our test cases
// add results with `isShortcutResult = false` and `directShareToShortcutInfos = emptyMap()`.
private fun assertShortcutsInOrder(
- expected: List<ChooserTarget>, actual: List<TargetInfo>, msg: String? = ""
+ expected: List<ChooserTarget>,
+ actual: List<TargetInfo>,
+ msg: String? = ""
) {
assertEquals(msg, expected.size, actual.size)
for (i in expected.indices) {
diff --git a/tests/unit/src/com/android/intentresolver/TestHelpers.kt b/tests/unit/src/com/android/intentresolver/TestHelpers.kt
index 5b583fef..812ecd1b 100644
--- a/tests/unit/src/com/android/intentresolver/TestHelpers.kt
+++ b/tests/unit/src/com/android/intentresolver/TestHelpers.kt
@@ -25,25 +25,17 @@ import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager.ShareShortcutInfo
import android.os.Bundle
import android.service.chooser.ChooserTarget
-import org.mockito.Mockito.`when` as whenever
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
internal fun createShareShortcutInfo(
id: String,
componentName: ComponentName,
rank: Int
-): ShareShortcutInfo =
- ShareShortcutInfo(
- createShortcutInfo(id, componentName, rank),
- componentName
- )
+): ShareShortcutInfo = ShareShortcutInfo(createShortcutInfo(id, componentName, rank), componentName)
-internal fun createShortcutInfo(
- id: String,
- componentName: ComponentName,
- rank: Int
-): ShortcutInfo {
- val context = mock<Context>()
- whenever(context.packageName).thenReturn(componentName.packageName)
+internal fun createShortcutInfo(id: String, componentName: ComponentName, rank: Int): ShortcutInfo {
+ val context = mock<Context> { on { packageName } doReturn componentName.packageName }
return ShortcutInfo.Builder(context, id)
.setShortLabel("Short Label $id")
.setLongLabel("Long Label $id")
@@ -60,7 +52,10 @@ internal fun createAppTarget(shortcutInfo: ShortcutInfo) =
)
fun createChooserTarget(
- title: String, score: Float, componentName: ComponentName, shortcutId: String
+ title: String,
+ score: Float,
+ componentName: ComponentName,
+ shortcutId: String
): ChooserTarget =
ChooserTarget(
title,
diff --git a/tests/unit/src/com/android/intentresolver/chooser/TargetInfoTest.kt b/tests/unit/src/com/android/intentresolver/chooser/TargetInfoTest.kt
index a7574c12..b346bee5 100644
--- a/tests/unit/src/com/android/intentresolver/chooser/TargetInfoTest.kt
+++ b/tests/unit/src/com/android/intentresolver/chooser/TargetInfoTest.kt
@@ -24,9 +24,10 @@ import android.content.pm.ActivityInfo
import android.content.pm.ResolveInfo
import android.graphics.drawable.AnimatedVectorDrawable
import android.os.UserHandle
-import android.test.UiThreadTest
+import androidx.test.annotation.UiThreadTest
import androidx.test.platform.app.InstrumentationRegistry
import com.android.intentresolver.ResolverDataProvider
+import com.android.intentresolver.ResolverDataProvider.createResolveInfo
import com.android.intentresolver.createChooserTarget
import com.android.intentresolver.createShortcutInfo
import com.android.intentresolver.mock
@@ -41,38 +42,37 @@ import org.mockito.Mockito.times
import org.mockito.Mockito.verify
class TargetInfoTest {
- private val PERSONAL_USER_HANDLE: UserHandle = InstrumentationRegistry
- .getInstrumentation().getTargetContext().getUser()
+ private val PERSONAL_USER_HANDLE: UserHandle =
+ InstrumentationRegistry.getInstrumentation().getTargetContext().getUser()
private val context = InstrumentationRegistry.getInstrumentation().getContext()
@Before
fun setup() {
// SelectableTargetInfo reads DeviceConfig and needs a permission for that.
- InstrumentationRegistry
- .getInstrumentation()
- .getUiAutomation()
+ InstrumentationRegistry.getInstrumentation()
+ .uiAutomation
.adoptShellPermissionIdentity("android.permission.READ_DEVICE_CONFIG")
}
@Test
fun testNewEmptyTargetInfo() {
val info = NotSelectableTargetInfo.newEmptyTargetInfo()
- assertThat(info.isEmptyTargetInfo()).isTrue()
- assertThat(info.isChooserTargetInfo()).isTrue() // From legacy inheritance model.
+ assertThat(info.isEmptyTargetInfo).isTrue()
+ assertThat(info.isChooserTargetInfo).isTrue() // From legacy inheritance model.
assertThat(info.hasDisplayIcon()).isFalse()
assertThat(info.getDisplayIconHolder().getDisplayIcon()).isNull()
}
- @UiThreadTest // AnimatedVectorDrawable needs to start from a thread with a Looper.
+ @UiThreadTest // AnimatedVectorDrawable needs to start from a thread with a Looper.
@Test
fun testNewPlaceholderTargetInfo() {
val info = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context)
assertThat(info.isPlaceHolderTargetInfo).isTrue()
- assertThat(info.isChooserTargetInfo).isTrue() // From legacy inheritance model.
+ assertThat(info.isChooserTargetInfo).isTrue() // From legacy inheritance model.
assertThat(info.hasDisplayIcon()).isTrue()
assertThat(info.displayIconHolder.displayIcon)
- .isInstanceOf(AnimatedVectorDrawable::class.java)
+ .isInstanceOf(AnimatedVectorDrawable::class.java)
// TODO: assert that the animation is pre-started/running (IIUC this requires synchronizing
// with some "render thread" per the `AnimatedVectorDrawable` docs). I believe this is
// possible using `AnimatorTestRule` but I couldn't find any sample usage in Kotlin nor get
@@ -82,34 +82,43 @@ class TargetInfoTest {
@Test
fun testNewSelectableTargetInfo() {
val resolvedIntent = Intent()
- val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo(
- resolvedIntent,
- ResolverDataProvider.createResolveInfo(1, 0, PERSONAL_USER_HANDLE),
- "label",
- "extended info",
- resolvedIntent
- )
- val chooserTarget = createChooserTarget(
- "title", 0.3f, ResolverDataProvider.createComponentName(2), "test_shortcut_id")
+ val baseDisplayInfo =
+ DisplayResolveInfo.newDisplayResolveInfo(
+ resolvedIntent,
+ createResolveInfo(1, 0, PERSONAL_USER_HANDLE),
+ "label",
+ "extended info",
+ resolvedIntent
+ )
+ val chooserTarget =
+ createChooserTarget(
+ "title",
+ 0.3f,
+ ResolverDataProvider.createComponentName(2),
+ "test_shortcut_id"
+ )
val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(3), 3)
- val appTarget = AppTarget(
- AppTargetId("id"),
- chooserTarget.componentName.packageName,
- chooserTarget.componentName.className,
- UserHandle.CURRENT)
-
- val targetInfo = SelectableTargetInfo.newSelectableTargetInfo(
- baseDisplayInfo,
- mock(),
- resolvedIntent,
- chooserTarget,
- 0.1f,
- shortcutInfo,
- appTarget,
- mock(),
- )
+ val appTarget =
+ AppTarget(
+ AppTargetId("id"),
+ chooserTarget.componentName.packageName,
+ chooserTarget.componentName.className,
+ UserHandle.CURRENT
+ )
+
+ val targetInfo =
+ SelectableTargetInfo.newSelectableTargetInfo(
+ baseDisplayInfo,
+ mock(),
+ resolvedIntent,
+ chooserTarget,
+ 0.1f,
+ shortcutInfo,
+ appTarget,
+ mock(),
+ )
assertThat(targetInfo.isSelectableTargetInfo).isTrue()
- assertThat(targetInfo.isChooserTargetInfo).isTrue() // From legacy inheritance model.
+ assertThat(targetInfo.isChooserTargetInfo).isTrue() // From legacy inheritance model.
assertThat(targetInfo.displayResolveInfo).isSameInstanceAs(baseDisplayInfo)
assertThat(targetInfo.chooserTargetComponentName).isEqualTo(chooserTarget.componentName)
assertThat(targetInfo.directShareShortcutId).isEqualTo(shortcutInfo.id)
@@ -121,33 +130,43 @@ class TargetInfoTest {
@Test
fun test_SelectableTargetInfo_componentName_no_source_info() {
- val chooserTarget = createChooserTarget(
- "title", 0.3f, ResolverDataProvider.createComponentName(1), "test_shortcut_id")
+ val chooserTarget =
+ createChooserTarget(
+ "title",
+ 0.3f,
+ ResolverDataProvider.createComponentName(1),
+ "test_shortcut_id"
+ )
val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(2), 3)
- val appTarget = AppTarget(
- AppTargetId("id"),
- chooserTarget.componentName.packageName,
- chooserTarget.componentName.className,
- UserHandle.CURRENT)
+ val appTarget =
+ AppTarget(
+ AppTargetId("id"),
+ chooserTarget.componentName.packageName,
+ chooserTarget.componentName.className,
+ UserHandle.CURRENT
+ )
val pkgName = "org.package"
val className = "MainActivity"
- val backupResolveInfo = ResolveInfo().apply {
- activityInfo = ActivityInfo().apply {
- packageName = pkgName
- name = className
+ val backupResolveInfo =
+ ResolveInfo().apply {
+ activityInfo =
+ ActivityInfo().apply {
+ packageName = pkgName
+ name = className
+ }
}
- }
-
- val targetInfo = SelectableTargetInfo.newSelectableTargetInfo(
- null,
- backupResolveInfo,
- mock(),
- chooserTarget,
- 0.1f,
- shortcutInfo,
- appTarget,
- mock(),
- )
+
+ val targetInfo =
+ SelectableTargetInfo.newSelectableTargetInfo(
+ null,
+ backupResolveInfo,
+ mock(),
+ chooserTarget,
+ 0.1f,
+ shortcutInfo,
+ appTarget,
+ mock(),
+ )
assertThat(targetInfo.resolvedComponentName).isEqualTo(ComponentName(pkgName, className))
}
@@ -156,32 +175,41 @@ class TargetInfoTest {
val resolvedIntent = Intent("DONT_REFINE_ME")
resolvedIntent.putExtra("resolvedIntent", true)
- val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo(
- resolvedIntent,
- ResolverDataProvider.createResolveInfo(1, 0),
- "label",
- "extended info",
- resolvedIntent
- )
- val chooserTarget = createChooserTarget(
- "title", 0.3f, ResolverDataProvider.createComponentName(2), "test_shortcut_id")
+ val baseDisplayInfo =
+ DisplayResolveInfo.newDisplayResolveInfo(
+ resolvedIntent,
+ createResolveInfo(1, 0),
+ "label",
+ "extended info",
+ resolvedIntent
+ )
+ val chooserTarget =
+ createChooserTarget(
+ "title",
+ 0.3f,
+ ResolverDataProvider.createComponentName(2),
+ "test_shortcut_id"
+ )
val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(3), 3)
- val appTarget = AppTarget(
- AppTargetId("id"),
- chooserTarget.componentName.packageName,
- chooserTarget.componentName.className,
- UserHandle.CURRENT)
-
- val targetInfo = SelectableTargetInfo.newSelectableTargetInfo(
- baseDisplayInfo,
- mock(),
- resolvedIntent,
- chooserTarget,
- 0.1f,
- shortcutInfo,
- appTarget,
- mock(),
- )
+ val appTarget =
+ AppTarget(
+ AppTargetId("id"),
+ chooserTarget.componentName.packageName,
+ chooserTarget.componentName.className,
+ UserHandle.CURRENT
+ )
+
+ val targetInfo =
+ SelectableTargetInfo.newSelectableTargetInfo(
+ baseDisplayInfo,
+ mock(),
+ resolvedIntent,
+ chooserTarget,
+ 0.1f,
+ shortcutInfo,
+ appTarget,
+ mock(),
+ )
val refinement = Intent("PROPOSED_REFINEMENT")
assertThat(targetInfo.tryToCloneWithAppliedRefinement(refinement)).isNull()
@@ -193,18 +221,19 @@ class TargetInfoTest {
intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending")
intent.setType("text/plain")
- val resolveInfo = ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE)
-
- val targetInfo = DisplayResolveInfo.newDisplayResolveInfo(
- intent,
- resolveInfo,
- "label",
- "extended info",
- intent
- )
- assertThat(targetInfo.isDisplayResolveInfo()).isTrue()
- assertThat(targetInfo.isMultiDisplayResolveInfo()).isFalse()
- assertThat(targetInfo.isChooserTargetInfo()).isFalse()
+ val resolveInfo = createResolveInfo(3, 0, PERSONAL_USER_HANDLE)
+
+ val targetInfo =
+ DisplayResolveInfo.newDisplayResolveInfo(
+ intent,
+ resolveInfo,
+ "label",
+ "extended info",
+ intent
+ )
+ assertThat(targetInfo.isDisplayResolveInfo).isTrue()
+ assertThat(targetInfo.isMultiDisplayResolveInfo).isFalse()
+ assertThat(targetInfo.isChooserTargetInfo).isFalse()
}
@Test
@@ -218,31 +247,30 @@ class TargetInfoTest {
val extraMatch = Intent("REFINE_ME")
extraMatch.putExtra("extraMatch", true)
- val originalInfo = DisplayResolveInfo.newDisplayResolveInfo(
- originalIntent,
- ResolverDataProvider.createResolveInfo(3, 0),
- "label",
- "extended info",
- originalIntent
- )
+ val originalInfo =
+ DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent,
+ createResolveInfo(3, 0),
+ "label",
+ "extended info",
+ originalIntent
+ )
originalInfo.addAlternateSourceIntent(mismatchedAlternate)
originalInfo.addAlternateSourceIntent(targetAlternate)
originalInfo.addAlternateSourceIntent(extraMatch)
- val refinement = Intent("REFINE_ME") // First match is `targetAlternate`
+ val refinement = Intent("REFINE_ME") // First match is `targetAlternate`
refinement.putExtra("refinement", true)
val refinedResult = checkNotNull(originalInfo.tryToCloneWithAppliedRefinement(refinement))
// Note `DisplayResolveInfo` targets merge refinements directly into their `resolvedIntent`.
- assertThat(refinedResult?.resolvedIntent?.getBooleanExtra("refinement", false)).isTrue()
- assertThat(refinedResult?.resolvedIntent?.getBooleanExtra("targetAlternate", false))
- .isTrue()
+ assertThat(refinedResult.resolvedIntent?.getBooleanExtra("refinement", false)).isTrue()
+ assertThat(refinedResult.resolvedIntent?.getBooleanExtra("targetAlternate", false)).isTrue()
// None of the other source intents got merged in (not even the later one that matched):
- assertThat(refinedResult?.resolvedIntent?.getBooleanExtra("originalIntent", false))
- .isFalse()
- assertThat(refinedResult?.resolvedIntent?.getBooleanExtra("mismatchedAlternate", false))
+ assertThat(refinedResult.resolvedIntent?.getBooleanExtra("originalIntent", false)).isFalse()
+ assertThat(refinedResult.resolvedIntent?.getBooleanExtra("mismatchedAlternate", false))
.isFalse()
- assertThat(refinedResult?.resolvedIntent?.getBooleanExtra("extraMatch", false)).isFalse()
+ assertThat(refinedResult.resolvedIntent?.getBooleanExtra("extraMatch", false)).isFalse()
}
@Test
@@ -252,13 +280,14 @@ class TargetInfoTest {
val mismatchedAlternate = Intent("DOESNT_MATCH")
mismatchedAlternate.putExtra("mismatchedAlternate", true)
- val originalInfo = DisplayResolveInfo.newDisplayResolveInfo(
- originalIntent,
- ResolverDataProvider.createResolveInfo(3, 0),
- "label",
- "extended info",
- originalIntent
- )
+ val originalInfo =
+ DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent,
+ createResolveInfo(3, 0),
+ "label",
+ "extended info",
+ originalIntent
+ )
originalInfo.addAlternateSourceIntent(mismatchedAlternate)
val refinement = Intent("PROPOSED_REFINEMENT")
@@ -271,41 +300,50 @@ class TargetInfoTest {
intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending")
intent.setType("text/plain")
- val resolveInfo = ResolverDataProvider.createResolveInfo(3, 0, PERSONAL_USER_HANDLE)
- val firstTargetInfo = DisplayResolveInfo.newDisplayResolveInfo(
- intent,
- resolveInfo,
- "label 1",
- "extended info 1",
- intent
- )
- val secondTargetInfo = DisplayResolveInfo.newDisplayResolveInfo(
- intent,
- resolveInfo,
- "label 2",
- "extended info 2",
- intent
- )
-
- val multiTargetInfo = MultiDisplayResolveInfo.newMultiDisplayResolveInfo(
- listOf(firstTargetInfo, secondTargetInfo))
-
- assertThat(multiTargetInfo.isMultiDisplayResolveInfo()).isTrue()
- assertThat(multiTargetInfo.isDisplayResolveInfo()).isTrue() // From legacy inheritance.
- assertThat(multiTargetInfo.isChooserTargetInfo()).isFalse()
-
- assertThat(multiTargetInfo.getExtendedInfo()).isNull()
-
- assertThat(multiTargetInfo.getAllDisplayTargets())
- .containsExactly(firstTargetInfo, secondTargetInfo)
+ val packageName = "org.pkg.app"
+ val componentA = ComponentName(packageName, "org.pkg.app.ActivityA")
+ val componentB = ComponentName(packageName, "org.pkg.app.ActivityB")
+ val resolveInfoA = createResolveInfo(componentA, 0, PERSONAL_USER_HANDLE)
+ val resolveInfoB = createResolveInfo(componentB, 0, PERSONAL_USER_HANDLE)
+ val firstTargetInfo =
+ DisplayResolveInfo.newDisplayResolveInfo(
+ intent,
+ resolveInfoA,
+ "label 1",
+ "extended info 1",
+ intent
+ )
+ val secondTargetInfo =
+ DisplayResolveInfo.newDisplayResolveInfo(
+ intent,
+ resolveInfoB,
+ "label 2",
+ "extended info 2",
+ intent
+ )
+
+ val multiTargetInfo =
+ MultiDisplayResolveInfo.newMultiDisplayResolveInfo(
+ listOf(firstTargetInfo, secondTargetInfo)
+ )
+
+ assertThat(multiTargetInfo.isMultiDisplayResolveInfo).isTrue()
+ assertThat(multiTargetInfo.isDisplayResolveInfo).isTrue() // From legacy inheritance.
+ assertThat(multiTargetInfo.isChooserTargetInfo).isFalse()
+
+ assertThat(multiTargetInfo.extendedInfo).isNull()
+
+ assertThat(multiTargetInfo.allDisplayTargets)
+ .containsExactly(firstTargetInfo, secondTargetInfo)
assertThat(multiTargetInfo.hasSelected()).isFalse()
- assertThat(multiTargetInfo.getSelectedTarget()).isNull()
+ assertThat(multiTargetInfo.selectedTarget).isNull()
multiTargetInfo.setSelected(1)
assertThat(multiTargetInfo.hasSelected()).isTrue()
- assertThat(multiTargetInfo.getSelectedTarget()).isEqualTo(secondTargetInfo)
+ assertThat(multiTargetInfo.selectedTarget).isEqualTo(secondTargetInfo)
+ assertThat(multiTargetInfo.resolvedComponentName).isEqualTo(componentB)
val refined = multiTargetInfo.tryToCloneWithAppliedRefinement(intent)
assertThat(refined).isInstanceOf(MultiDisplayResolveInfo::class.java)
@@ -321,37 +359,40 @@ class TargetInfoTest {
val sendImage = Intent("SEND").apply { type = "image/png" }
val sendUri = Intent("SEND").apply { type = "text/uri" }
- val resolveInfo = ResolverDataProvider.createResolveInfo(1, 0)
-
- val imageOnlyTarget = DisplayResolveInfo.newDisplayResolveInfo(
- sendImage,
- resolveInfo,
- "Send Image",
- "Sends only images",
- sendImage
- )
-
- val textOnlyTarget = DisplayResolveInfo.newDisplayResolveInfo(
- sendUri,
- resolveInfo,
- "Send Text",
- "Sends only text",
- sendUri
- )
-
- val imageOrTextTarget = DisplayResolveInfo.newDisplayResolveInfo(
- sendImage,
- resolveInfo,
- "Send Image or Text",
- "Sends images or text",
- sendImage
- ).apply {
- addAlternateSourceIntent(sendUri)
- }
-
- val multiTargetInfo = MultiDisplayResolveInfo.newMultiDisplayResolveInfo(
- listOf(imageOnlyTarget, textOnlyTarget, imageOrTextTarget)
- )
+ val resolveInfo = createResolveInfo(1, 0)
+
+ val imageOnlyTarget =
+ DisplayResolveInfo.newDisplayResolveInfo(
+ sendImage,
+ resolveInfo,
+ "Send Image",
+ "Sends only images",
+ sendImage
+ )
+
+ val textOnlyTarget =
+ DisplayResolveInfo.newDisplayResolveInfo(
+ sendUri,
+ resolveInfo,
+ "Send Text",
+ "Sends only text",
+ sendUri
+ )
+
+ val imageOrTextTarget =
+ DisplayResolveInfo.newDisplayResolveInfo(
+ sendImage,
+ resolveInfo,
+ "Send Image or Text",
+ "Sends images or text",
+ sendImage
+ )
+ .apply { addAlternateSourceIntent(sendUri) }
+
+ val multiTargetInfo =
+ MultiDisplayResolveInfo.newMultiDisplayResolveInfo(
+ listOf(imageOnlyTarget, textOnlyTarget, imageOrTextTarget)
+ )
multiTargetInfo.setSelected(0)
assertThat(multiTargetInfo.selectedTarget).isEqualTo(imageOnlyTarget)
@@ -370,22 +411,23 @@ class TargetInfoTest {
fun testNewMultiDisplayResolveInfo_tryToCloneWithAppliedRefinement_delegatedToSelectedTarget() {
val refined = Intent("SEND")
val sendImage = Intent("SEND")
- val targetOne = spy(
- DisplayResolveInfo.newDisplayResolveInfo(
- sendImage,
- ResolverDataProvider.createResolveInfo(1, 0),
- "Target One",
- "Target One",
- sendImage
+ val targetOne =
+ spy(
+ DisplayResolveInfo.newDisplayResolveInfo(
+ sendImage,
+ createResolveInfo(1, 0),
+ "Target One",
+ "Target One",
+ sendImage
+ )
)
- )
- val targetTwo = mock<DisplayResolveInfo> {
- whenever(tryToCloneWithAppliedRefinement(any())).thenReturn(this)
- }
-
- val multiTargetInfo = MultiDisplayResolveInfo.newMultiDisplayResolveInfo(
- listOf(targetOne, targetTwo)
- )
+ val targetTwo =
+ mock<DisplayResolveInfo> {
+ whenever(tryToCloneWithAppliedRefinement(any())).thenReturn(this)
+ }
+
+ val multiTargetInfo =
+ MultiDisplayResolveInfo.newMultiDisplayResolveInfo(listOf(targetOne, targetTwo))
multiTargetInfo.setSelected(1)
assertThat(multiTargetInfo.selectedTarget).isEqualTo(targetTwo)
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt
index 083ef180..e4489bd1 100644
--- a/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt
@@ -18,8 +18,11 @@ package com.android.intentresolver.contentpreview
import android.content.Intent
import android.net.Uri
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
+import com.android.intentresolver.ContentTypeHint
+import com.android.intentresolver.FakeImageLoader
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
@@ -30,6 +33,7 @@ import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.never
import org.mockito.Mockito.times
@@ -39,30 +43,46 @@ class ChooserContentPreviewUiTest {
private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher())
private val previewData = mock<PreviewDataProvider>()
private val headlineGenerator = mock<HeadlineGenerator>()
- private val imageLoader = TestPreviewImageLoader(emptyMap())
+ private val imageLoader = FakeImageLoader(emptyMap())
+ private val testMetadataText: CharSequence = "Test metadata text"
private val actionFactory =
object : ActionFactory {
override fun getCopyButtonRunnable(): Runnable? = null
+
override fun getEditButtonRunnable(): Runnable? = null
+
override fun createCustomActions(): List<ActionRow.Action> = emptyList()
+
override fun getModifyShareAction(): ActionRow.Action? = null
+
override fun getExcludeSharedTextAction(): Consumer<Boolean> = Consumer<Boolean> {}
}
private val transitionCallback = mock<ImagePreviewView.TransitionElementStatusCallback>()
+ @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+ private fun createContentPreviewUi(
+ targetIntent: Intent,
+ isPayloadTogglingEnabled: Boolean = false
+ ) =
+ ChooserContentPreviewUi(
+ testScope,
+ previewData,
+ targetIntent,
+ imageLoader,
+ actionFactory,
+ { null },
+ transitionCallback,
+ headlineGenerator,
+ ContentTypeHint.NONE,
+ testMetadataText,
+ isPayloadTogglingEnabled,
+ )
@Test
fun test_textPreviewType_useTextPreviewUi() {
whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_TEXT)
- val testSubject =
- ChooserContentPreviewUi(
- testScope,
- previewData,
- Intent(Intent.ACTION_VIEW),
- imageLoader,
- actionFactory,
- transitionCallback,
- headlineGenerator,
- )
+ val testSubject = createContentPreviewUi(targetIntent = Intent(Intent.ACTION_VIEW))
+
assertThat(testSubject.preferredContentPreview)
.isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT)
assertThat(testSubject.mContentPreviewUi).isInstanceOf(TextContentPreviewUi::class.java)
@@ -72,16 +92,7 @@ class ChooserContentPreviewUiTest {
@Test
fun test_filePreviewType_useFilePreviewUi() {
whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_FILE)
- val testSubject =
- ChooserContentPreviewUi(
- testScope,
- previewData,
- Intent(Intent.ACTION_SEND),
- imageLoader,
- actionFactory,
- transitionCallback,
- headlineGenerator,
- )
+ val testSubject = createContentPreviewUi(targetIntent = Intent(Intent.ACTION_SEND))
assertThat(testSubject.preferredContentPreview)
.isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
assertThat(testSubject.mContentPreviewUi).isInstanceOf(FileContentPreviewUi::class.java)
@@ -97,14 +108,9 @@ class ChooserContentPreviewUiTest {
.thenReturn(FileInfo.Builder(uri).withPreviewUri(uri).withMimeType("image/png").build())
whenever(previewData.imagePreviewFileInfoFlow).thenReturn(MutableSharedFlow())
val testSubject =
- ChooserContentPreviewUi(
- testScope,
- previewData,
- Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_TEXT, "Shared text") },
- imageLoader,
- actionFactory,
- transitionCallback,
- headlineGenerator,
+ createContentPreviewUi(
+ targetIntent =
+ Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_TEXT, "Shared text") }
)
assertThat(testSubject.mContentPreviewUi)
.isInstanceOf(FilesPlusTextContentPreviewUi::class.java)
@@ -120,20 +126,30 @@ class ChooserContentPreviewUiTest {
whenever(previewData.firstFileInfo)
.thenReturn(FileInfo.Builder(uri).withPreviewUri(uri).withMimeType("image/png").build())
whenever(previewData.imagePreviewFileInfoFlow).thenReturn(MutableSharedFlow())
- val testSubject =
- ChooserContentPreviewUi(
- testScope,
- previewData,
- Intent(Intent.ACTION_SEND),
- imageLoader,
- actionFactory,
- transitionCallback,
- headlineGenerator,
- )
+ val testSubject = createContentPreviewUi(targetIntent = Intent(Intent.ACTION_SEND))
assertThat(testSubject.preferredContentPreview)
.isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
assertThat(testSubject.mContentPreviewUi).isInstanceOf(UnifiedContentPreviewUi::class.java)
verify(previewData, times(1)).imagePreviewFileInfoFlow
verify(transitionCallback, never()).onAllTransitionElementsReady()
}
+
+ @Test
+ fun test_imagePayloadSelectionTypeWithEnabledFlag_usePayloadSelectionPreviewUi() {
+ // Event if we returned wrong type due to a bug, we should not use payload selection UI
+ val uri = Uri.parse("content://org.pkg.app/img.png")
+ whenever(previewData.previewType)
+ .thenReturn(ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION)
+ whenever(previewData.uriCount).thenReturn(2)
+ whenever(previewData.firstFileInfo)
+ .thenReturn(FileInfo.Builder(uri).withPreviewUri(uri).withMimeType("image/png").build())
+ whenever(previewData.imagePreviewFileInfoFlow).thenReturn(MutableSharedFlow())
+ val testSubject =
+ createContentPreviewUi(
+ targetIntent = Intent(Intent.ACTION_SEND),
+ isPayloadTogglingEnabled = true
+ )
+ assertThat(testSubject.mContentPreviewUi)
+ .isInstanceOf(ShareouselContentPreviewUi::class.java)
+ }
}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/FileContentPreviewUiTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/FileContentPreviewUiTest.kt
index d2d952ae..0e4e36ab 100644
--- a/tests/unit/src/com/android/intentresolver/contentpreview/FileContentPreviewUiTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/FileContentPreviewUiTest.kt
@@ -35,6 +35,7 @@ import org.junit.runner.RunWith
class FileContentPreviewUiTest {
private val fileCount = 2
private val text = "Sharing 2 files"
+ private val testMetadataText: CharSequence = "Test metadata text"
private val actionFactory =
object : ChooserContentPreviewUi.ActionFactory {
override fun getEditButtonRunnable(): Runnable? = null
@@ -54,46 +55,33 @@ class FileContentPreviewUiTest {
fileCount,
actionFactory,
headlineGenerator,
+ testMetadataText,
)
@Test
- fun test_display_titleIsDisplayed() {
+ fun test_display_titleAndMetadataIsDisplayed() {
val layoutInflater = LayoutInflater.from(context)
- val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup
+ val gridLayout =
+ layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false)
+ as ViewGroup
+ val headlineRow = gridLayout.requireViewById<View>(R.id.chooser_headline_row_container)
+
+ assertThat(headlineRow.findViewById<View>(R.id.headline)).isNull()
+ assertThat(headlineRow.findViewById<View>(R.id.metadata)).isNull()
val previewView =
testSubject.display(
context.resources,
layoutInflater,
gridLayout,
- /*headlineViewParent=*/ null
+ headlineRow,
)
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)
+ val headlineView = headlineRow.findViewById<TextView>(R.id.headline)
assertThat(headlineView).isNotNull()
assertThat(headlineView?.text).isEqualTo(text)
+ val metadataView = headlineRow.findViewById<TextView>(R.id.metadata)
+ assertThat(metadataView?.text).isEqualTo(testMetadataText)
}
}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt
index 7cc0b4b2..da0ddd12 100644
--- a/tests/unit/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt
@@ -21,6 +21,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
+import androidx.annotation.IdRes
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
import com.android.intentresolver.R
@@ -51,9 +52,13 @@ class FilesPlusTextContentPreviewUiTest {
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>()
@@ -63,189 +68,112 @@ class FilesPlusTextContentPreviewUiTest {
whenever(getVideosHeadline(anyInt())).thenReturn(HEADLINE_VIDEOS)
whenever(getFilesHeadline(anyInt())).thenReturn(HEADLINE_FILES)
}
+ private val testMetadataText: CharSequence = "Test metadata text"
private val context
get() = getInstrumentation().context
@Test
- fun test_displayImagesPlusTextWithoutUriMetadata_showImagesHeadline() {
+ fun test_displayImagesPlusTextWithoutUriMetadataHeader_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)
+ val (previewView, headlineRow) = testLoadingHeadline("image/*", sharedFileCount)
+ assertWithMessage("Preview parent should not be null").that(previewView).isNotNull()
verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
- verifyInternalHeadlineAbsence(previewView)
- verifyPreviewHeadline(headerParent, HEADLINE_IMAGES)
+ verifyPreviewHeadline(headlineRow, HEADLINE_IMAGES)
+ verifyPreviewMetadata(headlineRow, testMetadataText)
verifySharedText(previewView)
}
@Test
- fun test_displayVideosPlusTextWithoutUriMetadata_showVideosHeadline() {
+ fun test_displayVideosPlusTextWithoutUriMetadataHeader_showVideosHeadline() {
val sharedFileCount = 2
- val previewView = testLoadingHeadline("video/*", sharedFileCount)
+ val (previewView, headlineRow) = 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)
+ assertWithMessage("Preview parent should not be null").that(previewView).isNotNull()
+ verifyPreviewHeadline(headlineRow, HEADLINE_VIDEOS)
+ verifyPreviewMetadata(headlineRow, testMetadataText)
verifySharedText(previewView)
}
@Test
- fun test_displayMixedContentPlusTextWithoutUriMetadata_showFilesHeadline() {
+ fun test_displayDocsPlusTextWithoutUriMetadataHeader_showFilesHeadline() {
val sharedFileCount = 2
- val previewView = testLoadingHeadline("*/*", sharedFileCount)
+ val (previewView, headlineRow) = testLoadingHeadline("application/pdf", sharedFileCount)
verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
- verifyPreviewHeadline(previewView, HEADLINE_FILES)
+ assertWithMessage("Preview parent should not be null").that(previewView).isNotNull()
+ verifyPreviewHeadline(headlineRow, HEADLINE_FILES)
+ verifyPreviewMetadata(headlineRow, testMetadataText)
verifySharedText(previewView)
}
@Test
- fun test_displayMixedContentPlusTextWithoutUriMetadataExternalHeader_showFilesHeadline() {
+ fun test_displayMixedContentPlusTextWithoutUriMetadataHeader_showFilesHeadline() {
val sharedFileCount = 2
- val (previewView, headerParent) = testLoadingExternalHeadline("*/*", sharedFileCount)
+ val (previewView, headlineRow) = testLoadingHeadline("*/*", 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)
+ assertWithMessage("Preview parent should not be null").that(previewView).isNotNull()
+ verifyPreviewHeadline(headlineRow, HEADLINE_FILES)
+ verifyPreviewMetadata(headlineRow, testMetadataText)
verifySharedText(previewView)
}
@Test
- fun test_displayImagesPlusTextWithUriMetadataSetExternalHeader_showImagesHeadline() {
+ fun test_displayImagesPlusTextWithUriMetadataSetHeader_showImagesHeadline() {
val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "image/jpeg")
val sharedFileCount = loadedFileMetadata.size
- val (previewView, headerParent) =
- testLoadingExternalHeadline("image/*", sharedFileCount, loadedFileMetadata)
+ val (previewView, headlineRow) =
+ testLoadingHeadline("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)
+ assertWithMessage("Preview parent should not be null").that(previewView).isNotNull()
+ verifyPreviewHeadline(headlineRow, HEADLINE_IMAGES)
+ verifyPreviewMetadata(headlineRow, testMetadataText)
verifySharedText(previewView)
}
@Test
- fun test_displayVideosPlusTextWithUriMetadataSetExternalHeader_showVideosHeadline() {
+ fun test_displayVideosPlusTextWithUriMetadataSetHeader_showVideosHeadline() {
val loadedFileMetadata = createFileInfosWithMimeTypes("video/mp4", "video/mp4")
val sharedFileCount = loadedFileMetadata.size
- val (previewView, headerParent) =
- testLoadingExternalHeadline("video/*", sharedFileCount, loadedFileMetadata)
+ val (previewView, headlineRow) =
+ testLoadingHeadline("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)
+ assertWithMessage("Preview parent should not be null").that(previewView).isNotNull()
+ verifyPreviewHeadline(headlineRow, HEADLINE_VIDEOS)
+ verifyPreviewMetadata(headlineRow, testMetadataText)
verifySharedText(previewView)
}
@Test
- fun test_displayImagesAndVideosPlusTextWithUriMetadataSetExternalHeader_showFilesHeadline() {
+ fun test_displayImagesAndVideosPlusTextWithUriMetadataSetHeader_showFilesHeadline() {
val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "video/mp4")
val sharedFileCount = loadedFileMetadata.size
- val (previewView, headerParent) =
- testLoadingExternalHeadline("*/*", sharedFileCount, loadedFileMetadata)
+ val (previewView, headlineRow) =
+ testLoadingHeadline("*/*", sharedFileCount, loadedFileMetadata)
verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
- verifyInternalHeadlineAbsence(previewView)
- verifyPreviewHeadline(headerParent, HEADLINE_FILES)
+ assertWithMessage("Preview parent should not be null").that(previewView).isNotNull()
+ verifyPreviewHeadline(headlineRow, HEADLINE_FILES)
+ verifyPreviewMetadata(headlineRow, testMetadataText)
verifySharedText(previewView)
}
@Test
- fun test_displayDocsPlusTextWithUriMetadataSet_showFilesHeadline() {
+ fun test_displayDocsPlusTextWithUriMetadataSetHeader_showFilesHeadline() {
val loadedFileMetadata = createFileInfosWithMimeTypes("application/pdf", "application/pdf")
val sharedFileCount = loadedFileMetadata.size
- val previewView =
+ val (previewView, headlineRow) =
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)
+ assertWithMessage("Preview parent should not be null").that(previewView).isNotNull()
+ verifyPreviewHeadline(headlineRow, HEADLINE_FILES)
+ verifyPreviewMetadata(headlineRow, testMetadataText)
verifySharedText(previewView)
}
@@ -262,27 +190,37 @@ class FilesPlusTextContentPreviewUiTest {
actionFactory,
imageLoader,
DefaultMimeTypeClassifier,
- headlineGenerator
+ headlineGenerator,
+ testMetadataText,
)
val layoutInflater = LayoutInflater.from(context)
- val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup
+ val gridLayout =
+ layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false)
+ as ViewGroup
+ val headlineRow = gridLayout.requireViewById<View>(R.id.chooser_headline_row_container)
- val previewView =
- testSubject.display(context.resources, LayoutInflater.from(context), gridLayout, null)
+ testSubject.display(
+ context.resources,
+ LayoutInflater.from(context),
+ gridLayout,
+ headlineRow
+ )
verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
verify(headlineGenerator, never()).getImagesHeadline(sharedFileCount)
- verifyPreviewHeadline(previewView, HEADLINE_FILES)
+ verifyPreviewHeadline(headlineRow, HEADLINE_FILES)
+ verifyPreviewMetadata(headlineRow, testMetadataText)
testSubject.updatePreviewMetadata(createFileInfosWithMimeTypes("image/png", "image/jpg"))
verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
- verifyPreviewHeadline(previewView, HEADLINE_IMAGES)
+ verifyPreviewHeadline(headlineRow, HEADLINE_IMAGES)
+ verifyPreviewMetadata(headlineRow, testMetadataText)
}
@Test
- fun test_uriMetadataIsMoreSpecificThanIntentMimeTypeExternalHeader_headlineGetsUpdated() {
+ fun test_uriMetadataIsMoreSpecificThanIntentMimeTypeHeader_headlineGetsUpdated() {
val sharedFileCount = 2
val testSubject =
FilesPlusTextContentPreviewUi(
@@ -294,17 +232,20 @@ class FilesPlusTextContentPreviewUiTest {
actionFactory,
imageLoader,
DefaultMimeTypeClassifier,
- headlineGenerator
+ headlineGenerator,
+ testMetadataText,
)
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)
+ val headlineRow = 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))
+ assertWithMessage("Headline should not be inflated by default")
+ .that(headlineRow.findViewById<View>(R.id.headline))
+ .isNull()
+ assertWithMessage("Metadata should not be inflated by default")
+ .that(headlineRow.findViewById<View>(R.id.metadata))
.isNull()
val previewView =
@@ -312,54 +253,27 @@ class FilesPlusTextContentPreviewUiTest {
context.resources,
LayoutInflater.from(context),
gridLayout,
- externalHeaderView
+ headlineRow
)
verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
verify(headlineGenerator, never()).getImagesHeadline(sharedFileCount)
- verifyInternalHeadlineAbsence(previewView)
- verifyPreviewHeadline(externalHeaderView, HEADLINE_FILES)
+ assertWithMessage("Preview parent should not be null").that(previewView).isNotNull()
+ verifyPreviewHeadline(headlineRow, HEADLINE_FILES)
+ verifyPreviewMetadata(headlineRow, testMetadataText)
testSubject.updatePreviewMetadata(createFileInfosWithMimeTypes("image/png", "image/jpg"))
verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
- verifyPreviewHeadline(externalHeaderView, HEADLINE_IMAGES)
+ verifyPreviewHeadline(headlineRow, HEADLINE_IMAGES)
+ verifyPreviewMetadata(headlineRow, testMetadataText)
}
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(
@@ -371,17 +285,21 @@ class FilesPlusTextContentPreviewUiTest {
actionFactory,
imageLoader,
DefaultMimeTypeClassifier,
- headlineGenerator
+ headlineGenerator,
+ testMetadataText,
)
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)
+ val headlineRow = gridLayout.requireViewById<View>(R.id.chooser_headline_row_container)
+
+ assertWithMessage("Headline should not be inflated by default")
+ .that(headlineRow.findViewById<View>(R.id.headline))
+ .isNull()
- assertWithMessage("External headline should not be inflated by default")
- .that(externalHeaderView.findViewById<View>(R.id.headline))
+ assertWithMessage("Metadata should not be inflated by default")
+ .that(headlineRow.findViewById<View>(R.id.metadata))
.isNull()
loadedFileMetadata?.let(testSubject::updatePreviewMetadata)
@@ -389,8 +307,8 @@ class FilesPlusTextContentPreviewUiTest {
context.resources,
LayoutInflater.from(context),
gridLayout,
- externalHeaderView
- ) to externalHeaderView
+ headlineRow
+ ) to headlineRow
}
private fun createFileInfosWithMimeTypes(vararg mimeTypes: String): List<FileInfo> {
@@ -398,26 +316,26 @@ class FilesPlusTextContentPreviewUiTest {
return mimeTypes.map { mimeType -> FileInfo.Builder(uri).withMimeType(mimeType).build() }
}
+ private fun verifyTextViewText(
+ parentView: View?,
+ @IdRes textViewResId: Int,
+ expectedText: CharSequence,
+ ) {
+ assertThat(parentView).isNotNull()
+ val textView = parentView?.findViewById<TextView>(textViewResId)
+ assertThat(textView).isNotNull()
+ assertThat(textView?.text).isEqualTo(expectedText)
+ }
+
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)
+ verifyTextViewText(headerViewParent, R.id.headline, 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 verifyPreviewMetadata(headerViewParent: View?, expectedText: CharSequence) {
+ verifyTextViewText(headerViewParent, R.id.metadata, 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()
+ private fun verifySharedText(previewView: ViewGroup?) {
+ verifyTextViewText(previewView, R.id.content_preview_text, SHARED_TEXT)
}
}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt
index a65280e5..dbc37b44 100644
--- a/tests/unit/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt
@@ -18,44 +18,73 @@ package com.android.intentresolver.contentpreview
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
import org.junit.Test
import org.junit.runner.RunWith
-import com.google.common.truth.Truth.assertThat
@RunWith(AndroidJUnit4::class)
class HeadlineGeneratorImplTest {
- @Test
- fun testHeadlineGeneration() {
- val generator = HeadlineGeneratorImpl(
- InstrumentationRegistry.getInstrumentation().getTargetContext())
- val str = "Some string"
- val url = "http://www.google.com"
+ private val generator =
+ HeadlineGeneratorImpl(InstrumentationRegistry.getInstrumentation().targetContext)
+ private val str = "Some string"
+ private val url = "http://www.google.com"
+ @Test
+ fun testTextHeadline() {
assertThat(generator.getTextHeadline(str)).isEqualTo("Sharing text")
assertThat(generator.getTextHeadline(url)).isEqualTo("Sharing link")
+ }
+ @Test
+ fun testImagesWIthTextHeadline() {
assertThat(generator.getImagesWithTextHeadline(str, 1)).isEqualTo("Sharing image with text")
assertThat(generator.getImagesWithTextHeadline(url, 1)).isEqualTo("Sharing image with link")
- assertThat(generator.getImagesWithTextHeadline(str, 5)).isEqualTo("Sharing 5 images with text")
- assertThat(generator.getImagesWithTextHeadline(url, 5)).isEqualTo("Sharing 5 images with link")
+ assertThat(generator.getImagesWithTextHeadline(str, 5))
+ .isEqualTo("Sharing 5 images with text")
+ assertThat(generator.getImagesWithTextHeadline(url, 5))
+ .isEqualTo("Sharing 5 images with link")
+ }
+ @Test
+ fun testVideosWithTextHeadline() {
assertThat(generator.getVideosWithTextHeadline(str, 1)).isEqualTo("Sharing video with text")
assertThat(generator.getVideosWithTextHeadline(url, 1)).isEqualTo("Sharing video with link")
- assertThat(generator.getVideosWithTextHeadline(str, 5)).isEqualTo("Sharing 5 videos with text")
- assertThat(generator.getVideosWithTextHeadline(url, 5)).isEqualTo("Sharing 5 videos with link")
+ assertThat(generator.getVideosWithTextHeadline(str, 5))
+ .isEqualTo("Sharing 5 videos with text")
+ assertThat(generator.getVideosWithTextHeadline(url, 5))
+ .isEqualTo("Sharing 5 videos with link")
+ }
+ @Test
+ fun testFilesWithTextHeadline() {
assertThat(generator.getFilesWithTextHeadline(str, 1)).isEqualTo("Sharing file with text")
assertThat(generator.getFilesWithTextHeadline(url, 1)).isEqualTo("Sharing file with link")
- assertThat(generator.getFilesWithTextHeadline(str, 5)).isEqualTo("Sharing 5 files with text")
- assertThat(generator.getFilesWithTextHeadline(url, 5)).isEqualTo("Sharing 5 files with link")
+ assertThat(generator.getFilesWithTextHeadline(str, 5))
+ .isEqualTo("Sharing 5 files with text")
+ assertThat(generator.getFilesWithTextHeadline(url, 5))
+ .isEqualTo("Sharing 5 files with link")
+ }
+ @Test
+ fun testImagesHeadline() {
assertThat(generator.getImagesHeadline(1)).isEqualTo("Sharing image")
assertThat(generator.getImagesHeadline(4)).isEqualTo("Sharing 4 images")
+ }
+ @Test
+ fun testVideosHeadline() {
assertThat(generator.getVideosHeadline(1)).isEqualTo("Sharing video")
assertThat(generator.getVideosHeadline(4)).isEqualTo("Sharing 4 videos")
+ }
+ @Test
+ fun testFilesHeadline() {
assertThat(generator.getFilesHeadline(1)).isEqualTo("Sharing 1 file")
assertThat(generator.getFilesHeadline(4)).isEqualTo("Sharing 4 files")
}
+
+ @Test
+ fun testAlbumHeadline() {
+ assertThat(generator.getAlbumHeadline()).isEqualTo("Sharing album")
+ }
}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt
index 89978707..41989bda 100644
--- a/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt
@@ -20,9 +20,6 @@ import android.content.ContentResolver
import android.graphics.Bitmap
import android.net.Uri
import android.util.Size
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.coroutineScope
-import androidx.lifecycle.testing.TestLifecycleOwner
import com.android.intentresolver.any
import com.android.intentresolver.anyOrNull
import com.android.intentresolver.mock
@@ -38,25 +35,22 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart.UNDISPATCHED
-import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Runnable
import kotlinx.coroutines.async
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
-import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.resetMain
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
import org.mockito.Mockito.times
@@ -72,281 +66,287 @@ class ImagePreviewImageLoaderTest {
mock<ContentResolver> {
whenever(loadThumbnail(any(), any(), anyOrNull())).thenReturn(bitmap)
}
- private val lifecycleOwner = TestLifecycleOwner()
- private val dispatcher = UnconfinedTestDispatcher()
- private lateinit var testSubject: ImagePreviewImageLoader
-
- @Before
- fun setup() {
- Dispatchers.setMain(dispatcher)
- lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
- // create test subject after we've updated the lifecycle dispatcher
- testSubject =
- ImagePreviewImageLoader(
- lifecycleOwner.lifecycle.coroutineScope + dispatcher,
- imageSize.width,
- contentResolver,
- cacheSize = 1,
- )
- }
-
- @After
- fun cleanup() {
- lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
- Dispatchers.resetMain()
- }
+ private val scheduler = TestCoroutineScheduler()
+ private val dispatcher = UnconfinedTestDispatcher(scheduler)
+ private val scope = TestScope(dispatcher)
+ private val testSubject =
+ ImagePreviewImageLoader(
+ dispatcher,
+ imageSize.width,
+ contentResolver,
+ cacheSize = 1,
+ )
@Test
- fun prePopulate_cachesImagesUpToTheCacheSize() = runTest {
- testSubject.prePopulate(listOf(uriOne, uriTwo))
+ fun prePopulate_cachesImagesUpToTheCacheSize() =
+ scope.runTest {
+ testSubject.prePopulate(listOf(uriOne, uriTwo))
- verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null)
- verify(contentResolver, never()).loadThumbnail(uriTwo, imageSize, null)
+ verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null)
+ verify(contentResolver, never()).loadThumbnail(uriTwo, imageSize, null)
- testSubject(uriOne)
- verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null)
- }
+ testSubject(uriOne)
+ verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null)
+ }
@Test
- fun invoke_returnCachedImageWhenCalledTwice() = runTest {
- testSubject(uriOne)
- testSubject(uriOne)
+ fun invoke_returnCachedImageWhenCalledTwice() =
+ scope.runTest {
+ testSubject(uriOne)
+ testSubject(uriOne)
- verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull())
- }
+ verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull())
+ }
@Test
- fun invoke_whenInstructed_doesNotCache() = runTest {
- testSubject(uriOne, false)
- testSubject(uriOne, false)
+ fun invoke_whenInstructed_doesNotCache() =
+ scope.runTest {
+ testSubject(uriOne, false)
+ testSubject(uriOne, false)
- verify(contentResolver, times(2)).loadThumbnail(any(), any(), anyOrNull())
- }
+ verify(contentResolver, times(2)).loadThumbnail(any(), any(), anyOrNull())
+ }
@Test
- fun invoke_overlappedRequests_Deduplicate() = runTest {
- val scheduler = TestCoroutineScheduler()
- val dispatcher = StandardTestDispatcher(scheduler)
- val testSubject =
- ImagePreviewImageLoader(
- lifecycleOwner.lifecycle.coroutineScope + dispatcher,
- imageSize.width,
- contentResolver,
- cacheSize = 1,
- )
- coroutineScope {
- launch(start = UNDISPATCHED) { testSubject(uriOne, false) }
- launch(start = UNDISPATCHED) { testSubject(uriOne, false) }
- scheduler.advanceUntilIdle()
- }
+ fun invoke_overlappedRequests_Deduplicate() =
+ scope.runTest {
+ val dispatcher = StandardTestDispatcher(scheduler)
+ val testSubject =
+ ImagePreviewImageLoader(
+ dispatcher,
+ imageSize.width,
+ contentResolver,
+ cacheSize = 1,
+ )
+ coroutineScope {
+ launch(start = UNDISPATCHED) { testSubject(uriOne, false) }
+ launch(start = UNDISPATCHED) { testSubject(uriOne, false) }
+ scheduler.advanceUntilIdle()
+ }
- verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull())
- }
+ verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull())
+ }
@Test
- fun invoke_oldRecordsEvictedFromTheCache() = runTest {
- testSubject(uriOne)
- testSubject(uriTwo)
- testSubject(uriTwo)
- testSubject(uriOne)
-
- verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null)
- verify(contentResolver, times(1)).loadThumbnail(uriTwo, imageSize, null)
- }
+ fun invoke_oldRecordsEvictedFromTheCache() =
+ scope.runTest {
+ testSubject(uriOne)
+ testSubject(uriTwo)
+ testSubject(uriTwo)
+ testSubject(uriOne)
+
+ verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null)
+ verify(contentResolver, times(1)).loadThumbnail(uriTwo, imageSize, null)
+ }
@Test
- fun invoke_doNotCacheNulls() = runTest {
- whenever(contentResolver.loadThumbnail(any(), any(), anyOrNull())).thenReturn(null)
- testSubject(uriOne)
- testSubject(uriOne)
+ fun invoke_doNotCacheNulls() =
+ scope.runTest {
+ whenever(contentResolver.loadThumbnail(any(), any(), anyOrNull())).thenReturn(null)
+ testSubject(uriOne)
+ testSubject(uriOne)
- verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null)
- }
+ verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null)
+ }
@Test(expected = CancellationException::class)
- fun invoke_onClosedImageLoaderScope_throwsCancellationException() = runTest {
- lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
- testSubject(uriOne)
- }
+ fun invoke_onClosedImageLoaderScope_throwsCancellationException() =
+ scope.runTest {
+ val imageLoaderScope = CoroutineScope(coroutineContext)
+ val testSubject =
+ ImagePreviewImageLoader(
+ imageLoaderScope,
+ imageSize.width,
+ contentResolver,
+ cacheSize = 1,
+ )
+ imageLoaderScope.cancel()
+ testSubject(uriOne)
+ }
@Test(expected = CancellationException::class)
- fun invoke_imageLoaderScopeClosedMidflight_throwsCancellationException() = runTest {
- val scheduler = TestCoroutineScheduler()
- val dispatcher = StandardTestDispatcher(scheduler)
- val testSubject =
- ImagePreviewImageLoader(
- lifecycleOwner.lifecycle.coroutineScope + dispatcher,
- imageSize.width,
- contentResolver,
- cacheSize = 1,
- )
- coroutineScope {
- val deferred = async(start = UNDISPATCHED) { testSubject(uriOne, false) }
- lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
- scheduler.advanceUntilIdle()
- deferred.await()
+ fun invoke_imageLoaderScopeClosedMidflight_throwsCancellationException() =
+ scope.runTest {
+ val dispatcher = StandardTestDispatcher(scheduler)
+ val imageLoaderScope = CoroutineScope(coroutineContext + dispatcher)
+ val testSubject =
+ ImagePreviewImageLoader(
+ imageLoaderScope,
+ imageSize.width,
+ contentResolver,
+ cacheSize = 1,
+ )
+ coroutineScope {
+ val deferred = async(start = UNDISPATCHED) { testSubject(uriOne, false) }
+ imageLoaderScope.cancel()
+ scheduler.advanceUntilIdle()
+ deferred.await()
+ }
}
- }
@Test
- fun invoke_multipleCallsWithDifferentCacheInstructions_cachingPrevails() = runTest {
- val scheduler = TestCoroutineScheduler()
- val dispatcher = StandardTestDispatcher(scheduler)
- val testSubject =
- ImagePreviewImageLoader(
- lifecycleOwner.lifecycle.coroutineScope + dispatcher,
- imageSize.width,
- contentResolver,
- cacheSize = 1,
- )
- coroutineScope {
- launch(start = UNDISPATCHED) { testSubject(uriOne, false) }
- launch(start = UNDISPATCHED) { testSubject(uriOne, true) }
- scheduler.advanceUntilIdle()
- }
- testSubject(uriOne, true)
+ fun invoke_multipleCallsWithDifferentCacheInstructions_cachingPrevails() =
+ scope.runTest {
+ val dispatcher = StandardTestDispatcher(scheduler)
+ val imageLoaderScope = CoroutineScope(coroutineContext + dispatcher)
+ val testSubject =
+ ImagePreviewImageLoader(
+ imageLoaderScope,
+ imageSize.width,
+ contentResolver,
+ cacheSize = 1,
+ )
+ coroutineScope {
+ launch(start = UNDISPATCHED) { testSubject(uriOne, false) }
+ launch(start = UNDISPATCHED) { testSubject(uriOne, true) }
+ scheduler.advanceUntilIdle()
+ }
+ testSubject(uriOne, true)
- verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null)
- }
+ verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null)
+ }
@Test
- fun invoke_semaphoreGuardsContentResolverCalls() = runTest {
- val contentResolver =
- mock<ContentResolver> {
- whenever(loadThumbnail(any(), any(), anyOrNull()))
- .thenThrow(SecurityException("test"))
- }
- val acquireCount = AtomicInteger()
- val releaseCount = AtomicInteger()
- val testSemaphore =
- object : Semaphore {
- override val availablePermits: Int
- get() = error("Unexpected invocation")
-
- override suspend fun acquire() {
- acquireCount.getAndIncrement()
+ fun invoke_semaphoreGuardsContentResolverCalls() =
+ scope.runTest {
+ val contentResolver =
+ mock<ContentResolver> {
+ whenever(loadThumbnail(any(), any(), anyOrNull()))
+ .thenThrow(SecurityException("test"))
}
-
- override fun tryAcquire(): Boolean {
- error("Unexpected invocation")
+ val acquireCount = AtomicInteger()
+ val releaseCount = AtomicInteger()
+ val testSemaphore =
+ object : Semaphore {
+ override val availablePermits: Int
+ get() = error("Unexpected invocation")
+
+ override suspend fun acquire() {
+ acquireCount.getAndIncrement()
+ }
+
+ override fun tryAcquire(): Boolean {
+ error("Unexpected invocation")
+ }
+
+ override fun release() {
+ releaseCount.getAndIncrement()
+ }
}
- override fun release() {
- releaseCount.getAndIncrement()
- }
- }
-
- val testSubject =
- ImagePreviewImageLoader(
- lifecycleOwner.lifecycle.coroutineScope + dispatcher,
- imageSize.width,
- contentResolver,
- cacheSize = 1,
- testSemaphore,
- )
- testSubject(uriOne, false)
-
- verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null)
- assertThat(acquireCount.get()).isEqualTo(1)
- assertThat(releaseCount.get()).isEqualTo(1)
- }
+ val testSubject =
+ ImagePreviewImageLoader(
+ CoroutineScope(coroutineContext + dispatcher),
+ imageSize.width,
+ contentResolver,
+ cacheSize = 1,
+ testSemaphore,
+ )
+ testSubject(uriOne, false)
+
+ verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null)
+ assertThat(acquireCount.get()).isEqualTo(1)
+ assertThat(releaseCount.get()).isEqualTo(1)
+ }
@Test
- fun invoke_semaphoreIsReleasedAfterContentResolverFailure() = runTest {
- val semaphoreDeferred = CompletableDeferred<Unit>()
- val releaseCount = AtomicInteger()
- val testSemaphore =
- object : Semaphore {
- override val availablePermits: Int
- get() = error("Unexpected invocation")
-
- override suspend fun acquire() {
- semaphoreDeferred.await()
- }
-
- override fun tryAcquire(): Boolean {
- error("Unexpected invocation")
+ fun invoke_semaphoreIsReleasedAfterContentResolverFailure() =
+ scope.runTest {
+ val semaphoreDeferred = CompletableDeferred<Unit>()
+ val releaseCount = AtomicInteger()
+ val testSemaphore =
+ object : Semaphore {
+ override val availablePermits: Int
+ get() = error("Unexpected invocation")
+
+ override suspend fun acquire() {
+ semaphoreDeferred.await()
+ }
+
+ override fun tryAcquire(): Boolean {
+ error("Unexpected invocation")
+ }
+
+ override fun release() {
+ releaseCount.getAndIncrement()
+ }
}
- override fun release() {
- releaseCount.getAndIncrement()
- }
- }
-
- val testSubject =
- ImagePreviewImageLoader(
- lifecycleOwner.lifecycle.coroutineScope + dispatcher,
- imageSize.width,
- contentResolver,
- cacheSize = 1,
- testSemaphore,
- )
- launch(start = UNDISPATCHED) { testSubject(uriOne, false) }
+ val testSubject =
+ ImagePreviewImageLoader(
+ CoroutineScope(coroutineContext + dispatcher),
+ imageSize.width,
+ contentResolver,
+ cacheSize = 1,
+ testSemaphore,
+ )
+ launch(start = UNDISPATCHED) { testSubject(uriOne, false) }
- verify(contentResolver, never()).loadThumbnail(any(), any(), anyOrNull())
+ verify(contentResolver, never()).loadThumbnail(any(), any(), anyOrNull())
- semaphoreDeferred.complete(Unit)
+ semaphoreDeferred.complete(Unit)
- verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null)
- assertThat(releaseCount.get()).isEqualTo(1)
- }
+ verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null)
+ assertThat(releaseCount.get()).isEqualTo(1)
+ }
@Test
- fun invoke_multipleSimultaneousCalls_limitOnNumberOfSimultaneousOutgoingCallsIsRespected() {
- val requestCount = 4
- val thumbnailCallsCdl = CountDownLatch(requestCount)
- val pendingThumbnailCalls = ArrayDeque<CountDownLatch>()
- val contentResolver =
- mock<ContentResolver> {
- whenever(loadThumbnail(any(), any(), anyOrNull())).thenAnswer {
- val latch = CountDownLatch(1)
- synchronized(pendingThumbnailCalls) { pendingThumbnailCalls.offer(latch) }
- thumbnailCallsCdl.countDown()
- assertTrue("Timeout waiting thumbnail calls", latch.await(1, SECONDS))
- bitmap
+ fun invoke_multipleSimultaneousCalls_limitOnNumberOfSimultaneousOutgoingCallsIsRespected() =
+ scope.runTest {
+ val requestCount = 4
+ val thumbnailCallsCdl = CountDownLatch(requestCount)
+ val pendingThumbnailCalls = ArrayDeque<CountDownLatch>()
+ val contentResolver =
+ mock<ContentResolver> {
+ whenever(loadThumbnail(any(), any(), anyOrNull())).thenAnswer {
+ val latch = CountDownLatch(1)
+ synchronized(pendingThumbnailCalls) { pendingThumbnailCalls.offer(latch) }
+ thumbnailCallsCdl.countDown()
+ assertTrue("Timeout waiting thumbnail calls", latch.await(1, SECONDS))
+ bitmap
+ }
}
- }
- val name = "LoadImage"
- val maxSimultaneousRequests = 2
- val threadsStartedCdl = CountDownLatch(requestCount)
- val dispatcher = NewThreadDispatcher(name) { threadsStartedCdl.countDown() }
- val testSubject =
- ImagePreviewImageLoader(
- lifecycleOwner.lifecycle.coroutineScope + dispatcher + CoroutineName(name),
- imageSize.width,
- contentResolver,
- cacheSize = 1,
- maxSimultaneousRequests,
- )
- runTest {
- repeat(requestCount) {
- launch { testSubject(Uri.parse("content://org.pkg.app/image-$it.png")) }
- }
- yield()
- // wait for all requests to be dispatched
- assertThat(threadsStartedCdl.await(5, SECONDS)).isTrue()
+ val name = "LoadImage"
+ val maxSimultaneousRequests = 2
+ val threadsStartedCdl = CountDownLatch(requestCount)
+ val dispatcher = NewThreadDispatcher(name) { threadsStartedCdl.countDown() }
+ val testSubject =
+ ImagePreviewImageLoader(
+ CoroutineScope(coroutineContext + dispatcher + CoroutineName(name)),
+ imageSize.width,
+ contentResolver,
+ cacheSize = 1,
+ maxSimultaneousRequests,
+ )
+ coroutineScope {
+ repeat(requestCount) {
+ launch { testSubject(Uri.parse("content://org.pkg.app/image-$it.png")) }
+ }
+ yield()
+ // wait for all requests to be dispatched
+ assertThat(threadsStartedCdl.await(5, SECONDS)).isTrue()
- assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isFalse()
- synchronized(pendingThumbnailCalls) {
- assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests)
- }
+ assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isFalse()
+ synchronized(pendingThumbnailCalls) {
+ assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests)
+ }
- pendingThumbnailCalls.poll()?.countDown()
- assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isFalse()
- synchronized(pendingThumbnailCalls) {
- assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests)
- }
+ pendingThumbnailCalls.poll()?.countDown()
+ assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isFalse()
+ synchronized(pendingThumbnailCalls) {
+ assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests)
+ }
- pendingThumbnailCalls.poll()?.countDown()
- assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isTrue()
- synchronized(pendingThumbnailCalls) {
- assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests)
- }
- for (cdl in pendingThumbnailCalls) {
- cdl.countDown()
+ pendingThumbnailCalls.poll()?.countDown()
+ assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isTrue()
+ synchronized(pendingThumbnailCalls) {
+ assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests)
+ }
+ for (cdl in pendingThumbnailCalls) {
+ cdl.countDown()
+ }
}
}
- }
}
private class NewThreadDispatcher(
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt
index 4a8c1392..babfaaf5 100644
--- a/tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt
@@ -21,16 +21,20 @@ import android.content.Intent
import android.database.MatrixCursor
import android.media.MediaMetadata
import android.net.Uri
+import android.platform.test.flag.junit.CheckFlagsRule
+import android.platform.test.flag.junit.DeviceFlagsValueProvider
import android.provider.DocumentsContract
import com.android.intentresolver.mock
import com.android.intentresolver.whenever
import com.google.common.truth.Truth.assertThat
import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
+import org.junit.Rule
import org.junit.Test
import org.mockito.Mockito.any
import org.mockito.Mockito.never
@@ -42,12 +46,29 @@ class PreviewDataProviderTest {
private val contentResolver = mock<ContentInterface>()
private val mimeTypeClassifier = DefaultMimeTypeClassifier
private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher())
+ @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule()
+
+ private fun createDataProvider(
+ targetIntent: Intent,
+ scope: CoroutineScope = testScope,
+ additionalContentUri: Uri? = null,
+ resolver: ContentInterface = contentResolver,
+ typeClassifier: MimeTypeClassifier = mimeTypeClassifier,
+ isPayloadTogglingEnabled: Boolean = false
+ ) =
+ PreviewDataProvider(
+ scope,
+ targetIntent,
+ additionalContentUri,
+ resolver,
+ isPayloadTogglingEnabled,
+ typeClassifier,
+ )
@Test
fun test_nonSendIntentAction_resolvesToTextPreviewUiSynchronously() {
val targetIntent = Intent(Intent.ACTION_VIEW)
- val testSubject =
- PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+ val testSubject = createDataProvider(targetIntent)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT)
verify(contentResolver, never()).getType(any())
@@ -62,8 +83,7 @@ class PreviewDataProviderTest {
type = "text/plain"
}
whenever(contentResolver.getType(uri)).thenReturn("text/plain")
- val testSubject =
- PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+ val testSubject = createDataProvider(targetIntent)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
assertThat(testSubject.uriCount).isEqualTo(1)
@@ -74,8 +94,7 @@ class PreviewDataProviderTest {
@Test
fun test_sendIntentWithoutUris_resolvesToTextPreviewUiSynchronously() {
val targetIntent = Intent(Intent.ACTION_SEND).apply { type = "image/png" }
- val testSubject =
- PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+ val testSubject = createDataProvider(targetIntent)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT)
verify(contentResolver, never()).getType(any())
@@ -86,8 +105,7 @@ class PreviewDataProviderTest {
val uri = Uri.parse("content://org.pkg.app/image.png")
val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
whenever(contentResolver.getType(uri)).thenReturn("image/png")
- val testSubject =
- PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+ val testSubject = createDataProvider(targetIntent)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
assertThat(testSubject.uriCount).isEqualTo(1)
@@ -101,8 +119,7 @@ class PreviewDataProviderTest {
val uri = Uri.parse("content://org.pkg.app/paper.pdf")
val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
whenever(contentResolver.getType(uri)).thenReturn("application/pdf")
- val testSubject =
- PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+ val testSubject = createDataProvider(targetIntent)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
assertThat(testSubject.uriCount).isEqualTo(1)
@@ -120,8 +137,7 @@ class PreviewDataProviderTest {
putExtra(Intent.EXTRA_STREAM, uri)
}
whenever(contentResolver.getType(uri)).thenThrow(SecurityException("test failure"))
- val testSubject =
- PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+ val testSubject = createDataProvider(targetIntent)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
assertThat(testSubject.uriCount).isEqualTo(1)
@@ -142,8 +158,7 @@ class PreviewDataProviderTest {
.thenThrow(SecurityException("test failure"))
whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null))
.thenThrow(SecurityException("test failure"))
- val testSubject =
- PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+ val testSubject = createDataProvider(targetIntent)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
assertThat(testSubject.uriCount).isEqualTo(1)
@@ -158,8 +173,7 @@ class PreviewDataProviderTest {
val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
whenever(contentResolver.getStreamTypes(uri, "*/*"))
.thenReturn(arrayOf("application/pdf", "image/png"))
- val testSubject =
- PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+ val testSubject = createDataProvider(targetIntent)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
assertThat(testSubject.uriCount).isEqualTo(1)
@@ -195,8 +209,7 @@ class PreviewDataProviderTest {
val cursor = MatrixCursor(columns).apply { addRow(values) }
whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null)).thenReturn(cursor)
- val testSubject =
- PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+ val testSubject = createDataProvider(targetIntent)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
assertThat(testSubject.uriCount).isEqualTo(1)
@@ -214,8 +227,7 @@ class PreviewDataProviderTest {
val cursor = MatrixCursor(emptyArray())
whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null)).thenReturn(cursor)
- val testSubject =
- PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+ val testSubject = createDataProvider(targetIntent)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
verify(contentResolver, times(1)).query(uri, METADATA_COLUMNS, null, null)
@@ -238,8 +250,7 @@ class PreviewDataProviderTest {
}
whenever(contentResolver.getType(uri1)).thenReturn("image/png")
whenever(contentResolver.getType(uri2)).thenReturn("image/jpeg")
- val testSubject =
- PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+ val testSubject = createDataProvider(targetIntent)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
assertThat(testSubject.uriCount).isEqualTo(2)
@@ -265,8 +276,7 @@ class PreviewDataProviderTest {
}
)
}
- val testSubject =
- PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+ val testSubject = createDataProvider(targetIntent)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
assertThat(testSubject.uriCount).isEqualTo(2)
@@ -293,8 +303,7 @@ class PreviewDataProviderTest {
whenever(contentResolver.getType(uri1)).thenReturn("video/mpeg4")
whenever(contentResolver.getStreamTypes(uri1, "*/*")).thenReturn(arrayOf("image/png"))
whenever(contentResolver.getType(uri2)).thenReturn("application/pdf")
- val testSubject =
- PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+ val testSubject = createDataProvider(targetIntent)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
assertThat(testSubject.uriCount).isEqualTo(2)
@@ -319,8 +328,7 @@ class PreviewDataProviderTest {
}
whenever(contentResolver.getType(uri1)).thenReturn("text/html")
whenever(contentResolver.getType(uri2)).thenReturn("application/pdf")
- val testSubject =
- PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+ val testSubject = createDataProvider(targetIntent)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
assertThat(testSubject.uriCount).isEqualTo(2)
@@ -350,8 +358,7 @@ class PreviewDataProviderTest {
.thenReturn(arrayOf("text/html", "image/jpeg"))
whenever(contentResolver.getStreamTypes(uri2, "*/*"))
.thenReturn(arrayOf("application/pdf", "image/png"))
- val testSubject =
- PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+ val testSubject = createDataProvider(targetIntent)
val fileInfoListOne = testSubject.imagePreviewFileInfoFlow.toList()
val fileInfoListTwo = testSubject.imagePreviewFileInfoFlow.toList()
@@ -364,4 +371,74 @@ class PreviewDataProviderTest {
verify(contentResolver, times(1)).getType(uri2)
verify(contentResolver, times(1)).getStreamTypes(uri2, "*/*")
}
+
+ @Test
+ fun sendItemsWithAdditionalContentUri_showPayloadTogglingUi() {
+ val uri = Uri.parse("content://org.pkg.app/image.png")
+ val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
+ whenever(contentResolver.getType(uri)).thenReturn("image/png")
+ val testSubject =
+ createDataProvider(
+ targetIntent,
+ additionalContentUri = Uri.parse("content://org.pkg.app.extracontent"),
+ isPayloadTogglingEnabled = true,
+ )
+
+ assertThat(testSubject.previewType)
+ .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION)
+ assertThat(testSubject.uriCount).isEqualTo(1)
+ assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri)
+ assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri)
+ verify(contentResolver, times(1)).getType(any())
+ }
+
+ @Test
+ fun sendItemsWithAdditionalContentUri_showImagePreviewUi() {
+ val uri = Uri.parse("content://org.pkg.app/image.png")
+ val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
+ whenever(contentResolver.getType(uri)).thenReturn("image/png")
+ val testSubject =
+ createDataProvider(
+ targetIntent,
+ additionalContentUri = Uri.parse("content://org.pkg.app.extracontent"),
+ )
+
+ assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
+ assertThat(testSubject.uriCount).isEqualTo(1)
+ assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri)
+ assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri)
+ verify(contentResolver, times(1)).getType(any())
+ }
+
+ @Test
+ fun sendItemsWithAdditionalContentUriWithSameAuthority_showImagePreviewUi() {
+ val uri = Uri.parse("content://org.pkg.app/image.png")
+ val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
+ whenever(contentResolver.getType(uri)).thenReturn("image/png")
+ val testSubject =
+ createDataProvider(
+ targetIntent,
+ additionalContentUri = Uri.parse("content://org.pkg.app/extracontent"),
+ isPayloadTogglingEnabled = true,
+ )
+
+ assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
+ assertThat(testSubject.uriCount).isEqualTo(1)
+ assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri)
+ assertThat(testSubject.firstFileInfo?.previewUri).isEqualTo(uri)
+ verify(contentResolver, times(1)).getType(any())
+ }
+
+ @Test
+ fun test_nonSendIntentActionWithAdditionalContentUri_resolvesToTextPreviewUiSynchronously() {
+ val targetIntent = Intent(Intent.ACTION_VIEW)
+ val testSubject =
+ createDataProvider(
+ targetIntent,
+ additionalContentUri = Uri.parse("content://org.pkg.app/extracontent")
+ )
+
+ assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT)
+ verify(contentResolver, never()).getType(any())
+ }
}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/TextContentPreviewUiTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/TextContentPreviewUiTest.kt
index 35362401..9a15f90a 100644
--- a/tests/unit/src/com/android/intentresolver/contentpreview/TextContentPreviewUiTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/TextContentPreviewUiTest.kt
@@ -22,6 +22,7 @@ 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.ContentTypeHint
import com.android.intentresolver.R
import com.android.intentresolver.mock
import com.android.intentresolver.whenever
@@ -38,18 +39,27 @@ import org.junit.runner.RunWith
class TextContentPreviewUiTest {
private val text = "Shared Text"
private val title = "Preview Title"
+ private val albumHeadline = "Album headline"
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) }
+ mock<HeadlineGenerator> {
+ whenever(getTextHeadline(text)).thenReturn(text)
+ whenever(getAlbumHeadline()).thenReturn(albumHeadline)
+ }
+ private val testMetadataText: CharSequence = "Test metadata text"
private val context
get() = InstrumentationRegistry.getInstrumentation().context
@@ -59,50 +69,74 @@ class TextContentPreviewUiTest {
testScope,
text,
title,
+ testMetadataText,
/*previewThumbnail=*/ null,
actionFactory,
imageLoader,
headlineGenerator,
+ ContentTypeHint.NONE,
)
@Test
fun test_display_headlineIsDisplayed() {
val layoutInflater = LayoutInflater.from(context)
- val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup
+ val gridLayout =
+ layoutInflater.inflate(R.layout.chooser_grid_scrollable_preview, null, false)
+ as ViewGroup
+ val headlineRow = gridLayout.requireViewById<View>(R.id.chooser_headline_row_container)
val previewView =
testSubject.display(
context.resources,
layoutInflater,
gridLayout,
- /*headlineViewParent=*/ null
+ headlineRow,
)
assertThat(previewView).isNotNull()
- val headlineView = previewView?.findViewById<TextView>(R.id.headline)
+ val headlineView = headlineRow.findViewById<TextView>(R.id.headline)
assertThat(headlineView).isNotNull()
assertThat(headlineView?.text).isEqualTo(text)
+ val metadataView = headlineRow.findViewById<TextView>(R.id.metadata)
+ assertThat(metadataView).isNotNull()
+ assertThat(metadataView?.text).isEqualTo(testMetadataText)
}
@Test
- fun test_displayWithExternalHeaderView_externalHeaderIsDisplayed() {
+ fun test_display_albumHeadlineOverride() {
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)
+ val headlineRow = gridLayout.requireViewById<View>(R.id.chooser_headline_row_container)
- assertThat(externalHeaderView.findViewById<View>(R.id.headline)).isNull()
+ val albumSubject =
+ TextContentPreviewUi(
+ testScope,
+ text,
+ title,
+ testMetadataText,
+ /*previewThumbnail=*/ null,
+ actionFactory,
+ imageLoader,
+ headlineGenerator,
+ ContentTypeHint.ALBUM,
+ )
val previewView =
- testSubject.display(context.resources, layoutInflater, gridLayout, externalHeaderView)
+ albumSubject.display(
+ context.resources,
+ layoutInflater,
+ gridLayout,
+ headlineRow,
+ )
assertThat(previewView).isNotNull()
- assertThat(previewView.findViewById<View>(R.id.headline)).isNull()
-
- val headlineView = externalHeaderView.findViewById<TextView>(R.id.headline)
+ val headlineView = headlineRow.findViewById<TextView>(R.id.headline)
assertThat(headlineView).isNotNull()
- assertThat(headlineView?.text).isEqualTo(text)
+ assertThat(headlineView?.text).isEqualTo(albumHeadline)
+ val metadataView = headlineRow.findViewById<TextView>(R.id.metadata)
+ assertThat(metadataView).isNotNull()
+ assertThat(metadataView?.text).isEqualTo(testMetadataText)
}
}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt
index 7e07e0ca..98e6c381 100644
--- a/tests/unit/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt
@@ -21,13 +21,14 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
+import androidx.annotation.IdRes
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.assertThat
import com.google.common.truth.Truth.assertWithMessage
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.flow.MutableSharedFlow
@@ -60,229 +61,106 @@ class UnifiedContentPreviewUiTest {
whenever(getVideosHeadline(anyInt())).thenReturn(VIDEO_HEADLINE)
whenever(getFilesHeadline(anyInt())).thenReturn(FILES_HEADLINE)
}
+ private val testMetadataText: CharSequence = "Test metadata text"
private val context
get() = getInstrumentation().context
@Test
- fun test_displayImagesWithoutUriMetadata_showImagesHeadline() {
- testLoadingHeadline("image/*", files = null) { previewView ->
+ fun test_displayImagesWithoutUriMetadataHeader_showImagesHeadline() {
+ testLoadingHeadline("image/*", files = null) { headlineRow ->
verify(headlineGenerator, times(1)).getImagesHeadline(2)
- verifyPreviewHeadline(previewView, IMAGE_HEADLINE)
+ verifyPreviewHeadline(headlineRow, IMAGE_HEADLINE)
+ verifyPreviewMetadata(headlineRow, testMetadataText)
}
}
@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 ->
+ fun test_displayVideosWithoutUriMetadataHeader_showImagesHeadline() {
+ testLoadingHeadline("video/*", files = null) { headlineRow ->
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)
+ verifyPreviewHeadline(headlineRow, VIDEO_HEADLINE)
+ verifyPreviewMetadata(headlineRow, testMetadataText)
}
}
@Test
- fun test_displayMixedContentWithoutUriMetadata_showImagesHeadline() {
- testLoadingHeadline("*/*", files = null) { previewView ->
+ fun test_displayDocumentsWithoutUriMetadataHeader_showImagesHeadline() {
+ testLoadingHeadline("application/pdf", files = null) { headlineRow ->
verify(headlineGenerator, times(1)).getFilesHeadline(2)
- verifyPreviewHeadline(previewView, FILES_HEADLINE)
+ verifyPreviewHeadline(headlineRow, FILES_HEADLINE)
+ verifyPreviewMetadata(headlineRow, testMetadataText)
}
}
@Test
- fun test_displayMixedContentWithoutUriMetadataExternalHeader_showImagesHeadline() {
- testLoadingExternalHeadline("*/*", files = null) { externalHeader ->
+ fun test_displayMixedContentWithoutUriMetadataHeader_showImagesHeadline() {
+ testLoadingHeadline("*/*", files = null) { headlineRow ->
verify(headlineGenerator, times(1)).getFilesHeadline(2)
- verifyPreviewHeadline(externalHeader, FILES_HEADLINE)
+ verifyPreviewHeadline(headlineRow, FILES_HEADLINE)
+ verifyPreviewMetadata(headlineRow, testMetadataText)
}
}
@Test
- fun test_displayImagesWithUriMetadataSet_showImagesHeadline() {
+ fun test_displayImagesWithUriMetadataSetHeader_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 ->
+ testLoadingHeadline("image/*", files) { headlineRow ->
verify(headlineGenerator, times(1)).getImagesHeadline(2)
- verifyPreviewHeadline(preivewView, IMAGE_HEADLINE)
+ verifyPreviewHeadline(headlineRow, 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() {
+ fun test_displayVideosWithUriMetadataSetHeader_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 ->
+ testLoadingHeadline("video/*", files) { headlineRow ->
verify(headlineGenerator, times(1)).getVideosHeadline(2)
- verifyPreviewHeadline(previewView, VIDEO_HEADLINE)
+ verifyPreviewHeadline(headlineRow, 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() {
+ fun test_displayImagesAndVideosWithUriMetadataSetHeader_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 ->
+ testLoadingHeadline("*/*", files) { headlineRow ->
verify(headlineGenerator, times(1)).getFilesHeadline(2)
- verifyPreviewHeadline(previewView, FILES_HEADLINE)
+ verifyPreviewHeadline(headlineRow, 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() {
+ fun test_displayDocumentsWithUriMetadataSetHeader_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 ->
+ testLoadingHeadline("application/pdf", files) { headlineRow ->
verify(headlineGenerator, times(1)).getFilesHeadline(2)
- verifyPreviewHeadline(externalHeader, FILES_HEADLINE)
+ verifyPreviewHeadline(headlineRow, 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 {
@@ -302,47 +180,46 @@ class UnifiedContentPreviewUiTest {
},
files?.let { it.asFlow() } ?: emptySourceFlow.takeWhile { it !== endMarker },
/*itemCount=*/ 2,
- headlineGenerator
+ headlineGenerator,
+ testMetadataText,
)
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)
+ val headlineRow = 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))
+ assertWithMessage("Headline row should not be inflated by default")
+ .that(headlineRow.findViewById<View>(R.id.headline))
.isNull()
- val previewView =
- testSubject.display(
- context.resources,
- LayoutInflater.from(context),
- gridLayout,
- externalHeaderView,
- )
-
+ testSubject.display(
+ context.resources,
+ LayoutInflater.from(context),
+ gridLayout,
+ headlineRow,
+ )
emptySourceFlow.tryEmit(endMarker)
-
- verifyInternalHeadlineAbsence(previewView)
- verificationBlock(externalHeaderView)
+ verificationBlock(headlineRow)
}
}
+ private fun verifyTextViewText(
+ viewParent: View?,
+ @IdRes textViewResId: Int,
+ expectedText: CharSequence,
+ ) {
+ assertThat(viewParent).isNotNull()
+ val textView = viewParent?.findViewById<TextView>(textViewResId)
+ assertThat(textView).isNotNull()
+ assertThat(textView?.text).isEqualTo(expectedText)
+ }
+
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)
+ verifyTextViewText(headerViewParent, R.id.headline, 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()
+ private fun verifyPreviewMetadata(headerViewParent: View?, expectedText: CharSequence) {
+ verifyTextViewText(headerViewParent, R.id.metadata, expectedText)
}
}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/UriMetadataReaderTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/UriMetadataReaderTest.kt
new file mode 100644
index 00000000..07f3a3f2
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/UriMetadataReaderTest.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.content.ContentInterface
+import android.database.MatrixCursor
+import android.media.MediaMetadata
+import android.net.Uri
+import android.provider.DocumentsContract
+import com.android.intentresolver.any
+import com.android.intentresolver.anyOrNull
+import com.android.intentresolver.eq
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+
+class UriMetadataReaderTest {
+ private val uri = Uri.parse("content://org.pkg.app/item")
+ private val contentResolver = mock<ContentInterface>()
+
+ @Test
+ fun testImageUri() {
+ val mimeType = "image/png"
+ whenever(contentResolver.getType(uri)).thenReturn(mimeType)
+ val testSubject = UriMetadataReaderImpl(contentResolver, DefaultMimeTypeClassifier)
+
+ testSubject.getMetadata(uri).let { fileInfo ->
+ assertWithMessage("Wrong uri").that(fileInfo.uri).isEqualTo(uri)
+ assertWithMessage("Wrong mime type").that(fileInfo.mimeType).isEqualTo(mimeType)
+ assertWithMessage("Wrong preview URI").that(fileInfo.previewUri).isEqualTo(uri)
+ }
+ }
+
+ @Test
+ fun testFileUriWithImageTypeSupport() {
+ val mimeType = "application/pdf"
+ val imageType = "image/png"
+ whenever(contentResolver.getType(uri)).thenReturn(mimeType)
+ whenever(contentResolver.getStreamTypes(eq(uri), any())).thenReturn(arrayOf(imageType))
+ val testSubject = UriMetadataReaderImpl(contentResolver, DefaultMimeTypeClassifier)
+
+ testSubject.getMetadata(uri).let { fileInfo ->
+ assertWithMessage("Wrong uri").that(fileInfo.uri).isEqualTo(uri)
+ assertWithMessage("Wrong mime type").that(fileInfo.mimeType).isEqualTo(mimeType)
+ assertWithMessage("Wrong preview URI").that(fileInfo.previewUri).isEqualTo(uri)
+ }
+ }
+
+ @Test
+ fun testFileUriWithThumbnailSupport() {
+ val mimeType = "application/pdf"
+ whenever(contentResolver.getType(uri)).thenReturn(mimeType)
+ val columns = arrayOf(DocumentsContract.Document.COLUMN_FLAGS)
+ whenever(contentResolver.query(eq(uri), eq(columns), anyOrNull(), anyOrNull()))
+ .thenReturn(
+ MatrixCursor(columns).apply {
+ addRow(arrayOf(DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL))
+ }
+ )
+ val testSubject = UriMetadataReaderImpl(contentResolver, DefaultMimeTypeClassifier)
+
+ testSubject.getMetadata(uri).let { fileInfo ->
+ assertWithMessage("Wrong uri").that(fileInfo.uri).isEqualTo(uri)
+ assertWithMessage("Wrong mime type").that(fileInfo.mimeType).isEqualTo(mimeType)
+ assertWithMessage("Wrong preview URI").that(fileInfo.previewUri).isEqualTo(uri)
+ }
+ }
+
+ @Test
+ fun testFileUriWithPreviewUri() {
+ val mimeType = "application/pdf"
+ val previewUri = uri.buildUpon().appendQueryParameter("preview", null).build()
+ whenever(contentResolver.getType(uri)).thenReturn(mimeType)
+ val columns = arrayOf(MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI)
+ whenever(contentResolver.query(eq(uri), eq(columns), anyOrNull(), anyOrNull()))
+ .thenReturn(MatrixCursor(columns).apply { addRow(arrayOf(previewUri.toString())) })
+ val testSubject = UriMetadataReaderImpl(contentResolver, DefaultMimeTypeClassifier)
+
+ testSubject.getMetadata(uri).let { fileInfo ->
+ assertWithMessage("Wrong uri").that(fileInfo.uri).isEqualTo(uri)
+ assertWithMessage("Wrong mime type").that(fileInfo.mimeType).isEqualTo(mimeType)
+ assertWithMessage("Wrong preview URI").that(fileInfo.previewUri).isEqualTo(previewUri)
+ }
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifierImplTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifierImplTest.kt
new file mode 100644
index 00000000..7c36ef55
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/intent/TargetIntentModifierImplTest.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.payloadtoggle.domain.intent
+
+import android.content.Intent
+import android.content.Intent.ACTION_SEND
+import android.content.Intent.ACTION_SEND_MULTIPLE
+import android.content.Intent.EXTRA_STREAM
+import android.net.Uri
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class TargetIntentModifierImplTest {
+ @Test
+ fun testIntentActionChange() {
+ val testSubject =
+ TargetIntentModifierImpl<Uri>(Intent(ACTION_SEND), { this }, { "image/png" })
+
+ val u1 = createUri(1)
+ val u2 = createUri(2)
+ testSubject.intentFromSelection(listOf(u1, u2)).let { intent ->
+ assertThat(intent.action).isEqualTo(ACTION_SEND_MULTIPLE)
+ assertThat(intent.getParcelableArrayListExtra(EXTRA_STREAM, Uri::class.java))
+ .containsExactly(u1, u2)
+ .inOrder()
+ }
+
+ testSubject.intentFromSelection(listOf(u1)).let { intent ->
+ assertThat(intent.action).isEqualTo(ACTION_SEND)
+ assertThat(intent.getParcelableExtra(EXTRA_STREAM, Uri::class.java)).isEqualTo(u1)
+ }
+ }
+
+ @Test
+ fun testMimeTypeChange() {
+ val testSubject =
+ TargetIntentModifierImpl<Pair<Uri, String?>>(Intent(ACTION_SEND), { first }, { second })
+
+ val u1 = createUri(1)
+ val u2 = createUri(2)
+ testSubject.intentFromSelection(listOf(u1 to "image/png", u2 to "image/png")).let { intent
+ ->
+ assertThat(intent.type).isEqualTo("image/png")
+ }
+
+ testSubject.intentFromSelection(listOf(u1 to "image/png", u2 to "image/jpg")).let { intent
+ ->
+ assertThat(intent.type).isEqualTo("image/*")
+ }
+
+ testSubject.intentFromSelection(listOf(u1 to "image/png", u2 to "video/mpeg")).let { intent
+ ->
+ assertThat(intent.type).isEqualTo("*/*")
+ }
+
+ testSubject.intentFromSelection(listOf(u1 to "image/png", u2 to null)).let { intent ->
+ assertThat(intent.type).isEqualTo("*/*")
+ }
+ }
+
+ // TODO: test that the original intent's extras and flags remains the same
+}
+
+private fun createUri(id: Int) = Uri.parse("content://org.pkg/$id")
+
+private data class Item(val uri: Uri, val mimeType: String?)
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt
new file mode 100644
index 00000000..af6de833
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import android.database.MatrixCursor
+import android.net.Uri
+import androidx.core.os.bundleOf
+import com.android.intentresolver.contentpreview.FileInfo
+import com.android.intentresolver.contentpreview.UriMetadataReader
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.cursorPreviewsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import com.android.intentresolver.contentpreview.uriMetadataReader
+import com.android.intentresolver.util.KosmosTestScope
+import com.android.intentresolver.util.cursor.viewBy
+import com.android.intentresolver.util.runTest
+import com.android.systemui.kosmos.Kosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import org.junit.Test
+
+class CursorPreviewsInteractorTest {
+
+ private fun runTestWithDeps(
+ initialSelection: Iterable<Int> = (1..2),
+ focusedItemIndex: Int = initialSelection.count() / 2,
+ cursor: Iterable<Int> = (0 until 4),
+ cursorStartPosition: Int = cursor.count() / 2,
+ pageSize: Int = 16,
+ maxLoadedPages: Int = 3,
+ block: KosmosTestScope.(TestDeps) -> Unit,
+ ) {
+ with(Kosmos()) {
+ this.focusedItemIndex = focusedItemIndex
+ this.pageSize = pageSize
+ this.maxLoadedPages = maxLoadedPages
+ uriMetadataReader = UriMetadataReader {
+ FileInfo.Builder(it).withMimeType("image/bitmap").build()
+ }
+ runTest {
+ block(
+ TestDeps(
+ initialSelection,
+ cursor,
+ cursorStartPosition,
+ )
+ )
+ }
+ }
+ }
+
+ private class TestDeps(
+ initialSelectionRange: Iterable<Int>,
+ private val cursorRange: Iterable<Int>,
+ private val cursorStartPosition: Int,
+ ) {
+ val cursor =
+ MatrixCursor(arrayOf("uri"))
+ .apply {
+ extras = bundleOf("position" to cursorStartPosition)
+ for (i in cursorRange) {
+ newRow().add("uri", uri(i).toString())
+ }
+ }
+ .viewBy { getString(0)?.let(Uri::parse) }
+ val initialPreviews: List<PreviewModel> =
+ initialSelectionRange.map { i -> PreviewModel(uri = uri(i), mimeType = "image/bitmap") }
+
+ private fun uri(index: Int) = Uri.fromParts("scheme$index", "ssp$index", "fragment$index")
+ }
+
+ @Test
+ fun initialCursorLoad() = runTestWithDeps { deps ->
+ backgroundScope.launch {
+ cursorPreviewsInteractor.launch(deps.cursor, deps.initialPreviews)
+ }
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.startIdx).isEqualTo(0)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft).isNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreRight).isNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels)
+ .containsExactly(
+ PreviewModel(Uri.fromParts("scheme0", "ssp0", "fragment0"), "image/bitmap"),
+ PreviewModel(Uri.fromParts("scheme1", "ssp1", "fragment1"), "image/bitmap"),
+ PreviewModel(Uri.fromParts("scheme2", "ssp2", "fragment2"), "image/bitmap"),
+ PreviewModel(Uri.fromParts("scheme3", "ssp3", "fragment3"), "image/bitmap"),
+ )
+ .inOrder()
+ }
+
+ @Test
+ fun loadMoreLeft_evictRight() =
+ runTestWithDeps(
+ initialSelection = listOf(24),
+ cursor = (0 until 48),
+ pageSize = 16,
+ maxLoadedPages = 1,
+ ) { deps ->
+ backgroundScope.launch {
+ cursorPreviewsInteractor.launch(deps.cursor, deps.initialPreviews)
+ }
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(16)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme16", "ssp16", "fragment16"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme31", "ssp31", "fragment31"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft).isNotNull()
+
+ cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft!!.invoke()
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(16)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme0", "ssp0", "fragment0"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme15", "ssp15", "fragment15"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft).isNull()
+ }
+
+ @Test
+ fun loadMoreLeft_keepRight() =
+ runTestWithDeps(
+ initialSelection = listOf(24),
+ cursor = (0 until 48),
+ pageSize = 16,
+ maxLoadedPages = 2,
+ ) { deps ->
+ backgroundScope.launch {
+ cursorPreviewsInteractor.launch(deps.cursor, deps.initialPreviews)
+ }
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(16)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme16", "ssp16", "fragment16"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme31", "ssp31", "fragment31"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft).isNotNull()
+
+ cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft!!.invoke()
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(32)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme0", "ssp0", "fragment0"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme31", "ssp31", "fragment31"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft).isNull()
+ }
+
+ @Test
+ fun loadMoreRight_evictLeft() =
+ runTestWithDeps(
+ initialSelection = listOf(24),
+ cursor = (0 until 48),
+ pageSize = 16,
+ maxLoadedPages = 1,
+ ) { deps ->
+ backgroundScope.launch {
+ cursorPreviewsInteractor.launch(deps.cursor, deps.initialPreviews)
+ }
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(16)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme16", "ssp16", "fragment16"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme31", "ssp31", "fragment31"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreRight).isNotNull()
+
+ cursorPreviewsRepository.previewsModel.value!!.loadMoreRight!!.invoke()
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(16)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme32", "ssp32", "fragment32"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme47", "ssp47", "fragment47"))
+ }
+
+ @Test
+ fun loadMoreRight_keepLeft() =
+ runTestWithDeps(
+ initialSelection = listOf(24),
+ cursor = (0 until 48),
+ pageSize = 16,
+ maxLoadedPages = 2,
+ ) { deps ->
+ backgroundScope.launch {
+ cursorPreviewsInteractor.launch(deps.cursor, deps.initialPreviews)
+ }
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(16)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme16", "ssp16", "fragment16"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme31", "ssp31", "fragment31"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreRight).isNotNull()
+
+ cursorPreviewsRepository.previewsModel.value!!.loadMoreRight!!.invoke()
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(32)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme16", "ssp16", "fragment16"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme47", "ssp47", "fragment47"))
+ }
+
+ @Test
+ fun noMoreRight_appendUnclaimedFromInitialSelection() =
+ runTestWithDeps(
+ initialSelection = listOf(24, 50),
+ cursor = listOf(24),
+ pageSize = 16,
+ maxLoadedPages = 2,
+ ) { deps ->
+ backgroundScope.launch {
+ cursorPreviewsInteractor.launch(deps.cursor, deps.initialPreviews)
+ }
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(2)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme24", "ssp24", "fragment24"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme50", "ssp50", "fragment50"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreRight).isNull()
+ }
+
+ @Test
+ fun noMoreLeft_appendUnclaimedFromInitialSelection() =
+ runTestWithDeps(
+ initialSelection = listOf(0, 24),
+ cursor = listOf(24),
+ pageSize = 16,
+ maxLoadedPages = 2,
+ ) { deps ->
+ backgroundScope.launch {
+ cursorPreviewsInteractor.launch(deps.cursor, deps.initialPreviews)
+ }
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(2)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme0", "ssp0", "fragment0"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme24", "ssp24", "fragment24"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft).isNull()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractorTest.kt
new file mode 100644
index 00000000..2bbda0cc
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CustomActionsInteractorTest.kt
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import android.app.Activity
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.activityResultRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ActionModel
+import com.android.intentresolver.data.model.ChooserRequest
+import com.android.intentresolver.data.repository.ChooserRequestRepository
+import com.android.intentresolver.data.repository.chooserRequestRepository
+import com.android.intentresolver.icon.BitmapIcon
+import com.android.intentresolver.util.comparingElementsUsingTransform
+import com.android.intentresolver.util.runKosmosTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.stateIn
+import org.junit.Test
+
+class CustomActionsInteractorTest {
+
+ @Test
+ fun customActions_initialRepoValue() = runKosmosTest {
+ val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8)
+ val icon = Icon.createWithBitmap(bitmap)
+ chooserRequestRepository =
+ ChooserRequestRepository(
+ initialRequest =
+ ChooserRequest(targetIntent = Intent(), launchedFromPackage = "pkg"),
+ initialActions =
+ listOf(
+ CustomActionModel(label = "label1", icon = icon, performAction = {}),
+ ),
+ )
+ val underTest = customActionsInteractor
+ val customActions: StateFlow<List<ActionModel>> =
+ underTest.customActions.stateIn(backgroundScope)
+ assertThat(customActions.value)
+ .comparingElementsUsingTransform("has a label of") { model: ActionModel -> model.label }
+ .containsExactly("label1")
+ .inOrder()
+ assertThat(customActions.value)
+ .comparingElementsUsingTransform("has an icon of") { model: ActionModel -> model.icon }
+ .containsExactly(BitmapIcon(icon.bitmap))
+ .inOrder()
+ }
+
+ @Test
+ fun customActions_tracksRepoUpdates() = runKosmosTest {
+ val underTest = customActionsInteractor
+
+ val customActions: StateFlow<List<ActionModel>> =
+ underTest.customActions.stateIn(backgroundScope)
+ val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8)
+ val icon = Icon.createWithBitmap(bitmap)
+ val chooserActions = listOf(CustomActionModel("label1", icon) {})
+ chooserRequestRepository.customActions.value = chooserActions
+ runCurrent()
+
+ assertThat(customActions.value)
+ .comparingElementsUsingTransform("has a label of") { model: ActionModel -> model.label }
+ .containsExactly("label1")
+ .inOrder()
+ assertThat(customActions.value)
+ .comparingElementsUsingTransform("has an icon of") { model: ActionModel -> model.icon }
+ .containsExactly(BitmapIcon(icon.bitmap))
+ .inOrder()
+ }
+
+ @Test
+ fun customActions_performAction_sendsPendingIntent() = runKosmosTest {
+ val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8)
+ val icon = Icon.createWithBitmap(bitmap)
+ var actionSent = false
+ chooserRequestRepository =
+ ChooserRequestRepository(
+ initialRequest =
+ ChooserRequest(targetIntent = Intent(), launchedFromPackage = "pkg"),
+ initialActions =
+ listOf(
+ CustomActionModel(
+ label = "label1",
+ icon = icon,
+ performAction = { actionSent = true },
+ )
+ ),
+ )
+ val underTest = customActionsInteractor
+
+ val customActions: StateFlow<List<ActionModel>> =
+ underTest.customActions.stateIn(backgroundScope)
+
+ assertThat(customActions.value).hasSize(1)
+
+ customActions.value[0].performAction(123)
+
+ assertThat(actionSent).isTrue()
+ assertThat(activityResultRepository.activityResult.value).isEqualTo(Activity.RESULT_OK)
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt
new file mode 100644
index 00000000..f012fcc6
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/FetchPreviewsInteractorTest.kt
@@ -0,0 +1,316 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import android.database.MatrixCursor
+import android.net.Uri
+import androidx.core.os.bundleOf
+import com.android.intentresolver.contentpreview.FileInfo
+import com.android.intentresolver.contentpreview.UriMetadataReader
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.cursorPreviewsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.CursorResolver
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.cursor.payloadToggleCursorResolver
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
+import com.android.intentresolver.contentpreview.uriMetadataReader
+import com.android.intentresolver.inject.contentUris
+import com.android.intentresolver.util.KosmosTestScope
+import com.android.intentresolver.util.cursor.CursorView
+import com.android.intentresolver.util.cursor.viewBy
+import com.android.intentresolver.util.runTest as runKosmosTest
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.sync.Mutex
+import kotlinx.coroutines.sync.withLock
+import org.junit.Test
+
+class FetchPreviewsInteractorTest {
+
+ private fun runTest(
+ initialSelection: Iterable<Int> = (1..2),
+ focusedItemIndex: Int = initialSelection.count() / 2,
+ cursor: Iterable<Int> = (0 until 4),
+ cursorStartPosition: Int = cursor.count() / 2,
+ pageSize: Int = 16,
+ maxLoadedPages: Int = 3,
+ block: KosmosTestScope.() -> Unit,
+ ) {
+ with(Kosmos()) {
+ fakeCursorResolver =
+ FakeCursorResolver(cursorRange = cursor, cursorStartPosition = cursorStartPosition)
+ payloadToggleCursorResolver = fakeCursorResolver
+ contentUris = initialSelection.map { uri(it) }
+ this.focusedItemIndex = focusedItemIndex
+ uriMetadataReader = UriMetadataReader {
+ FileInfo.Builder(it).withMimeType("image/bitmap").build()
+ }
+ this.pageSize = pageSize
+ this.maxLoadedPages = maxLoadedPages
+ runKosmosTest { block() }
+ }
+ }
+
+ private var Kosmos.fakeCursorResolver: FakeCursorResolver by Fixture()
+
+ private class FakeCursorResolver(
+ private val cursorRange: Iterable<Int>,
+ private val cursorStartPosition: Int,
+ ) : CursorResolver<Uri?> {
+ private val mutex = Mutex(locked = true)
+
+ fun complete() = mutex.unlock()
+
+ override suspend fun getCursor(): CursorView<Uri?> =
+ mutex.withLock {
+ MatrixCursor(arrayOf("uri"))
+ .apply {
+ extras = bundleOf("position" to cursorStartPosition)
+ for (i in cursorRange) {
+ newRow().add("uri", uri(i).toString())
+ }
+ }
+ .viewBy { getString(0)?.let(Uri::parse) }
+ }
+ }
+
+ @Test
+ fun setsInitialPreviews() = runTest {
+ backgroundScope.launch { fetchPreviewsInteractor.activate() }
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value)
+ .isEqualTo(
+ PreviewsModel(
+ previewModels =
+ setOf(
+ PreviewModel(
+ Uri.fromParts("scheme1", "ssp1", "fragment1"),
+ "image/bitmap",
+ ),
+ PreviewModel(
+ Uri.fromParts("scheme2", "ssp2", "fragment2"),
+ "image/bitmap",
+ ),
+ ),
+ startIdx = 1,
+ loadMoreLeft = null,
+ loadMoreRight = null,
+ )
+ )
+ }
+
+ @Test
+ fun lookupCursorFromContentResolver() = runTest {
+ backgroundScope.launch { fetchPreviewsInteractor.activate() }
+ fakeCursorResolver.complete()
+ runCurrent()
+
+ with(cursorPreviewsRepository) {
+ assertThat(previewsModel.value).isNotNull()
+ assertThat(previewsModel.value!!.startIdx).isEqualTo(0)
+ assertThat(previewsModel.value!!.loadMoreLeft).isNull()
+ assertThat(previewsModel.value!!.loadMoreRight).isNull()
+ assertThat(previewsModel.value!!.previewModels)
+ .containsExactly(
+ PreviewModel(Uri.fromParts("scheme0", "ssp0", "fragment0"), "image/bitmap"),
+ PreviewModel(Uri.fromParts("scheme1", "ssp1", "fragment1"), "image/bitmap"),
+ PreviewModel(Uri.fromParts("scheme2", "ssp2", "fragment2"), "image/bitmap"),
+ PreviewModel(Uri.fromParts("scheme3", "ssp3", "fragment3"), "image/bitmap"),
+ )
+ .inOrder()
+ }
+ }
+
+ @Test
+ fun loadMoreLeft_evictRight() =
+ runTest(
+ initialSelection = listOf(24),
+ cursor = (0 until 48),
+ pageSize = 16,
+ maxLoadedPages = 1,
+ ) {
+ backgroundScope.launch { fetchPreviewsInteractor.activate() }
+ fakeCursorResolver.complete()
+ runCurrent()
+
+ with(cursorPreviewsRepository) {
+ assertThat(previewsModel.value).isNotNull()
+ assertThat(previewsModel.value!!.previewModels).hasSize(16)
+ assertThat(previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme16", "ssp16", "fragment16"))
+ assertThat(previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme31", "ssp31", "fragment31"))
+ assertThat(previewsModel.value!!.loadMoreLeft).isNotNull()
+ }
+
+ cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft!!.invoke()
+ runCurrent()
+
+ with(cursorPreviewsRepository) {
+ assertThat(previewsModel.value).isNotNull()
+ assertThat(previewsModel.value!!.previewModels).hasSize(16)
+ assertThat(previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme0", "ssp0", "fragment0"))
+ assertThat(previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme15", "ssp15", "fragment15"))
+ assertThat(previewsModel.value!!.loadMoreLeft).isNull()
+ }
+ }
+
+ @Test
+ fun loadMoreLeft_keepRight() =
+ runTest(
+ initialSelection = listOf(24),
+ cursor = (0 until 48),
+ pageSize = 16,
+ maxLoadedPages = 2,
+ ) {
+ backgroundScope.launch { fetchPreviewsInteractor.activate() }
+ fakeCursorResolver.complete()
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(16)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme16", "ssp16", "fragment16"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme31", "ssp31", "fragment31"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft).isNotNull()
+
+ cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft!!.invoke()
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(32)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme0", "ssp0", "fragment0"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme31", "ssp31", "fragment31"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft).isNull()
+ }
+
+ @Test
+ fun loadMoreRight_evictLeft() =
+ runTest(
+ initialSelection = listOf(24),
+ cursor = (0 until 48),
+ pageSize = 16,
+ maxLoadedPages = 1,
+ ) {
+ backgroundScope.launch { fetchPreviewsInteractor.activate() }
+ fakeCursorResolver.complete()
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(16)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme16", "ssp16", "fragment16"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme31", "ssp31", "fragment31"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreRight).isNotNull()
+
+ cursorPreviewsRepository.previewsModel.value!!.loadMoreRight!!.invoke()
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(16)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme32", "ssp32", "fragment32"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme47", "ssp47", "fragment47"))
+ }
+
+ @Test
+ fun loadMoreRight_keepLeft() =
+ runTest(
+ initialSelection = listOf(24),
+ cursor = (0 until 48),
+ pageSize = 16,
+ maxLoadedPages = 2,
+ ) {
+ backgroundScope.launch { fetchPreviewsInteractor.activate() }
+ fakeCursorResolver.complete()
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(16)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme16", "ssp16", "fragment16"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme31", "ssp31", "fragment31"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreRight).isNotNull()
+
+ cursorPreviewsRepository.previewsModel.value!!.loadMoreRight!!.invoke()
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(32)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme16", "ssp16", "fragment16"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme47", "ssp47", "fragment47"))
+ }
+
+ @Test
+ fun noMoreRight_appendUnclaimedFromInitialSelection() =
+ runTest(
+ initialSelection = listOf(24, 50),
+ cursor = listOf(24),
+ pageSize = 16,
+ maxLoadedPages = 2,
+ ) {
+ backgroundScope.launch { fetchPreviewsInteractor.activate() }
+ fakeCursorResolver.complete()
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(2)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme24", "ssp24", "fragment24"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme50", "ssp50", "fragment50"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreRight).isNull()
+ }
+
+ @Test
+ fun noMoreLeft_appendUnclaimedFromInitialSelection() =
+ runTest(
+ initialSelection = listOf(0, 24),
+ cursor = listOf(24),
+ pageSize = 16,
+ maxLoadedPages = 2,
+ ) {
+ backgroundScope.launch { fetchPreviewsInteractor.activate() }
+ fakeCursorResolver.complete()
+ runCurrent()
+
+ assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull()
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels).hasSize(2)
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.first().uri)
+ .isEqualTo(Uri.fromParts("scheme0", "ssp0", "fragment0"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels.last().uri)
+ .isEqualTo(Uri.fromParts("scheme24", "ssp24", "fragment24"))
+ assertThat(cursorPreviewsRepository.previewsModel.value!!.loadMoreLeft).isNull()
+ }
+}
+
+private fun uri(index: Int) = Uri.fromParts("scheme$index", "ssp$index", "fragment$index")
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractorTest.kt
new file mode 100644
index 00000000..f8fc4911
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractorTest.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import android.content.Intent
+import android.net.Uri
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.pendingSelectionCallbackRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.previewSelectionsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.TargetIntentModifier
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.targetIntentModifier
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import com.android.intentresolver.data.repository.chooserRequestRepository
+import com.android.intentresolver.util.runKosmosTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import org.junit.Test
+
+class SelectablePreviewInteractorTest {
+
+ @Test
+ fun reflectPreviewRepo_initState() = runKosmosTest {
+ targetIntentModifier = TargetIntentModifier { error("unexpected invocation") }
+ val underTest =
+ SelectablePreviewInteractor(
+ key = PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), null),
+ selectionInteractor = selectionInteractor,
+ )
+ runCurrent()
+
+ assertThat(underTest.isSelected.first()).isFalse()
+ }
+
+ @Test
+ fun reflectPreviewRepo_updatedState() = runKosmosTest {
+ targetIntentModifier = TargetIntentModifier { error("unexpected invocation") }
+ val underTest =
+ SelectablePreviewInteractor(
+ key = PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), "image/bitmap"),
+ selectionInteractor = selectionInteractor,
+ )
+
+ assertThat(underTest.isSelected.first()).isFalse()
+
+ previewSelectionsRepository.selections.value =
+ setOf(PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), "image/bitmap"))
+ runCurrent()
+
+ assertThat(underTest.isSelected.first()).isTrue()
+ }
+
+ @Test
+ fun setSelected_updatesChooserRequestRepo() = runKosmosTest {
+ val modifiedIntent = Intent()
+ targetIntentModifier = TargetIntentModifier { modifiedIntent }
+ val underTest =
+ SelectablePreviewInteractor(
+ key = PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), "image/bitmap"),
+ selectionInteractor = selectionInteractor,
+ )
+
+ underTest.setSelected(true)
+ runCurrent()
+
+ assertThat(previewSelectionsRepository.selections.value)
+ .containsExactly(
+ PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), "image/bitmap")
+ )
+
+ assertThat(chooserRequestRepository.chooserRequest.value.targetIntent)
+ .isSameInstanceAs(modifiedIntent)
+ assertThat(pendingSelectionCallbackRepository.pendingTargetIntent.value)
+ .isSameInstanceAs(modifiedIntent)
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractorTest.kt
new file mode 100644
index 00000000..5fa5cab4
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewsInteractorTest.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import android.net.Uri
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.cursorPreviewsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.previewSelectionsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.TargetIntentModifier
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.targetIntentModifier
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
+import com.android.intentresolver.util.runKosmosTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.stateIn
+import org.junit.Test
+
+class SelectablePreviewsInteractorTest {
+
+ @Test
+ fun keySet_reflectsRepositoryInit() = runKosmosTest {
+ cursorPreviewsRepository.previewsModel.value =
+ PreviewsModel(
+ previewModels =
+ setOf(
+ PreviewModel(
+ Uri.fromParts("scheme", "ssp", "fragment"),
+ "image/bitmap",
+ ),
+ PreviewModel(
+ Uri.fromParts("scheme2", "ssp2", "fragment2"),
+ "image/bitmap",
+ ),
+ ),
+ startIdx = 0,
+ loadMoreLeft = null,
+ loadMoreRight = null,
+ )
+ previewSelectionsRepository.selections.value =
+ setOf(
+ PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), null),
+ )
+ targetIntentModifier = TargetIntentModifier { error("unexpected invocation") }
+ val underTest = selectablePreviewsInteractor
+ val keySet = underTest.previews.stateIn(backgroundScope)
+
+ assertThat(keySet.value).isNotNull()
+ assertThat(keySet.value!!.previewModels)
+ .containsExactly(
+ PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), "image/bitmap"),
+ PreviewModel(Uri.fromParts("scheme2", "ssp2", "fragment2"), "image/bitmap"),
+ )
+ .inOrder()
+ assertThat(keySet.value!!.startIdx).isEqualTo(0)
+ assertThat(keySet.value!!.loadMoreLeft).isNull()
+ assertThat(keySet.value!!.loadMoreRight).isNull()
+
+ val firstModel =
+ underTest.preview(PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), null))
+ assertThat(firstModel.isSelected.first()).isTrue()
+
+ val secondModel =
+ underTest.preview(PreviewModel(Uri.fromParts("scheme2", "ssp2", "fragment2"), null))
+ assertThat(secondModel.isSelected.first()).isFalse()
+ }
+
+ @Test
+ fun keySet_reflectsRepositoryUpdate() = runKosmosTest {
+ previewSelectionsRepository.selections.value =
+ setOf(
+ PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), null),
+ )
+ targetIntentModifier = TargetIntentModifier { error("unexpected invocation") }
+ val underTest = selectablePreviewsInteractor
+
+ val previews = underTest.previews.stateIn(backgroundScope)
+ val firstModel =
+ underTest.preview(PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), null))
+
+ assertThat(previews.value).isNull()
+ assertThat(firstModel.isSelected.first()).isTrue()
+
+ var loadRequested = false
+
+ cursorPreviewsRepository.previewsModel.value =
+ PreviewsModel(
+ previewModels =
+ setOf(
+ PreviewModel(
+ Uri.fromParts("scheme", "ssp", "fragment"),
+ "image/bitmap",
+ ),
+ PreviewModel(
+ Uri.fromParts("scheme2", "ssp2", "fragment2"),
+ "image/bitmap",
+ ),
+ ),
+ startIdx = 5,
+ loadMoreLeft = null,
+ loadMoreRight = { loadRequested = true },
+ )
+ previewSelectionsRepository.selections.value = emptySet()
+ runCurrent()
+
+ assertThat(previews.value).isNotNull()
+ assertThat(previews.value!!.previewModels)
+ .containsExactly(
+ PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), "image/bitmap"),
+ PreviewModel(Uri.fromParts("scheme2", "ssp2", "fragment2"), "image/bitmap"),
+ )
+ .inOrder()
+ assertThat(previews.value!!.startIdx).isEqualTo(5)
+ assertThat(previews.value!!.loadMoreLeft).isNull()
+ assertThat(previews.value!!.loadMoreRight).isNotNull()
+
+ assertThat(firstModel.isSelected.first()).isFalse()
+
+ previews.value!!.loadMoreRight!!.invoke()
+
+ assertThat(loadRequested).isTrue()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SetCursorPreviewsInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SetCursorPreviewsInteractorTest.kt
new file mode 100644
index 00000000..5aac7b55
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SetCursorPreviewsInteractorTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import android.net.Uri
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.cursorPreviewsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.LoadDirection
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import com.android.intentresolver.util.runKosmosTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.stateIn
+import org.junit.Test
+
+class SetCursorPreviewsInteractorTest {
+ @Test
+ fun setPreviews_noAdditionalData() = runKosmosTest {
+ val loadState =
+ setCursorPreviewsInteractor.setPreviews(
+ previewsByKey =
+ setOf(
+ PreviewModel(
+ uri = Uri.fromParts("scheme", "ssp", "fragment"),
+ mimeType = null,
+ )
+ ),
+ startIndex = 100,
+ hasMoreLeft = false,
+ hasMoreRight = false,
+ )
+
+ assertThat(loadState.first()).isNull()
+ cursorPreviewsRepository.previewsModel.value.let {
+ assertThat(it).isNotNull()
+ it!!
+ assertThat(it.loadMoreRight).isNull()
+ assertThat(it.loadMoreLeft).isNull()
+ assertThat(it.startIdx).isEqualTo(100)
+ assertThat(it.previewModels)
+ .containsExactly(
+ PreviewModel(
+ uri = Uri.fromParts("scheme", "ssp", "fragment"),
+ mimeType = null,
+ )
+ )
+ .inOrder()
+ }
+ }
+
+ @Test
+ fun setPreviews_additionalData() = runKosmosTest {
+ val loadState =
+ setCursorPreviewsInteractor
+ .setPreviews(
+ previewsByKey =
+ setOf(
+ PreviewModel(
+ uri = Uri.fromParts("scheme", "ssp", "fragment"),
+ mimeType = null,
+ )
+ ),
+ startIndex = 100,
+ hasMoreLeft = true,
+ hasMoreRight = true,
+ )
+ .stateIn(backgroundScope)
+
+ assertThat(loadState.value).isNull()
+ cursorPreviewsRepository.previewsModel.value.let {
+ assertThat(it).isNotNull()
+ it!!
+ assertThat(it.loadMoreRight).isNotNull()
+ assertThat(it.loadMoreLeft).isNotNull()
+
+ it.loadMoreRight!!.invoke()
+ runCurrent()
+ assertThat(loadState.value).isEqualTo(LoadDirection.Right)
+
+ it.loadMoreLeft!!.invoke()
+ runCurrent()
+ assertThat(loadState.value).isEqualTo(LoadDirection.Left)
+ }
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractorTest.kt
new file mode 100644
index 00000000..570c346c
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractorTest.kt
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor
+
+import android.content.Intent
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.pendingSelectionCallbackRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ShareouselUpdate
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.update.SelectionChangeCallback
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.update.selectionChangeCallback
+import com.android.intentresolver.data.repository.chooserRequestRepository
+import com.android.intentresolver.util.runKosmosTest
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import org.junit.Test
+
+class UpdateChooserRequestInteractorTest {
+ @Test
+ fun updateTargetIntentWithSelection() = runKosmosTest {
+ val selectionCallbackResult = ShareouselUpdate(metadataText = ValueUpdate.Value("update"))
+ selectionChangeCallback = SelectionChangeCallback { selectionCallbackResult }
+
+ backgroundScope.launch { processTargetIntentUpdatesInteractor.activate() }
+
+ updateTargetIntentInteractor.updateTargetIntent(Intent())
+ runCurrent()
+
+ assertThat(pendingSelectionCallbackRepository.pendingTargetIntent.value).isNull()
+ assertThat(chooserRequestRepository.chooserRequest.value.metadataText).isEqualTo("update")
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallbackImplTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallbackImplTest.kt
new file mode 100644
index 00000000..55b32509
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallbackImplTest.kt
@@ -0,0 +1,462 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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.payloadtoggle.domain.update
+
+import android.app.PendingIntent
+import android.content.ComponentName
+import android.content.ContentInterface
+import android.content.Intent
+import android.content.Intent.ACTION_CHOOSER
+import android.content.Intent.ACTION_SEND
+import android.content.Intent.ACTION_SEND_MULTIPLE
+import android.content.Intent.EXTRA_ALTERNATE_INTENTS
+import android.content.Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS
+import android.content.Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION
+import android.content.Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER
+import android.content.Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER
+import android.content.Intent.EXTRA_CHOOSER_TARGETS
+import android.content.Intent.EXTRA_INTENT
+import android.content.Intent.EXTRA_METADATA_TEXT
+import android.content.Intent.EXTRA_STREAM
+import android.graphics.drawable.Icon
+import android.net.Uri
+import android.os.Bundle
+import android.service.chooser.AdditionalContentContract.MethodNames.ON_SELECTION_CHANGED
+import android.service.chooser.ChooserAction
+import android.service.chooser.ChooserTarget
+import android.service.chooser.Flags
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.intentresolver.any
+import com.android.intentresolver.argumentCaptor
+import com.android.intentresolver.capture
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate.Absent
+import com.android.intentresolver.inject.FakeChooserServiceFlags
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import com.google.common.truth.Correspondence
+import com.google.common.truth.Correspondence.BinaryPredicate
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.lang.IllegalArgumentException
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class SelectionChangeCallbackImplTest {
+ private val uri = Uri.parse("content://org.pkg/content-provider")
+ private val chooserIntent = Intent(ACTION_CHOOSER)
+ private val contentResolver = mock<ContentInterface>()
+ private val context = InstrumentationRegistry.getInstrumentation().context
+ private val flags =
+ FakeChooserServiceFlags().apply {
+ setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, false)
+ setFlag(Flags.FLAG_CHOOSER_ALBUM_TEXT, false)
+ setFlag(Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA, false)
+ }
+
+ @Test
+ fun testPayloadChangeCallbackContact() = runTest {
+ val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags)
+
+ val u1 = createUri(1)
+ val u2 = createUri(2)
+ val targetIntent =
+ Intent(ACTION_SEND_MULTIPLE).apply {
+ val uris =
+ ArrayList<Uri>().apply {
+ add(u1)
+ add(u2)
+ }
+ putExtra(EXTRA_STREAM, uris)
+ type = "image/jpg"
+ }
+ testSubject.onSelectionChanged(targetIntent)
+
+ val authorityCaptor = argumentCaptor<String>()
+ val methodCaptor = argumentCaptor<String>()
+ val argCaptor = argumentCaptor<String>()
+ val extraCaptor = argumentCaptor<Bundle>()
+ verify(contentResolver, times(1))
+ .call(
+ capture(authorityCaptor),
+ capture(methodCaptor),
+ capture(argCaptor),
+ capture(extraCaptor)
+ )
+ assertWithMessage("Wrong additional content provider authority")
+ .that(authorityCaptor.value)
+ .isEqualTo(uri.authority)
+ assertWithMessage("Wrong additional content provider #call() method name")
+ .that(methodCaptor.value)
+ .isEqualTo(ON_SELECTION_CHANGED)
+ assertWithMessage("Wrong additional content provider argument value")
+ .that(argCaptor.value)
+ .isEqualTo(uri.toString())
+ val extraBundle = extraCaptor.value
+ assertWithMessage("Additional content provider #call() should have a non-null extras arg.")
+ .that(extraBundle)
+ .isNotNull()
+ requireNotNull(extraBundle)
+ val argChooserIntent = extraBundle.getParcelable(EXTRA_INTENT, Intent::class.java)
+ assertWithMessage("#call() extras arg. should contain Intent#EXTRA_INTENT")
+ .that(argChooserIntent)
+ .isNotNull()
+ requireNotNull(argChooserIntent)
+ assertWithMessage("#call() extras arg's Intent#EXTRA_INTENT should be a Chooser intent")
+ .that(argChooserIntent.action)
+ .isEqualTo(chooserIntent.action)
+ val argTargetIntent = argChooserIntent.getParcelableExtra(EXTRA_INTENT, Intent::class.java)
+ assertWithMessage(
+ "A chooser intent passed into #call() method should contain updated target intent"
+ )
+ .that(argTargetIntent)
+ .isNotNull()
+ requireNotNull(argTargetIntent)
+ assertWithMessage("Incorrect target intent")
+ .that(argTargetIntent.action)
+ .isEqualTo(targetIntent.action)
+ assertWithMessage("Incorrect target intent")
+ .that(argTargetIntent.getParcelableArrayListExtra(EXTRA_STREAM, Uri::class.java))
+ .containsExactly(u1, u2)
+ .inOrder()
+ }
+
+ @Test
+ fun testPayloadChangeCallbackUpdatesCustomActions() = runTest {
+ val a1 =
+ ChooserAction.Builder(
+ Icon.createWithContentUri(createUri(10)),
+ "Action 1",
+ PendingIntent.getBroadcast(
+ context,
+ 1,
+ Intent("test"),
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ )
+ .build()
+ val a2 =
+ ChooserAction.Builder(
+ Icon.createWithContentUri(createUri(11)),
+ "Action 2",
+ PendingIntent.getBroadcast(
+ context,
+ 1,
+ Intent("test"),
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ )
+ .build()
+ whenever(contentResolver.call(any<String>(), any(), any(), any()))
+ .thenReturn(
+ Bundle().apply { putParcelableArray(EXTRA_CHOOSER_CUSTOM_ACTIONS, arrayOf(a1, a2)) }
+ )
+
+ val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags)
+
+ val targetIntent = Intent(ACTION_SEND_MULTIPLE)
+ val result = testSubject.onSelectionChanged(targetIntent)
+ assertWithMessage("Callback result should not be null").that(result).isNotNull()
+ requireNotNull(result)
+ assertWithMessage("Unexpected custom actions")
+ .that(result.customActions.getOrThrow().map { it.icon to it.label })
+ .containsExactly(a1.icon to a1.label, a2.icon to a2.label)
+ .inOrder()
+
+ assertThat(result.modifyShareAction).isEqualTo(Absent)
+ assertThat(result.alternateIntents).isEqualTo(Absent)
+ assertThat(result.callerTargets).isEqualTo(Absent)
+ assertThat(result.refinementIntentSender).isEqualTo(Absent)
+ assertThat(result.resultIntentSender).isEqualTo(Absent)
+ assertThat(result.metadataText).isEqualTo(Absent)
+ }
+
+ @Test
+ fun testPayloadChangeCallbackUpdatesReselectionAction() = runTest {
+ val modifyShare =
+ ChooserAction.Builder(
+ Icon.createWithContentUri(createUri(10)),
+ "Modify Share",
+ PendingIntent.getBroadcast(
+ context,
+ 1,
+ Intent("test"),
+ PendingIntent.FLAG_IMMUTABLE
+ )
+ )
+ .build()
+ whenever(contentResolver.call(any<String>(), any(), any(), any()))
+ .thenReturn(
+ Bundle().apply { putParcelable(EXTRA_CHOOSER_MODIFY_SHARE_ACTION, modifyShare) }
+ )
+
+ val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags)
+
+ val targetIntent = Intent(ACTION_SEND)
+ val result = testSubject.onSelectionChanged(targetIntent)
+ assertWithMessage("Callback result should not be null").that(result).isNotNull()
+ requireNotNull(result)
+ assertWithMessage("Unexpected modify share action: wrong icon")
+ .that(result.modifyShareAction.getOrThrow()?.icon)
+ .isEqualTo(modifyShare.icon)
+ assertWithMessage("Unexpected modify share action: wrong label")
+ .that(result.modifyShareAction.getOrThrow()?.label)
+ .isEqualTo(modifyShare.label)
+
+ assertThat(result.customActions).isEqualTo(Absent)
+ assertThat(result.alternateIntents).isEqualTo(Absent)
+ assertThat(result.callerTargets).isEqualTo(Absent)
+ assertThat(result.refinementIntentSender).isEqualTo(Absent)
+ assertThat(result.resultIntentSender).isEqualTo(Absent)
+ assertThat(result.metadataText).isEqualTo(Absent)
+ }
+
+ @Test
+ fun testPayloadChangeCallbackUpdatesAlternateIntents() = runTest {
+ val alternateIntents =
+ arrayOf(
+ Intent(ACTION_SEND_MULTIPLE).apply {
+ addCategory("test")
+ type = ""
+ }
+ )
+ whenever(contentResolver.call(any<String>(), any(), any(), any()))
+ .thenReturn(
+ Bundle().apply { putParcelableArray(EXTRA_ALTERNATE_INTENTS, alternateIntents) }
+ )
+
+ val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags)
+
+ val targetIntent = Intent(ACTION_SEND)
+ val result = testSubject.onSelectionChanged(targetIntent)
+ assertWithMessage("Callback result should not be null").that(result).isNotNull()
+ requireNotNull(result)
+ assertWithMessage("Wrong number of alternate intents")
+ .that(result.alternateIntents.getOrThrow())
+ .hasSize(1)
+ assertWithMessage("Wrong alternate intent: action")
+ .that(result.alternateIntents.getOrThrow()[0].action)
+ .isEqualTo(alternateIntents[0].action)
+ assertWithMessage("Wrong alternate intent: categories")
+ .that(result.alternateIntents.getOrThrow()[0].categories)
+ .containsExactlyElementsIn(alternateIntents[0].categories)
+ assertWithMessage("Wrong alternate intent: mime type")
+ .that(result.alternateIntents.getOrThrow()[0].type)
+ .isEqualTo(alternateIntents[0].type)
+
+ assertThat(result.customActions).isEqualTo(Absent)
+ assertThat(result.modifyShareAction).isEqualTo(Absent)
+ assertThat(result.callerTargets).isEqualTo(Absent)
+ assertThat(result.refinementIntentSender).isEqualTo(Absent)
+ assertThat(result.resultIntentSender).isEqualTo(Absent)
+ assertThat(result.metadataText).isEqualTo(Absent)
+ }
+
+ @Test
+ fun testPayloadChangeCallbackUpdatesCallerTargets() = runTest {
+ val t1 =
+ ChooserTarget(
+ "Target 1",
+ Icon.createWithContentUri(createUri(1)),
+ 0.99f,
+ ComponentName("org.pkg.app", ".ClassA"),
+ null
+ )
+ val t2 =
+ ChooserTarget(
+ "Target 2",
+ Icon.createWithContentUri(createUri(1)),
+ 1f,
+ ComponentName("org.pkg.app", ".ClassB"),
+ null
+ )
+ whenever(contentResolver.call(any<String>(), any(), any(), any()))
+ .thenReturn(
+ Bundle().apply { putParcelableArray(EXTRA_CHOOSER_TARGETS, arrayOf(t1, t2)) }
+ )
+
+ val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags)
+
+ val targetIntent = Intent(ACTION_SEND)
+ val result = testSubject.onSelectionChanged(targetIntent)
+ assertWithMessage("Callback result should not be null").that(result).isNotNull()
+ requireNotNull(result)
+ assertWithMessage("Wrong caller targets")
+ .that(result.callerTargets.getOrThrow())
+ .comparingElementsUsing(
+ Correspondence.from(
+ BinaryPredicate<ChooserTarget?, ChooserTarget> { actual, expected ->
+ expected.componentName == actual?.componentName &&
+ expected.title == actual?.title &&
+ expected.icon == actual?.icon &&
+ expected.score == actual?.score
+ },
+ ""
+ )
+ )
+ .containsExactly(t1, t2)
+ .inOrder()
+
+ assertThat(result.customActions).isEqualTo(Absent)
+ assertThat(result.modifyShareAction).isEqualTo(Absent)
+ assertThat(result.alternateIntents).isEqualTo(Absent)
+ assertThat(result.refinementIntentSender).isEqualTo(Absent)
+ assertThat(result.resultIntentSender).isEqualTo(Absent)
+ assertThat(result.metadataText).isEqualTo(Absent)
+ }
+
+ @Test
+ fun testPayloadChangeCallbackUpdatesRefinementIntentSender() = runTest {
+ val broadcast =
+ PendingIntent.getBroadcast(context, 1, Intent("test"), PendingIntent.FLAG_IMMUTABLE)
+
+ whenever(contentResolver.call(any<String>(), any(), any(), any()))
+ .thenReturn(
+ Bundle().apply {
+ putParcelable(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER, broadcast.intentSender)
+ }
+ )
+
+ val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags)
+
+ val targetIntent = Intent(ACTION_SEND)
+ val result = testSubject.onSelectionChanged(targetIntent)
+ assertWithMessage("Callback result should not be null").that(result).isNotNull()
+ requireNotNull(result)
+ assertThat(result.customActions).isEqualTo(Absent)
+ assertThat(result.modifyShareAction).isEqualTo(Absent)
+ assertThat(result.alternateIntents).isEqualTo(Absent)
+ assertThat(result.callerTargets).isEqualTo(Absent)
+ assertThat(result.refinementIntentSender.getOrThrow()).isNotNull()
+ assertThat(result.resultIntentSender).isEqualTo(Absent)
+ assertThat(result.metadataText).isEqualTo(Absent)
+ }
+
+ @Test
+ fun testPayloadChangeCallbackUpdatesResultIntentSender() = runTest {
+ val broadcast =
+ PendingIntent.getBroadcast(context, 1, Intent("test"), PendingIntent.FLAG_IMMUTABLE)
+
+ whenever(contentResolver.call(any<String>(), any(), any(), any()))
+ .thenReturn(
+ Bundle().apply {
+ putParcelable(EXTRA_CHOOSER_RESULT_INTENT_SENDER, broadcast.intentSender)
+ }
+ )
+
+ val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags)
+
+ val targetIntent = Intent(ACTION_SEND)
+ val result = testSubject.onSelectionChanged(targetIntent)
+ assertWithMessage("Callback result should not be null").that(result).isNotNull()
+ requireNotNull(result)
+ assertThat(result.customActions).isEqualTo(Absent)
+ assertThat(result.modifyShareAction).isEqualTo(Absent)
+ assertThat(result.alternateIntents).isEqualTo(Absent)
+ assertThat(result.callerTargets).isEqualTo(Absent)
+ assertThat(result.refinementIntentSender).isEqualTo(Absent)
+ assertThat(result.resultIntentSender.getOrThrow()).isNotNull()
+ assertThat(result.metadataText).isEqualTo(Absent)
+ }
+
+ @Test
+ fun testPayloadChangeCallbackUpdatesMetadataTextWithDisabledFlag_noUpdates() = runTest {
+ val metadataText = "[Metadata]"
+ whenever(contentResolver.call(any<String>(), any(), any(), any()))
+ .thenReturn(Bundle().apply { putCharSequence(EXTRA_METADATA_TEXT, metadataText) })
+
+ val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags)
+
+ val targetIntent = Intent(ACTION_SEND)
+ val result = testSubject.onSelectionChanged(targetIntent)
+ assertWithMessage("Callback result should not be null").that(result).isNotNull()
+ requireNotNull(result)
+ assertThat(result.customActions).isEqualTo(Absent)
+ assertThat(result.modifyShareAction).isEqualTo(Absent)
+ assertThat(result.alternateIntents).isEqualTo(Absent)
+ assertThat(result.callerTargets).isEqualTo(Absent)
+ assertThat(result.refinementIntentSender).isEqualTo(Absent)
+ assertThat(result.resultIntentSender).isEqualTo(Absent)
+ assertThat(result.metadataText).isEqualTo(Absent)
+ }
+
+ @Test
+ fun testPayloadChangeCallbackUpdatesMetadataTextWithEnabledFlag_valueUpdated() = runTest {
+ val metadataText = "[Metadata]"
+ flags.setFlag(Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA, true)
+ whenever(contentResolver.call(any<String>(), any(), any(), any()))
+ .thenReturn(Bundle().apply { putCharSequence(EXTRA_METADATA_TEXT, metadataText) })
+
+ val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags)
+
+ val targetIntent = Intent(ACTION_SEND)
+ val result = testSubject.onSelectionChanged(targetIntent)
+ assertWithMessage("Callback result should not be null").that(result).isNotNull()
+ requireNotNull(result)
+ assertThat(result.customActions).isEqualTo(Absent)
+ assertThat(result.modifyShareAction).isEqualTo(Absent)
+ assertThat(result.alternateIntents).isEqualTo(Absent)
+ assertThat(result.callerTargets).isEqualTo(Absent)
+ assertThat(result.refinementIntentSender).isEqualTo(Absent)
+ assertThat(result.resultIntentSender).isEqualTo(Absent)
+ assertThat(result.metadataText.getOrThrow()).isEqualTo(metadataText)
+ }
+
+ @Test
+ fun testPayloadChangeCallbackProvidesInvalidData_invalidDataIgnored() = runTest {
+ flags.setFlag(Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA, true)
+ whenever(contentResolver.call(any<String>(), any(), any(), any()))
+ .thenReturn(
+ Bundle().apply {
+ putParcelableArrayList(EXTRA_CHOOSER_CUSTOM_ACTIONS, ArrayList<ChooserAction>())
+ putParcelable(EXTRA_CHOOSER_MODIFY_SHARE_ACTION, createUri(1))
+ putParcelableArrayList(EXTRA_ALTERNATE_INTENTS, ArrayList<Intent>())
+ putParcelableArrayList(EXTRA_CHOOSER_TARGETS, ArrayList<ChooserTarget>())
+ putParcelable(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER, createUri(2))
+ putParcelable(EXTRA_CHOOSER_RESULT_INTENT_SENDER, createUri(1))
+ putInt(EXTRA_METADATA_TEXT, 123)
+ }
+ )
+
+ val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags)
+
+ val targetIntent = Intent(ACTION_SEND)
+ val result = testSubject.onSelectionChanged(targetIntent)
+ assertWithMessage("Callback result should not be null").that(result).isNotNull()
+ requireNotNull(result)
+ assertThat(result.customActions.getOrThrow()).isEmpty()
+ assertThat(result.modifyShareAction.getOrThrow()).isNull()
+ assertThat(result.alternateIntents.getOrThrow()).isEmpty()
+ assertThat(result.callerTargets.getOrThrow()).isEmpty()
+ assertThat(result.refinementIntentSender.getOrThrow()).isNull()
+ assertThat(result.resultIntentSender.getOrThrow()).isNull()
+ assertThat(result.metadataText.getOrThrow()).isNull()
+ }
+}
+
+private fun <T> ValueUpdate<T>.getOrThrow(): T =
+ when (this) {
+ is ValueUpdate.Value -> value
+ else -> throw IllegalArgumentException("Value is expected")
+ }
+
+private fun createUri(id: Int) = Uri.parse("content://org.pkg.images/$id.png")
diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt
new file mode 100644
index 00000000..35ef6613
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt
@@ -0,0 +1,244 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.intentresolver.contentpreview.payloadtoggle.ui.viewmodel
+
+import android.app.Activity
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.drawable.Icon
+import android.net.Uri
+import com.android.intentresolver.FakeImageLoader
+import com.android.intentresolver.contentpreview.HeadlineGenerator
+import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.activityResultRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.cursorPreviewsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.previewSelectionsRepository
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.PendingIntentSender
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.TargetIntentModifier
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.pendingIntentSender
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.targetIntentModifier
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.chooserRequestInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.customActionsInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.headlineGenerator
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.payloadToggleImageLoader
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.selectablePreviewsInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor.selectionInteractor
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel
+import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewsModel
+import com.android.intentresolver.data.model.ChooserRequest
+import com.android.intentresolver.data.repository.chooserRequestRepository
+import com.android.intentresolver.icon.BitmapIcon
+import com.android.intentresolver.logging.FakeEventLog
+import com.android.intentresolver.logging.eventLog
+import com.android.intentresolver.util.KosmosTestScope
+import com.android.intentresolver.util.comparingElementsUsingTransform
+import com.android.intentresolver.util.runKosmosTest
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.first
+import org.junit.Test
+
+class ShareouselViewModelTest {
+
+ private var Kosmos.viewModelScope: CoroutineScope by Fixture()
+ private val Kosmos.shareouselViewModel: ShareouselViewModel by Fixture {
+ ShareouselViewModelModule.create(
+ interactor = selectablePreviewsInteractor,
+ imageLoader = payloadToggleImageLoader,
+ actionsInteractor = customActionsInteractor,
+ headlineGenerator = headlineGenerator,
+ chooserRequestInteractor = chooserRequestInteractor,
+ selectionInteractor = selectionInteractor,
+ scope = viewModelScope,
+ )
+ }
+
+ @Test
+ fun headline() = runTest {
+ assertThat(shareouselViewModel.headline.first()).isEqualTo("IMAGES: 1")
+ previewSelectionsRepository.selections.value =
+ setOf(
+ PreviewModel(
+ Uri.fromParts("scheme", "ssp", "fragment"),
+ null,
+ ),
+ PreviewModel(
+ Uri.fromParts("scheme1", "ssp1", "fragment1"),
+ null,
+ )
+ )
+ runCurrent()
+ assertThat(shareouselViewModel.headline.first()).isEqualTo("IMAGES: 2")
+ }
+
+ @Test
+ fun metadataText() = runTest {
+ val request =
+ ChooserRequest(
+ targetIntent = Intent(),
+ launchedFromPackage = "",
+ metadataText = "Hello"
+ )
+ chooserRequestRepository.chooserRequest.value = request
+
+ runCurrent()
+
+ assertThat(shareouselViewModel.metadataText.first()).isEqualTo("Hello")
+ }
+
+ @Test
+ fun previews() =
+ runTest(targetIntentModifier = { Intent() }) {
+ cursorPreviewsRepository.previewsModel.value =
+ PreviewsModel(
+ previewModels =
+ setOf(
+ PreviewModel(
+ Uri.fromParts("scheme", "ssp", "fragment"),
+ null,
+ ),
+ PreviewModel(
+ Uri.fromParts("scheme1", "ssp1", "fragment1"),
+ null,
+ )
+ ),
+ startIdx = 1,
+ loadMoreLeft = null,
+ loadMoreRight = null,
+ )
+ runCurrent()
+
+ assertWithMessage("previewsKeys is null")
+ .that(shareouselViewModel.previews.first())
+ .isNotNull()
+ assertThat(shareouselViewModel.previews.first()!!.previewModels)
+ .comparingElementsUsingTransform("has uri of") { it: PreviewModel -> it.uri }
+ .containsExactly(
+ Uri.fromParts("scheme", "ssp", "fragment"),
+ Uri.fromParts("scheme1", "ssp1", "fragment1"),
+ )
+ .inOrder()
+
+ val previewVm =
+ shareouselViewModel.preview(
+ PreviewModel(Uri.fromParts("scheme1", "ssp1", "fragment1"), null)
+ )
+
+ assertWithMessage("preview bitmap is null").that(previewVm.bitmap.first()).isNotNull()
+ assertThat(previewVm.isSelected.first()).isFalse()
+
+ previewVm.setSelected(true)
+
+ assertThat(previewSelectionsRepository.selections.value)
+ .comparingElementsUsingTransform("has uri of") { model: PreviewModel -> model.uri }
+ .contains(Uri.fromParts("scheme1", "ssp1", "fragment1"))
+ }
+
+ @Test
+ fun actions() {
+ runTest {
+ assertThat(shareouselViewModel.actions.first()).isEmpty()
+
+ val bitmap = Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8)
+ val icon = Icon.createWithBitmap(bitmap)
+ var actionSent = false
+ chooserRequestRepository.customActions.value =
+ listOf(
+ CustomActionModel(
+ label = "label1",
+ icon = icon,
+ performAction = { actionSent = true },
+ )
+ )
+ runCurrent()
+
+ assertThat(shareouselViewModel.actions.first())
+ .comparingElementsUsingTransform("has a label of") { vm: ActionChipViewModel ->
+ vm.label
+ }
+ .containsExactly("label1")
+ .inOrder()
+ assertThat(shareouselViewModel.actions.first())
+ .comparingElementsUsingTransform("has an icon of") { vm: ActionChipViewModel ->
+ vm.icon
+ }
+ .containsExactly(BitmapIcon(icon.bitmap))
+ .inOrder()
+
+ shareouselViewModel.actions.first()[0].onClicked()
+
+ assertThat(actionSent).isTrue()
+ assertThat(eventLog.customActionSelected)
+ .isEqualTo(FakeEventLog.CustomActionSelected(0))
+ assertThat(activityResultRepository.activityResult.value).isEqualTo(Activity.RESULT_OK)
+ }
+ }
+
+ private fun runTest(
+ pendingIntentSender: PendingIntentSender = PendingIntentSender {},
+ targetIntentModifier: TargetIntentModifier<PreviewModel> = TargetIntentModifier {
+ error("unexpected invocation")
+ },
+ block: suspend KosmosTestScope.() -> Unit,
+ ): Unit = runKosmosTest {
+ viewModelScope = backgroundScope
+ this.pendingIntentSender = pendingIntentSender
+ this.targetIntentModifier = targetIntentModifier
+ previewSelectionsRepository.selections.value =
+ setOf(PreviewModel(Uri.fromParts("scheme", "ssp", "fragment"), null))
+ payloadToggleImageLoader =
+ FakeImageLoader(
+ initialBitmaps =
+ mapOf(
+ Uri.fromParts("scheme1", "ssp1", "fragment1") to
+ Bitmap.createBitmap(100, 100, Bitmap.Config.ALPHA_8)
+ )
+ )
+ headlineGenerator =
+ object : HeadlineGenerator {
+ override fun getImagesHeadline(count: Int): String = "IMAGES: $count"
+
+ override fun getTextHeadline(text: CharSequence): String = error("not supported")
+
+ override fun getAlbumHeadline(): String = error("not supported")
+
+ override fun getImagesWithTextHeadline(text: CharSequence, count: Int): String =
+ error("not supported")
+
+ override fun getVideosWithTextHeadline(text: CharSequence, count: Int): String =
+ error("not supported")
+
+ override fun getFilesWithTextHeadline(text: CharSequence, count: Int): String =
+ error("not supported")
+
+ override fun getVideosHeadline(count: Int): String = error("not supported")
+
+ override fun getFilesHeadline(count: Int): String = error("not supported")
+ }
+ // instantiate the view model, and then runCurrent() so that it is fully hydrated before
+ // starting the test
+ shareouselViewModel
+ runCurrent()
+ block()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/coroutines/Flow.kt b/tests/unit/src/com/android/intentresolver/coroutines/Flow.kt
index a5677d94..ca60824d 100644
--- a/tests/unit/src/com/android/intentresolver/v2/coroutines/Flow.kt
+++ b/tests/unit/src/com/android/intentresolver/coroutines/Flow.kt
@@ -1,6 +1,6 @@
@file:Suppress("OPT_IN_USAGE")
-package com.android.intentresolver.v2.coroutines
+package com.android.intentresolver.coroutines
/*
* Copyright (C) 2022 The Android Open Source Project
diff --git a/tests/unit/src/com/android/intentresolver/data/repository/FakeUserRepositoryTest.kt b/tests/unit/src/com/android/intentresolver/data/repository/FakeUserRepositoryTest.kt
new file mode 100644
index 00000000..2fad37f2
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/data/repository/FakeUserRepositoryTest.kt
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.data.repository
+
+import com.android.intentresolver.coroutines.collectLastValue
+import com.android.intentresolver.shared.model.User
+import com.google.common.truth.Truth.assertThat
+import kotlin.random.Random
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+
+class FakeUserRepositoryTest {
+ private val baseId = Random.nextInt(1000, 2000)
+
+ private val personalUser = User(id = baseId, role = User.Role.PERSONAL)
+ private val cloneUser = User(id = baseId + 1, role = User.Role.CLONE)
+ private val workUser = User(id = baseId + 2, role = User.Role.WORK)
+ private val privateUser = User(id = baseId + 3, role = User.Role.PRIVATE)
+
+ @Test
+ fun init() = runTest {
+ val repo = FakeUserRepository(listOf(personalUser, workUser, privateUser))
+
+ val users by collectLastValue(repo.users)
+ assertThat(users).containsExactly(personalUser, workUser, privateUser)
+ }
+
+ @Test
+ fun addUser() = runTest {
+ val repo = FakeUserRepository(emptyList())
+
+ val users by collectLastValue(repo.users)
+ assertThat(users).isEmpty()
+
+ repo.addUser(personalUser, true)
+ assertThat(users).containsExactly(personalUser)
+
+ repo.addUser(workUser, false)
+ assertThat(users).containsExactly(personalUser, workUser)
+ }
+
+ @Test
+ fun removeUser() = runTest {
+ val repo = FakeUserRepository(listOf(personalUser, workUser))
+
+ val users by collectLastValue(repo.users)
+ repo.removeUser(workUser)
+ assertThat(users).containsExactly(personalUser)
+
+ repo.removeUser(personalUser)
+ assertThat(users).isEmpty()
+ }
+
+ @Test
+ fun isAvailable_defaultValue() = runTest {
+ val repo = FakeUserRepository(listOf(personalUser, workUser))
+
+ val available by collectLastValue(repo.availability)
+
+ repo.requestState(workUser, false)
+ assertThat(available!![workUser]).isFalse()
+
+ repo.requestState(workUser, true)
+ assertThat(available!![workUser]).isTrue()
+ }
+
+ @Test
+ fun isAvailable() = runTest {
+ val repo = FakeUserRepository(listOf(personalUser, workUser))
+
+ val available by collectLastValue(repo.availability)
+ assertThat(available!![workUser]).isTrue()
+
+ repo.requestState(workUser, false)
+ assertThat(available!![workUser]).isFalse()
+
+ repo.requestState(workUser, true)
+ assertThat(available!![workUser]).isTrue()
+ }
+
+ @Test
+ fun isAvailable_addRemove() = runTest {
+ val repo = FakeUserRepository(listOf(personalUser, workUser))
+
+ val available by collectLastValue(repo.availability)
+ assertThat(available!![workUser]).isTrue()
+
+ repo.removeUser(workUser)
+ assertThat(available!![workUser]).isNull()
+
+ repo.addUser(workUser, true)
+ assertThat(available!![workUser]).isTrue()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt b/tests/unit/src/com/android/intentresolver/data/repository/UserRepositoryImplTest.kt
index 4f514db5..8db0bb56 100644
--- a/tests/unit/src/com/android/intentresolver/v2/data/repository/UserRepositoryImplTest.kt
+++ b/tests/unit/src/com/android/intentresolver/data/repository/UserRepositoryImplTest.kt
@@ -1,18 +1,31 @@
-package com.android.intentresolver.v2.data.repository
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.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.android.intentresolver.coroutines.collectLastValue
+import com.android.intentresolver.platform.FakeUserManager
+import com.android.intentresolver.platform.FakeUserManager.ProfileType
+import com.android.intentresolver.shared.model.User
+import com.android.intentresolver.shared.model.User.Role
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import kotlinx.coroutines.Dispatchers
@@ -20,8 +33,9 @@ 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
+import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
internal class UserRepositoryImplTest {
private val userManager = FakeUserManager()
@@ -34,10 +48,7 @@ internal class UserRepositoryImplTest {
assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull()
assertThat(users)
- .containsExactly(
- userState.primaryUserHandle,
- User(userState.primaryUserHandle.identifier, Role.PERSONAL)
- )
+ .containsExactly(User(userState.primaryUserHandle.identifier, Role.PERSONAL))
}
@Test
@@ -46,10 +57,11 @@ internal class UserRepositoryImplTest {
val users by collectLastValue(repo.users)
assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull()
- assertThat(users!!.values.filter { it.role.type == User.Type.PROFILE }).isEmpty()
+ assertThat(users).hasSize(1)
val profile = userState.createProfile(ProfileType.WORK)
- assertThat(users).containsEntry(profile, User(profile.identifier, Role.WORK))
+ assertThat(users).hasSize(2)
+ assertThat(users).contains(User(profile.identifier, Role.WORK))
}
@Test
@@ -59,47 +71,60 @@ internal class UserRepositoryImplTest {
assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull()
val work = userState.createProfile(ProfileType.WORK)
- assertThat(users).containsEntry(work, User(work.identifier, Role.WORK))
+ assertThat(users).contains(User(work.identifier, Role.WORK))
userState.removeProfile(work)
- assertThat(users).doesNotContainEntry(work, User(work.identifier, Role.WORK))
+ assertThat(users).doesNotContain(User(work.identifier, Role.WORK))
}
@Test
fun isAvailable() = runTest {
val repo = createUserRepository(userManager)
val work = userState.createProfile(ProfileType.WORK)
+ val workUser = User(work.identifier, Role.WORK)
- val available by collectLastValue(repo.isAvailable(work))
- assertThat(available).isTrue()
+ val available by collectLastValue(repo.availability)
+ assertThat(available?.get(workUser)).isTrue()
userState.setQuietMode(work, true)
- assertThat(available).isFalse()
+ assertThat(available?.get(workUser)).isFalse()
userState.setQuietMode(work, false)
- assertThat(available).isTrue()
+ assertThat(available?.get(workUser)).isTrue()
}
@Test
- fun requestState() = runTest {
+ fun onHandleAvailabilityChange_userStateMaintained() = runTest {
val repo = createUserRepository(userManager)
- val work = userState.createProfile(ProfileType.WORK)
+ val private = userState.createProfile(ProfileType.PRIVATE)
+ val privateUser = User(private.identifier, Role.PRIVATE)
+
+ val users by collectLastValue(repo.users)
- val available by collectLastValue(repo.isAvailable(work))
- assertThat(available).isTrue()
+ repo.requestState(privateUser, false)
+ repo.requestState(privateUser, true)
- repo.requestState(work, false)
- assertThat(available).isFalse()
+ assertWithMessage("users.size").that(users?.size ?: 0).isEqualTo(2) // personal + private
- repo.requestState(work, true)
- assertThat(available).isTrue()
+ assertWithMessage("No duplicate IDs")
+ .that(users?.count { it.id == private.identifier })
+ .isEqualTo(1)
}
- @Test(expected = IllegalArgumentException::class)
- fun requestState_invalidForFullUser() = runTest {
+ @Test
+ fun requestState() = runTest {
val repo = createUserRepository(userManager)
- val primaryUser = User(userState.primaryUserHandle.identifier, Role.PERSONAL)
- repo.requestState(primaryUser, available = false)
+ val work = userState.createProfile(ProfileType.WORK)
+ val workUser = User(work.identifier, Role.WORK)
+
+ val available by collectLastValue(repo.availability)
+ assertThat(available?.get(workUser)).isTrue()
+
+ repo.requestState(workUser, false)
+ assertThat(available?.get(workUser)).isFalse()
+
+ repo.requestState(workUser, true)
+ assertThat(available?.get(workUser)).isTrue()
}
/**
@@ -111,13 +136,7 @@ internal class UserRepositoryImplTest {
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 events = flowOf(ProfileAdded(UserHandle.of(UserHandle.USER_NULL)))
val repo =
UserRepositoryImpl(
profileParent = SYSTEM,
@@ -129,20 +148,14 @@ internal class UserRepositoryImplTest {
val users by collectLastValue(repo.users)
assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull()
- assertThat(users).containsExactly(SYSTEM, User(USER_SYSTEM, Role.PERSONAL))
+ assertThat(users).containsExactly(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 events = flowOf(ProfileRemoved(UserHandle.of(UserHandle.USER_NULL)))
val repo =
UserRepositoryImpl(
profileParent = SYSTEM,
@@ -154,36 +167,27 @@ internal class UserRepositoryImplTest {
val users by collectLastValue(repo.users)
assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull()
- assertThat(users).containsExactly(SYSTEM, User(USER_SYSTEM, Role.PERSONAL))
+ assertThat(users).containsExactly(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 events = flowOf(AvailabilityChange(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))
+ assertThat(users).containsExactly(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 events = flowOf(UnknownEvent("UNKNOWN_EVENT"))
val repo =
UserRepositoryImpl(
profileParent = SYSTEM,
@@ -195,28 +199,26 @@ internal class UserRepositoryImplTest {
val users by collectLastValue(repo.users)
assertWithMessage("collectLastValue(repo.users)").that(users).isNotNull()
- assertThat(users).containsExactly(SYSTEM, User(USER_SYSTEM, Role.PERSONAL))
+ assertThat(users).containsExactly(User(USER_SYSTEM, Role.PERSONAL))
}
}
-@Suppress("SameParameterValue", "DEPRECATION")
+@Suppress("SameParameterValue")
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))
+ on { getEnabledProfiles(any()) } doReturn listOf(info)
+ on { getUserInfo(validUser) } doReturn info
+ on { getEnabledProfiles(invalidUser) } doReturn listOf()
+ on { getUserInfo(invalidUser) } doReturn null
}
-private fun TestScope.createUserRepository(userManager: FakeUserManager) =
- UserRepositoryImpl(
+private fun TestScope.createUserRepository(userManager: FakeUserManager): UserRepositoryImpl {
+ return 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/domain/interactor/UserInteractorTest.kt b/tests/unit/src/com/android/intentresolver/domain/interactor/UserInteractorTest.kt
new file mode 100644
index 00000000..4d6f2e5b
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/domain/interactor/UserInteractorTest.kt
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.domain.interactor
+
+import com.android.intentresolver.coroutines.collectLastValue
+import com.android.intentresolver.data.repository.FakeUserRepository
+import com.android.intentresolver.shared.model.Profile
+import com.android.intentresolver.shared.model.Profile.Type.PERSONAL
+import com.android.intentresolver.shared.model.Profile.Type.PRIVATE
+import com.android.intentresolver.shared.model.Profile.Type.WORK
+import com.android.intentresolver.shared.model.User
+import com.android.intentresolver.shared.model.User.Role
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlin.random.Random
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class UserInteractorTest {
+ private val baseId = Random.nextInt(1000, 2000)
+
+ private val personalUser = User(id = baseId, role = Role.PERSONAL)
+ private val cloneUser = User(id = baseId + 1, role = Role.CLONE)
+ private val workUser = User(id = baseId + 2, role = Role.WORK)
+ private val privateUser = User(id = baseId + 3, role = Role.PRIVATE)
+
+ val personalProfile = Profile(PERSONAL, personalUser)
+ val workProfile = Profile(WORK, workUser)
+ val privateProfile = Profile(PRIVATE, privateUser)
+
+ @Test
+ fun launchedByProfile(): Unit = runTest {
+ val profileInteractor =
+ UserInteractor(
+ userRepository = FakeUserRepository(listOf(personalUser, cloneUser)),
+ launchedAs = personalUser.handle
+ )
+
+ val launchedAsProfile by collectLastValue(profileInteractor.launchedAsProfile)
+
+ assertThat(launchedAsProfile).isEqualTo(Profile(PERSONAL, personalUser, cloneUser))
+ }
+
+ @Test
+ fun launchedByProfile_asClone(): Unit = runTest {
+ val profileInteractor =
+ UserInteractor(
+ userRepository = FakeUserRepository(listOf(personalUser, cloneUser)),
+ launchedAs = cloneUser.handle
+ )
+ val profiles by collectLastValue(profileInteractor.launchedAsProfile)
+
+ assertThat(profiles).isEqualTo(Profile(PERSONAL, personalUser, cloneUser))
+ }
+
+ @Test
+ fun profiles_withPersonal(): Unit = runTest {
+ val profileInteractor =
+ UserInteractor(
+ userRepository = FakeUserRepository(listOf(personalUser)),
+ launchedAs = personalUser.handle
+ )
+
+ val profiles by collectLastValue(profileInteractor.profiles)
+
+ assertThat(profiles).containsExactly(Profile(PERSONAL, personalUser))
+ }
+
+ @Test
+ fun profiles_addClone(): Unit = runTest {
+ val fakeUserRepo = FakeUserRepository(listOf(personalUser))
+ val profileInteractor =
+ UserInteractor(userRepository = fakeUserRepo, launchedAs = personalUser.handle)
+
+ val profiles by collectLastValue(profileInteractor.profiles)
+ assertThat(profiles).containsExactly(Profile(PERSONAL, personalUser))
+
+ fakeUserRepo.addUser(cloneUser, available = true)
+ assertThat(profiles).containsExactly(Profile(PERSONAL, personalUser, cloneUser))
+ }
+
+ @Test
+ fun profiles_withPersonalAndClone(): Unit = runTest {
+ val profileInteractor =
+ UserInteractor(
+ userRepository = FakeUserRepository(listOf(personalUser, cloneUser)),
+ launchedAs = personalUser.handle
+ )
+ val profiles by collectLastValue(profileInteractor.profiles)
+
+ assertThat(profiles).containsExactly(Profile(PERSONAL, personalUser, cloneUser))
+ }
+
+ @Test
+ fun profiles_withAllSupportedTypes(): Unit = runTest {
+ val profileInteractor =
+ UserInteractor(
+ userRepository =
+ FakeUserRepository(listOf(personalUser, cloneUser, workUser, privateUser)),
+ launchedAs = personalUser.handle
+ )
+ val profiles by collectLastValue(profileInteractor.profiles)
+
+ assertThat(profiles)
+ .containsExactly(
+ Profile(PERSONAL, personalUser, cloneUser),
+ Profile(WORK, workUser),
+ Profile(PRIVATE, privateUser)
+ )
+ }
+
+ @Test
+ fun profiles_preservesIterationOrder(): Unit = runTest {
+ val profileInteractor =
+ UserInteractor(
+ userRepository =
+ FakeUserRepository(listOf(workUser, cloneUser, privateUser, personalUser)),
+ launchedAs = personalUser.handle
+ )
+
+ val profiles by collectLastValue(profileInteractor.profiles)
+
+ assertThat(profiles)
+ .containsExactly(
+ Profile(WORK, workUser),
+ Profile(PRIVATE, privateUser),
+ Profile(PERSONAL, personalUser, cloneUser),
+ )
+ }
+
+ @Test
+ fun isAvailable_defaultValue() = runTest {
+ val userRepo = FakeUserRepository(listOf(personalUser))
+ userRepo.addUser(workUser, false)
+
+ val interactor = UserInteractor(userRepository = userRepo, launchedAs = personalUser.handle)
+
+ val availability by collectLastValue(interactor.availability)
+
+ assertWithMessage("personalAvailable").that(availability?.get(personalProfile)).isTrue()
+ assertWithMessage("workAvailable").that(availability?.get(workProfile)).isFalse()
+ }
+
+ @Test
+ fun isAvailable() = runTest {
+ val userRepo = FakeUserRepository(listOf(workUser, personalUser))
+ val interactor = UserInteractor(userRepository = userRepo, launchedAs = personalUser.handle)
+
+ val availability by collectLastValue(interactor.availability)
+
+ // Default state is enabled in FakeUserManager
+ assertWithMessage("workAvailable").that(availability?.get(workProfile)).isTrue()
+
+ // Making user unavailable makes profile unavailable
+ userRepo.requestState(workUser, false)
+ assertWithMessage("workAvailable").that(availability?.get(workProfile)).isFalse()
+
+ // Making user available makes profile available again
+ userRepo.requestState(workUser, true)
+ assertWithMessage("workAvailable").that(availability?.get(workProfile)).isTrue()
+
+ // When a user is removed availability is removed as well.
+ userRepo.removeUser(workUser)
+ assertWithMessage("workAvailable").that(availability?.get(workProfile)).isNull()
+ }
+
+ /**
+ * Similar to the above test in reverse: uses UserInteractor to modify state, and verify the
+ * state of the UserRepository.
+ */
+ @Test
+ fun updateState() = runTest {
+ val userRepo = FakeUserRepository(listOf(workUser, personalUser))
+ val userInteractor =
+ UserInteractor(userRepository = userRepo, launchedAs = personalUser.handle)
+ val workProfile = Profile(Profile.Type.WORK, workUser)
+
+ val availability by collectLastValue(userRepo.availability)
+
+ // Default state is enabled in FakeUserManager
+ assertWithMessage("workAvailable").that(availability?.get(workUser)).isTrue()
+
+ userInteractor.updateState(workProfile, false)
+ assertWithMessage("workAvailable").that(availability?.get(workUser)).isFalse()
+
+ userInteractor.updateState(workProfile, true)
+ assertWithMessage("workAvailable").that(availability?.get(workUser)).isTrue()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/emptystate/CrossProfileIntentsCheckerTest.kt b/tests/unit/src/com/android/intentresolver/emptystate/CrossProfileIntentsCheckerTest.kt
index 2bcddf59..8cf87ebe 100644
--- a/tests/unit/src/com/android/intentresolver/emptystate/CrossProfileIntentsCheckerTest.kt
+++ b/tests/unit/src/com/android/intentresolver/emptystate/CrossProfileIntentsCheckerTest.kt
@@ -19,14 +19,14 @@ 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
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
class CrossProfileIntentsCheckerTest {
private val PERSONAL_USER_ID = 10
@@ -38,15 +38,14 @@ class CrossProfileIntentsCheckerTest {
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)
- )
+ on {
+ canForwardTo(
+ any(Intent::class.java),
+ nullable(String::class.java),
+ eq(PERSONAL_USER_ID),
+ eq(WORK_USER_ID)
)
- .thenReturn(true)
+ } doReturn (true)
}
val checker = CrossProfileIntentsChecker(contentResolver, packageManager)
val intents = listOf(Intent())
@@ -57,15 +56,14 @@ class CrossProfileIntentsCheckerTest {
fun testChecker_noCrossProfileIntents() {
val packageManager =
mock<IPackageManager> {
- whenever(
- canForwardTo(
- any(Intent::class.java),
- nullable(String::class.java),
- anyInt(),
- anyInt()
- )
+ on {
+ canForwardTo(
+ any(Intent::class.java),
+ nullable(String::class.java),
+ anyInt(),
+ anyInt()
)
- .thenReturn(false)
+ } doReturn (false)
}
val checker = CrossProfileIntentsChecker(contentResolver, packageManager)
val intents = listOf(Intent())
diff --git a/tests/unit/src/com/android/intentresolver/emptystate/EmptyStateUiHelperTest.kt b/tests/unit/src/com/android/intentresolver/emptystate/EmptyStateUiHelperTest.kt
index bc5545db..174b8d59 100644
--- a/tests/unit/src/com/android/intentresolver/emptystate/EmptyStateUiHelperTest.kt
+++ b/tests/unit/src/com/android/intentresolver/emptystate/EmptyStateUiHelperTest.kt
@@ -20,17 +20,31 @@ 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.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.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.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 emptyStateTitleView: View
- lateinit var emptyStateSubtitleView: View
+ 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
@@ -47,21 +61,26 @@ class EmptyStateUiHelperTest {
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)
+ 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
@@ -105,9 +124,104 @@ class EmptyStateUiHelperTest {
@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/emptystate/NoCrossProfileEmptyStateProviderTest.kt b/tests/unit/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProviderTest.kt
new file mode 100644
index 00000000..fe3e844b
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProviderTest.kt
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.emptystate
+
+import android.content.Intent
+import com.android.intentresolver.ProfileHelper
+import com.android.intentresolver.ResolverListAdapter
+import com.android.intentresolver.annotation.JavaInterop
+import com.android.intentresolver.data.repository.FakeUserRepository
+import com.android.intentresolver.domain.interactor.UserInteractor
+import com.android.intentresolver.inject.FakeIntentResolverFlags
+import com.android.intentresolver.shared.model.User
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyList
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.never
+import org.mockito.kotlin.same
+import org.mockito.kotlin.times
+import org.mockito.kotlin.verify
+
+@OptIn(JavaInterop::class)
+class NoCrossProfileEmptyStateProviderTest {
+
+ private val personalUser = User(0, User.Role.PERSONAL)
+ private val workUser = User(10, User.Role.WORK)
+ private val flags = FakeIntentResolverFlags()
+ private val personalBlocker = mock<EmptyState>()
+ private val workBlocker = mock<EmptyState>()
+
+ private val userRepository = FakeUserRepository(listOf(personalUser, workUser))
+
+ private val personalIntents = listOf(Intent("PERSONAL"))
+ private val personalListAdapter =
+ mock<ResolverListAdapter> {
+ on { userHandle } doReturn personalUser.handle
+ on { intents } doReturn personalIntents
+ }
+ private val workIntents = listOf(Intent("WORK"))
+ private val workListAdapter =
+ mock<ResolverListAdapter> {
+ on { userHandle } doReturn workUser.handle
+ on { intents } doReturn workIntents
+ }
+
+ // Pretend that no intent can ever be forwarded
+ val crossProfileIntentsChecker =
+ mock<CrossProfileIntentsChecker> {
+ on {
+ hasCrossProfileIntents(
+ /* intents = */ anyList(),
+ /* source = */ anyInt(),
+ /* target = */ anyInt()
+ )
+ } doReturn false
+ }
+ private val sourceUserId = argumentCaptor<Int>()
+ private val targetUserId = argumentCaptor<Int>()
+
+ @Test
+ fun testPersonalToWork() {
+ val userInteractor = UserInteractor(userRepository, launchedAs = personalUser.handle)
+
+ val profileHelper =
+ ProfileHelper(
+ userInteractor,
+ CoroutineScope(Dispatchers.Unconfined),
+ Dispatchers.Unconfined,
+ flags
+ )
+
+ val provider =
+ NoCrossProfileEmptyStateProvider(
+ /* profileHelper = */ profileHelper,
+ /* noWorkToPersonalEmptyState = */ personalBlocker,
+ /* noPersonalToWorkEmptyState = */ workBlocker,
+ /* crossProfileIntentsChecker = */ crossProfileIntentsChecker
+ )
+
+ // Personal to personal, not blocked
+ assertThat(provider.getEmptyState(personalListAdapter)).isNull()
+ // Not called because sourceUser == targetUser
+ verify(crossProfileIntentsChecker, never())
+ .hasCrossProfileIntents(anyList(), anyInt(), anyInt())
+
+ // Personal to work, blocked
+ assertThat(provider.getEmptyState(workListAdapter)).isSameInstanceAs(workBlocker)
+
+ verify(crossProfileIntentsChecker, times(1))
+ .hasCrossProfileIntents(
+ same(workIntents),
+ sourceUserId.capture(),
+ targetUserId.capture()
+ )
+ assertThat(sourceUserId.firstValue).isEqualTo(personalUser.id)
+ assertThat(targetUserId.firstValue).isEqualTo(workUser.id)
+ }
+
+ @Test
+ fun testWorkToPersonal() {
+ val userInteractor = UserInteractor(userRepository, launchedAs = workUser.handle)
+
+ val profileHelper =
+ ProfileHelper(
+ userInteractor,
+ CoroutineScope(Dispatchers.Unconfined),
+ Dispatchers.Unconfined,
+ flags
+ )
+
+ val provider =
+ NoCrossProfileEmptyStateProvider(
+ /* profileHelper = */ profileHelper,
+ /* noWorkToPersonalEmptyState = */ personalBlocker,
+ /* noPersonalToWorkEmptyState = */ workBlocker,
+ /* crossProfileIntentsChecker = */ crossProfileIntentsChecker
+ )
+
+ // Work to work, not blocked
+ assertThat(provider.getEmptyState(workListAdapter)).isNull()
+ // Not called because sourceUser == targetUser
+ verify(crossProfileIntentsChecker, never())
+ .hasCrossProfileIntents(anyList(), anyInt(), anyInt())
+
+ // Work to personal, blocked
+ assertThat(provider.getEmptyState(personalListAdapter)).isSameInstanceAs(personalBlocker)
+
+ verify(crossProfileIntentsChecker, times(1))
+ .hasCrossProfileIntents(
+ same(personalIntents),
+ sourceUserId.capture(),
+ targetUserId.capture()
+ )
+ assertThat(sourceUserId.firstValue).isEqualTo(workUser.id)
+ assertThat(targetUserId.firstValue).isEqualTo(personalUser.id)
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/ext/CreationExtrasExtTest.kt b/tests/unit/src/com/android/intentresolver/ext/CreationExtrasExtTest.kt
new file mode 100644
index 00000000..c09047a1
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/ext/CreationExtrasExtTest.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.ext
+
+import android.graphics.Point
+import androidx.core.os.bundleOf
+import androidx.lifecycle.DEFAULT_ARGS_KEY
+import androidx.lifecycle.viewmodel.CreationExtras
+import androidx.lifecycle.viewmodel.MutableCreationExtras
+import androidx.test.ext.truth.os.BundleSubject.assertThat
+import org.junit.Test
+
+class CreationExtrasExtTest {
+ @Test
+ fun addDefaultArgs_addsWhenAbsent() {
+ val creationExtras: CreationExtras = MutableCreationExtras() // empty
+
+ val updated = creationExtras.addDefaultArgs("POINT" to Point(1, 1))
+
+ val defaultArgs = updated[DEFAULT_ARGS_KEY]
+ assertThat(defaultArgs).containsKey("POINT")
+ assertThat(defaultArgs).parcelable<Point>("POINT").marshallsEquallyTo(Point(1, 1))
+ }
+
+ @Test
+ fun addDefaultArgs_addsToExisting() {
+ val creationExtras: CreationExtras =
+ MutableCreationExtras().apply {
+ set(DEFAULT_ARGS_KEY, bundleOf("POINT1" to Point(1, 1)))
+ }
+
+ val updated = creationExtras.addDefaultArgs("POINT2" to Point(2, 2))
+
+ val defaultArgs = updated[DEFAULT_ARGS_KEY]
+ assertThat(defaultArgs).containsKey("POINT1")
+ assertThat(defaultArgs).containsKey("POINT2")
+ assertThat(defaultArgs).parcelable<Point>("POINT1").marshallsEquallyTo(Point(1, 1))
+ assertThat(defaultArgs).parcelable<Point>("POINT2").marshallsEquallyTo(Point(2, 2))
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/ext/IntentExtTest.kt b/tests/unit/src/com/android/intentresolver/ext/IntentExtTest.kt
new file mode 100644
index 00000000..bf1e159c
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/ext/IntentExtTest.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.ext
+
+import android.content.ComponentName
+import android.content.Intent
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.util.function.Predicate
+import org.junit.Test
+
+class IntentExtTest {
+
+ private val hasSendAction =
+ Predicate<Intent> {
+ it?.action == Intent.ACTION_SEND || it?.action == Intent.ACTION_SEND_MULTIPLE
+ }
+
+ @Test
+ fun hasAction() {
+ val sendIntent = Intent(Intent.ACTION_SEND)
+ assertThat(sendIntent.hasAction(Intent.ACTION_SEND)).isTrue()
+ assertThat(sendIntent.hasAction(Intent.ACTION_VIEW)).isFalse()
+ }
+
+ @Test
+ fun hasComponent() {
+ assertThat(Intent().hasComponent()).isFalse()
+ assertThat(Intent().setComponent(ComponentName("A", "B")).hasComponent()).isTrue()
+ }
+
+ @Test
+ fun hasSendAction() {
+ assertThat(Intent(Intent.ACTION_SEND).hasSendAction()).isTrue()
+ assertThat(Intent(Intent.ACTION_SEND_MULTIPLE).hasSendAction()).isTrue()
+ assertThat(Intent(Intent.ACTION_SENDTO).hasSendAction()).isFalse()
+ assertThat(Intent(Intent.ACTION_VIEW).hasSendAction()).isFalse()
+ }
+
+ @Test
+ fun hasSingleCategory() {
+ val intent = Intent().addCategory(Intent.CATEGORY_HOME)
+ assertThat(intent.hasSingleCategory(Intent.CATEGORY_HOME)).isTrue()
+ assertThat(intent.hasSingleCategory(Intent.CATEGORY_DEFAULT)).isFalse()
+
+ intent.addCategory(Intent.CATEGORY_TEST)
+ assertThat(intent.hasSingleCategory(Intent.CATEGORY_TEST)).isFalse()
+ }
+
+ @Test
+ fun ifMatch_matched() {
+ val sendIntent = Intent(Intent.ACTION_SEND)
+ val sendMultipleIntent = Intent(Intent.ACTION_SEND_MULTIPLE)
+
+ sendIntent.ifMatch(hasSendAction) { addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) }
+ sendMultipleIntent.ifMatch(hasSendAction) { addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) }
+ assertWithMessage("sendIntent flags")
+ .that(sendIntent.flags)
+ .isEqualTo(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL)
+ assertWithMessage("sendMultipleIntent flags")
+ .that(sendMultipleIntent.flags)
+ .isEqualTo(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL)
+ }
+
+ @Test
+ fun ifMatch_notMatched() {
+ val viewIntent = Intent(Intent.ACTION_VIEW)
+
+ viewIntent.ifMatch(hasSendAction) { addFlags(Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) }
+ assertWithMessage("viewIntent flags").that(viewIntent.flags).isEqualTo(0)
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/logging/EventLogImplTest.java b/tests/unit/src/com/android/intentresolver/logging/EventLogImplTest.java
index d75ea99b..feb277ea 100644
--- a/tests/unit/src/com/android/intentresolver/logging/EventLogImplTest.java
+++ b/tests/unit/src/com/android/intentresolver/logging/EventLogImplTest.java
@@ -32,10 +32,10 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.content.Intent;
import android.metrics.LogMaker;
+import com.android.intentresolver.contentpreview.ContentPreviewType;
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;
diff --git a/tests/unit/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java b/tests/unit/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java
index 2140a67d..5cec9734 100644
--- a/tests/unit/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java
+++ b/tests/unit/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java
@@ -25,7 +25,7 @@ import android.content.pm.ActivityInfo;
import android.content.pm.ResolveInfo;
import android.os.Message;
-import androidx.test.InstrumentationRegistry;
+import androidx.test.platform.app.InstrumentationRegistry;
import com.android.intentresolver.ResolvedComponentInfo;
import com.android.intentresolver.chooser.TargetInfo;
@@ -47,7 +47,7 @@ public class AbstractResolverComparatorTest {
ResolvedComponentInfo r2 = createResolvedComponentInfo(
new ComponentName("zackage", "zlass"));
- Context context = InstrumentationRegistry.getTargetContext();
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
AbstractResolverComparator comparator = getTestComparator(context, null);
assertEquals("Pinned ranks over unpinned", -1, comparator.compare(r1, r2));
@@ -64,7 +64,7 @@ public class AbstractResolverComparatorTest {
new ComponentName("zackage", "zlass"));
r2.setPinned(true);
- Context context = InstrumentationRegistry.getTargetContext();
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
AbstractResolverComparator comparator = getTestComparator(context, null);
assertEquals("Both pinned should rank alphabetically", -1, comparator.compare(r1, r2));
@@ -78,7 +78,7 @@ public class AbstractResolverComparatorTest {
ResolvedComponentInfo r2 = createResolvedComponentInfo(
new ComponentName("package", "class"));
- Context context = InstrumentationRegistry.getTargetContext();
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
AbstractResolverComparator comparator = getTestComparator(context, promoteToFirst);
assertEquals("PromoteToFirst ranks over non-cemented", -1, comparator.compare(r1, r2));
@@ -94,7 +94,7 @@ public class AbstractResolverComparatorTest {
new ComponentName("package", "class"));
r2.setPinned(true);
- Context context = InstrumentationRegistry.getTargetContext();
+ Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
AbstractResolverComparator comparator = getTestComparator(context, cementedComponent);
assertEquals("PromoteToFirst ranks over pinned", -1, comparator.compare(r1, r2));
diff --git a/tests/unit/src/com/android/intentresolver/v2/platform/FakeSecureSettingsTest.kt b/tests/unit/src/com/android/intentresolver/platform/FakeSecureSettingsTest.kt
index 04c7093d..fd74b50a 100644
--- a/tests/unit/src/com/android/intentresolver/v2/platform/FakeSecureSettingsTest.kt
+++ b/tests/unit/src/com/android/intentresolver/platform/FakeSecureSettingsTest.kt
@@ -1,4 +1,20 @@
-package com.android.intentresolver.v2.platform
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.platform
import com.google.common.truth.Truth.assertThat
diff --git a/tests/unit/src/com/android/intentresolver/v2/platform/FakeUserManagerTest.kt b/tests/unit/src/com/android/intentresolver/platform/FakeUserManagerTest.kt
index a2239192..fdc32207 100644
--- a/tests/unit/src/com/android/intentresolver/v2/platform/FakeUserManagerTest.kt
+++ b/tests/unit/src/com/android/intentresolver/platform/FakeUserManagerTest.kt
@@ -1,10 +1,26 @@
-package com.android.intentresolver.v2.platform
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.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.android.intentresolver.platform.FakeUserManager.ProfileType
import com.google.common.truth.Correspondence
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
diff --git a/tests/unit/src/com/android/intentresolver/v2/platform/NearbyShareModuleTest.kt b/tests/unit/src/com/android/intentresolver/platform/NearbyShareModuleTest.kt
index fd5c8b3f..71ef2919 100644
--- a/tests/unit/src/com/android/intentresolver/v2/platform/NearbyShareModuleTest.kt
+++ b/tests/unit/src/com/android/intentresolver/platform/NearbyShareModuleTest.kt
@@ -1,17 +1,29 @@
-package com.android.intentresolver.v2.platform
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.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
@@ -58,8 +70,8 @@ class NearbyShareModuleTest {
val nearbyShareComponent = NearbyShareModule.nearbyShareComponent(resources, secureSettings)
- assertThat(nearbyShareComponent).hasValue(
- ComponentName.unflattenFromString("com.example/.ComponentName"))
+ assertThat(nearbyShareComponent)
+ .hasValue(ComponentName.unflattenFromString("com.example/.ComponentName"))
}
@Test
@@ -77,7 +89,7 @@ class NearbyShareModuleTest {
val nearbyShareComponent = NearbyShareModule.nearbyShareComponent(resources, secureSettings)
- assertThat(nearbyShareComponent).hasValue(
- ComponentName.unflattenFromString("com.example/.BComponent"))
+ assertThat(nearbyShareComponent)
+ .hasValue(ComponentName.unflattenFromString("com.example/.BComponent"))
}
}
diff --git a/tests/unit/src/com/android/intentresolver/v2/MultiProfilePagerAdapterTest.kt b/tests/unit/src/com/android/intentresolver/profiles/MultiProfilePagerAdapterTest.kt
index f5dc0935..edeb5c8c 100644
--- a/tests/unit/src/com/android/intentresolver/v2/MultiProfilePagerAdapterTest.kt
+++ b/tests/unit/src/com/android/intentresolver/profiles/MultiProfilePagerAdapterTest.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.intentresolver.v2
+package com.android.intentresolver.profiles
import android.os.UserHandle
import android.view.LayoutInflater
@@ -22,12 +22,12 @@ 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.profiles.MultiProfilePagerAdapter.PROFILE_PERSONAL
+import com.android.intentresolver.profiles.MultiProfilePagerAdapter.PROFILE_WORK
import com.android.intentresolver.whenever
import com.google.common.collect.ImmutableList
import com.google.common.truth.Truth.assertThat
@@ -55,7 +55,15 @@ class MultiProfilePagerAdapterTest {
{ listView: ListView, bindAdapter: ResolverListAdapter ->
listView.setAdapter(bindAdapter)
},
- ImmutableList.of(personalListAdapter),
+ ImmutableList.of(
+ TabConfig(
+ PROFILE_PERSONAL,
+ "personal",
+ "personal_a11y",
+ "TAG_PERSONAL",
+ personalListAdapter
+ )
+ ),
object : EmptyStateProvider {},
{ false },
PROFILE_PERSONAL,
@@ -67,9 +75,8 @@ class MultiProfilePagerAdapterTest {
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.getPageAdapterForIndex(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)
@@ -89,7 +96,16 @@ class MultiProfilePagerAdapterTest {
{ listView: ListView, bindAdapter: ResolverListAdapter ->
listView.setAdapter(bindAdapter)
},
- ImmutableList.of(personalListAdapter, workListAdapter),
+ ImmutableList.of(
+ TabConfig(
+ PROFILE_PERSONAL,
+ "personal",
+ "personal_a11y",
+ "TAG_PERSONAL",
+ personalListAdapter
+ ),
+ TabConfig(PROFILE_WORK, "work", "work_a11y", "TAG_WORK", workListAdapter)
+ ),
object : EmptyStateProvider {},
{ false },
PROFILE_PERSONAL,
@@ -101,10 +117,9 @@ class MultiProfilePagerAdapterTest {
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.getPageAdapterForIndex(0)).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.getPageAdapterForIndex(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)
@@ -128,7 +143,16 @@ class MultiProfilePagerAdapterTest {
{ listView: ListView, bindAdapter: ResolverListAdapter ->
listView.setAdapter(bindAdapter)
},
- ImmutableList.of(personalListAdapter, workListAdapter),
+ ImmutableList.of(
+ TabConfig(
+ PROFILE_PERSONAL,
+ "personal",
+ "personal_a11y",
+ "TAG_PERSONAL",
+ personalListAdapter
+ ),
+ TabConfig(PROFILE_WORK, "work", "work_a11y", "TAG_WORK", workListAdapter)
+ ),
object : EmptyStateProvider {},
{ false },
PROFILE_WORK, // <-- This test specifically requests we start on work profile.
@@ -140,10 +164,9 @@ class MultiProfilePagerAdapterTest {
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.getPageAdapterForIndex(0)).isSameInstanceAs(personalListAdapter)
+ assertThat(pagerAdapter.getPageAdapterForIndex(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)
@@ -163,7 +186,15 @@ class MultiProfilePagerAdapterTest {
{ listView: ListView, bindAdapter: ResolverListAdapter ->
listView.setAdapter(bindAdapter)
},
- ImmutableList.of(personalListAdapter),
+ ImmutableList.of(
+ TabConfig(
+ PROFILE_PERSONAL,
+ "personal",
+ "personal_a11y",
+ "TAG_PERSONAL",
+ personalListAdapter
+ )
+ ),
object : EmptyStateProvider {},
{ false },
PROFILE_PERSONAL,
@@ -194,7 +225,15 @@ class MultiProfilePagerAdapterTest {
{ listView: ListView, bindAdapter: ResolverListAdapter ->
listView.setAdapter(bindAdapter)
},
- ImmutableList.of(personalListAdapter),
+ ImmutableList.of(
+ TabConfig(
+ PROFILE_PERSONAL,
+ "personal",
+ "personal_a11y",
+ "TAG_PERSONAL",
+ personalListAdapter
+ )
+ ),
object : EmptyStateProvider {},
{ false },
PROFILE_PERSONAL,
@@ -236,7 +275,16 @@ class MultiProfilePagerAdapterTest {
{ listView: ListView, bindAdapter: ResolverListAdapter ->
listView.setAdapter(bindAdapter)
},
- ImmutableList.of(personalListAdapter, workListAdapter),
+ ImmutableList.of(
+ TabConfig(
+ PROFILE_PERSONAL,
+ "personal",
+ "personal_a11y",
+ "TAG_PERSONAL",
+ personalListAdapter
+ ),
+ TabConfig(PROFILE_WORK, "work", "work_a11y", "TAG_WORK", workListAdapter)
+ ),
object : EmptyStateProvider {},
{ true }, // <-- Work mode is quiet.
PROFILE_WORK,
@@ -270,7 +318,16 @@ class MultiProfilePagerAdapterTest {
{ listView: ListView, bindAdapter: ResolverListAdapter ->
listView.setAdapter(bindAdapter)
},
- ImmutableList.of(personalListAdapter, workListAdapter),
+ ImmutableList.of(
+ TabConfig(
+ PROFILE_PERSONAL,
+ "personal",
+ "personal_a11y",
+ "TAG_PERSONAL",
+ personalListAdapter
+ ),
+ TabConfig(PROFILE_WORK, "work", "work_a11y", "TAG_WORK", workListAdapter)
+ ),
object : EmptyStateProvider {},
{ false }, // <-- Work mode is not quiet.
PROFILE_WORK,
diff --git a/tests/unit/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/tests/unit/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
index 43d0df79..4eeae872 100644
--- a/tests/unit/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
+++ b/tests/unit/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
@@ -36,6 +36,7 @@ import com.android.intentresolver.createShareShortcutInfo
import com.android.intentresolver.createShortcutInfo
import com.android.intentresolver.mock
import com.android.intentresolver.whenever
+import com.google.common.truth.Truth.assertWithMessage
import java.util.function.Consumer
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScheduler
@@ -395,6 +396,47 @@ class ShortcutLoaderTest {
}
@Test
+ fun test_nullIntentFilterNoAppAppPredictorResults_returnEmptyResult() =
+ scope.runTest {
+ val shortcutManager = mock<ShortcutManager>()
+ whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
+ val testSubject =
+ ShortcutLoader(
+ context,
+ backgroundScope,
+ appPredictor,
+ UserHandle.of(0),
+ isPersonalProfile = true,
+ targetIntentFilter = null,
+ 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())
+
+ verify(shortcutManager, never()).getShareTargets(any())
+ val resultCaptor = argumentCaptor<ShortcutLoader.Result>()
+ verify(callback, times(1)).accept(capture(resultCaptor))
+
+ val result = resultCaptor.value
+ assertWithMessage("A ShortcutManager result is expected")
+ .that(result.isFromAppPredictor)
+ .isFalse()
+ assertArrayEquals(
+ "Wrong input app targets in the result",
+ appTargets,
+ result.appTargets
+ )
+ assertWithMessage("An empty result is expected").that(result.shortcutsByApp).isEmpty()
+ }
+
+ @Test
fun test_workProfileNotRunning_doNotCallServices() {
testDisabledWorkProfileDoNotCallSystem(isUserRunning = false)
}
diff --git a/tests/unit/src/com/android/intentresolver/ui/ShareResultSenderImplTest.kt b/tests/unit/src/com/android/intentresolver/ui/ShareResultSenderImplTest.kt
new file mode 100644
index 00000000..c254a856
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/ui/ShareResultSenderImplTest.kt
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.ui
+
+import android.app.PendingIntent
+import android.compat.testing.PlatformCompatChangeRule
+import android.content.ComponentName
+import android.content.Intent
+import android.os.Process
+import android.service.chooser.ChooserResult
+import android.service.chooser.Flags
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.intentresolver.inject.FakeChooserServiceFlags
+import com.android.intentresolver.ui.model.ShareAction
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges
+import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.TestRule
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class ShareResultSenderImplTest {
+
+ private val context = InstrumentationRegistry.getInstrumentation().context
+
+ @get:Rule val compatChangeRule: TestRule = PlatformCompatChangeRule()
+
+ val flags = FakeChooserServiceFlags()
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @EnableCompatChanges(ChooserResult.SEND_CHOOSER_RESULT)
+ @Test
+ fun onComponentSelected_chooserResultEnabled() = runTest {
+ val pi = PendingIntent.getBroadcast(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
+ val deferred = CompletableDeferred<Intent>()
+ val intentDispatcher = IntentSenderDispatcher { _, intent -> deferred.complete(intent) }
+
+ flags.setFlag(Flags.FLAG_ENABLE_CHOOSER_RESULT, true)
+
+ val resultSender =
+ ShareResultSenderImpl(
+ flags = flags,
+ scope = this,
+ backgroundDispatcher = UnconfinedTestDispatcher(testScheduler),
+ callerUid = Process.myUid(),
+ resultSender = pi.intentSender,
+ intentDispatcher = intentDispatcher
+ )
+
+ resultSender.onComponentSelected(ComponentName("example.com", "Foo"), true)
+ runCurrent()
+
+ val intentReceived = deferred.await()
+ val chooserResult =
+ intentReceived.getParcelableExtra(
+ Intent.EXTRA_CHOOSER_RESULT,
+ ChooserResult::class.java
+ )
+ assertThat(chooserResult).isNotNull()
+ assertThat(chooserResult?.type).isEqualTo(ChooserResult.CHOOSER_RESULT_SELECTED_COMPONENT)
+ assertThat(chooserResult?.selectedComponent).isEqualTo(ComponentName("example.com", "Foo"))
+ assertThat(chooserResult?.isShortcut).isTrue()
+ }
+
+ @DisableCompatChanges(ChooserResult.SEND_CHOOSER_RESULT)
+ @Test
+ fun onComponentSelected_chooserResultDisabled() = runTest {
+ val pi = PendingIntent.getBroadcast(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
+ val deferred = CompletableDeferred<Intent>()
+ val intentDispatcher = IntentSenderDispatcher { _, intent -> deferred.complete(intent) }
+
+ flags.setFlag(Flags.FLAG_ENABLE_CHOOSER_RESULT, true)
+
+ val resultSender =
+ ShareResultSenderImpl(
+ flags = flags,
+ scope = this,
+ backgroundDispatcher = UnconfinedTestDispatcher(testScheduler),
+ callerUid = Process.myUid(),
+ resultSender = pi.intentSender,
+ intentDispatcher = intentDispatcher
+ )
+
+ resultSender.onComponentSelected(ComponentName("example.com", "Foo"), true)
+ runCurrent()
+
+ val intentReceived = deferred.await()
+ val componentName =
+ intentReceived.getParcelableExtra(
+ Intent.EXTRA_CHOSEN_COMPONENT,
+ ComponentName::class.java
+ )
+
+ assertWithMessage("EXTRA_CHOSEN_COMPONENT from received intent")
+ .that(componentName)
+ .isEqualTo(ComponentName("example.com", "Foo"))
+
+ assertWithMessage("received intent has EXTRA_CHOOSER_RESULT")
+ .that(intentReceived.hasExtra(Intent.EXTRA_CHOOSER_RESULT))
+ .isFalse()
+ }
+
+ @EnableCompatChanges(ChooserResult.SEND_CHOOSER_RESULT)
+ @Test
+ fun onActionSelected_chooserResultEnabled() = runTest {
+ val pi = PendingIntent.getBroadcast(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
+ val deferred = CompletableDeferred<Intent>()
+ val intentDispatcher = IntentSenderDispatcher { _, intent -> deferred.complete(intent) }
+
+ flags.setFlag(Flags.FLAG_ENABLE_CHOOSER_RESULT, true)
+
+ val resultSender =
+ ShareResultSenderImpl(
+ flags = flags,
+ scope = this,
+ backgroundDispatcher = UnconfinedTestDispatcher(testScheduler),
+ callerUid = Process.myUid(),
+ resultSender = pi.intentSender,
+ intentDispatcher = intentDispatcher
+ )
+
+ resultSender.onActionSelected(ShareAction.SYSTEM_COPY)
+ runCurrent()
+
+ val intentReceived = deferred.await()
+ val chosenComponent =
+ intentReceived.getParcelableExtra(
+ Intent.EXTRA_CHOSEN_COMPONENT,
+ ChooserResult::class.java
+ )
+ assertThat(chosenComponent).isNull()
+
+ val chooserResult =
+ intentReceived.getParcelableExtra(
+ Intent.EXTRA_CHOOSER_RESULT,
+ ChooserResult::class.java
+ )
+ assertThat(chooserResult).isNotNull()
+ assertThat(chooserResult?.type).isEqualTo(ChooserResult.CHOOSER_RESULT_COPY)
+ assertThat(chooserResult?.selectedComponent).isNull()
+ assertThat(chooserResult?.isShortcut).isFalse()
+ }
+
+ @DisableCompatChanges(ChooserResult.SEND_CHOOSER_RESULT)
+ @Test
+ fun onActionSelected_chooserResultDisabled() = runTest {
+ val pi = PendingIntent.getBroadcast(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE)
+ val deferred = CompletableDeferred<Intent>()
+ val intentDispatcher = IntentSenderDispatcher { _, intent -> deferred.complete(intent) }
+
+ flags.setFlag(Flags.FLAG_ENABLE_CHOOSER_RESULT, true)
+
+ val resultSender =
+ ShareResultSenderImpl(
+ flags = flags,
+ scope = this,
+ backgroundDispatcher = UnconfinedTestDispatcher(testScheduler),
+ callerUid = Process.myUid(),
+ resultSender = pi.intentSender,
+ intentDispatcher = intentDispatcher
+ )
+
+ resultSender.onActionSelected(ShareAction.SYSTEM_COPY)
+ runCurrent()
+
+ // No result should have been sent, this should never complete
+ assertWithMessage("deferred result isComplete").that(deferred.isCompleted).isFalse()
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/ui/model/ActivityModelTest.kt b/tests/unit/src/com/android/intentresolver/ui/model/ActivityModelTest.kt
new file mode 100644
index 00000000..737f02fe
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/ui/model/ActivityModelTest.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.ui.model
+
+import android.content.Intent
+import android.content.Intent.ACTION_CHOOSER
+import android.content.Intent.EXTRA_TEXT
+import android.net.Uri
+import com.android.intentresolver.ext.toParcelAndBack
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+
+class ActivityModelTest {
+
+ @Test
+ fun testDefaultValues() {
+ val input = ActivityModel(Intent(ACTION_CHOOSER), 0, "example.com", null)
+
+ val output = input.toParcelAndBack()
+
+ assertEquals(input, output)
+ }
+
+ @Test
+ fun testCommonValues() {
+ val intent = Intent(ACTION_CHOOSER).apply { putExtra(EXTRA_TEXT, "Test") }
+ val input =
+ ActivityModel(intent, 1234, "com.example", Uri.parse("android-app://example.com"))
+
+ val output = input.toParcelAndBack()
+
+ assertEquals(input, output)
+ }
+
+ @Test
+ fun testReferrerPackage_withAppReferrer_usesReferrer() {
+ val launch1 =
+ ActivityModel(
+ intent = Intent(),
+ launchedFromUid = 1000,
+ launchedFromPackage = "other.example.com",
+ referrer = Uri.parse("android-app://app.example.com")
+ )
+
+ assertThat(launch1.referrerPackage).isEqualTo("app.example.com")
+ }
+
+ @Test
+ fun testReferrerPackage_httpReferrer_isNull() {
+ val launch =
+ ActivityModel(
+ intent = Intent(),
+ launchedFromUid = 1000,
+ launchedFromPackage = "example.com",
+ referrer = Uri.parse("http://some.other.value")
+ )
+
+ assertThat(launch.referrerPackage).isNull()
+ }
+
+ @Test
+ fun testReferrerPackage_nullReferrer_isNull() {
+ val launch =
+ ActivityModel(
+ intent = Intent(),
+ launchedFromUid = 1000,
+ launchedFromPackage = "example.com",
+ referrer = null
+ )
+
+ assertThat(launch.referrerPackage).isNull()
+ }
+
+ private fun assertEquals(expected: ActivityModel, actual: ActivityModel) {
+ // Test fields separately: Intent does not override equals()
+ assertWithMessage("%s.filterEquals(%s)", actual.intent, expected.intent)
+ .that(actual.intent.filterEquals(expected.intent))
+ .isTrue()
+
+ assertWithMessage("actual fromUid is equal to expected")
+ .that(actual.launchedFromUid)
+ .isEqualTo(expected.launchedFromUid)
+
+ assertWithMessage("actual fromPackage is equal to expected")
+ .that(actual.launchedFromPackage)
+ .isEqualTo(expected.launchedFromPackage)
+
+ assertWithMessage("actual referrer is equal to expected")
+ .that(actual.referrer)
+ .isEqualTo(expected.referrer)
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/ui/viewmodel/ChooserRequestTest.kt b/tests/unit/src/com/android/intentresolver/ui/viewmodel/ChooserRequestTest.kt
new file mode 100644
index 00000000..56c019fd
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/ui/viewmodel/ChooserRequestTest.kt
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.ui.viewmodel
+
+import android.content.Intent
+import android.content.Intent.ACTION_CHOOSER
+import android.content.Intent.ACTION_SEND
+import android.content.Intent.ACTION_SEND_MULTIPLE
+import android.content.Intent.ACTION_VIEW
+import android.content.Intent.EXTRA_ALTERNATE_INTENTS
+import android.content.Intent.EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI
+import android.content.Intent.EXTRA_CHOOSER_FOCUSED_ITEM_POSITION
+import android.content.Intent.EXTRA_INTENT
+import android.content.Intent.EXTRA_REFERRER
+import android.net.Uri
+import android.service.chooser.Flags
+import androidx.core.net.toUri
+import androidx.core.os.bundleOf
+import com.android.intentresolver.ContentTypeHint
+import com.android.intentresolver.data.model.ChooserRequest
+import com.android.intentresolver.inject.FakeChooserServiceFlags
+import com.android.intentresolver.ui.model.ActivityModel
+import com.android.intentresolver.validation.Importance
+import com.android.intentresolver.validation.Invalid
+import com.android.intentresolver.validation.NoValue
+import com.android.intentresolver.validation.Valid
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+private fun createActivityModel(
+ targetIntent: Intent?,
+ referrer: Uri? = null,
+ additionalIntents: List<Intent>? = null
+) =
+ ActivityModel(
+ Intent(ACTION_CHOOSER).apply {
+ targetIntent?.also { putExtra(EXTRA_INTENT, it) }
+ additionalIntents?.also { putExtra(EXTRA_ALTERNATE_INTENTS, it.toTypedArray()) }
+ },
+ launchedFromUid = 10000,
+ launchedFromPackage = "com.android.example",
+ referrer = referrer ?: "android-app://com.android.example".toUri()
+ )
+
+class ChooserRequestTest {
+
+ private val fakeChooserServiceFlags =
+ FakeChooserServiceFlags().apply {
+ setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, false)
+ setFlag(Flags.FLAG_CHOOSER_ALBUM_TEXT, false)
+ setFlag(Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA, false)
+ }
+
+ @Test
+ fun missingIntent() {
+ val model = createActivityModel(targetIntent = null)
+ val result = readChooserRequest(model, fakeChooserServiceFlags)
+
+ assertThat(result).isInstanceOf(Invalid::class.java)
+ result as Invalid<ChooserRequest>
+
+ assertThat(result.errors)
+ .containsExactly(NoValue(EXTRA_INTENT, Importance.CRITICAL, Intent::class))
+ }
+
+ @Test
+ fun referrerFillIn() {
+ val referrer = Uri.parse("android-app://example.com")
+ val model = createActivityModel(targetIntent = Intent(ACTION_SEND), referrer)
+ model.intent.putExtras(bundleOf(EXTRA_REFERRER to referrer))
+
+ val result = readChooserRequest(model, fakeChooserServiceFlags)
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<ChooserRequest>
+
+ val fillIn = result.value.getReferrerFillInIntent()
+ assertThat(fillIn.hasExtra(EXTRA_REFERRER)).isTrue()
+ assertThat(fillIn.getParcelableExtra(EXTRA_REFERRER, Uri::class.java)).isEqualTo(referrer)
+ }
+
+ @Test
+ fun referrerPackage_isNullWithNonAppReferrer() {
+ val referrer = Uri.parse("http://example.com")
+ val intent = Intent().putExtras(bundleOf(EXTRA_INTENT to Intent(ACTION_SEND)))
+
+ val model = createActivityModel(targetIntent = intent, referrer = referrer)
+
+ val result = readChooserRequest(model, fakeChooserServiceFlags)
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<ChooserRequest>
+
+ assertThat(result.value.referrerPackage).isNull()
+ }
+
+ @Test
+ fun referrerPackage_fromAppReferrer() {
+ val referrer = Uri.parse("android-app://example.com")
+ val model = createActivityModel(targetIntent = Intent(ACTION_SEND), referrer)
+
+ model.intent.putExtras(bundleOf(EXTRA_REFERRER to referrer))
+
+ val result = readChooserRequest(model, fakeChooserServiceFlags)
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<ChooserRequest>
+
+ assertThat(result.value.referrerPackage).isEqualTo(referrer.authority)
+ }
+
+ @Test
+ fun payloadIntents_includesTargetThenAdditional() {
+ val intent1 = Intent(ACTION_SEND)
+ val intent2 = Intent(ACTION_SEND_MULTIPLE)
+ val model = createActivityModel(targetIntent = intent1, additionalIntents = listOf(intent2))
+
+ val result = readChooserRequest(model, fakeChooserServiceFlags)
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<ChooserRequest>
+
+ assertThat(result.value.payloadIntents).containsExactly(intent1, intent2)
+ }
+
+ @Test
+ fun testRequest_withOnlyRequiredValues() {
+ val intent = Intent().putExtras(bundleOf(EXTRA_INTENT to Intent(ACTION_SEND)))
+ val model = createActivityModel(targetIntent = intent)
+
+ val result = readChooserRequest(model, fakeChooserServiceFlags)
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<ChooserRequest>
+
+ assertThat(result.value.launchedFromPackage).isEqualTo(model.launchedFromPackage)
+ }
+
+ @Test
+ fun testRequest_actionSendWithAdditionalContentUri() {
+ fakeChooserServiceFlags.setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, true)
+ val uri = Uri.parse("content://org.pkg/path")
+ val position = 10
+ val model =
+ createActivityModel(targetIntent = Intent(ACTION_SEND)).apply {
+ intent.putExtra(EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI, uri)
+ intent.putExtra(EXTRA_CHOOSER_FOCUSED_ITEM_POSITION, position)
+ }
+
+ val result = readChooserRequest(model, fakeChooserServiceFlags)
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<ChooserRequest>
+
+ assertThat(result.value.additionalContentUri).isEqualTo(uri)
+ assertThat(result.value.focusedItemPosition).isEqualTo(position)
+ }
+
+ @Test
+ fun testRequest_actionSendWithAdditionalContentUri_parametersIgnoredWhenFlagDisabled() {
+ fakeChooserServiceFlags.setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, false)
+ val uri = Uri.parse("content://org.pkg/path")
+ val position = 10
+ val model =
+ createActivityModel(targetIntent = Intent(ACTION_SEND)).apply {
+ intent.putExtra(EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI, uri)
+ intent.putExtra(EXTRA_CHOOSER_FOCUSED_ITEM_POSITION, position)
+ }
+ val result = readChooserRequest(model, fakeChooserServiceFlags)
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<ChooserRequest>
+
+ assertThat(result.value.additionalContentUri).isNull()
+ assertThat(result.value.focusedItemPosition).isEqualTo(0)
+ assertThat(result.warnings).isEmpty()
+ }
+
+ @Test
+ fun testRequest_actionSendWithInvalidAdditionalContentUri() {
+ fakeChooserServiceFlags.setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, true)
+ val model =
+ createActivityModel(targetIntent = Intent(ACTION_SEND)).apply {
+ intent.putExtra(EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI, "__invalid__")
+ intent.putExtra(EXTRA_CHOOSER_FOCUSED_ITEM_POSITION, "__invalid__")
+ }
+
+ val result = readChooserRequest(model, fakeChooserServiceFlags)
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<ChooserRequest>
+
+ assertThat(result.value.additionalContentUri).isNull()
+ assertThat(result.value.focusedItemPosition).isEqualTo(0)
+ }
+
+ @Test
+ fun testRequest_actionSendWithoutAdditionalContentUri() {
+ fakeChooserServiceFlags.setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, true)
+ val model = createActivityModel(targetIntent = Intent(ACTION_SEND))
+
+ val result = readChooserRequest(model, fakeChooserServiceFlags)
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<ChooserRequest>
+
+ assertThat(result.value.additionalContentUri).isNull()
+ assertThat(result.value.focusedItemPosition).isEqualTo(0)
+ }
+
+ @Test
+ fun testRequest_actionViewWithAdditionalContentUri() {
+ fakeChooserServiceFlags.setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, true)
+ val uri = Uri.parse("content://org.pkg/path")
+ val position = 10
+ val model =
+ createActivityModel(targetIntent = Intent(ACTION_VIEW)).apply {
+ intent.putExtra(EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI, uri)
+ intent.putExtra(EXTRA_CHOOSER_FOCUSED_ITEM_POSITION, position)
+ }
+
+ val result = readChooserRequest(model, fakeChooserServiceFlags)
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<ChooserRequest>
+
+ assertThat(result.value.additionalContentUri).isNull()
+ assertThat(result.value.focusedItemPosition).isEqualTo(0)
+ assertThat(result.warnings).isEmpty()
+ }
+
+ @Test
+ fun testAlbumType() {
+ fakeChooserServiceFlags.setFlag(Flags.FLAG_CHOOSER_ALBUM_TEXT, true)
+ val model = createActivityModel(Intent(ACTION_SEND))
+ model.intent.putExtra(
+ Intent.EXTRA_CHOOSER_CONTENT_TYPE_HINT,
+ Intent.CHOOSER_CONTENT_TYPE_ALBUM
+ )
+
+ val result = readChooserRequest(model, fakeChooserServiceFlags)
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<ChooserRequest>
+
+ assertThat(result.value.contentTypeHint).isEqualTo(ContentTypeHint.ALBUM)
+ assertThat(result.warnings).isEmpty()
+ }
+
+ @Test
+ fun metadataText_whenFlagFalse_isNull() {
+ fakeChooserServiceFlags.setFlag(Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA, false)
+ val metadataText: CharSequence = "Test metadata text"
+ val model =
+ createActivityModel(targetIntent = Intent()).apply {
+ intent.putExtra(Intent.EXTRA_METADATA_TEXT, metadataText)
+ }
+
+ val result = readChooserRequest(model, fakeChooserServiceFlags)
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<ChooserRequest>
+
+ assertThat(result.value.metadataText).isNull()
+ }
+
+ @Test
+ fun metadataText_whenFlagTrue_isPassedText() {
+ // Arrange
+ fakeChooserServiceFlags.setFlag(Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA, true)
+ val metadataText: CharSequence = "Test metadata text"
+ val model =
+ createActivityModel(targetIntent = Intent()).apply {
+ intent.putExtra(Intent.EXTRA_METADATA_TEXT, metadataText)
+ }
+
+ val result = readChooserRequest(model, fakeChooserServiceFlags)
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<ChooserRequest>
+
+ assertThat(result.value.metadataText).isEqualTo(metadataText)
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/ui/viewmodel/ResolverRequestTest.kt b/tests/unit/src/com/android/intentresolver/ui/viewmodel/ResolverRequestTest.kt
new file mode 100644
index 00000000..bd80235d
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/ui/viewmodel/ResolverRequestTest.kt
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.ui.viewmodel
+
+import android.content.Intent
+import android.content.Intent.ACTION_VIEW
+import android.net.Uri
+import android.os.UserHandle
+import androidx.core.net.toUri
+import androidx.core.os.bundleOf
+import com.android.intentresolver.ResolverActivity.PROFILE_WORK
+import com.android.intentresolver.shared.model.Profile.Type.WORK
+import com.android.intentresolver.ui.model.ActivityModel
+import com.android.intentresolver.ui.model.ResolverRequest
+import com.android.intentresolver.validation.Invalid
+import com.android.intentresolver.validation.UncaughtException
+import com.android.intentresolver.validation.Valid
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import org.junit.Test
+
+private val targetUri = Uri.parse("content://example.com/123")
+
+private fun createActivityModel(
+ targetIntent: Intent,
+ referrer: Uri? = null,
+) =
+ ActivityModel(
+ intent = targetIntent,
+ launchedFromUid = 10000,
+ launchedFromPackage = "com.android.example",
+ referrer = referrer ?: "android-app://com.android.example".toUri()
+ )
+
+class ResolverRequestTest {
+ @Test
+ fun testDefaults() {
+ val intent = Intent(ACTION_VIEW).apply { data = targetUri }
+ val activity = createActivityModel(intent)
+
+ val result = readResolverRequest(activity)
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<ResolverRequest>
+
+ assertThat(result.warnings).isEmpty()
+
+ assertThat(result.value.intent.filterEquals(activity.intent)).isTrue()
+ assertThat(result.value.callingUser).isNull()
+ assertThat(result.value.selectedProfile).isNull()
+ }
+
+ @Test
+ fun testInvalidSelectedProfile() {
+ val intent =
+ Intent(ACTION_VIEW).apply {
+ data = targetUri
+ putExtra(EXTRA_SELECTED_PROFILE, -1000)
+ }
+
+ val activity = createActivityModel(intent)
+
+ val result = readResolverRequest(activity)
+
+ assertThat(result).isInstanceOf(Invalid::class.java)
+ result as Invalid<ResolverRequest>
+
+ assertWithMessage("the first finding")
+ .that(result.errors.firstOrNull())
+ .isInstanceOf(UncaughtException::class.java)
+ }
+
+ @Test
+ fun payloadIntents_includesOnlyTarget() {
+ val intent2 = Intent(Intent.ACTION_SEND_MULTIPLE)
+ val intent1 =
+ Intent(Intent.ACTION_SEND).apply {
+ putParcelableArrayListExtra(Intent.EXTRA_ALTERNATE_INTENTS, arrayListOf(intent2))
+ }
+ val activity = createActivityModel(targetIntent = intent1)
+
+ val result = readResolverRequest(activity)
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<ResolverRequest>
+
+ // Assert that payloadIntents does NOT include EXTRA_ALTERNATE_INTENTS
+ // that is only supported for Chooser and should be not be added here.
+ assertThat(result.value.payloadIntents).containsExactly(intent1)
+ }
+
+ @Test
+ fun testAllValues() {
+ val intent = Intent(ACTION_VIEW).apply { data = Uri.parse("content://example.com/123") }
+ val activity = createActivityModel(targetIntent = intent)
+
+ activity.intent.putExtras(
+ bundleOf(
+ EXTRA_CALLING_USER to UserHandle.of(123),
+ EXTRA_SELECTED_PROFILE to PROFILE_WORK,
+ EXTRA_IS_AUDIO_CAPTURE_DEVICE to true,
+ )
+ )
+
+ val result = readResolverRequest(activity)
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<ResolverRequest>
+
+ assertThat(result.value.intent.filterEquals(activity.intent)).isTrue()
+ assertThat(result.value.isAudioCaptureDevice).isTrue()
+ assertThat(result.value.callingUser).isEqualTo(UserHandle.of(123))
+ assertThat(result.value.selectedProfile).isEqualTo(WORK)
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/util/TestKosmos.kt b/tests/unit/src/com/android/intentresolver/util/TestKosmos.kt
new file mode 100644
index 00000000..473d9b72
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/util/TestKosmos.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.util
+
+import com.android.intentresolver.backgroundDispatcher
+import com.android.systemui.kosmos.Kosmos
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+
+fun Kosmos.runTest(
+ dispatcher: TestDispatcher = StandardTestDispatcher(),
+ block: suspend KosmosTestScope.() -> Unit,
+) {
+ val kosmos = this
+ backgroundDispatcher = dispatcher
+ kotlinx.coroutines.test.runTest(dispatcher) { KosmosTestScope(kosmos, this).block() }
+}
+
+fun runKosmosTest(
+ dispatcher: TestDispatcher = StandardTestDispatcher(),
+ block: suspend KosmosTestScope.() -> Unit,
+) {
+ Kosmos().runTest(dispatcher, block)
+}
+
+class KosmosTestScope(
+ kosmos: Kosmos,
+ private val testScope: TestScope,
+) : Kosmos by kosmos {
+ val backgroundScope
+ get() = testScope.backgroundScope
+
+ @ExperimentalCoroutinesApi fun runCurrent() = testScope.runCurrent()
+}
diff --git a/tests/unit/src/com/android/intentresolver/util/TruthUtils.kt b/tests/unit/src/com/android/intentresolver/util/TruthUtils.kt
new file mode 100644
index 00000000..b96b6f05
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/util/TruthUtils.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.util
+
+import com.google.common.truth.Correspondence
+import com.google.common.truth.IterableSubject
+
+fun <A, B> IterableSubject.comparingElementsUsingTransform(
+ description: String,
+ function: (A) -> B,
+): IterableSubject.UsingCorrespondence<A, B> =
+ comparingElementsUsing(Correspondence.transforming(function, description))
diff --git a/tests/unit/src/com/android/intentresolver/util/UriFiltersTest.kt b/tests/unit/src/com/android/intentresolver/util/UriFiltersTest.kt
index 18218064..32c19f13 100644
--- a/tests/unit/src/com/android/intentresolver/util/UriFiltersTest.kt
+++ b/tests/unit/src/com/android/intentresolver/util/UriFiltersTest.kt
@@ -1,3 +1,19 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package com.android.intentresolver.util
import android.app.PendingIntent
diff --git a/tests/unit/src/com/android/intentresolver/v2/ChooserActionFactoryTest.kt b/tests/unit/src/com/android/intentresolver/v2/ChooserActionFactoryTest.kt
deleted file mode 100644
index b3486bb1..00000000
--- a/tests/unit/src/com/android/intentresolver/v2/ChooserActionFactoryTest.kt
+++ /dev/null
@@ -1,244 +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.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/emptystate/EmptyStateUiHelperTest.kt b/tests/unit/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelperTest.kt
deleted file mode 100644
index 696dd1fd..00000000
--- a/tests/unit/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelperTest.kt
+++ /dev/null
@@ -1,228 +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.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
deleted file mode 100644
index 59494bed..00000000
--- a/tests/unit/src/com/android/intentresolver/v2/listcontroller/ChooserRequestFilteredComponentsTest.kt
+++ /dev/null
@@ -1,61 +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.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
deleted file mode 100644
index ce40567e..00000000
--- a/tests/unit/src/com/android/intentresolver/v2/listcontroller/FakeResolverComparator.kt
+++ /dev/null
@@ -1,83 +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.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
deleted file mode 100644
index 396505e6..00000000
--- a/tests/unit/src/com/android/intentresolver/v2/listcontroller/FilterableComponentsTest.kt
+++ /dev/null
@@ -1,77 +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.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
deleted file mode 100644
index 09f6d373..00000000
--- a/tests/unit/src/com/android/intentresolver/v2/listcontroller/IntentResolverTest.kt
+++ /dev/null
@@ -1,499 +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.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
deleted file mode 100644
index ce5e52b1..00000000
--- a/tests/unit/src/com/android/intentresolver/v2/listcontroller/LastChosenManagerTest.kt
+++ /dev/null
@@ -1,111 +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.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
deleted file mode 100644
index 112342ad..00000000
--- a/tests/unit/src/com/android/intentresolver/v2/listcontroller/PinnableComponentsTest.kt
+++ /dev/null
@@ -1,74 +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.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
deleted file mode 100644
index 26f0199e..00000000
--- a/tests/unit/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduperTest.kt
+++ /dev/null
@@ -1,125 +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.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
deleted file mode 100644
index 9786b801..00000000
--- a/tests/unit/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFilteringTest.kt
+++ /dev/null
@@ -1,309 +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.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
deleted file mode 100644
index 39b328ee..00000000
--- a/tests/unit/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSortingTest.kt
+++ /dev/null
@@ -1,197 +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.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
deleted file mode 100644
index 9d6394fa..00000000
--- a/tests/unit/src/com/android/intentresolver/v2/listcontroller/SharedPreferencesPinnedComponentsTest.kt
+++ /dev/null
@@ -1,63 +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.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/validation/ValidationTest.kt b/tests/unit/src/com/android/intentresolver/v2/validation/ValidationTest.kt
deleted file mode 100644
index 43fb448c..00000000
--- a/tests/unit/src/com/android/intentresolver/v2/validation/ValidationTest.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-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/SimpleValueTest.kt b/tests/unit/src/com/android/intentresolver/v2/validation/types/SimpleValueTest.kt
deleted file mode 100644
index 13bb4b33..00000000
--- a/tests/unit/src/com/android/intentresolver/v2/validation/types/SimpleValueTest.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-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/tests/unit/src/com/android/intentresolver/validation/ValidationTest.kt b/tests/unit/src/com/android/intentresolver/validation/ValidationTest.kt
new file mode 100644
index 00000000..93a5ec0c
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/validation/ValidationTest.kt
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.validation
+
+import com.android.intentresolver.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).isInstanceOf(Valid::class.java)
+ result as Valid<String>
+
+ assertThat(result.value).isEqualTo("return value: 1")
+ assertThat(result.warnings).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).isInstanceOf(Invalid::class.java)
+ result as Invalid<String>
+
+ assertThat(result.errors).containsExactly(NoValue("key", Importance.CRITICAL, 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).isInstanceOf(Valid::class.java)
+ result as Valid<String>
+
+ assertThat(result.value).isEqualTo("return value: 1")
+ assertThat(result.warnings).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).isInstanceOf(Valid::class.java)
+ result as Valid<String>
+
+ assertThat(result.value).isEqualTo("return value: null")
+ assertThat(result.warnings).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).isInstanceOf(Valid::class.java)
+ result as Valid<String>
+
+ assertThat(result.value).isEqualTo("result value")
+ assertThat(result.warnings).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).isInstanceOf(Valid::class.java)
+ result as Valid<String>
+
+ assertThat(result.value).isEqualTo("result value")
+ assertThat(result.warnings).isEmpty()
+ }
+
+ /** Test handling of exceptions in the validation function. */
+ @Test
+ fun thrown_exception() {
+ val result: ValidationResult<String> = validateFrom({ null }) { error("something") }
+
+ assertThat(result).isInstanceOf(Invalid::class.java)
+ result as Invalid<String>
+
+ val errorType = result.errors.map { it::class }.first()
+ assertThat(errorType).isEqualTo(UncaughtException::class)
+ }
+}
diff --git a/tests/unit/src/com/android/intentresolver/v2/validation/types/IntentOrUriTest.kt b/tests/unit/src/com/android/intentresolver/validation/types/IntentOrUriTest.kt
index ad230488..f8622ce0 100644
--- a/tests/unit/src/com/android/intentresolver/v2/validation/types/IntentOrUriTest.kt
+++ b/tests/unit/src/com/android/intentresolver/validation/types/IntentOrUriTest.kt
@@ -1,15 +1,32 @@
-package com.android.intentresolver.v2.validation.types
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.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.android.intentresolver.validation.Importance.CRITICAL
+import com.android.intentresolver.validation.Importance.WARNING
+import com.android.intentresolver.validation.Invalid
+import com.android.intentresolver.validation.NoValue
+import com.android.intentresolver.validation.Valid
+import com.android.intentresolver.validation.ValueIsWrongType
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -22,7 +39,9 @@ class IntentOrUriTest {
val values = mapOf("key" to Intent("GO"))
val result = keyValidator.validate(values::get, CRITICAL)
- assertThat(result).findings().isEmpty()
+
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<Intent>
assertThat(result.value).hasAction("GO")
}
@@ -33,7 +52,9 @@ class IntentOrUriTest {
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).isInstanceOf(Valid::class.java)
+ result as Valid<Intent>
assertThat(result.value).hasAction("GO")
}
@@ -44,8 +65,10 @@ class IntentOrUriTest {
val result = keyValidator.validate({ null }, CRITICAL)
- assertThat(result).value().isNull()
- assertThat(result).findings().containsExactly(RequiredValueMissing("key", Intent::class))
+ assertThat(result).isInstanceOf(Invalid::class.java)
+ result as Invalid<Intent>
+
+ assertThat(result.errors).containsExactly(NoValue("key", CRITICAL, Intent::class))
}
/** Check validation passes when value is null and importance is [WARNING] (optional). */
@@ -55,8 +78,9 @@ class IntentOrUriTest {
val result = keyValidator.validate(source = { null }, WARNING)
- assertThat(result).findings().isEmpty()
- assertThat(result.value).isNull()
+ assertThat(result).isInstanceOf(Invalid::class.java)
+ result as Invalid<List<Intent>>
+ assertThat(result.errors).isEmpty()
}
/**
@@ -69,9 +93,10 @@ class IntentOrUriTest {
val result = keyValidator.validate(values::get, CRITICAL)
- assertThat(result).value().isNull()
- assertThat(result)
- .findings()
+ assertThat(result).isInstanceOf(Invalid::class.java)
+ result as Invalid<Intent>
+
+ assertThat(result.errors)
.containsExactly(
ValueIsWrongType(
"key",
@@ -82,9 +107,7 @@ class IntentOrUriTest {
)
}
- /**
- * Test for warnings when the value is neither Intent nor Uri, with importance WARNING.
- */
+ /** Test for warnings when the value is neither Intent nor Uri, with importance WARNING. */
@Test
fun wrongType_optional() {
val keyValidator = IntentOrUri("key")
@@ -92,16 +115,17 @@ class IntentOrUriTest {
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)
- )
+ assertThat(result).isInstanceOf(Invalid::class.java)
+ result as Invalid<Intent>
+
+ assertThat(result.errors)
+ .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/validation/types/ParceledArrayTest.kt
index d4dca01b..5284cbec 100644
--- a/tests/unit/src/com/android/intentresolver/v2/validation/types/ParceledArrayTest.kt
+++ b/tests/unit/src/com/android/intentresolver/validation/types/ParceledArrayTest.kt
@@ -1,13 +1,30 @@
-package com.android.intentresolver.v2.validation.types
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.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.android.intentresolver.validation.Importance.CRITICAL
+import com.android.intentresolver.validation.Importance.WARNING
+import com.android.intentresolver.validation.Invalid
+import com.android.intentresolver.validation.NoValue
+import com.android.intentresolver.validation.Valid
+import com.android.intentresolver.validation.ValueIsWrongType
+import com.android.intentresolver.validation.WrongElementType
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -21,7 +38,8 @@ class ParceledArrayTest {
val result = keyValidator.validate(values::get, CRITICAL)
- assertThat(result).findings().isEmpty()
+ assertThat(result).isInstanceOf(Valid::class.java)
+ result as Valid<List<String>>
assertThat(result.value).containsExactly("String")
}
@@ -33,9 +51,10 @@ class ParceledArrayTest {
val result = keyValidator.validate(values::get, CRITICAL)
- assertThat(result).value().isNull()
- assertThat(result)
- .findings()
+ assertThat(result).isInstanceOf(Invalid::class.java)
+ result as Invalid<List<Intent>>
+
+ assertThat(result.errors)
.containsExactly(
// TODO: report with a new class `WrongElementType` to improve clarity
WrongElementType(
@@ -55,8 +74,10 @@ class ParceledArrayTest {
val result = keyValidator.validate(source = { null }, CRITICAL)
- assertThat(result).value().isNull()
- assertThat(result).findings().containsExactly(RequiredValueMissing("key", Intent::class))
+ assertThat(result).isInstanceOf(Invalid::class.java)
+ result as Invalid<List<Intent>>
+
+ assertThat(result.errors).containsExactly(NoValue("key", CRITICAL, Intent::class))
}
/** Check validation passes when value is null and importance is [WARNING] (optional). */
@@ -66,8 +87,10 @@ class ParceledArrayTest {
val result = keyValidator.validate(source = { null }, WARNING)
- assertThat(result).findings().isEmpty()
- assertThat(result.value).isNull()
+ assertThat(result).isInstanceOf(Invalid::class.java)
+ result as Invalid<List<Intent>>
+
+ assertThat(result.errors).isEmpty()
}
/** Check correct failure result when the array value itself is the wrong type. */
@@ -78,9 +101,10 @@ class ParceledArrayTest {
val result = keyValidator.validate(values::get, CRITICAL)
- assertThat(result).value().isNull()
- assertThat(result)
- .findings()
+ assertThat(result).isInstanceOf(Invalid::class.java)
+ result as Invalid<List<Intent>>
+
+ assertThat(result.errors)
.containsExactly(
ValueIsWrongType(
"key",
diff --git a/tests/unit/src/com/android/intentresolver/validation/types/SimpleValueTest.kt b/tests/unit/src/com/android/intentresolver/validation/types/SimpleValueTest.kt
new file mode 100644
index 00000000..1b6bace1
--- /dev/null
+++ b/tests/unit/src/com/android/intentresolver/validation/types/SimpleValueTest.kt
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.validation.types
+
+import com.android.intentresolver.validation.Importance.CRITICAL
+import com.android.intentresolver.validation.Importance.WARNING
+import com.android.intentresolver.validation.Invalid
+import com.android.intentresolver.validation.NoValue
+import com.android.intentresolver.validation.Valid
+import com.android.intentresolver.validation.ValueIsWrongType
+import com.google.common.truth.Truth.assertThat
+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).isInstanceOf(Valid::class.java)
+ result as Valid<Double>
+ 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).isInstanceOf(Invalid::class.java)
+ result as Invalid<Double>
+ assertThat(result.errors)
+ .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).isInstanceOf(Invalid::class.java)
+ result as Invalid<Double>
+
+ assertThat(result.errors).containsExactly(NoValue("key", CRITICAL, Double::class))
+ }
+
+ /** Test the failure result when the value is missing. */
+ @Test
+ fun optional() {
+ val keyValidator = SimpleValue("key", expected = Double::class)
+
+ val result = keyValidator.validate(source = { null }, WARNING)
+
+ assertThat(result).isInstanceOf(Invalid::class.java)
+ result as Invalid<Double>
+
+ // Note: As single optional validation result, the return must be Invalid
+ // when there is no value to return, but no errors will be reported because
+ // an optional value cannot be "missing".
+ assertThat(result.errors).isEmpty()
+ }
+}