From 6f0791d450bfcbfbb2424912531338de354644f7 Mon Sep 17 00:00:00 2001 From: Mark Renouf Date: Wed, 3 Apr 2024 15:37:12 -0400 Subject: Collapse v2 fork This is an internal merge of v2.* code back into a single set of code. The ChooserSelector mechanism is removed and usage of the 'modular_framework' AConfig flag is also removed. Test: atest --test-mapping packages/modules/IntentResolver Bug: NA Flag: None Change-Id: I8bb34613e5a042cfbcd8fe2654b8121560a47b03 --- .../intentresolver/AnnotatedUserHandles.java | 217 -- .../intentresolver/ChooserActionFactory.java | 112 +- .../android/intentresolver/ChooserActivity.java | 1784 +++++++++---- .../com/android/intentresolver/ChooserHelper.kt | 168 ++ .../ChooserIntegratedDeviceComponents.java | 85 - .../android/intentresolver/ChooserListAdapter.java | 9 +- .../intentresolver/ChooserListController.java | 65 + .../ChooserMultiProfilePagerAdapter.java | 214 -- .../com/android/intentresolver/ChooserSelector.kt | 36 + .../intentresolver/IntentForwarderActivity.java | 9 +- .../com/android/intentresolver/IntentForwarding.kt | 111 + .../com/android/intentresolver/JavaFlowHelper.kt | 30 + .../intentresolver/MultiProfilePagerAdapter.java | 583 ----- .../android/intentresolver/ProfileAvailability.kt | 85 + .../com/android/intentresolver/ProfileHelper.kt | 97 + .../android/intentresolver/ResolverActivity.java | 1594 +++++------- .../com/android/intentresolver/ResolverHelper.kt | 129 + .../ResolverMultiProfilePagerAdapter.java | 120 - .../intentresolver/ShortcutSelectionLogic.java | 4 +- .../intentresolver/annotation/JavaInterop.kt | 28 + .../DisplayResolveInfoAzInfoComparator.java | 44 + .../contentpreview/ShareouselContentPreviewUi.kt | 2 +- .../domain/interactor/ChooserRequestInteractor.kt | 2 +- .../interactor/UpdateChooserRequestInteractor.kt | 2 +- .../domain/update/SelectionChangeCallback.kt | 18 +- .../intentresolver/data/BroadcastSubscriber.kt | 73 + .../intentresolver/data/model/ChooserRequest.kt | 195 ++ .../data/repository/ChooserRequestRepository.kt | 39 + .../data/repository/DevicePolicyResources.kt | 100 + .../intentresolver/data/repository/UserInfoExt.kt | 45 + .../data/repository/UserRepository.kt | 329 +++ .../data/repository/UserRepositoryModule.kt | 53 + .../data/repository/UserScopedService.kt | 67 + .../domain/interactor/UserInteractor.kt | 92 + .../emptystate/EmptyStateUiHelper.java | 117 +- .../NoAppsAvailableEmptyStateProvider.java | 19 +- .../NoCrossProfileEmptyStateProvider.java | 71 +- .../WorkProfilePausedEmptyStateProvider.java | 43 +- .../intentresolver/ext/CreationExtrasExt.kt | 34 + .../com/android/intentresolver/ext/IntentExt.kt | 45 + .../com/android/intentresolver/ext/ParcelExt.kt | 27 + .../intentresolver/icons/TargetDataLoaderModule.kt | 38 + .../intentresolver/inject/ActivityModelModule.kt | 10 +- .../intentresolver/inject/SystemServices.kt | 4 +- .../model/AbstractResolverComparator.java | 35 +- .../model/ResolveInfoAzInfoComparator.java | 44 + .../intentresolver/platform/AppPredictionModule.kt | 42 + .../intentresolver/platform/ImageEditorModule.kt | 35 + .../intentresolver/platform/NearbyShareModule.kt | 32 + .../platform/PlatformSecureSettings.kt | 30 + .../intentresolver/platform/SecureSettings.kt | 25 + .../platform/SecureSettingsModule.kt | 14 + .../intentresolver/profiles/AdapterBinder.java | 31 + .../profiles/ChooserMultiProfilePagerAdapter.java | 212 ++ .../profiles/MultiProfilePagerAdapter.java | 694 ++++++ .../profiles/OnProfileSelectedListener.java | 46 + .../profiles/OnSwitchOnWorkSelectedListener.java | 27 + .../intentresolver/profiles/ProfileDescriptor.java | 82 + .../profiles/ResolverMultiProfilePagerAdapter.java | 112 + .../android/intentresolver/profiles/TabConfig.java | 38 + .../android/intentresolver/shared/model/Profile.kt | 52 + .../android/intentresolver/shared/model/User.kt | 52 + .../com/android/intentresolver/ui/ActionTitle.java | 88 + .../intentresolver/ui/ProfilePagerResources.kt | 53 + .../android/intentresolver/ui/ShareResultSender.kt | 163 ++ .../intentresolver/ui/ShortcutPolicyModule.kt | 94 + .../intentresolver/ui/model/ActivityModel.kt | 81 + .../intentresolver/ui/model/ResolverRequest.kt | 68 + .../android/intentresolver/ui/model/ShareAction.kt | 23 + .../ui/viewmodel/ChooserRequestReader.kt | 198 ++ .../ui/viewmodel/ChooserViewModel.kt | 94 + .../ui/viewmodel/ResolverRequestReader.kt | 59 + .../ui/viewmodel/ResolverViewModel.kt | 70 + .../intentresolver/v2/ChooserActionFactory.java | 400 --- .../android/intentresolver/v2/ChooserActivity.java | 2612 -------------------- .../com/android/intentresolver/v2/ChooserHelper.kt | 168 -- .../intentresolver/v2/ChooserListController.java | 66 - .../android/intentresolver/v2/ChooserSelector.kt | 36 - .../android/intentresolver/v2/IntentForwarding.kt | 111 - .../android/intentresolver/v2/JavaFlowHelper.kt | 30 - .../intentresolver/v2/ProfileAvailability.kt | 85 - .../com/android/intentresolver/v2/ProfileHelper.kt | 97 - .../intentresolver/v2/ResolverActivity.java | 1947 --------------- .../android/intentresolver/v2/ResolverHelper.kt | 129 - .../intentresolver/v2/annotation/JavaInterop.kt | 28 - .../intentresolver/v2/data/BroadcastSubscriber.kt | 73 - .../intentresolver/v2/data/model/ChooserRequest.kt | 209 -- .../v2/data/repository/ChooserRequestRepository.kt | 39 - .../v2/data/repository/DevicePolicyResources.kt | 100 - .../v2/data/repository/UserInfoExt.kt | 45 - .../v2/data/repository/UserRepository.kt | 328 --- .../v2/data/repository/UserRepositoryModule.kt | 53 - .../v2/data/repository/UserScopedService.kt | 67 - .../v2/domain/interactor/UserInteractor.kt | 92 - .../v2/emptystate/EmptyStateUiHelper.java | 141 -- .../NoAppsAvailableEmptyStateProvider.java | 157 -- .../NoCrossProfileEmptyStateProvider.java | 155 -- .../WorkProfilePausedEmptyStateProvider.java | 131 - .../intentresolver/v2/ext/CreationExtrasExt.kt | 34 - .../com/android/intentresolver/v2/ext/IntentExt.kt | 45 - .../com/android/intentresolver/v2/ext/ParcelExt.kt | 27 - .../v2/icons/TargetDataLoaderModule.kt | 40 - .../v2/listcontroller/FilterableComponents.kt | 39 - .../v2/listcontroller/IntentResolver.kt | 70 - .../v2/listcontroller/LastChosenManager.kt | 77 - .../v2/listcontroller/ListController.kt | 21 - .../v2/listcontroller/PermissionChecker.kt | 34 - .../v2/listcontroller/PinnableComponents.kt | 39 - .../v2/listcontroller/ResolveListDeduper.kt | 69 - .../listcontroller/ResolvedComponentFiltering.kt | 121 - .../v2/listcontroller/ResolvedComponentSorting.kt | 108 - .../v2/platform/AppPredictionModule.kt | 42 - .../v2/platform/ImageEditorModule.kt | 35 - .../v2/platform/NearbyShareModule.kt | 32 - .../v2/platform/PlatformSecureSettings.kt | 30 - .../intentresolver/v2/platform/SecureSettings.kt | 25 - .../v2/platform/SecureSettingsModule.kt | 14 - .../intentresolver/v2/profiles/AdapterBinder.java | 31 - .../profiles/ChooserMultiProfilePagerAdapter.java | 212 -- .../v2/profiles/MultiProfilePagerAdapter.java | 694 ------ .../v2/profiles/OnProfileSelectedListener.java | 46 - .../profiles/OnSwitchOnWorkSelectedListener.java | 27 - .../v2/profiles/ProfileDescriptor.java | 82 - .../profiles/ResolverMultiProfilePagerAdapter.java | 112 - .../intentresolver/v2/profiles/TabConfig.java | 38 - .../intentresolver/v2/shared/model/Profile.kt | 52 - .../android/intentresolver/v2/shared/model/User.kt | 52 - .../android/intentresolver/v2/ui/ActionTitle.java | 88 - .../intentresolver/v2/ui/ProfilePagerResources.kt | 53 - .../intentresolver/v2/ui/ShareResultSender.kt | 163 -- .../intentresolver/v2/ui/ShortcutPolicyModule.kt | 94 - .../intentresolver/v2/ui/model/ActivityModel.kt | 81 - .../intentresolver/v2/ui/model/ResolverRequest.kt | 68 - .../intentresolver/v2/ui/model/ShareAction.kt | 23 - .../v2/ui/viewmodel/ChooserRequestReader.kt | 198 -- .../v2/ui/viewmodel/ChooserViewModel.kt | 94 - .../v2/ui/viewmodel/ResolverRequestReader.kt | 59 - .../v2/ui/viewmodel/ResolverViewModel.kt | 70 - .../android/intentresolver/v2/util/MutableLazy.kt | 36 - .../intentresolver/v2/validation/Findings.kt | 120 - .../intentresolver/v2/validation/Validation.kt | 137 - .../v2/validation/ValidationResult.kt | 26 - .../v2/validation/types/IntentOrUri.kt | 62 - .../v2/validation/types/ParceledArray.kt | 84 - .../v2/validation/types/SimpleValue.kt | 60 - .../v2/validation/types/Validators.kt | 26 - .../android/intentresolver/validation/Findings.kt | 120 + .../intentresolver/validation/Validation.kt | 137 + .../intentresolver/validation/ValidationResult.kt | 26 + .../intentresolver/validation/types/IntentOrUri.kt | 62 + .../validation/types/ParceledArray.kt | 84 + .../intentresolver/validation/types/SimpleValue.kt | 60 + .../intentresolver/validation/types/Validators.kt | 26 + 153 files changed, 7110 insertions(+), 14037 deletions(-) delete mode 100644 java/src/com/android/intentresolver/AnnotatedUserHandles.java create mode 100644 java/src/com/android/intentresolver/ChooserHelper.kt delete mode 100644 java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java create mode 100644 java/src/com/android/intentresolver/ChooserListController.java delete mode 100644 java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java create mode 100644 java/src/com/android/intentresolver/ChooserSelector.kt create mode 100644 java/src/com/android/intentresolver/IntentForwarding.kt create mode 100644 java/src/com/android/intentresolver/JavaFlowHelper.kt delete mode 100644 java/src/com/android/intentresolver/MultiProfilePagerAdapter.java create mode 100644 java/src/com/android/intentresolver/ProfileAvailability.kt create mode 100644 java/src/com/android/intentresolver/ProfileHelper.kt create mode 100644 java/src/com/android/intentresolver/ResolverHelper.kt delete mode 100644 java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java create mode 100644 java/src/com/android/intentresolver/annotation/JavaInterop.kt create mode 100644 java/src/com/android/intentresolver/chooser/DisplayResolveInfoAzInfoComparator.java create mode 100644 java/src/com/android/intentresolver/data/BroadcastSubscriber.kt create mode 100644 java/src/com/android/intentresolver/data/model/ChooserRequest.kt create mode 100644 java/src/com/android/intentresolver/data/repository/ChooserRequestRepository.kt create mode 100644 java/src/com/android/intentresolver/data/repository/DevicePolicyResources.kt create mode 100644 java/src/com/android/intentresolver/data/repository/UserInfoExt.kt create mode 100644 java/src/com/android/intentresolver/data/repository/UserRepository.kt create mode 100644 java/src/com/android/intentresolver/data/repository/UserRepositoryModule.kt create mode 100644 java/src/com/android/intentresolver/data/repository/UserScopedService.kt create mode 100644 java/src/com/android/intentresolver/domain/interactor/UserInteractor.kt create mode 100644 java/src/com/android/intentresolver/ext/CreationExtrasExt.kt create mode 100644 java/src/com/android/intentresolver/ext/IntentExt.kt create mode 100644 java/src/com/android/intentresolver/ext/ParcelExt.kt create mode 100644 java/src/com/android/intentresolver/icons/TargetDataLoaderModule.kt create mode 100644 java/src/com/android/intentresolver/model/ResolveInfoAzInfoComparator.java create mode 100644 java/src/com/android/intentresolver/platform/AppPredictionModule.kt create mode 100644 java/src/com/android/intentresolver/platform/ImageEditorModule.kt create mode 100644 java/src/com/android/intentresolver/platform/NearbyShareModule.kt create mode 100644 java/src/com/android/intentresolver/platform/PlatformSecureSettings.kt create mode 100644 java/src/com/android/intentresolver/platform/SecureSettings.kt create mode 100644 java/src/com/android/intentresolver/platform/SecureSettingsModule.kt create mode 100644 java/src/com/android/intentresolver/profiles/AdapterBinder.java create mode 100644 java/src/com/android/intentresolver/profiles/ChooserMultiProfilePagerAdapter.java create mode 100644 java/src/com/android/intentresolver/profiles/MultiProfilePagerAdapter.java create mode 100644 java/src/com/android/intentresolver/profiles/OnProfileSelectedListener.java create mode 100644 java/src/com/android/intentresolver/profiles/OnSwitchOnWorkSelectedListener.java create mode 100644 java/src/com/android/intentresolver/profiles/ProfileDescriptor.java create mode 100644 java/src/com/android/intentresolver/profiles/ResolverMultiProfilePagerAdapter.java create mode 100644 java/src/com/android/intentresolver/profiles/TabConfig.java create mode 100644 java/src/com/android/intentresolver/shared/model/Profile.kt create mode 100644 java/src/com/android/intentresolver/shared/model/User.kt create mode 100644 java/src/com/android/intentresolver/ui/ActionTitle.java create mode 100644 java/src/com/android/intentresolver/ui/ProfilePagerResources.kt create mode 100644 java/src/com/android/intentresolver/ui/ShareResultSender.kt create mode 100644 java/src/com/android/intentresolver/ui/ShortcutPolicyModule.kt create mode 100644 java/src/com/android/intentresolver/ui/model/ActivityModel.kt create mode 100644 java/src/com/android/intentresolver/ui/model/ResolverRequest.kt create mode 100644 java/src/com/android/intentresolver/ui/model/ShareAction.kt create mode 100644 java/src/com/android/intentresolver/ui/viewmodel/ChooserRequestReader.kt create mode 100644 java/src/com/android/intentresolver/ui/viewmodel/ChooserViewModel.kt create mode 100644 java/src/com/android/intentresolver/ui/viewmodel/ResolverRequestReader.kt create mode 100644 java/src/com/android/intentresolver/ui/viewmodel/ResolverViewModel.kt delete mode 100644 java/src/com/android/intentresolver/v2/ChooserActionFactory.java delete mode 100644 java/src/com/android/intentresolver/v2/ChooserActivity.java delete mode 100644 java/src/com/android/intentresolver/v2/ChooserHelper.kt delete mode 100644 java/src/com/android/intentresolver/v2/ChooserListController.java delete mode 100644 java/src/com/android/intentresolver/v2/ChooserSelector.kt delete mode 100644 java/src/com/android/intentresolver/v2/IntentForwarding.kt delete mode 100644 java/src/com/android/intentresolver/v2/JavaFlowHelper.kt delete mode 100644 java/src/com/android/intentresolver/v2/ProfileAvailability.kt delete mode 100644 java/src/com/android/intentresolver/v2/ProfileHelper.kt delete mode 100644 java/src/com/android/intentresolver/v2/ResolverActivity.java delete mode 100644 java/src/com/android/intentresolver/v2/ResolverHelper.kt delete mode 100644 java/src/com/android/intentresolver/v2/annotation/JavaInterop.kt delete mode 100644 java/src/com/android/intentresolver/v2/data/BroadcastSubscriber.kt delete mode 100644 java/src/com/android/intentresolver/v2/data/model/ChooserRequest.kt delete mode 100644 java/src/com/android/intentresolver/v2/data/repository/ChooserRequestRepository.kt delete mode 100644 java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt delete mode 100644 java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt delete mode 100644 java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt delete mode 100644 java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt delete mode 100644 java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt delete mode 100644 java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt delete mode 100644 java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java delete mode 100644 java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java delete mode 100644 java/src/com/android/intentresolver/v2/emptystate/NoCrossProfileEmptyStateProvider.java delete mode 100644 java/src/com/android/intentresolver/v2/emptystate/WorkProfilePausedEmptyStateProvider.java delete mode 100644 java/src/com/android/intentresolver/v2/ext/CreationExtrasExt.kt delete mode 100644 java/src/com/android/intentresolver/v2/ext/IntentExt.kt delete mode 100644 java/src/com/android/intentresolver/v2/ext/ParcelExt.kt delete mode 100644 java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt delete mode 100644 java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt delete mode 100644 java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt delete mode 100644 java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt delete mode 100644 java/src/com/android/intentresolver/v2/listcontroller/ListController.kt delete mode 100644 java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt delete mode 100644 java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt delete mode 100644 java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt delete mode 100644 java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt delete mode 100644 java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt delete mode 100644 java/src/com/android/intentresolver/v2/platform/AppPredictionModule.kt delete mode 100644 java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt delete mode 100644 java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt delete mode 100644 java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt delete mode 100644 java/src/com/android/intentresolver/v2/platform/SecureSettings.kt delete mode 100644 java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt delete mode 100644 java/src/com/android/intentresolver/v2/profiles/AdapterBinder.java delete mode 100644 java/src/com/android/intentresolver/v2/profiles/ChooserMultiProfilePagerAdapter.java delete mode 100644 java/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapter.java delete mode 100644 java/src/com/android/intentresolver/v2/profiles/OnProfileSelectedListener.java delete mode 100644 java/src/com/android/intentresolver/v2/profiles/OnSwitchOnWorkSelectedListener.java delete mode 100644 java/src/com/android/intentresolver/v2/profiles/ProfileDescriptor.java delete mode 100644 java/src/com/android/intentresolver/v2/profiles/ResolverMultiProfilePagerAdapter.java delete mode 100644 java/src/com/android/intentresolver/v2/profiles/TabConfig.java delete mode 100644 java/src/com/android/intentresolver/v2/shared/model/Profile.kt delete mode 100644 java/src/com/android/intentresolver/v2/shared/model/User.kt delete mode 100644 java/src/com/android/intentresolver/v2/ui/ActionTitle.java delete mode 100644 java/src/com/android/intentresolver/v2/ui/ProfilePagerResources.kt delete mode 100644 java/src/com/android/intentresolver/v2/ui/ShareResultSender.kt delete mode 100644 java/src/com/android/intentresolver/v2/ui/ShortcutPolicyModule.kt delete mode 100644 java/src/com/android/intentresolver/v2/ui/model/ActivityModel.kt delete mode 100644 java/src/com/android/intentresolver/v2/ui/model/ResolverRequest.kt delete mode 100644 java/src/com/android/intentresolver/v2/ui/model/ShareAction.kt delete mode 100644 java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt delete mode 100644 java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt delete mode 100644 java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestReader.kt delete mode 100644 java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverViewModel.kt delete mode 100644 java/src/com/android/intentresolver/v2/util/MutableLazy.kt delete mode 100644 java/src/com/android/intentresolver/v2/validation/Findings.kt delete mode 100644 java/src/com/android/intentresolver/v2/validation/Validation.kt delete mode 100644 java/src/com/android/intentresolver/v2/validation/ValidationResult.kt delete mode 100644 java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt delete mode 100644 java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt delete mode 100644 java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt delete mode 100644 java/src/com/android/intentresolver/v2/validation/types/Validators.kt create mode 100644 java/src/com/android/intentresolver/validation/Findings.kt create mode 100644 java/src/com/android/intentresolver/validation/Validation.kt create mode 100644 java/src/com/android/intentresolver/validation/ValidationResult.kt create mode 100644 java/src/com/android/intentresolver/validation/types/IntentOrUri.kt create mode 100644 java/src/com/android/intentresolver/validation/types/ParceledArray.kt create mode 100644 java/src/com/android/intentresolver/validation/types/SimpleValue.kt create mode 100644 java/src/com/android/intentresolver/validation/types/Validators.kt (limited to 'java') 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..ffe83fa6 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; + private Runnable mEditButtonRunnable; private final ImmutableList mCustomActions; - private final @Nullable ChooserAction mModifyShareAction; private final Consumer mExcludeSharedTextAction; + @Nullable private final ShareResultSender mShareResultSender; private final Consumer 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,34 +119,39 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio */ public ChooserActionFactory( Context context, - ChooserRequestParameters chooserRequest, - ChooserIntegratedDeviceComponents integratedDeviceComponents, + Intent targetIntent, + String referrerPackageName, + List chooserActions, + Optional imageEditor, EventLog log, Consumer onUpdateSharedTextIsExcluded, Callable firstVisibleImageQuery, ActionActivityStarter activityStarter, - Consumer finishCallback) { + @Nullable ShareResultSender shareResultSender, + Consumer 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 @@ -151,18 +160,31 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio @Nullable Runnable copyButtonRunnable, Runnable editButtonRunnable, List customActions, - @Nullable ChooserAction modifyShareAction, Consumer onUpdateSharedTextIsExcluded, EventLog log, + @Nullable ShareResultSender shareResultSender, Consumer finishCallback) { mContext = context; mCopyButtonRunnable = copyButtonRunnable; mEditButtonRunnable = editButtonRunnable; mCustomActions = ImmutableList.copyOf(customActions); - mModifyShareAction = modifyShareAction; mExcludeSharedTextAction = onUpdateSharedTextIsExcluded; mLog = log; + mShareResultSender = shareResultSender; mFinishCallback = finishCallback; + + if (mShareResultSender != null) { + mEditButtonRunnable = () -> { + mShareResultSender.onActionSelected(ShareAction.SYSTEM_EDIT); + editButtonRunnable.run(); + }; + if (mCopyButtonRunnable != null) { + mCopyButtonRunnable = () -> { + mShareResultSender.onActionSelected(ShareAction.SYSTEM_COPY); + copyButtonRunnable.run(); + }; + } + } } @Override @@ -186,11 +208,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); } @@ -198,21 +218,6 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio 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); - }); - } - /** *

* Creates an exclude-text action that can be called when the user changes shared text @@ -229,7 +234,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio @Nullable private static Runnable makeCopyButtonRunnable( - Context context, + ClipboardManager clipboardManager, Intent targetIntent, String referrerPackageName, Consumer finishCallback, @@ -245,8 +250,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); @@ -281,15 +284,14 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio private static TargetInfo getEditSharingTarget( Context context, Intent originalIntent, - ChooserIntegratedDeviceComponents integratedComponents) { - final ComponentName editorComponent = integratedComponents.getEditSharingComponent(); + Optional 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 +310,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; } @@ -347,12 +349,13 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio } @Nullable - private static ActionRow.Action createCustomAction( + static ActionRow.Action createCustomAction( Context context, - ChooserAction action, - Consumer finishCallback, - Runnable loggingRunnable) { - if (action == null || action.getAction() == null) { + @Nullable ChooserAction action, + Runnable loggingRunnable, + ShareResultSender shareResultSender, + Consumer finishCallback) { + if (action == null) { return null; } Drawable icon = action.getIcon().loadDrawable(context); @@ -382,8 +385,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 9557b25b..a2bde24c 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,25 +63,36 @@ 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.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; @@ -81,49 +104,83 @@ 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.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 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; +import kotlinx.coroutines.CoroutineDispatcher; + /** * The Chooser Activity handles intent resolution specifically for sharing intents - * 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, PackagesChangedListener, StartsSelectedItem { private static final String TAG = "ChooserActivity"; @@ -139,7 +196,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 +204,38 @@ 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 View mProfileView; + + protected ResolverDrawerLayout mResolverDrawerLayout; + 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 +244,37 @@ public class ChooserActivity extends Hilt_ChooserActivity implements private final Map mDirectShareAppTargetCache = new HashMap<>(); private final Map 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 mImageEditor; + @Inject @NearbyShare public Optional 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 +302,12 @@ 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 View mContentView = null; - private final SparseArray mProfileRecords = new SparseArray<>(); + private final Map mProfileRecords = new HashMap<>(); private boolean mExcludeSharedText = false; /** @@ -232,56 +318,254 @@ 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); + } + } + + @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); + 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 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(), - getPackageManager().getAppPredictionServicePackageName() != null), - 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, + mFeatureFlags); - mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class); + 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(); + } + }); + + 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 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(); @@ -293,100 +577,714 @@ public class ChooserActivity extends Hilt_ChooserActivity implements // 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); + final ResolveInfo ri = targetInfo.getResolveInfo(); + final Intent intent1 = targetInfo.getResolvedIntent(); + + safelyStartActivity(targetInfo); + + // Rely on the ActivityManager to pop up a dialog regarding app suspension + // and return false + targetInfo.isSuspended(); } finish(); } - }); + }); + BasePreviewViewModel previewViewModel = + new ViewModelProvider(this, createPreviewViewModelFactory()) + .get(BasePreviewViewModel.class); + previewViewModel.init( + mRequest.getTargetIntent(), + mRequest.getAdditionalContentUri(), + mChooserServiceFeatureFlags.chooserPayloadToggling()); + mChooserContentPreviewUi = new ChooserContentPreviewUi( + getCoroutineScope(getLifecycle()), + previewViewModel.getPreviewDataProvider(), + mRequest.getTargetIntent(), + previewViewModel.getImageLoader(), + createChooserActionFactory(), + createModifyShareActionFactory(), + mEnterTransitionAnimationDelegate, + new HeadlineGeneratorImpl(this), + mRequest.getContentTypeHint(), + mRequest.getMetadataText(), + mChooserServiceFeatureFlags.chooserPayloadToggling()); + updateStickyContentPreview(); + if (shouldShowStickyContentPreview() + || mChooserMultiProfilePagerAdapter + .getCurrentRootAdapter().getSystemRowCount() != 0) { + getEventLog().logActionShareWithPreview( + mChooserContentPreviewUi.getPreferredContentPreview()); + } + mChooserShownTime = System.currentTimeMillis(); + final long systemCost = mChooserShownTime - mIntentReceivedTime.get(); + getEventLog().logChooserActivityShown( + isWorkProfile(), mRequest.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( + mRequest.getReferrerPackage(), + mRequest.getTargetType(), + mRequest.getCallerChooserTargets().size(), + mRequest.getInitialIntents().size(), + isWorkProfile(), + mChooserContentPreviewUi.getPreferredContentPreview(), + mRequest.getTargetAction(), + mRequest.getChooserActions().size(), + mRequest.getModifyShareAction() != null + ); + mEnterTransitionAnimationDelegate.postponeTransition(); + Tracer.INSTANCE.markLaunched(); + } + + private void onChooserRequestChanged(ChooserRequest chooserRequest) { + // intentional reference comarison + if (mRequest == chooserRequest) { + return; + } + boolean recreateAdapters = shouldUpdateAdapters(mRequest, chooserRequest); + mRequest = chooserRequest; + updateShareResultSender(); + mChooserContentPreviewUi.updateModifyShareAction(); + if (recreateAdapters) { + recreatePagerAdapter(); + } + } + + 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 oldAltIntents = oldChooserRequest.getAdditionalTargets(); + List 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() + ); + + if (mChooserMultiProfilePagerAdapter != null) { + mChooserMultiProfilePagerAdapter.destroy(); + } + mChooserMultiProfilePagerAdapter = createMultiProfilePagerAdapter( + /* context = */ this, + mProfilePagerResources, + mRequest, + mProfiles, + mProfileAvailability, + mRequest.getInitialIntents(), + mMaxTargetsPerRow, + mFeatureFlags); + mChooserMultiProfilePagerAdapter.setupViewPager( + requireViewById(com.android.internal.R.id.profile_pager)); + 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())); + } + + @Override + protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { + ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); + if (viewPager != null) { + viewPager.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( + this, + profileHelper.getWorkHandle(), + profileHelper.getPersonalHandle(), + getMetricsCategory(), + profileHelper.getTabOwnerUserHandleForLaunch() + ); + + // 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 getResolverRankerServiceUserHandleList(UserHandle userHandle) { + return getResolverRankerServiceUserHandleListInternal(userHandle); + } + + private List getResolverRankerServiceUserHandleListInternal(UserHandle userHandle) { + List 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); + } + + protected WindowInsets super_onApplyWindowInsets(View v, WindowInsets insets) { + mSystemWindowInsets = insets.getSystemWindowInsets(); + + 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); + + return insets.consumeSystemWindowInsets(); + } + + @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); + } - BasePreviewViewModel previewViewModel = - new ViewModelProvider(this, createPreviewViewModelFactory()) - .get(BasePreviewViewModel.class); - previewViewModel.init( - mChooserRequest.getTargetIntent(), - /*additionalContentUri = */ null, - /*isPayloadTogglingEnabled = */ false); - final ChooserActionFactory chooserActionFactory = createChooserActionFactory(); - mChooserContentPreviewUi = new ChooserContentPreviewUi( - getCoroutineScope(getLifecycle()), - previewViewModel.getPreviewDataProvider(), - mChooserRequest.getTargetIntent(), - previewViewModel.getImageLoader(), - chooserActionFactory, - chooserActionFactory::getModifyShareAction, - mEnterTransitionAnimationDelegate, - new HeadlineGeneratorImpl(this), - ContentTypeHint.NONE, - mChooserRequest.getMetadataText(), - /*isPayloadTogglingEnabled =*/ false - ); + mPickOptionRequest = new ResolverActivity.PickTargetOptionRequest( + new VoiceInteractor.Prompt(getTitle()), options, null); + getVoiceInteractor().submitRequest(mPickOptionRequest); + } - updateStickyContentPreview(); - if (shouldShowStickyContentPreview() - || mChooserMultiProfilePagerAdapter - .getCurrentRootAdapter().getSystemRowCount() != 0) { - getEventLog().logActionShareWithPreview( - mChooserContentPreviewUi.getPreferredContentPreview()); - } + /** + * Sets up the content view. + * @return true 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 = mFeatureFlags.scrollablePreview() + ? R.layout.chooser_grid_scrollable_preview + : R.layout.chooser_grid; - mChooserShownTime = System.currentTimeMillis(); - final long systemCost = mChooserShownTime - intentReceivedTime; - getEventLog().logChooserActivityShown( - isWorkProfile(), mChooserRequest.getTargetType(), systemCost); + setContentView(mLayoutId); + mChooserMultiProfilePagerAdapter.setupViewPager( + requireViewById(com.android.internal.R.id.profile_pager)); + boolean result = postRebuildList(rebuildCompleted); + Trace.endSection(); + return result; + } - if (mResolverDrawerLayout != null) { - mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange); + /** + * Finishing procedures to be performed after the list has been rebuilt. + *

Subclasses must call postRebuildListInternal at the end of postRebuildList. + * @param rebuildCompleted + * @return true if the activity is finishing and creation should halt. + */ + protected boolean postRebuildList(boolean rebuildCompleted) { + return postRebuildListInternal(rebuildCompleted); + } - mResolverDrawerLayout.setOnCollapsedChangedListener( - isCollapsed -> { - mChooserMultiProfilePagerAdapter.setIsCollapsed(isCollapsed); - getEventLog().logSharesheetExpansionChanged(isCollapsed); - }); + /** + * 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 true if the activity is finishing and creation should halt. + */ + final boolean postRebuildListInternal(boolean rebuildCompleted) { + int count = mChooserMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount(); - if (DEBUG) { - Log.d(TAG, "System Time Cost is " + systemCost); + // 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; } - getEventLog().logShareStarted( - getReferrerPackageName(), - mChooserRequest.getTargetType(), - mChooserRequest.getCallerChooserTargets().size(), - (mChooserRequest.getInitialIntents() == null) - ? 0 : mChooserRequest.getInitialIntents().length, - isWorkProfile(), - mChooserContentPreviewUi.getPreferredContentPreview(), - mChooserRequest.getTargetAction(), - mChooserRequest.getChooserActions().size(), - mChooserRequest.getModifyShareAction() != null - ); + setupViewVisibilities(); - mEnterTransitionAnimationDelegate.postponeTransition(); - } + if (mProfiles.getWorkProfilePresent() + || (mProfiles.getPrivateProfilePresent() + && mProfileAvailability.isAvailable( + requireNonNull(mProfiles.getPrivateProfile())))) { + setupProfileTabs(); + } - @VisibleForTesting - protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() { - return ChooserIntegratedDeviceComponents.get(this, new SecureSettings()); + return false; } - @Override - protected int appliedThemeResId() { - return R.style.Theme_DeviceDefault_Chooser; + private void setupProfileTabs() { + TabHost tabHost = findViewById(com.android.internal.R.id.profile_tabhost); + ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); + + mChooserMultiProfilePagerAdapter.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) {} + + @Override + public void onProfilePageStateChanged(int state) { + onHorizontalSwipeStateChanged(state); + } + }); + mOnSwitchOnWorkSelectedListener = () -> { + View workTab = tabHost.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( @@ -407,7 +1305,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 @@ -430,25 +1328,76 @@ public class ChooserActivity extends Hilt_ChooserActivity implements return context.getSharedPreferences(PINNED_SHARED_PREFS_NAME, MODE_PRIVATE); } - @Override - protected ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter( - Intent[] initialIntents, - List 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, + mFeatureFlags); + } + + private ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter( + Context context, + ProfilePagerResources profilePagerResources, + ChooserRequest request, + ProfileHelper profileHelper, + ProfileAvailability profileAvailability, + List initialIntents, + int maxTargetsPerRow, + FeatureFlags featureFlags) { + Log.d(TAG, "createMultiProfilePagerAdapter"); + + Profile launchedAs = profileHelper.getLaunchedAsProfile(); + + Intent[] initialIntentArray = initialIntents.toArray(new Intent[0]); + List payloadIntents = request.getPayloadIntents(); + + List> 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 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, + featureFlags); } - @Override protected EmptyStateProvider createBlockerEmptyStateProvider() { - final boolean isSendAction = mChooserRequest.isSendActionTarget(); + final boolean isSendAction = mRequest.isSendActionTarget(); final EmptyState noWorkToPersonalEmptyState = new DevicePolicyBlockerEmptyState( @@ -477,79 +1426,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 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 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(); } /** @@ -557,12 +1441,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 @@ -589,39 +1472,24 @@ 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); - } - updateProfileViewButton(); - } - - private void handlePackageChangePerProfile(ResolverListAdapter adapter) { - ProfileRecord record = getProfileRecord(adapter.getUserHandle()); - if (record != null && record.shortcutLoader != null) { - record.shortcutLoader.reset(); + listAdapter.handlePackagesChanged(); } - 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); + mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); + + if (mSystemWindowInsets != null) { + mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top, + mSystemWindowInsets.right, 0); + } ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); if (viewPager.isLayoutRtl()) { - mMultiProfilePagerAdapter.setupViewPager(viewPager); + mChooserMultiProfilePagerAdapter.setupViewPager(viewPager); } mShouldDisplayLandscape = shouldDisplayLandscape(newConfig.orientation); @@ -651,7 +1519,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 @@ -711,47 +1579,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); @@ -770,33 +1608,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()); @@ -804,28 +1631,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) { @@ -840,8 +1658,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(); @@ -858,22 +1677,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 @@ -896,8 +1718,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 @@ -917,7 +1754,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, @@ -954,7 +1791,6 @@ public class ChooserActivity extends Hilt_ChooserActivity implements mIsSuccessfullySelected, selectionCost ); - return; } } } @@ -976,19 +1812,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) { @@ -1008,7 +1833,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(); @@ -1036,7 +1861,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) { @@ -1106,93 +1931,38 @@ 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 { - Comparator 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 payloadIntents, Intent[] initialIntents, - List 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(); + return mProfiles.getWorkProfilePresent(); } @Override @@ -1236,11 +2006,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, @@ -1252,54 +2019,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 @@ -1310,8 +2093,10 @@ public class ChooserActivity extends Hilt_ChooserActivity implements private ChooserActionFactory createChooserActionFactory() { return new ChooserActionFactory( this, - mChooserRequest, - mIntegratedDeviceComponents, + mRequest.getTargetIntent(), + mRequest.getLaunchedFromPackage(), + mRequest.getChooserActions(), + mImageEditor, getEventLog(), (isExcluded) -> mExcludeSharedText = isExcluded, this::getFirstVisibleImgPreviewView, @@ -1319,7 +2104,9 @@ public class ChooserActivity extends Hilt_ChooserActivity implements @Override public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) { safelyStartActivityAsUser( - targetInfo, getAnnotatedUserHandles().personalProfileUserHandle); + targetInfo, + mProfiles.getPersonalHandle() + ); finish(); } @@ -1330,19 +2117,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 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(); } /* @@ -1387,8 +2187,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; @@ -1437,7 +2236,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(); } @@ -1460,7 +2259,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(); } @@ -1471,41 +2271,15 @@ public class ChooserActivity extends Hilt_ChooserActivity implements /** * 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; + return rowsToShow == 1 + && mChooserMultiProfilePagerAdapter + .shouldShowEmptyStateScreenInAnyInactiveAdapter(); } - private ViewGroup getActiveEmptyStateView() { - int currentPage = mChooserMultiProfilePagerAdapter.getCurrentPage(); - return mChooserMultiProfilePagerAdapter.getEmptyStateView(currentPage); - } - - @Override // ResolverListCommunicator - public void onHandlePackagesChanged(ResolverListAdapter listAdapter) { - mChooserMultiProfilePagerAdapter.getActiveListAdapter().notifyDataSetChanged(); - super.onHandlePackagesChanged(listAdapter); - } - - @Override protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) { setupScrollListener(); maybeSetupGlobalLayoutListener(); @@ -1575,7 +2349,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"); @@ -1590,7 +2364,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 = @@ -1598,7 +2373,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; @@ -1613,7 +2388,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) { @@ -1628,7 +2403,7 @@ public class ChooserActivity extends Hilt_ChooserActivity implements } private void maybeSetupGlobalLayoutListener() { - if (shouldShowTabs()) { + if (mProfiles.getWorkProfilePresent()) { return; } final View recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView(); @@ -1662,10 +2437,10 @@ public class ChooserActivity extends Hilt_ChooserActivity implements if (!shouldShowContentPreview()) { return false; } - ResolverListAdapter adapter = mMultiProfilePagerAdapter.getListAdapterForUserHandle( + ResolverListAdapter adapter = mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle( UserHandle.of(UserHandle.myUserId())); boolean isEmpty = adapter == null || adapter.getCount() == 0; - return (mFeatureFlags.scrollablePreview() || shouldShowTabs()) + return (mFeatureFlags.scrollablePreview() || mProfiles.getWorkProfilePresent()) && (!isEmpty || shouldShowContentPreviewWhenEmpty()); } @@ -1684,7 +2459,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() { @@ -1728,34 +2503,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 @@ -1765,16 +2528,13 @@ public class ChooserActivity extends Hilt_ChooserActivity implements } } - @Override protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { - if (shouldShowTabs()) { + if (mProfiles.getWorkProfilePresent()) { mChooserMultiProfilePagerAdapter .setEmptyStateBottomOffset(insets.getSystemWindowInsetBottom()); - mChooserMultiProfilePagerAdapter.setupContainerPadding( - getActiveEmptyStateView().findViewById(com.android.internal.R.id.resolver_empty_state_container)); } - WindowInsets result = super.onApplyWindowInsets(v, insets); + WindowInsets result = super_onApplyWindowInsets(v, insets); if (mResolverDrawerLayout != null) { mResolverDrawerLayout.requestLayout(); } @@ -1793,7 +2553,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) { @@ -1808,7 +2567,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..25c2b40f --- /dev/null +++ b/java/src/com/android/intentresolver/ChooserHelper.kt @@ -0,0 +1,168 @@ +/* + * 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.data.model.ChooserRequest +import com.android.intentresolver.domain.interactor.UserInteractor +import com.android.intentresolver.inject.Background +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.CoroutineDispatcher +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.first +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 userInteractor: UserInteractor, + private val activityResultRepo: ActivityResultRepository, + @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() + + private lateinit var activityInitializer: Runnable + + var onChooserRequestChanged: Consumer = Consumer {} + + 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 { + activity.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.request.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) { + request.errors.forEach { it.log(TAG) } + activity.finish() + } + + private fun initializeActivity(request: Valid) { + 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 5060f4f1..e8d4fdde 100644 --- a/java/src/com/android/intentresolver/ChooserListAdapter.java +++ b/java/src/com/android/intentresolver/ChooserListAdapter.java @@ -48,6 +48,7 @@ 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; @@ -478,8 +479,8 @@ public class ChooserListAdapter extends ResolverListAdapter { } public void updateAlphabeticalList() { - final ChooserActivity.AzInfoComparator comparator = - new ChooserActivity.AzInfoComparator(mContext); + final DisplayResolveInfoAzInfoComparator + comparator = new DisplayResolveInfoAzInfoComparator(mContext); final List allTargets = new ArrayList<>(); allTargets.addAll(getTargetsInCurrentDisplayList()); allTargets.addAll(mCallerTargets); @@ -711,7 +712,7 @@ public class ChooserListAdapter extends ResolverListAdapter { public void addServiceResults( @Nullable DisplayResolveInfo origTarget, List targets, - @ChooserActivity.ShareTargetType int targetType, + int targetType, Map directShareToShortcutInfos, Map directShareToAppTargets) { // Avoid inserting any potentially late results. @@ -748,7 +749,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; } 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 mFilteredComponents; + private final SharedPreferences mPinnedComponents; + + public ChooserListController( + Context context, + PackageManager pm, + Intent targetIntent, + String referrerPackageName, + int launchedFromUid, + AbstractResolverComparator resolverComparator, + UserHandle queryIntentsAsUser, + List 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 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 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 gridAdapters, - EmptyStateProvider emptyStateProvider, - Supplier 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> { - private final Context mContext; - private int mBottomOffset; - - BottomPaddingOverrideSupplier(Context context) { - mContext = context; - } - - public void setEmptyStateBottomOffset(int bottomOffset) { - mBottomOffset = bottomOffset; - } - - public Optional 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 { - 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/ChooserSelector.kt b/java/src/com/android/intentresolver/ChooserSelector.kt new file mode 100644 index 00000000..378bc06c --- /dev/null +++ b/java/src/com/android/intentresolver/ChooserSelector.kt @@ -0,0 +1,36 @@ +package com.android.intentresolver.v2 + +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import com.android.intentresolver.FeatureFlags +import dagger.hilt.android.AndroidEntryPoint +import javax.inject.Inject + +@AndroidEntryPoint(BroadcastReceiver::class) +class ChooserSelector : Hilt_ChooserSelector() { + + @Inject lateinit var featureFlags: FeatureFlags + + override fun onReceive(context: Context, intent: Intent) { + super.onReceive(context, intent) + if (intent.action == Intent.ACTION_BOOT_COMPLETED) { + context.packageManager.setComponentEnabledSetting( + ComponentName(CHOOSER_PACKAGE, CHOOSER_PACKAGE + CHOOSER_CLASS), + if (featureFlags.modularFramework()) { + PackageManager.COMPONENT_ENABLED_STATE_ENABLED + } else { + PackageManager.COMPONENT_ENABLED_STATE_DEFAULT + }, + /* flags = */ 0, + ) + } + } + + companion object { + private const val CHOOSER_PACKAGE = "com.android.intentresolver" + private const val CHOOSER_CLASS = ".v2.ChooserActivity" + } +} diff --git a/java/src/com/android/intentresolver/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/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 collect(scope: CoroutineScope, flow: Flow, collector: Consumer): 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 the type of the widget that represents the contents of a page in this adapter - * @param the type of a "root" adapter class to be instantiated and included in - * the per-profile records. - * @param 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 (as in {@link MultiProfilePagerAdapter}). - * @param (as in {@link MultiProfilePagerAdapter}). - */ - public interface AdapterBinder { - /** - * 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 mListAdapterExtractor; - private final AdapterBinder mAdapterBinder; - private final Supplier mPageViewInflater; - private final Supplier> mContainerBottomPaddingOverrideSupplier; - - private final ImmutableList> mItems; - - private final EmptyStateProvider mEmptyStateProvider; - private final UserHandle mWorkProfileUserHandle; - private final UserHandle mCloneProfileUserHandle; - private final Supplier mWorkProfileQuietModeChecker; // True when work is quiet. - - private Set mLoadedPages; - private int mCurrentPage; - private OnProfileSelectedListener mOnProfileSelectedListener; - - protected MultiProfilePagerAdapter( - Function listAdapterExtractor, - AdapterBinder adapterBinder, - ImmutableList adapters, - EmptyStateProvider emptyStateProvider, - Supplier workProfileQuietModeChecker, - @Profile int defaultProfile, - UserHandle workProfileUserHandle, - UserHandle cloneProfileUserHandle, - Supplier pageViewInflater, - Supplier> containerBottomPaddingOverrideSupplier) { - mCurrentPage = defaultProfile; - mLoadedPages = new HashSet<>(); - mWorkProfileUserHandle = workProfileUserHandle; - mCloneProfileUserHandle = cloneProfileUserHandle; - mEmptyStateProvider = emptyStateProvider; - mWorkProfileQuietModeChecker = workProfileQuietModeChecker; - - mListAdapterExtractor = listAdapterExtractor; - mAdapterBinder = adapterBinder; - mPageViewInflater = pageViewInflater; - mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier; - - ImmutableList.Builder> items = - new ImmutableList.Builder<>(); - for (SinglePageAdapterT adapter : adapters) { - items.add(createProfileDescriptor(adapter)); - } - mItems = items.build(); - } - - private ProfileDescriptor 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 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 pageIndex. - *
    - *
  • For a device with only one user, pageIndex value of - * 0 would return the personal profile {@link ProfileDescriptor}.
  • - *
  • For a device with a work profile, pageIndex value of 0 would - * return the personal profile {@link ProfileDescriptor}, and pageIndex value of - * 1 would return the work profile {@link ProfileDescriptor}.
  • - *
- */ - private ProfileDescriptor getItem(int pageIndex) { - return mItems.get(pageIndex); - } - - public ViewGroup getEmptyStateView(int pageIndex) { - return getItem(pageIndex).getEmptyStateView(); - } - - /** - * Returns the number of {@link ProfileDescriptor} objects. - *

For a normal consumer device with only one user returns 1. - *

For a device with a work profile returns 2. - */ - 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 - * pageIndex. - *

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 pageIndex. - */ - public final void setupListAdapter(int pageIndex) { - mAdapterBinder.bind(getListViewForIndex(pageIndex), getAdapterForIndex(pageIndex)); - } - - /** - * Returns the {@link ListAdapterT} instance of the profile that represents - * userHandle. If there is no such adapter for the specified - * userHandle, returns {@code null}. - *

For example, if there is a work profile on the device with user id 10, calling this method - * with UserHandle.of(10) 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. - *

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 not currently visible to the user. Otherwise returns - * {@code null}. - *

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. - *

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. - *

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: - *

    - *
  1. (highest priority) cross-profile disabled by policy (handled in - * {@link #rebuildTab(ListAdapterT, boolean)})
  2. - *
  3. no apps available
  4. - *
  5. (least priority) work is off
  6. - *
- * - * 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 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 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. - *

This method is meant to be overridden so that subclasses can customize the padding. - */ - public void setupContainerPadding(View container) { - Optional bottomPaddingOverride = mContainerBottomPaddingOverrideSupplier.get(); - bottomPaddingOverride.ifPresent(paddingBottom -> - container.setPadding( - container.getPaddingLeft(), - container.getPaddingTop(), - container.getPaddingRight(), - paddingBottom)); - } - - public void showListView(ListAdapterT activeListAdapter) { - ProfileDescriptor 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 { - 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. - *

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/ProfileAvailability.kt b/java/src/com/android/intentresolver/ProfileAvailability.kt new file mode 100644 index 00000000..cf3e566e --- /dev/null +++ b/java/src/com/android/intentresolver/ProfileAvailability.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 + +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() + } + } + + /** 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..17e957ae 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,22 +78,20 @@ 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.EmptyState; @@ -115,22 +102,41 @@ import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider.De 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.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 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; + +import kotlinx.coroutines.CoroutineDispatcher; /** * This is a copy of ResolverActivity to support IntentResolver's ChooserActivity. This code is @@ -138,47 +144,33 @@ 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 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 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 +181,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; - - // 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. - *

Can only be used if there is a work profile. - *

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. - *

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 ResolverMultiProfilePagerAdapter mMultiProfilePagerAdapter; - 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 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 +219,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 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 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 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(); + + mProfiles = new ProfileHelper( + mUserInteractor, + getCoroutineScope(getLifecycle()), + mBackgroundDispatcher, + mFeatureFlags); + + mProfileAvailability = new ProfileAvailability( + mUserInteractor, + getCoroutineScope(getLifecycle()), + mBackgroundDispatcher); - mTitle = title; - mDefaultTitleResId = defaultTitleRes; + mProfileAvailability.setOnProfileStatusChange(this::onWorkProfileStatusUpdated); - mSupportsAlwaysUseOption = supportsAlwaysUseOption; - mSafeForwardingMode = safeForwardingMode; - mTargetDataLoader = targetDataLoader; + 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 +395,7 @@ public class ResolverActivity extends FragmentActivity implements } }); - boolean hasTouchScreen = getPackageManager() + boolean hasTouchScreen = mPackageManager .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN); if (isVoiceInteraction() || !hasTouchScreen) { @@ -487,13 +408,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 categories = intent.getCategories(); MetricsLogger.action(this, mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem() ? MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_APP_FEATURED @@ -502,19 +417,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 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 +479,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 +494,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 +502,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 +533,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 +551,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 +570,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 +583,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 +598,6 @@ public class ResolverActivity extends FragmentActivity implements } } - /** - * Replace me in subclasses! - */ @Override // ResolverListCommunicator public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { return defIntent; @@ -737,7 +606,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 +621,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 +665,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 +723,7 @@ public class ResolverActivity extends FragmentActivity implements } int bestMatch = 0; - for (int i=0; iSubclasses must call postRebuildListInternal at the end of postRebuildList. - * @param rebuildCompleted + * * @return true 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. - *

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 +849,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 +857,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 +898,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 +930,8 @@ public class ResolverActivity extends FragmentActivity implements Intent[] initialIntents, List 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 +940,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 +951,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) { @@ -1157,9 +966,9 @@ public class ResolverActivity extends FragmentActivity implements final EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider( this, workProfileUserHandle, - getAnnotatedUserHandles().personalProfileUserHandle, + mProfiles.getPersonalHandle(), getMetricsCategory(), - getAnnotatedUserHandles().tabOwnerUserHandleForLaunch + mProfiles.getTabOwnerUserHandleForLaunch() ); // Return composite provider, the order matters (the higher, the more priority) @@ -1170,76 +979,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 resolutionList, - boolean filterLastUsed, - TargetDataLoader targetDataLoader) { - ResolverListAdapter adapter = createResolverListAdapter( + private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForOneProfile( + Intent[] initialIntents, + List 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 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 +1038,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 (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 +1118,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 +1160,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 +1182,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 +1203,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 +1221,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 +1248,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 +1318,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 +1337,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 +1353,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 +1370,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 +1422,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 true 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 +1492,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 +1529,6 @@ public class ResolverActivity extends FragmentActivity implements return true; } - /** - * Finishing procedures to be performed after the list has been rebuilt. - * @param rebuildCompleted - * @return true 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 +1551,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 +1609,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. - * - *

This means meeting the following condition: - *

    - *
  • The app's {@link ApplicationInfo#crossProfile} flag must be true, and at least - * one of the following conditions must be fulfilled
  • - *
  • {@code Manifest.permission.INTERACT_ACROSS_USERS_FULL} granted.
  • - *
  • {@code Manifest.permission.INTERACT_ACROSS_USERS} granted.
  • - *
  • {@code Manifest.permission.INTERACT_ACROSS_PROFILES} granted, or the corresponding - * AppOps {@code android:interact_across_profiles} is set to "allow".
  • - *
- * + * @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 +1677,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 +1707,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 +1718,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 +1773,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 +1832,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 +1906,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 +1926,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() + + 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) { + request.errors.forEach { it.log(TAG) } + activity.finish() + } + + private fun initializeActivity(request: Valid) { + Log.d(TAG, "initializeActivity") + request.warnings.forEach { it.log(TAG) } + + activityInitializer.run() + } +} diff --git a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java deleted file mode 100644 index 591c23b7..00000000 --- a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java +++ /dev/null @@ -1,120 +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 android.widget.ListView; - -import androidx.viewpager.widget.PagerAdapter; - -import com.android.intentresolver.emptystate.EmptyStateProvider; -import com.android.internal.annotations.VisibleForTesting; - -import com.google.common.collect.ImmutableList; - -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 { - private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier; - - public ResolverMultiProfilePagerAdapter( - Context context, - ResolverListAdapter adapter, - EmptyStateProvider emptyStateProvider, - Supplier 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 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 listAdapters, - EmptyStateProvider emptyStateProvider, - Supplier 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); - } - - private static class BottomPaddingOverrideSupplier implements Supplier> { - private boolean mUseLayoutWithDefault; - - public void setUseLayoutWithDefault(boolean useLayoutWithDefault) { - mUseLayoutWithDefault = useLayoutWithDefault; - } - - @Override - public Optional get() { - return mUseLayoutWithDefault ? Optional.empty() : Optional.of(0); - } - } -} diff --git a/java/src/com/android/intentresolver/ShortcutSelectionLogic.java b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java index 12465184..2d5ec451 100644 --- a/java/src/com/android/intentresolver/ShortcutSelectionLogic.java +++ b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java @@ -30,8 +30,8 @@ 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.v2.ui.AppShortcutLimit; -import com.android.intentresolver.v2.ui.EnforceShortcutLimit; +import com.android.intentresolver.ui.AppShortcutLimit; +import com.android.intentresolver.ui.EnforceShortcutLimit; import java.util.Collections; import java.util.Comparator; 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 { + Comparator 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/contentpreview/ShareouselContentPreviewUi.kt b/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt index 3530ede1..fa0859e0 100644 --- a/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt +++ b/java/src/com/android/intentresolver/contentpreview/ShareouselContentPreviewUi.kt @@ -32,7 +32,7 @@ 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.v2.ui.viewmodel.ChooserViewModel +import com.android.intentresolver.ui.viewmodel.ChooserViewModel @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) class ShareouselContentPreviewUi : ContentPreviewUi() { 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 index 61c04ac1..c70fc83e 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ChooserRequestInteractor.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/ChooserRequestInteractor.kt @@ -18,7 +18,7 @@ package com.android.intentresolver.contentpreview.payloadtoggle.domain.interacto import android.content.Intent import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel -import com.android.intentresolver.v2.data.repository.ChooserRequestRepository +import com.android.intentresolver.data.repository.ChooserRequestRepository import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.asSharedFlow 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 index 9e48cd28..941dfca1 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractor.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractor.kt @@ -23,7 +23,7 @@ import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.toC 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.v2.data.repository.ChooserRequestRepository +import com.android.intentresolver.data.repository.ChooserRequestRepository import javax.inject.Inject import kotlinx.coroutines.flow.update 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 index 20af264a..1d34dc75 100644 --- a/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallback.kt +++ b/java/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallback.kt @@ -37,15 +37,15 @@ import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.Valu import com.android.intentresolver.inject.AdditionalContent import com.android.intentresolver.inject.ChooserIntent import com.android.intentresolver.inject.ChooserServiceFlags -import com.android.intentresolver.v2.ui.viewmodel.readAlternateIntents -import com.android.intentresolver.v2.ui.viewmodel.readChooserActions -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.log -import com.android.intentresolver.v2.validation.types.array -import com.android.intentresolver.v2.validation.types.value -import com.android.intentresolver.v2.validation.validateFrom +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 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 createFlow( + filter: IntentFilter, + user: UserHandle, + transform: (Intent) -> T?, + ): Flow = 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 = 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 = 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 = 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 = 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 + */ + 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 = 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, +) { + /** All information from the sharing application pertaining to the chooser. */ + val chooserRequest: MutableStateFlow = 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> = 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..c396b720 --- /dev/null +++ b/java/src/com/android/intentresolver/data/repository/DevicePolicyResources.kt @@ -0,0 +1,100 @@ +/* + * 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_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) + } + ) + } + + 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 = + * 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> + + /** + * A [Flow] of availability. Only profile users may become unavailable. + * + * Availability is currently defined as not being in [quietMode][UserInfo.isQuietModeEnabled]. + */ + val availability: Flow> + + /** + * 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 + +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, + 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 = + 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> = + usersWithState.map { userStates -> userStates.map { it.user } }.distinctUntilChanged() + + override val availability: Flow> = + 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.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 { + 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): 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 { + * return UserScopedServiceImpl(ctx, UserManager::class) + * } + * + * class MyUserHelper @Inject constructor( + * private val userMgr: UserScopedService, + * ) { + * fun isPrivateProfile(user: UserHandle): UserManager { + * return userMgr.forUser(user).isPrivateProfile() + * } + * } + * ``` + */ +fun interface UserScopedService { + /** Create a service instance for the given user. */ + fun forUser(user: UserHandle): T +} + +class UserScopedServiceImpl( + @ApplicationContext private val context: Context, + private val serviceType: KClass, +) : UserScopedService { + 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> = + 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 = + 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> = + 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/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> 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> 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 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/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java index 5f10cf32..7bfea4f8 100644 --- a/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java @@ -52,11 +52,9 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider { @NonNull private final UserHandle mTabOwnerUserHandleForLaunch; - public NoAppsAvailableEmptyStateProvider( - @NonNull Context context, + public NoAppsAvailableEmptyStateProvider(@NonNull Context context, @Nullable UserHandle workProfileUserHandle, - @Nullable UserHandle personalProfileUserHandle, - @NonNull String metricsCategory, + @Nullable UserHandle personalProfileUserHandle, @NonNull String metricsCategory, @NonNull UserHandle tabOwnerUserHandleForLaunch) { mContext = context; mWorkProfileUserHandle = workProfileUserHandle; @@ -125,22 +123,21 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider { public static class NoAppsAvailableEmptyState implements EmptyState { @NonNull - private String mTitle; + private final String mTitle; @NonNull - private String mMetricsCategory; + private final String mMetricsCategory; - private boolean mIsPersonalProfile; + private final boolean mIsPersonalProfile; - public NoAppsAvailableEmptyState(@NonNull String title, - @NonNull String metricsCategory, - boolean isPersonalProfile) { + public NoAppsAvailableEmptyState(@NonNull String title, @NonNull String metricsCategory, + boolean isPersonalProfile) { mTitle = title; mMetricsCategory = metricsCategory; mIsPersonalProfile = isPersonalProfile; } - @Nullable + @NonNull @Override public String getTitle() { return mTitle; diff --git a/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java index ce7bd8d9..e6d5d1c4 100644 --- a/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java @@ -19,13 +19,19 @@ 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 @@ -33,45 +39,56 @@ import com.android.intentresolver.ResolverListAdapter; */ 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 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; + // 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, tabOwnerHandle)) { + return null; } - } + switch (launchedAsProfile.getType()) { + case WORK: return mNoWorkToPersonalEmptyState; + case PERSONAL: return mNoPersonalToWorkEmptyState; + } + return null; + } /** * Empty state that gets strings from the device policy manager and tracks events into @@ -91,14 +108,10 @@ public class NoCrossProfileEmptyStateProvider implements EmptyStateProvider { @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) { + 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; diff --git a/java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java index 612828e0..cef88ce3 100644 --- a/java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java +++ b/java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java @@ -18,6 +18,8 @@ package com.android.intentresolver.emptystate; import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE; +import static java.util.Objects.requireNonNull; + import android.app.admin.DevicePolicyEventLogger; import android.app.admin.DevicePolicyManager; import android.content.Context; @@ -27,10 +29,12 @@ 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 +42,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,22 +63,33 @@ 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); } 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): 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, + 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 Parcel.requireParcelable(): T { + return requireNotNull(readParcelable()) { "A non-value required from this parcel was null!" } +} + +inline fun Parcel.readParcelable(): T? { + return readParcelable(T::class.java.classLoader, T::class.java) +} diff --git a/java/src/com/android/intentresolver/icons/TargetDataLoaderModule.kt b/java/src/com/android/intentresolver/icons/TargetDataLoaderModule.kt new file mode 100644 index 00000000..32c040b8 --- /dev/null +++ b/java/src/com/android/intentresolver/icons/TargetDataLoaderModule.kt @@ -0,0 +1,38 @@ +/* + * 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.icons + +import android.content.Context +import androidx.lifecycle.Lifecycle +import com.android.intentresolver.inject.ActivityOwned +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityComponent +import dagger.hilt.android.qualifiers.ActivityContext +import dagger.hilt.android.scopes.ActivityScoped + +@Module +@InstallIn(ActivityComponent::class) +object TargetDataLoaderModule { + @Provides + @ActivityScoped + fun targetDataLoader( + @ActivityContext context: Context, + @ActivityOwned lifecycle: Lifecycle, + ): TargetDataLoader = DefaultTargetDataLoader(context, lifecycle, isAudioCaptureDevice = false) +} diff --git a/java/src/com/android/intentresolver/inject/ActivityModelModule.kt b/java/src/com/android/intentresolver/inject/ActivityModelModule.kt index ff2bb14b..bbd25eb7 100644 --- a/java/src/com/android/intentresolver/inject/ActivityModelModule.kt +++ b/java/src/com/android/intentresolver/inject/ActivityModelModule.kt @@ -20,12 +20,12 @@ 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.v2.data.model.ChooserRequest -import com.android.intentresolver.v2.ui.model.ActivityModel -import com.android.intentresolver.v2.ui.viewmodel.readChooserRequest -import com.android.intentresolver.v2.validation.Valid -import com.android.intentresolver.v2.validation.ValidationResult +import com.android.intentresolver.validation.Valid +import com.android.intentresolver.validation.ValidationResult import dagger.Module import dagger.Provides import dagger.hilt.InstallIn diff --git a/java/src/com/android/intentresolver/inject/SystemServices.kt b/java/src/com/android/intentresolver/inject/SystemServices.kt index c09598e0..2a123dc7 100644 --- a/java/src/com/android/intentresolver/inject/SystemServices.kt +++ b/java/src/com/android/intentresolver/inject/SystemServices.kt @@ -27,8 +27,8 @@ import android.content.pm.ShortcutManager import android.os.UserManager import android.view.WindowManager import androidx.core.content.getSystemService -import com.android.intentresolver.v2.data.repository.UserScopedService -import com.android.intentresolver.v2.data.repository.UserScopedServiceImpl +import com.android.intentresolver.data.repository.UserScopedService +import com.android.intentresolver.data.repository.UserScopedServiceImpl import dagger.Binds import dagger.Module import dagger.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= IntentFilter.MATCH_CATEGORY_HOST + && match <= IntentFilter.MATCH_CATEGORY_PATH; + } + /** * Delegated to when used as a {@link Comparator} 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 { - 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/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 implements Comparator { + 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/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, etc. */ + @Provides + @Singleton + @AppPredictionAvailable + fun isAppPredictionAvailable(packageManager: PackageManager): Boolean { + return packageManager.appPredictionServicePackageName != null + } +} diff --git a/java/src/com/android/intentresolver/platform/ImageEditorModule.kt b/java/src/com/android/intentresolver/platform/ImageEditorModule.kt new file mode 100644 index 00000000..54b93939 --- /dev/null +++ b/java/src/com/android/intentresolver/platform/ImageEditorModule.kt @@ -0,0 +1,35 @@ +package com.android.intentresolver.platform + +import android.content.ComponentName +import android.content.res.Resources +import androidx.annotation.StringRes +import com.android.intentresolver.R +import com.android.intentresolver.inject.ApplicationOwned +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import java.util.Optional +import javax.inject.Qualifier +import javax.inject.Singleton + +internal fun Resources.componentName(@StringRes resId: Int): ComponentName? { + check(getResourceTypeName(resId) == "string") { "resId must be a string" } + return ComponentName.unflattenFromString(getString(resId)) +} + +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ImageEditor + +@Module +@InstallIn(SingletonComponent::class) +object ImageEditorModule { + /** + * The name of the preferred Activity to launch for editing images. This is added to Intents to + * edit images using Intent.ACTION_EDIT. + */ + @Provides + @Singleton + @ImageEditor + fun imageEditorComponent(@ApplicationOwned resources: Resources) = + Optional.ofNullable(resources.componentName(R.string.config_systemImageEditor)) +} diff --git a/java/src/com/android/intentresolver/platform/NearbyShareModule.kt b/java/src/com/android/intentresolver/platform/NearbyShareModule.kt new file mode 100644 index 00000000..4eaa24c0 --- /dev/null +++ b/java/src/com/android/intentresolver/platform/NearbyShareModule.kt @@ -0,0 +1,32 @@ +package com.android.intentresolver.platform + +import android.content.ComponentName +import android.content.res.Resources +import android.provider.Settings.Secure.NEARBY_SHARING_COMPONENT +import com.android.intentresolver.R +import com.android.intentresolver.inject.ApplicationOwned +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import java.util.Optional +import javax.inject.Qualifier +import javax.inject.Singleton + +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class NearbyShare + +@Module +@InstallIn(SingletonComponent::class) +object NearbyShareModule { + + @Provides + @Singleton + @NearbyShare + fun nearbyShareComponent(@ApplicationOwned resources: Resources, settings: SecureSettings) = + Optional.ofNullable( + ComponentName.unflattenFromString( + settings.getString(NEARBY_SHARING_COMPONENT)?.ifEmpty { null } + ?: resources.getString(R.string.config_defaultNearbySharingComponent), + ) + ) +} diff --git a/java/src/com/android/intentresolver/platform/PlatformSecureSettings.kt b/java/src/com/android/intentresolver/platform/PlatformSecureSettings.kt new file mode 100644 index 00000000..d2319873 --- /dev/null +++ b/java/src/com/android/intentresolver/platform/PlatformSecureSettings.kt @@ -0,0 +1,30 @@ +package com.android.intentresolver.platform + +import android.content.ContentResolver +import android.provider.Settings +import javax.inject.Inject + +/** + * Implements [SecureSettings] backed by Settings.Secure and a ContentResolver. + * + * These methods make Binder calls and may block, so use on the Main thread should be avoided. + */ +class PlatformSecureSettings @Inject constructor(private val resolver: ContentResolver) : + SecureSettings { + + override fun getString(name: String): String? { + return Settings.Secure.getString(resolver, name) + } + + override fun getInt(name: String): Int? { + return runCatching { Settings.Secure.getInt(resolver, name) }.getOrNull() + } + + override fun getLong(name: String): Long? { + return runCatching { Settings.Secure.getLong(resolver, name) }.getOrNull() + } + + override fun getFloat(name: String): Float? { + return runCatching { Settings.Secure.getFloat(resolver, name) }.getOrNull() + } +} diff --git a/java/src/com/android/intentresolver/platform/SecureSettings.kt b/java/src/com/android/intentresolver/platform/SecureSettings.kt new file mode 100644 index 00000000..86fc8e98 --- /dev/null +++ b/java/src/com/android/intentresolver/platform/SecureSettings.kt @@ -0,0 +1,25 @@ +package com.android.intentresolver.platform + +import android.provider.Settings.SettingNotFoundException + +/** + * A component which provides access to values from [android.provider.Settings.Secure]. + * + * All methods return nullable types instead of throwing [SettingNotFoundException] which yields + * cleaner, more idiomatic Kotlin code: + * + * // apply a default: val foo = settings.getInt(FOO) ?: DEFAULT_FOO + * + * // assert if missing: val required = settings.getInt(REQUIRED_VALUE) ?: error("required value + * missing") + */ +interface SecureSettings { + + fun getString(name: String): String? + + fun getInt(name: String): Int? + + fun getLong(name: String): Long? + + fun getFloat(name: String): Float? +} diff --git a/java/src/com/android/intentresolver/platform/SecureSettingsModule.kt b/java/src/com/android/intentresolver/platform/SecureSettingsModule.kt new file mode 100644 index 00000000..260e50a1 --- /dev/null +++ b/java/src/com/android/intentresolver/platform/SecureSettingsModule.kt @@ -0,0 +1,14 @@ +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 (as in {@link MultiProfilePagerAdapter}). + * @param (as in {@link MultiProfilePagerAdapter}). + */ +public interface AdapterBinder { + /** + * 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/profiles/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/profiles/ChooserMultiProfilePagerAdapter.java new file mode 100644 index 00000000..4d0f4a49 --- /dev/null +++ b/java/src/com/android/intentresolver/profiles/ChooserMultiProfilePagerAdapter.java @@ -0,0 +1,212 @@ +/* + * 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.content.Context; +import android.os.UserHandle; +import android.view.LayoutInflater; +import android.view.ViewGroup; + +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.viewpager.widget.PagerAdapter; + +import com.android.intentresolver.ChooserListAdapter; +import com.android.intentresolver.ChooserRecyclerViewAccessibilityDelegate; +import com.android.intentresolver.FeatureFlags; +import com.android.intentresolver.R; +import com.android.intentresolver.emptystate.EmptyStateProvider; +import com.android.intentresolver.grid.ChooserGridAdapter; +import com.android.intentresolver.measurements.Tracer; + +import com.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. + */ +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, + ImmutableList> tabs, + EmptyStateProvider emptyStateProvider, + Supplier workProfileQuietModeChecker, + @ProfileType int defaultProfile, + UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle, + int maxTargetsPerRow, + FeatureFlags featureFlags) { + this( + context, + new ChooserProfileAdapterBinder(maxTargetsPerRow), + tabs, + emptyStateProvider, + workProfileQuietModeChecker, + defaultProfile, + workProfileUserHandle, + cloneProfileUserHandle, + new BottomPaddingOverrideSupplier(context), + featureFlags); + } + + private ChooserMultiProfilePagerAdapter( + Context context, + ChooserProfileAdapterBinder adapterBinder, + ImmutableList> tabs, + EmptyStateProvider emptyStateProvider, + Supplier workProfileQuietModeChecker, + @ProfileType int defaultProfile, + UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle, + BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier, + FeatureFlags featureFlags) { + super( + gridAdapter -> gridAdapter.getListAdapter(), + adapterBinder, + tabs, + emptyStateProvider, + workProfileQuietModeChecker, + defaultProfile, + workProfileUserHandle, + cloneProfileUserHandle, + () -> makeProfileView(context, featureFlags), + bottomPaddingOverrideSupplier); + mAdapterBinder = adapterBinder; + mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier; + } + + public void setMaxTargetsPerRow(int maxTargetsPerRow) { + mAdapterBinder.setMaxTargetsPerRow(maxTargetsPerRow); + } + + public void setEmptyStateBottomOffset(int bottomOffset) { + mBottomPaddingOverrideSupplier.setEmptyStateBottomOffset(bottomOffset); + setupContainerPadding(); + } + + /** + * Notify adapter about the drawer's collapse state. This will affect the app divider's + * visibility. + */ + public void setIsCollapsed(boolean isCollapsed) { + for (int i = 0, size = getItemCount(); i < size; i++) { + getPageAdapterForIndex(i).setAzLabelVisibility(!isCollapsed); + } + } + + private static ViewGroup makeProfileView( + Context context, FeatureFlags featureFlags) { + LayoutInflater inflater = LayoutInflater.from(context); + ViewGroup rootView = featureFlags.scrollablePreview() + ? (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile_wrap, null, false) + : (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile, null, false); + RecyclerView recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list); + recyclerView.setAccessibilityDelegateCompat( + new ChooserRecyclerViewAccessibilityDelegate(recyclerView)); + return rootView; + } + + @Override + public boolean onHandlePackagesChanged( + ChooserListAdapter listAdapter, boolean waitingToEnableWorkProfile) { + // TODO: why do we need to do the extra `notifyDataSetChanged()` in (only) the Chooser case? + getActiveListAdapter().notifyDataSetChanged(); + return super.onHandlePackagesChanged(listAdapter, waitingToEnableWorkProfile); + } + + @Override + protected final boolean rebuildTab(ChooserListAdapter listAdapter, boolean doPostProcessing) { + if (doPostProcessing) { + Tracer.INSTANCE.beginAppTargetLoadingSection(listAdapter.getUserHandle()); + } + return super.rebuildTab(listAdapter, doPostProcessing); + } + + /** Apply the specified {@code height} as the footer in each tab's adapter. */ + public void setFooterHeightInEveryAdapter(int height) { + for (int i = 0; i < getItemCount(); ++i) { + 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(); + } + } + } + + private static class BottomPaddingOverrideSupplier implements Supplier> { + private final Context mContext; + private int mBottomOffset; + + BottomPaddingOverrideSupplier(Context context) { + mContext = context; + } + + public void setEmptyStateBottomOffset(int bottomOffset) { + mBottomOffset = bottomOffset; + } + + @Override + public Optional 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 { + 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/profiles/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/profiles/MultiProfilePagerAdapter.java new file mode 100644 index 00000000..48de37de --- /dev/null +++ b/java/src/com/android/intentresolver/profiles/MultiProfilePagerAdapter.java @@ -0,0 +1,694 @@ +/* + * 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.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; + +import com.android.intentresolver.ResolverListAdapter; +import com.android.intentresolver.emptystate.EmptyState; +import com.android.intentresolver.emptystate.EmptyStateProvider; +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). + * + * @param the type of the widget that represents the contents of a page in this adapter + * @param the type of a "root" adapter class to be instantiated and included in + * the per-profile records. + * @param 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 + * mListAdapterExtractor. + */ +public class MultiProfilePagerAdapter< + PageViewT extends ViewGroup, + SinglePageAdapterT, + ListAdapterT extends ResolverListAdapter> extends PagerAdapter { + + public static final int PROFILE_PERSONAL = Profile.Type.PERSONAL.ordinal(); + public static final int PROFILE_WORK = Profile.Type.WORK.ordinal(); + + // Removed, must be constants. This is only used for linting anyway. + // @IntDef({PROFILE_PERSONAL, PROFILE_WORK}) + public @interface ProfileType {} + + private final Function mListAdapterExtractor; + private final AdapterBinder mAdapterBinder; + private final Supplier mPageViewInflater; + + private final ImmutableList> mItems; + + private final EmptyStateProvider mEmptyStateProvider; + private final UserHandle mWorkProfileUserHandle; + private final UserHandle mCloneProfileUserHandle; + private final Supplier mWorkProfileQuietModeChecker; // True when work is quiet. + + private final Set mLoadedPages; + private int mCurrentPage; + private OnProfileSelectedListener mOnProfileSelectedListener; + + protected MultiProfilePagerAdapter( + Function listAdapterExtractor, + AdapterBinder adapterBinder, + ImmutableList> tabs, + EmptyStateProvider emptyStateProvider, + Supplier workProfileQuietModeChecker, + @ProfileType int defaultProfile, + UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle, + Supplier pageViewInflater, + Supplier> containerBottomPaddingOverrideSupplier) { + mLoadedPages = new HashSet<>(); + mWorkProfileUserHandle = workProfileUserHandle; + mCloneProfileUserHandle = cloneProfileUserHandle; + mEmptyStateProvider = emptyStateProvider; + mWorkProfileQuietModeChecker = workProfileQuietModeChecker; + + mListAdapterExtractor = listAdapterExtractor; + mAdapterBinder = adapterBinder; + mPageViewInflater = pageViewInflater; + + ImmutableList.Builder> items = + new ImmutableList.Builder<>(); + for (TabConfig 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 createProfileDescriptor( + @ProfileType int profile, + String tabLabel, + String tabAccessibilityLabel, + String tabTag, + SinglePageAdapterT adapter, + Supplier> containerBottomPaddingOverrideSupplier) { + return new ProfileDescriptor<>( + profile, + tabLabel, + tabAccessibilityLabel, + tabTag, + mPageViewInflater.get(), + adapter, + containerBottomPaddingOverrideSupplier); + } + + 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 + * userHandle. If there is no such adapter for the specified + * userHandle, returns {@code null}. + *

For example, if there is a work profile on the device with user id 10, calling this method + * with UserHandle.of(10) returns the work profile {@link ListAdapterT}. + */ + @Nullable + public final ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) { + return getListAdapterForPageNumber(getPageNumberForUserHandle(userHandle)); + } + + @Nullable + private ProfileDescriptor 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 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); + } + }; + } + + /** + * 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.onProfilePageSelected( + getProfileForPageNumber(position), 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() { + forEachInactivePage(pageNumber -> mLoadedPages.remove(pageNumber)); + } + + @Override + public final ViewGroup instantiateItem(ViewGroup container, int position) { + setupListAdapter(position); + final ProfileDescriptor descriptor = getItem(position); + container.addView(descriptor.mRootView); + return descriptor.mRootView; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object view) { + container.removeView((View) view); + } + + @Override + public int getCount() { + return getItemCount(); + } + + public int getCurrentPage() { + return mCurrentPage; + } + + public final @ProfileType int getActiveProfile() { + return getProfileForPageNumber(getCurrentPage()); + } + + @VisibleForTesting + public UserHandle getCurrentUserHandle() { + return getActiveListAdapter().getUserHandle(); + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return view == object; + } + + @Override + public CharSequence getPageTitle(int position) { + return null; + } + + public UserHandle getCloneUserHandle() { + return mCloneProfileUserHandle; + } + + /** + * Returns the {@link ProfileDescriptor} relevant to the given pageIndex. + *

    + *
  • For a device with only one user, pageIndex value of + * 0 would return the personal profile {@link ProfileDescriptor}.
  • + *
  • For a device with a work profile, pageIndex value of 0 would + * return the personal profile {@link ProfileDescriptor}, and pageIndex value of + * 1 would return the work profile {@link ProfileDescriptor}.
  • + *
+ */ + @Nullable + private ProfileDescriptor getItem(int pageIndex) { + if (!hasPageForIndex(pageIndex)) { + return null; + } + return mItems.get(pageIndex); + } + + private ViewGroup getEmptyStateView(int pageIndex) { + return getItem(pageIndex).getEmptyStateView(); + } + + public ViewGroup getActiveEmptyStateView() { + return getEmptyStateView(getCurrentPage()); + } + + /** + * Returns the number of {@link ProfileDescriptor} objects. + *

For a normal consumer device with only one user returns 1. + *

For a device with a work profile returns 2. + */ + public final int getItemCount() { + return mItems.size(); + } + + public final PageViewT getListViewForIndex(int index) { + return getItem(index).getView(); + } + + /** + * Returns the adapter of the list view for the relevant page specified by + * pageIndex. + *

This method is meant to be implemented with an implementation-specific return type + * depending on the adapter type. + */ + @VisibleForTesting + public final SinglePageAdapterT getPageAdapterForIndex(int index) { + if (!hasPageForIndex(index)) { + return null; + } + return getItem(index).getAdapter(); + } + + /** + * Performs view-related initialization procedures for the adapter specified + * by pageIndex. + */ + public final void setupListAdapter(int pageIndex) { + mAdapterBinder.bind(getListViewForIndex(pageIndex), getPageAdapterForIndex(pageIndex)); + } + + /** + * Returns the {@link ListAdapterT} instance of the profile that is currently visible + * to the user. + *

For example, if the user is viewing the work tab in the share sheet, this method returns + * the work profile {@link ListAdapterT}. + */ + @VisibleForTesting + public final ListAdapterT getActiveListAdapter() { + return getListAdapterForPageNumber(getCurrentPage()); + } + + public final ListAdapterT getPersonalListAdapter() { + return getListAdapterForPageNumber(getPageNumberForProfile(PROFILE_PERSONAL)); + } + + @Nullable + public final ListAdapterT getWorkListAdapter() { + if (!hasPageForProfile(PROFILE_WORK)) { + return null; + } + return getListAdapterForPageNumber(getPageNumberForProfile(PROFILE_WORK)); + } + + public final SinglePageAdapterT getCurrentRootAdapter() { + return getPageAdapterForIndex(getCurrentPage()); + } + + public final PageViewT getActiveAdapterView() { + return getListViewForIndex(getCurrentPage()); + } + + private boolean anyAdapterHasItems() { + for (int i = 0; i < mItems.size(); ++i) { + ListAdapterT listAdapter = getListAdapterForPageNumber(i); + if (listAdapter.getCount() > 0) { + return true; + } + } + return false; + } + + public void refreshPackagesInAllTabs() { + // 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(); + forEachInactivePage(page -> getListAdapterForPageNumber(page).handlePackagesChanged()); + } + + /** + * Notify that there has been a package change which could potentially modify the set of targets + * that should be shown in the specified {@code listAdapter}. This may result in + * "rebuilding" the target list for that adapter. + * + * @param listAdapter an adapter that may need to be updated after the package-change event. + * @param waitingToEnableWorkProfile whether we've turned on the work profile, but haven't yet + * seen an {@code ACTION_USER_UNLOCKED} broadcast. In this case we skip the rebuild of any + * work-profile adapter because we wouldn't expect meaningful results -- but another rebuild + * will be prompted when we eventually get the broadcast. + * + * @return whether we're able to proceed with a Sharesheet session after processing this + * package-change event. If false, we were able to rebuild the targets but determined that there + * aren't any we could present in the UI without the app looking broken, so we should just quit. + */ + public boolean onHandlePackagesChanged( + ListAdapterT listAdapter, boolean waitingToEnableWorkProfile) { + if (listAdapter == getActiveListAdapter()) { + if (listAdapter.getUserHandle().equals(mWorkProfileUserHandle) + && waitingToEnableWorkProfile) { + // We have just turned on the work profile and entered the passcode to start it, + // now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no + // point in reloading the list now, since the work profile user is still turning on. + return true; + } + + boolean listRebuilt = rebuildActiveTab(true); + if (listRebuilt) { + listAdapter.notifyDataSetChanged(); + } + + // TODO: shouldn't we check that the inactive tabs are built before declaring that we + // have to quit for lack of items? + return anyAdapterHasItems(); + } else { + clearInactiveProfileCache(); + return true; + } + } + + /** + * Fully-rebuild the active tab and, if specified, partially-rebuild any other inactive tabs. + */ + public boolean rebuildTabs(boolean includePartialRebuildOfInactiveTabs) { + // TODO: we may be able to determine `includePartialRebuildOfInactiveTabs` ourselves as + // a function of our own instance state. OTOH the purpose of this "partial rebuild" is to + // be able to evaluate the intermediate state of one particular profile tab (i.e. work + // profile) that may not generalize well when we have other "inactive tabs." I.e., either we + // rebuild *all* the inactive tabs just to evaluate some auto-launch conditions that only + // depend on personal and/or work tabs, or we have to explicitly specify the ones we care + // about. It's not the pager-adapter's business to know "which ones we care about," so maybe + // they should be rebuilt lazily when-and-if it comes up (e.g. during the evaluation of + // autolaunch conditions). + boolean rebuildCompleted = rebuildActiveTab(true) || getActiveListAdapter().isTabLoaded(); + if (includePartialRebuildOfInactiveTabs) { + // 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; + } + + /** + * Rebuilds the tab that is currently visible to the user. + *

Returns {@code true} if rebuild has completed. + */ + public final boolean rebuildActiveTab(boolean doPostProcessing) { + Trace.beginSection("MultiProfilePagerAdapter#rebuildActiveTab"); + boolean result = rebuildTab(getActiveListAdapter(), doPostProcessing); + Trace.endSection(); + return result; + } + + /** + * Rebuilds any tabs that are not currently visible to the user. + *

Returns {@code true} if rebuild has completed in all inactive tabs. + */ + private boolean rebuildInactiveTabs(boolean doPostProcessing) { + Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab"); + 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 allRebuildsComplete.get(); + } + + protected void forEachPage(Consumer pageNumberHandler) { + for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) { + pageNumberHandler.accept(pageNumber); + } + } + + protected void forEachInactivePage(Consumer inactivePageNumberHandler) { + forEachPage(pageNumber -> { + if (pageNumber != getCurrentPage()) { + inactivePageNumberHandler.accept(pageNumber); + } + }); + } + + protected boolean rebuildTab(ListAdapterT activeListAdapter, boolean doPostProcessing) { + if (shouldSkipRebuild(activeListAdapter)) { + activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true); + return false; + } + return activeListAdapter.rebuildList(doPostProcessing); + } + + private boolean shouldSkipRebuild(ListAdapterT activeListAdapter) { + EmptyState emptyState = mEmptyStateProvider.getEmptyState(activeListAdapter); + return emptyState != null && emptyState.shouldSkipDataRebuild(); + } + + /** + * The empty state screens are shown according to their priority: + *

    + *
  1. (highest priority) cross-profile disabled by policy (handled in + * {@link #rebuildTab(ListAdapterT, boolean)})
  2. + *
  3. no apps available
  4. + *
  5. (least priority) work is off
  6. + *
+ * + * The intention is to prevent the user from having to turn + * the work profile on if there will not be any apps resolved + * anyway. + * + * TODO: move this comment to the place where we configure our composite provider. + */ + public void showEmptyResolverListEmptyState(ListAdapterT listAdapter) { + final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter); + + if (emptyState == null) { + return; + } + + emptyState.onEmptyStateShown(); + + View.OnClickListener clickListener = null; + + if (emptyState.getButtonClickListener() != null) { + clickListener = v -> emptyState.getButtonClickListener().onClick(() -> { + ProfileDescriptor descriptor = + getDescriptorForUserHandle(listAdapter.getUserHandle()); + descriptor.mEmptyStateUi.showSpinner(); + }); + } + + showEmptyState(listAdapter, emptyState, clickListener); + } + + private void showEmptyState( + ListAdapterT activeListAdapter, + EmptyState emptyState, + View.OnClickListener buttonOnClick) { + ProfileDescriptor descriptor = + getDescriptorForUserHandle(activeListAdapter.getUserHandle()); + descriptor.mEmptyStateUi.showEmptyState(emptyState, buttonOnClick); + activeListAdapter.markTabLoaded(); + } + + /** + * Sets up the padding of the view containing the empty state screens for the current adapter + * view. + */ + protected final void setupContainerPadding() { + getItem(getCurrentPage()).setupContainerPadding(); + } + + public void showListView(ListAdapterT activeListAdapter) { + ProfileDescriptor descriptor = + getDescriptorForUserHandle(activeListAdapter.getUserHandle()); + descriptor.mEmptyStateUi.hide(); + } + + /** + * @return whether any "inactive" tab's adapter would show an empty-state screen in our current + * application state. + */ + public final boolean shouldShowEmptyStateScreenInAnyInactiveAdapter() { + 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) { + int count = listAdapter.getUnfilteredCount(); + return (count == 0 && listAdapter.getPlaceholderCount() == 0) + || (listAdapter.getUserHandle().equals(mWorkProfileUserHandle) + && mWorkProfileQuietModeChecker.get()); + } + +} 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. + *

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 { + 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> 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/profiles/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/profiles/ResolverMultiProfilePagerAdapter.java new file mode 100644 index 00000000..0c669510 --- /dev/null +++ b/java/src/com/android/intentresolver/profiles/ResolverMultiProfilePagerAdapter.java @@ -0,0 +1,112 @@ +/* + * 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.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.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. + */ +public class ResolverMultiProfilePagerAdapter extends + MultiProfilePagerAdapter { + private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier; + + public ResolverMultiProfilePagerAdapter(Context context, + ImmutableList> tabs, + EmptyStateProvider emptyStateProvider, + Supplier workProfileQuietModeChecker, + @ProfileType int defaultProfile, + UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle) { + this( + context, + tabs, + emptyStateProvider, + workProfileQuietModeChecker, + defaultProfile, + workProfileUserHandle, + cloneProfileUserHandle, + new BottomPaddingOverrideSupplier()); + } + + private ResolverMultiProfilePagerAdapter( + Context context, + ImmutableList> tabs, + EmptyStateProvider emptyStateProvider, + Supplier workProfileQuietModeChecker, + @ProfileType int defaultProfile, + UserHandle workProfileUserHandle, + UserHandle cloneProfileUserHandle, + BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) { + super( + listAdapter -> listAdapter, + (listView, bindAdapter) -> listView.setAdapter(bindAdapter), + tabs, + 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: 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> { + private boolean mUseLayoutWithDefault; + + public void setUseLayoutWithDefault(boolean useLayoutWithDefault) { + mUseLayoutWithDefault = useLayoutWithDefault; + } + + @Override + public Optional get() { + return mUseLayoutWithDefault ? Optional.empty() : Optional.of(0); + } + } +} 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 { + 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/shared/model/User.kt b/java/src/com/android/intentresolver/shared/model/User.kt new file mode 100644 index 00000000..b544a390 --- /dev/null +++ b/java/src/com/android/intentresolver/shared/model/User.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 android.annotation.UserIdInt +import android.os.UserHandle + +/** + * A User represents the owner of a distinct set of content. + * * maps 1:1 to a UserHandle or UserId (Int) value. + * * refers to either [Full][Type.FULL], or a [Profile][Type.PROFILE] user, as indicated by the + * [type] property. + * + * See + * [Users for system developers](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/Users.md) + * + * ``` + * val users = listOf( + * User(id = 0, role = PERSONAL), + * User(id = 10, role = WORK), + * User(id = 11, role = CLONE), + * User(id = 12, role = PRIVATE), + * ) + * ``` + */ +data class User( + @UserIdInt val id: Int, + val role: Role, +) { + val handle: UserHandle = UserHandle.of(id) + + enum class Role { + PERSONAL, + PRIVATE, + WORK, + CLONE + } +} diff --git a/java/src/com/android/intentresolver/ui/ActionTitle.java b/java/src/com/android/intentresolver/ui/ActionTitle.java new file mode 100644 index 00000000..1cc96fa9 --- /dev/null +++ b/java/src/com/android/intentresolver/ui/ActionTitle.java @@ -0,0 +1,88 @@ +/* + * 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.ui; + +import android.content.Intent; +import android.provider.MediaStore; + +import androidx.annotation.StringRes; + +import com.android.intentresolver.R; + +/** + * Provides a set of related resources for different use cases. + */ +public enum ActionTitle { + VIEW(Intent.ACTION_VIEW, + R.string.whichViewApplication, + R.string.whichViewApplicationNamed, + R.string.whichViewApplicationLabel), + EDIT(Intent.ACTION_EDIT, + R.string.whichEditApplication, + R.string.whichEditApplicationNamed, + R.string.whichEditApplicationLabel), + SEND(Intent.ACTION_SEND, + R.string.whichSendApplication, + R.string.whichSendApplicationNamed, + R.string.whichSendApplicationLabel), + SENDTO(Intent.ACTION_SENDTO, + R.string.whichSendToApplication, + R.string.whichSendToApplicationNamed, + R.string.whichSendToApplicationLabel), + SEND_MULTIPLE(Intent.ACTION_SEND_MULTIPLE, + R.string.whichSendApplication, + R.string.whichSendApplicationNamed, + R.string.whichSendApplicationLabel), + CAPTURE_IMAGE(MediaStore.ACTION_IMAGE_CAPTURE, + R.string.whichImageCaptureApplication, + R.string.whichImageCaptureApplicationNamed, + R.string.whichImageCaptureApplicationLabel), + DEFAULT(null, + R.string.whichApplication, + R.string.whichApplicationNamed, + R.string.whichApplicationLabel), + HOME(Intent.ACTION_MAIN, + R.string.whichHomeApplication, + R.string.whichHomeApplicationNamed, + R.string.whichHomeApplicationLabel); + + // titles for layout that deals with http(s) intents + public static final int BROWSABLE_TITLE_RES = R.string.whichOpenLinksWith; + public static final int BROWSABLE_HOST_TITLE_RES = R.string.whichOpenHostLinksWith; + public static final int BROWSABLE_HOST_APP_TITLE_RES = R.string.whichOpenHostLinksWithApp; + public static final int BROWSABLE_APP_TITLE_RES = R.string.whichOpenLinksWithApp; + + public final String action; + public final int titleRes; + public final int namedTitleRes; + public final @StringRes int labelRes; + + ActionTitle(String action, int titleRes, int namedTitleRes, @StringRes int labelRes) { + this.action = action; + this.titleRes = titleRes; + this.namedTitleRes = namedTitleRes; + this.labelRes = labelRes; + } + + public static ActionTitle forAction(String action) { + for (ActionTitle title : values()) { + if (title != HOME && action != null && action.equals(title.action)) { + return title; + } + } + return DEFAULT; + } +} diff --git a/java/src/com/android/intentresolver/ui/ProfilePagerResources.kt b/java/src/com/android/intentresolver/ui/ProfilePagerResources.kt new file mode 100644 index 00000000..baab9a4c --- /dev/null +++ b/java/src/com/android/intentresolver/ui/ProfilePagerResources.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.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 + } + } +} 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 { + override fun newArray(size: Int) = arrayOfNulls(size) + override fun createFromParcel(source: Parcel) = ActivityModel(source) + } + + @JvmStatic + fun createFrom(activity: Activity): ActivityModel { + return ActivityModel( + activity.intent, + activity.launchedFromUid, + Objects.requireNonNull(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? = 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 = 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 { + 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(EXTRA_REPLACEMENT_EXTRAS)) + + val (customTitle, defaultTitleResource) = + if (isSendAction) { + ignored( + value(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(EXTRA_TITLE)) + custom to (custom?.let { 0 } ?: R.string.chooseActivity) + } + + val initialIntents = + optional(array(EXTRA_INITIAL_INTENTS))?.take(MAX_INITIAL_INTENTS)?.map { + it.maybeAddSendActionFlags() + } + ?: emptyList() + + val chosenComponentSender = + optional(value(EXTRA_CHOOSER_RESULT_INTENT_SENDER)) + ?: optional(value(EXTRA_CHOSEN_COMPONENT_INTENT_SENDER)) + + val refinementIntentSender = + optional(value(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER)) + + val filteredComponents = + optional(array(EXTRA_EXCLUDE_COMPONENTS)) ?: emptyList() + + @Suppress("DEPRECATION") + val callerChooserTargets = + optional(array(EXTRA_CHOOSER_TARGETS)) ?: emptyList() + + val retainInOnStop = + optional(value(ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP)) ?: false + + val sharedText = optional(value(EXTRA_TEXT)) + + val chooserActions = readChooserActions() ?: emptyList() + + val modifyShareAction = optional(value(EXTRA_CHOOSER_MODIFY_SHARE_ACTION)) + + val additionalContentUri: Uri? + val focusedItemPos: Int + if (isSendAction && flags.chooserPayloadToggling()) { + additionalContentUri = optional(value(Intent.EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI)) + focusedItemPos = optional(value(Intent.EXTRA_CHOOSER_FOCUSED_ITEM_POSITION)) ?: 0 + } else { + additionalContentUri = null + focusedItemPos = 0 + } + + val contentTypeHint = + if (flags.chooserAlbumText()) { + when (optional(value(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(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? = + optional(array(EXTRA_ALTERNATE_INTENTS))?.map { it.maybeAddSendActionFlags() } + +fun Validation.readChooserActions(): List? = + optional(array(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, + private val processUpdatesInteractor: Lazy, + private val fetchPreviewsInteractor: Lazy, + @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, + private val chooserRequestRepository: Lazy, +) : 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 + 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 { + @Suppress("DEPRECATION") + return validateFrom((launch.intent.extras ?: Bundle())::get) { + val callingUser = optional(value(EXTRA_CALLING_USER)) + val selectedProfile = checkSelectedProfile() + val audioDevice = optional(value(EXTRA_IS_AUDIO_CAPTURE_DEVICE)) ?: false + ResolverRequest(launch.intent, selectedProfile, callingUser, audioDevice) + } +} + +private fun Validation.checkSelectedProfile(): Profile.Type? { + return when (val selected = optional(value(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 + + /** + * A [StateFlow] of [ResolverRequest]. + * + * Note: Only safe to access after checking if [initialRequest] is [Valid]. + */ + lateinit var request: StateFlow + 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/v2/ChooserActionFactory.java b/java/src/com/android/intentresolver/v2/ChooserActionFactory.java deleted file mode 100644 index efd5bfd1..00000000 --- a/java/src/com/android/intentresolver/v2/ChooserActionFactory.java +++ /dev/null @@ -1,400 +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.v2.ui.ShareResultSender; -import com.android.intentresolver.v2.ui.model.ShareAction; -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 Runnable mCopyButtonRunnable; - private Runnable mEditButtonRunnable; - private final ImmutableList mCustomActions; - private final Consumer mExcludeSharedTextAction; - @Nullable private final ShareResultSender mShareResultSender; - private final Consumer 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 chooserActions, - Optional imageEditor, - EventLog log, - Consumer onUpdateSharedTextIsExcluded, - Callable firstVisibleImageQuery, - ActionActivityStarter activityStarter, - @Nullable ShareResultSender shareResultSender, - Consumer finishCallback, - ClipboardManager clipboardManager) { - this( - context, - makeCopyButtonRunnable( - clipboardManager, - targetIntent, - referrerPackageName, - finishCallback, - log), - makeEditButtonRunnable( - getEditSharingTarget( - context, - targetIntent, - imageEditor), - firstVisibleImageQuery, - activityStarter, - log), - chooserActions, - onUpdateSharedTextIsExcluded, - log, - shareResultSender, - finishCallback); - - } - - @VisibleForTesting - ChooserActionFactory( - Context context, - @Nullable Runnable copyButtonRunnable, - Runnable editButtonRunnable, - List customActions, - Consumer onUpdateSharedTextIsExcluded, - EventLog log, - @Nullable ShareResultSender shareResultSender, - Consumer finishCallback) { - mContext = context; - mCopyButtonRunnable = copyButtonRunnable; - mEditButtonRunnable = editButtonRunnable; - mCustomActions = ImmutableList.copyOf(customActions); - mExcludeSharedTextAction = onUpdateSharedTextIsExcluded; - mLog = log; - mShareResultSender = shareResultSender; - mFinishCallback = finishCallback; - - if (mShareResultSender != null) { - mEditButtonRunnable = () -> { - mShareResultSender.onActionSelected(ShareAction.SYSTEM_EDIT); - editButtonRunnable.run(); - }; - if (mCopyButtonRunnable != null) { - mCopyButtonRunnable = () -> { - mShareResultSender.onActionSelected(ShareAction.SYSTEM_COPY); - copyButtonRunnable.run(); - }; - } - } - } - - @Override - @Nullable - public Runnable getEditButtonRunnable() { - return mEditButtonRunnable; - } - - @Override - @Nullable - public Runnable getCopyButtonRunnable() { - return mCopyButtonRunnable; - } - - /** Create custom actions */ - @Override - public List createCustomActions() { - List actions = new ArrayList<>(); - for (int i = 0; i < mCustomActions.size(); i++) { - final int position = i; - ActionRow.Action actionRow = createCustomAction( - mContext, - mCustomActions.get(i), - () -> logCustomAction(position), - mShareResultSender, - mFinishCallback); - if (actionRow != null) { - actions.add(actionRow); - } - } - return actions; - } - - /** - *

- * Creates an exclude-text action that can be called when the user changes shared text - * status in the Media + Text preview. - *

- *

- * true argument value indicates that the text should be excluded. - *

- */ - @Override - public Consumer getExcludeSharedTextAction() { - return mExcludeSharedTextAction; - } - - @Nullable - private static Runnable makeCopyButtonRunnable( - ClipboardManager clipboardManager, - Intent targetIntent, - String referrerPackageName, - Consumer 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.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 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 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 - static ActionRow.Action createCustomAction( - Context context, - @Nullable ChooserAction action, - Runnable loggingRunnable, - ShareResultSender shareResultSender, - Consumer finishCallback) { - if (action == 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(); - } - 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/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java deleted file mode 100644 index 5f3129f8..00000000 --- a/java/src/com/android/intentresolver/v2/ChooserActivity.java +++ /dev/null @@ -1,2612 +0,0 @@ -/* - * 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 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.v2.ext.CreationExtrasExtKt.addDefaultArgs; -import static com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter.PROFILE_PERSONAL; -import static com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter.PROFILE_WORK; -import static com.android.intentresolver.v2.ui.model.ActivityModel.ACTIVITY_MODEL_KEY; -import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET; - -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; -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.StrictMode; -import android.os.SystemClock; -import android.os.Trace; -import android.os.UserHandle; -import android.service.chooser.ChooserTarget; -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.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.TextView; -import android.widget.Toast; - -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.ChooserGridLayoutManager; -import com.android.intentresolver.ChooserListAdapter; -import com.android.intentresolver.ChooserRefinementManager; -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.PackagesChangedListener; -import com.android.intentresolver.R; -import com.android.intentresolver.ResolverListAdapter; -import com.android.intentresolver.ResolverListController; -import com.android.intentresolver.ResolverViewPager; -import com.android.intentresolver.StartsSelectedItem; -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.CompositeEmptyStateProvider; -import com.android.intentresolver.emptystate.CrossProfileIntentsChecker; -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.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.shortcuts.AppPredictorFactory; -import com.android.intentresolver.shortcuts.ShortcutLoader; -import com.android.intentresolver.v2.data.model.ChooserRequest; -import com.android.intentresolver.v2.data.repository.DevicePolicyResources; -import com.android.intentresolver.v2.domain.interactor.UserInteractor; -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.platform.AppPredictionAvailable; -import com.android.intentresolver.v2.platform.ImageEditor; -import com.android.intentresolver.v2.platform.NearbyShare; -import com.android.intentresolver.v2.profiles.ChooserMultiProfilePagerAdapter; -import com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter.ProfileType; -import com.android.intentresolver.v2.profiles.OnProfileSelectedListener; -import com.android.intentresolver.v2.profiles.OnSwitchOnWorkSelectedListener; -import com.android.intentresolver.v2.profiles.TabConfig; -import com.android.intentresolver.v2.shared.model.Profile; -import com.android.intentresolver.v2.ui.ActionTitle; -import com.android.intentresolver.v2.ui.ProfilePagerResources; -import com.android.intentresolver.v2.ui.ShareResultSender; -import com.android.intentresolver.v2.ui.ShareResultSenderFactory; -import com.android.intentresolver.v2.ui.model.ActivityModel; -import com.android.intentresolver.v2.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 kotlin.Pair; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -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; - -import kotlinx.coroutines.CoroutineDispatcher; - -/** - * 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(FragmentActivity.class) -public class ChooserActivity extends Hilt_ChooserActivity implements - ResolverListAdapter.ResolverListCommunicator, PackagesChangedListener, StartsSelectedItem { - 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. - */ - 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"; - - ////////////////////////////////////////////////////////////////////////////////////////////// - // 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 View mProfileView; - - protected ResolverDrawerLayout mResolverDrawerLayout; - 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`. - // 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 mDirectShareAppTargetCache = new HashMap<>(); - private final Map 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 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; - @Inject @AppPredictionAvailable public boolean mAppPredictionAvailable; - @Inject @ImageEditor public Optional mImageEditor; - @Inject @NearbyShare public Optional 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; - - 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; - - private final EnterTransitionAnimationDelegate mEnterTransitionAnimationDelegate = - new EnterTransitionAnimationDelegate(this, () -> mResolverDrawerLayout); - - private final View mContentView = null; - - private final Map mProfileRecords = new HashMap<>(); - - 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); - - 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) { - super.onCreate(savedInstanceState); - Log.i(TAG, "onCreate"); - - 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); - } - } - - @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(); - } - } - - @Override - protected final void onSaveInstanceState(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 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); - updateShareResultSender(); - - mMaxTargetsPerRow = - getResources().getInteger(R.integer.config_chooser_max_targets_per_row); - mShouldDisplayLandscape = - shouldDisplayLandscape(getResources().getConfiguration().orientation); - - setRetainInOnStop(mRequest.shouldRetainInOnStop()); - createProfileRecords( - new AppPredictorFactory( - this, - Objects.toString(mRequest.getSharedText(), null), - mRequest.getShareTargetFilter(), - mAppPredictionAvailable - ), - mRequest.getShareTargetFilter() - ); - - - mChooserMultiProfilePagerAdapter = createMultiProfilePagerAdapter( - /* context = */ this, - mProfilePagerResources, - mRequest, - mProfiles, - mProfileAvailability, - mRequest.getInitialIntents(), - mMaxTargetsPerRow, - mFeatureFlags); - - 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(); - } - }); - - 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 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. - final ResolveInfo ri = targetInfo.getResolveInfo(); - final Intent intent1 = targetInfo.getResolvedIntent(); - - safelyStartActivity(targetInfo); - - // Rely on the ActivityManager to pop up a dialog regarding app suspension - // and return false - targetInfo.isSuspended(); - } - - finish(); - } - }); - BasePreviewViewModel previewViewModel = - new ViewModelProvider(this, createPreviewViewModelFactory()) - .get(BasePreviewViewModel.class); - previewViewModel.init( - mRequest.getTargetIntent(), - mRequest.getAdditionalContentUri(), - mChooserServiceFeatureFlags.chooserPayloadToggling()); - mChooserContentPreviewUi = new ChooserContentPreviewUi( - getCoroutineScope(getLifecycle()), - previewViewModel.getPreviewDataProvider(), - mRequest.getTargetIntent(), - previewViewModel.getImageLoader(), - createChooserActionFactory(), - createModifyShareActionFactory(), - mEnterTransitionAnimationDelegate, - new HeadlineGeneratorImpl(this), - mRequest.getContentTypeHint(), - mRequest.getMetadataText(), - mChooserServiceFeatureFlags.chooserPayloadToggling()); - updateStickyContentPreview(); - if (shouldShowStickyContentPreview() - || mChooserMultiProfilePagerAdapter - .getCurrentRootAdapter().getSystemRowCount() != 0) { - getEventLog().logActionShareWithPreview( - mChooserContentPreviewUi.getPreferredContentPreview()); - } - mChooserShownTime = System.currentTimeMillis(); - final long systemCost = mChooserShownTime - mIntentReceivedTime.get(); - getEventLog().logChooserActivityShown( - isWorkProfile(), mRequest.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( - mRequest.getReferrerPackage(), - mRequest.getTargetType(), - mRequest.getCallerChooserTargets().size(), - mRequest.getInitialIntents().size(), - isWorkProfile(), - mChooserContentPreviewUi.getPreferredContentPreview(), - mRequest.getTargetAction(), - mRequest.getChooserActions().size(), - mRequest.getModifyShareAction() != null - ); - mEnterTransitionAnimationDelegate.postponeTransition(); - Tracer.INSTANCE.markLaunched(); - } - - private void onChooserRequestChanged(ChooserRequest chooserRequest) { - // intentional reference comarison - if (mRequest == chooserRequest) { - return; - } - boolean recreateAdapters = shouldUpdateAdapters(mRequest, chooserRequest); - mRequest = chooserRequest; - updateShareResultSender(); - mChooserContentPreviewUi.updateModifyShareAction(); - if (recreateAdapters) { - recreatePagerAdapter(); - } - } - - 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 oldAltIntents = oldChooserRequest.getAdditionalTargets(); - List 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() - ); - - if (mChooserMultiProfilePagerAdapter != null) { - mChooserMultiProfilePagerAdapter.destroy(); - } - mChooserMultiProfilePagerAdapter = createMultiProfilePagerAdapter( - /* context = */ this, - mProfilePagerResources, - mRequest, - mProfiles, - mProfileAvailability, - mRequest.getInitialIntents(), - mMaxTargetsPerRow, - mFeatureFlags); - mChooserMultiProfilePagerAdapter.setupViewPager( - requireViewById(com.android.internal.R.id.profile_pager)); - 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())); - } - - @Override - protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) { - ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); - if (viewPager != null) { - viewPager.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( - this, - profileHelper.getWorkHandle(), - profileHelper.getPersonalHandle(), - getMetricsCategory(), - profileHelper.getTabOwnerUserHandleForLaunch() - ); - - // 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 getResolverRankerServiceUserHandleList(UserHandle userHandle) { - return getResolverRankerServiceUserHandleListInternal(userHandle); - } - - private List getResolverRankerServiceUserHandleListInternal(UserHandle userHandle) { - List 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); - } - - protected WindowInsets super_onApplyWindowInsets(View v, WindowInsets insets) { - mSystemWindowInsets = insets.getSystemWindowInsets(); - - 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); - - return insets.consumeSystemWindowInsets(); - } - - @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 true 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 = mFeatureFlags.scrollablePreview() - ? R.layout.chooser_grid_scrollable_preview - : R.layout.chooser_grid; - - setContentView(mLayoutId); - mChooserMultiProfilePagerAdapter.setupViewPager( - requireViewById(com.android.internal.R.id.profile_pager)); - boolean result = postRebuildList(rebuildCompleted); - Trace.endSection(); - return result; - } - - /** - * Finishing procedures to be performed after the list has been rebuilt. - *

Subclasses must call postRebuildListInternal at the end of postRebuildList. - * @param rebuildCompleted - * @return true 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 true 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() { - TabHost tabHost = findViewById(com.android.internal.R.id.profile_tabhost); - ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); - - mChooserMultiProfilePagerAdapter.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) {} - - @Override - public void onProfilePageStateChanged(int state) { - onHorizontalSwipeStateChanged(state); - } - }); - mOnSwitchOnWorkSelectedListener = () -> { - View workTab = tabHost.getTabWidget().getChildAt( - mChooserMultiProfilePagerAdapter.getPageNumberForProfile(PROFILE_WORK)); - workTab.setFocusable(true); - workTab.setFocusableInTouchMode(true); - workTab.requestFocus(); - }; - } - - ////////////////////////////////////////////////////////////////////////////////////////////// - ////////////////////////////////////////////////////////////////////////////////////////////// - - private void createProfileRecords( - AppPredictorFactory factory, IntentFilter targetIntentFilter) { - UserHandle mainUserHandle = mProfiles.getPersonalHandle(); - ProfileRecord record = createProfileRecord(mainUserHandle, targetIntentFilter, factory); - if (record.shortcutLoader == null) { - Tracer.INSTANCE.endLaunchToShortcutTrace(); - } - - 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( - 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()); - } - - @VisibleForTesting - protected ShortcutLoader createShortcutLoader( - Context context, - AppPredictor appPredictor, - UserHandle userHandle, - IntentFilter targetIntentFilter, - Consumer callback) { - return new ShortcutLoader( - context, - getCoroutineScope(getLifecycle()), - appPredictor, - userHandle, - targetIntentFilter, - callback); - } - - private SharedPreferences getPinnedSharedPrefs(Context context) { - return context.getSharedPreferences(PINNED_SHARED_PREFS_NAME, MODE_PRIVATE); - } - - protected ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter() { - return createMultiProfilePagerAdapter( - /* context = */ this, - mProfilePagerResources, - mViewModel.getRequest().getValue(), - mProfiles, - mProfileAvailability, - mRequest.getInitialIntents(), - mMaxTargetsPerRow, - mFeatureFlags); - } - - private ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter( - Context context, - ProfilePagerResources profilePagerResources, - ChooserRequest request, - ProfileHelper profileHelper, - ProfileAvailability profileAvailability, - List initialIntents, - int maxTargetsPerRow, - FeatureFlags featureFlags) { - Log.d(TAG, "createMultiProfilePagerAdapter"); - - Profile launchedAs = profileHelper.getLaunchedAsProfile(); - - Intent[] initialIntentArray = initialIntents.toArray(new Intent[0]); - List payloadIntents = request.getPayloadIntents(); - - List> 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 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, - featureFlags); - } - - protected EmptyStateProvider createBlockerEmptyStateProvider() { - final boolean isSendAction = mRequest.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( - mProfiles, - noWorkToPersonalEmptyState, - noPersonalToWorkEmptyState, - createCrossProfileIntentsChecker()); - } - - private int findSelectedProfile() { - return mProfiles.getLaunchedAsProfileType().ordinal(); - } - - /** - * 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) - */ - private boolean isWorkProfile() { - return mProfiles.getLaunchedAsProfileType() == Profile.Type.WORK; - } - - //@Override - protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) { - return new PackageMonitor() { - @Override - public void onSomePackagesChanged() { - handlePackagesChanged(listAdapter); - } - }; - } - - /** - * Update UI to reflect changes in data. - */ - @Override - public void handlePackagesChanged() { - handlePackagesChanged(/* listAdapter */ null); - } - - /** - * Update UI to reflect changes in data. - *

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(); - } - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - super.onConfigurationChanged(newConfig); - mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged(); - - if (mSystemWindowInsets != null) { - mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top, - mSystemWindowInsets.right, 0); - } - ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager); - if (viewPager.isLayoutRtl()) { - mChooserMultiProfilePagerAdapter.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 (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 - // 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); - } - - private void destroyProfileRecords() { - mProfileRecords.values().forEach(ProfileRecord::destroy); - mProfileRecords.clear(); - } - - @Override // ResolverListCommunicator - public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) { - Intent result = defIntent; - if (mRequest.getReplacementExtras() != null) { - final Bundle replExtras = - mRequest.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; - } - - private void maybeSendShareResult(TargetInfo cti) { - if (mShareResultSender != null) { - final ComponentName target = cti.getResolvedComponentName(); - if (target != null) { - mShareResultSender.onComponentSelected(target, cti.isChooserTargetInfo()); - } - } - } - - private void addCallerChooserTargets() { - if (!mRequest.getCallerChooserTargets().isEmpty()) { - // Send the caller's chooser targets only to the default profile. - if (mChooserMultiProfilePagerAdapter.getActiveProfile() == findSelectedProfile()) { - mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults( - /* origTarget */ null, - new ArrayList<>(mRequest.getCallerChooserTargets()), - TARGET_TYPE_DEFAULT, - /* directShareShortcutInfoCache */ Collections.emptyMap(), - /* directShareAppTargetCache */ Collections.emptyMap()); - } - } - } - - @Override // ResolverListCommunicator - public boolean shouldGetActivityMetadata() { - return true; - } - - public boolean shouldAutoLaunchSingleChoice(TargetInfo target) { - if (target.isSuspended()) { - return false; - } - - // TODO: migrate to ChooserRequest - return mViewModel.getActivityModel().getIntent() - .getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true); - } - - private void showTargetDetails(TargetInfo targetInfo) { - if (targetInfo == null) return; - - List 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; - intentFilter = targetInfo.isSelectableTargetInfo() - ? mRequest.getShareTargetFilter() : 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); - } - - protected boolean onTargetSelected(TargetInfo target) { - if (mRefinementManager.maybeHandleSelection( - target, - mRequest.getRefinementIntentSender(), - getApplication(), - getMainThreadHandler())) { - return false; - } - updateModelAndChooserCounts(target); - maybeRemoveSharedText(target); - 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, /* unused */ 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; - } - } - if (isFinishing()) { - return; - } - - 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 - // 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), - mRequest.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 - ); - } - } - } - - 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; - } - - 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 = mRequest.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(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) { - 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 surfacedTargetInfo = adapter.getSurfacedTargetInfo(); - List 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) || (mProfiles.getCloneUserPresent())) - ? null : record.appPredictor; - } - - protected EventLog getEventLog() { - return mEventLog; - } - - private ChooserGridAdapter createChooserGridAdapter( - Context context, - List payloadIntents, - Intent[] initialIntents, - UserHandle userHandle) { - ChooserListAdapter chooserListAdapter = createChooserListAdapter( - context, - payloadIntents, - initialIntents, - /* TODO: not used, remove. rList= */ null, - /* TODO: not used, remove. filterLastUsed= */ false, - createListController(userHandle), - userHandle, - mRequest.getTargetIntent(), - mRequest.getReferrerFillInIntent(), - mMaxTargetsPerRow - ); - - return new ChooserGridAdapter( - context, - new ChooserGridAdapter.ChooserActivityDelegate() { - @Override - public boolean shouldShowTabs() { - return mProfiles.getWorkProfilePresent(); - } - - @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); - } - } - }, - chooserListAdapter, - shouldShowContentPreview(), - mMaxTargetsPerRow, - mFeatureFlags); - } - - @VisibleForTesting - public ChooserListAdapter createChooserListAdapter( - Context context, - List payloadIntents, - Intent[] initialIntents, - List rList, - boolean filterLastUsed, - ResolverListController resolverListController, - UserHandle userHandle, - Intent targetIntent, - Intent referrerFillInIntent, - int maxTargetsPerRow) { - UserHandle initialIntentsUserSpace = mProfiles.getQueryIntentsHandle(userHandle); - return new ChooserListAdapter( - context, - payloadIntents, - initialIntents, - rList, - filterLastUsed, - createListController(userHandle), - userHandle, - targetIntent, - referrerFillInIntent, - this, - mPackageManager, - getEventLog(), - maxTargetsPerRow, - initialIntentsUserSpace, - mTargetDataLoader, - () -> { - ProfileRecord record = getProfileRecord(userHandle); - if (record != null && record.shortcutLoader != null) { - record.shortcutLoader.reset(); - } - }, - mFeatureFlags); - } - - private void onWorkProfileStatusUpdated() { - UserHandle workUser = mProfiles.getWorkHandle(); - ProfileRecord record = workUser == null ? null : getProfileRecord(workUser); - if (record != null && record.shortcutLoader != null) { - record.shortcutLoader.reset(); - } - if (mChooserMultiProfilePagerAdapter.getCurrentUserHandle().equals( - mProfiles.getWorkHandle())) { - mChooserMultiProfilePagerAdapter.rebuildActiveTab(true); - } else { - mChooserMultiProfilePagerAdapter.clearInactiveProfileCache(); - } - } - - @VisibleForTesting - protected ChooserListController createListController(UserHandle userHandle) { - AppPredictor appPredictor = getAppPredictor(userHandle); - AbstractResolverComparator resolverComparator; - if (appPredictor != null) { - resolverComparator = new AppPredictionServiceResolverComparator( - this, - mRequest.getTargetIntent(), - mRequest.getLaunchedFromPackage(), - appPredictor, - userHandle, - getEventLog(), - mNearbyShare.orElse(null) - ); - } else { - resolverComparator = - new ResolverRankerServiceResolverComparator( - this, - mRequest.getTargetIntent(), - mRequest.getReferrerPackage(), - null, - getEventLog(), - getResolverRankerServiceUserHandleList(userHandle), - mNearbyShare.orElse(null)); - } - - return new ChooserListController( - this, - mPackageManager, - mRequest.getTargetIntent(), - mRequest.getReferrerPackage(), - mViewModel.getActivityModel().getLaunchedFromUid(), - resolverComparator, - mProfiles.getQueryIntentsHandle(userHandle), - mRequest.getFilteredComponentNames(), - mPinnedSharedPrefs); - } - - @VisibleForTesting - protected ViewModelProvider.Factory createPreviewViewModelFactory() { - return PreviewViewModel.Companion.getFactory(); - } - - private ChooserActionFactory createChooserActionFactory() { - return new ChooserActionFactory( - this, - mRequest.getTargetIntent(), - mRequest.getLaunchedFromPackage(), - mRequest.getChooserActions(), - mImageEditor, - getEventLog(), - (isExcluded) -> mExcludeSharedText = isExcluded, - this::getFirstVisibleImgPreviewView, - new ChooserActionFactory.ActionActivityStarter() { - @Override - public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) { - safelyStartActivityAsUser( - targetInfo, - mProfiles.getPersonalHandle() - ); - finish(); - } - - @Override - public void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition( - TargetInfo targetInfo, View sharedElement, String sharedElementName) { - ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation( - ChooserActivity.this, sharedElement, sharedElementName); - safelyStartActivityAsUser( - targetInfo, - mProfiles.getPersonalHandle(), - options.toBundle()); - // Can't finish right away because the shared element transition may not - // be ready to start. - mFinishWhenStopped = true; - } - }, - mShareResultSender, - this::finishWithStatus, - mClipboardManager); - } - - private Supplier 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(); - } - - /* - * 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(); - } - - int currentProfile = mChooserMultiProfilePagerAdapter.getActiveProfile(); - 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.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 (mProfiles.getWorkProfilePresent()) { - 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(); - } - - 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 (mChooserMultiProfilePagerAdapter.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 = 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 = - 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 (mProfiles.getWorkProfilePresent()) { - 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; - } - ResolverListAdapter adapter = mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle( - UserHandle.of(UserHandle.myUserId())); - boolean isEmpty = adapter == null || adapter.getCount() == 0; - return (mFeatureFlags.scrollablePreview() || mProfiles.getWorkProfilePresent()) - && (!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() { - return mRequest.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); - } - - protected String getMetricsCategory() { - return METRICS_CATEGORY_CHOOSER; - } - - 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 - setVerticalScrollEnabled(true); - if (mResolverDrawerLayout != null) { - mResolverDrawerLayout.scrollNestedScrollableChildBackToTop(); - } - } - - protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) { - if (mProfiles.getWorkProfilePresent()) { - 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); - } - - 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); - } - } - } - - 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/ChooserHelper.kt b/java/src/com/android/intentresolver/v2/ChooserHelper.kt deleted file mode 100644 index 9da0d605..00000000 --- a/java/src/com/android/intentresolver/v2/ChooserHelper.kt +++ /dev/null @@ -1,168 +0,0 @@ -/* - * 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.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.contentpreview.payloadtoggle.data.repository.ActivityResultRepository -import com.android.intentresolver.inject.Background -import com.android.intentresolver.v2.annotation.JavaInterop -import com.android.intentresolver.v2.data.model.ChooserRequest -import com.android.intentresolver.v2.domain.interactor.UserInteractor -import com.android.intentresolver.v2.ui.viewmodel.ChooserViewModel -import com.android.intentresolver.v2.validation.Invalid -import com.android.intentresolver.v2.validation.Valid -import com.android.intentresolver.v2.validation.log -import dagger.hilt.android.scopes.ActivityScoped -import java.util.function.Consumer -import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher -import kotlinx.coroutines.flow.filterNotNull -import kotlinx.coroutines.flow.first -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 userInteractor: UserInteractor, - private val activityResultRepo: ActivityResultRepository, - @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() - - private lateinit var activityInitializer: Runnable - - var onChooserRequestChanged: Consumer = Consumer {} - - 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 { - activity.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { - viewModel.request.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) { - request.errors.forEach { it.log(TAG) } - activity.finish() - } - - private fun initializeActivity(request: Valid) { - request.warnings.forEach { it.log(TAG) } - activityInitializer.run() - } -} diff --git a/java/src/com/android/intentresolver/v2/ChooserListController.java b/java/src/com/android/intentresolver/v2/ChooserListController.java deleted file mode 100644 index 467f343b..00000000 --- a/java/src/com/android/intentresolver/v2/ChooserListController.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * 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.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.ResolverListController; -import com.android.intentresolver.model.AbstractResolverComparator; - -import java.util.List; - -public class ChooserListController extends ResolverListController { - private final List mFilteredComponents; - private final SharedPreferences mPinnedComponents; - - public ChooserListController( - Context context, - PackageManager pm, - Intent targetIntent, - String referrerPackageName, - int launchedFromUid, - AbstractResolverComparator resolverComparator, - UserHandle queryIntentsAsUser, - List 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/v2/ChooserSelector.kt b/java/src/com/android/intentresolver/v2/ChooserSelector.kt deleted file mode 100644 index 378bc06c..00000000 --- a/java/src/com/android/intentresolver/v2/ChooserSelector.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.android.intentresolver.v2 - -import android.content.BroadcastReceiver -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import com.android.intentresolver.FeatureFlags -import dagger.hilt.android.AndroidEntryPoint -import javax.inject.Inject - -@AndroidEntryPoint(BroadcastReceiver::class) -class ChooserSelector : Hilt_ChooserSelector() { - - @Inject lateinit var featureFlags: FeatureFlags - - override fun onReceive(context: Context, intent: Intent) { - super.onReceive(context, intent) - if (intent.action == Intent.ACTION_BOOT_COMPLETED) { - context.packageManager.setComponentEnabledSetting( - ComponentName(CHOOSER_PACKAGE, CHOOSER_PACKAGE + CHOOSER_CLASS), - if (featureFlags.modularFramework()) { - PackageManager.COMPONENT_ENABLED_STATE_ENABLED - } else { - PackageManager.COMPONENT_ENABLED_STATE_DEFAULT - }, - /* flags = */ 0, - ) - } - } - - companion object { - private const val CHOOSER_PACKAGE = "com.android.intentresolver" - private const val CHOOSER_CLASS = ".v2.ChooserActivity" - } -} diff --git a/java/src/com/android/intentresolver/v2/IntentForwarding.kt b/java/src/com/android/intentresolver/v2/IntentForwarding.kt deleted file mode 100644 index 3d366d10..00000000 --- a/java/src/com/android/intentresolver/v2/IntentForwarding.kt +++ /dev/null @@ -1,111 +0,0 @@ -/* - * 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.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.v2.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/v2/JavaFlowHelper.kt b/java/src/com/android/intentresolver/v2/JavaFlowHelper.kt deleted file mode 100644 index 3c4bddd1..00000000 --- a/java/src/com/android/intentresolver/v2/JavaFlowHelper.kt +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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.v2 - -import com.android.intentresolver.v2.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 collect(scope: CoroutineScope, flow: Flow, collector: Consumer): Job = - scope.launch { flow.collect { collector.accept(it) } } diff --git a/java/src/com/android/intentresolver/v2/ProfileAvailability.kt b/java/src/com/android/intentresolver/v2/ProfileAvailability.kt deleted file mode 100644 index 27d8c6bb..00000000 --- a/java/src/com/android/intentresolver/v2/ProfileAvailability.kt +++ /dev/null @@ -1,85 +0,0 @@ -/* - * 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 androidx.annotation.MainThread -import com.android.intentresolver.v2.annotation.JavaInterop -import com.android.intentresolver.v2.domain.interactor.UserInteractor -import com.android.intentresolver.v2.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() - } - } - - /** 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/v2/ProfileHelper.kt b/java/src/com/android/intentresolver/v2/ProfileHelper.kt deleted file mode 100644 index 87948150..00000000 --- a/java/src/com/android/intentresolver/v2/ProfileHelper.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * 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.os.UserHandle -import androidx.annotation.MainThread -import com.android.intentresolver.inject.IntentResolverFlags -import com.android.intentresolver.v2.annotation.JavaInterop -import com.android.intentresolver.v2.domain.interactor.UserInteractor -import com.android.intentresolver.v2.shared.model.Profile -import com.android.intentresolver.v2.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/v2/ResolverActivity.java b/java/src/com/android/intentresolver/v2/ResolverActivity.java deleted file mode 100644 index 86f32864..00000000 --- a/java/src/com/android/intentresolver/v2/ResolverActivity.java +++ /dev/null @@ -1,1947 +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_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.v2.ext.CreationExtrasExtKt.addDefaultArgs; -import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED; - -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.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.PackageManager.NameNotFoundException; -import android.content.pm.ResolveInfo; -import android.content.pm.UserInfo; -import android.content.res.Configuration; -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.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.fragment.app.FragmentActivity; -import androidx.lifecycle.ViewModelProvider; -import androidx.lifecycle.viewmodel.CreationExtras; -import androidx.viewpager.widget.ViewPager; - -import com.android.intentresolver.FeatureFlags; -import com.android.intentresolver.R; -import com.android.intentresolver.ResolverListAdapter; -import com.android.intentresolver.ResolverListController; -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.DefaultTargetDataLoader; -import com.android.intentresolver.icons.TargetDataLoader; -import com.android.intentresolver.inject.Background; -import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; -import com.android.intentresolver.v2.data.repository.DevicePolicyResources; -import com.android.intentresolver.v2.domain.interactor.UserInteractor; -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.profiles.MultiProfilePagerAdapter; -import com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter.ProfileType; -import com.android.intentresolver.v2.profiles.OnProfileSelectedListener; -import com.android.intentresolver.v2.profiles.OnSwitchOnWorkSelectedListener; -import com.android.intentresolver.v2.profiles.ResolverMultiProfilePagerAdapter; -import com.android.intentresolver.v2.profiles.TabConfig; -import com.android.intentresolver.v2.shared.model.Profile; -import com.android.intentresolver.v2.ui.ActionTitle; -import com.android.intentresolver.v2.ui.model.ActivityModel; -import com.android.intentresolver.v2.ui.model.ResolverRequest; -import com.android.intentresolver.v2.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.google.common.collect.ImmutableList; - -import dagger.hilt.android.AndroidEntryPoint; - -import kotlin.Pair; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; -import java.util.Set; - -import javax.inject.Inject; - -import kotlinx.coroutines.CoroutineDispatcher; - -/** - * 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. - */ -@AndroidEntryPoint(FragmentActivity.class) -public class ResolverActivity extends Hilt_ResolverActivity implements - ResolverListAdapter.ResolverListCommunicator { - - @Inject @Background public CoroutineDispatcher mBackgroundDispatcher; - @Inject public UserInteractor mUserInteractor; - @Inject public ResolverHelper mResolverHelper; - @Inject public PackageManager mPackageManager; - @Inject public DevicePolicyResources mDevicePolicyResources; - @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 int mLayoutId; - private PickTargetOptionRequest mPickOptionRequest; - // Expected to be true if this object is ResolverActivity or is ResolverWrapperActivity. - protected ResolverDrawerLayout mResolverDrawerLayout; - - 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; - - 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 final boolean mWorkProfileHasBeenEnabled = false; - - protected static final String TAB_TAG_PERSONAL = "personal"; - protected static final String TAB_TAG_WORK = "work"; - - private PackageMonitor mPersonalPackageMonitor; - private PackageMonitor mWorkPackageMonitor; - - protected ResolverMultiProfilePagerAdapter mMultiProfilePagerAdapter; - - public static final int PROFILE_PERSONAL = MultiProfilePagerAdapter.PROFILE_PERSONAL; - public static final int PROFILE_WORK = MultiProfilePagerAdapter.PROFILE_WORK; - - private UserHandle mHeaderCreatorUser; - - @Nullable - private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener; - - protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) { - return new PackageMonitor() { - @Override - public void onSomePackagesChanged() { - listAdapter.handlePackagesChanged(); - } - - @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 ActivityModel createActivityModel() { - return ActivityModel.createFrom(this); - } - - @NonNull - @Override - public CreationExtras getDefaultViewModelCreationExtras() { - return addDefaultArgs( - super.getDefaultViewModelCreationExtras(), - new Pair<>(ActivityModel.ACTIVITY_MODEL_KEY, createActivityModel())); - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - Log.i(TAG, "onCreate"); - setTheme(R.style.Theme_DeviceDefault_Resolver); - mResolverHelper.setInitializer(this::initialize); - } - - @Override - protected final void onStart() { - super.onStart(); - this.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); - } - - @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) { - // 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(); - } - } - } - - @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 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(); - } - - @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(); - - mProfiles = new ProfileHelper( - mUserInteractor, - getCoroutineScope(getLifecycle()), - mBackgroundDispatcher, - mFeatureFlags); - - 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 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 = !isVoiceInteraction() - && !mProfiles.getWorkProfilePresent() && !mProfiles.getCloneUserPresent(); - mMultiProfilePagerAdapter = createMultiProfilePagerAdapter( - new Intent[0], - /* resolutionList = */ mRequest.getResolutionList(), - filterLastUsed - ); - if (configureContentView(mTargetDataLoader)) { - return; - } - - mPersonalPackageMonitor = createPackageMonitor( - mMultiProfilePagerAdapter.getPersonalListAdapter()); - mPersonalPackageMonitor.register( - this, - getMainLooper(), - mProfiles.getPersonalHandle(), - false - ); - if (mProfiles.getWorkProfilePresent()) { - mWorkPackageMonitor = createPackageMonitor( - mMultiProfilePagerAdapter.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(); - } - }); - - 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 = mViewModel.getRequest().getValue().getIntent(); - final Set 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()) : "")); - } - - 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 resolutionList, - boolean filterLastUsed) { - ResolverMultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null; - if (mProfiles.getWorkProfilePresent()) { - resolverMultiProfilePagerAdapter = - createResolverMultiProfilePagerAdapterForTwoProfiles( - initialIntents, resolutionList, filterLastUsed); - } else { - resolverMultiProfilePagerAdapter = createResolverMultiProfilePagerAdapterForOneProfile( - initialIntents, resolutionList, filterLastUsed); - } - 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( - mProfiles, - noWorkToPersonalEmptyState, - noPersonalToWorkEmptyState, - createCrossProfileIntentsChecker()); - } - - /** - * 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); - return buttonBar == null || buttonBar.getVisibility() == View.GONE; - } - - 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 (mProfiles.getWorkProfilePresent() && !useLayoutWithDefault() - && !shouldUseMiniResolver()) { - updateIntentPickerPaddings(); - } - - if (mSystemWindowInsets != null) { - mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top, - mSystemWindowInsets.right, 0); - } - } - - public int getLayoutResource() { - return R.layout.resolver_list; - } - - // referenced by layout XML: android:onClick="onButtonClick" - 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 (mResolvingHome && hasManagedProfile() && !supportsManagedProfiles(ri)) { - String launcherName = ri.activityInfo.loadLabel(mPackageManager).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) { - MetricsLogger.action( - this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_ALWAYS); - } else { - MetricsLogger.action( - this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_JUST_ONCE); - } - MetricsLogger.action(this, - mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem() - ? MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_APP_FEATURED - : MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_NONE_FEATURED); - finish(); - } - } - - @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 (mProfiles.getWorkProfilePresent()) { - 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 /*&& 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 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 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 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 = mPackageManager; - - // 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); - } - } - } - } - - safelyStartActivity(target); - - // Rely on the ActivityManager to pop up a dialog regarding app suspension - // and return false - return !target.isSuspended(); - } - - @Override // ResolverListCommunicator - public boolean shouldGetActivityMetadata() { - return false; - } - - public boolean shouldAutoLaunchSingleChoice(TargetInfo target) { - return !target.isSuspended(); - } - - @VisibleForTesting - protected ResolverListController createListController(UserHandle userHandle) { - ResolverRankerServiceResolverComparator resolverComparator = - new ResolverRankerServiceResolverComparator( - this, - mRequest.getIntent(), - mViewModel.getActivityModel().getReferrerPackage(), - null, - null, - getResolverRankerServiceUserHandleList(userHandle), - null); - return new ResolverListController( - this, - mPackageManager, - mRequest.getIntent(), - mViewModel.getActivityModel().getReferrerPackage(), - mViewModel.getActivityModel().getLaunchedFromUid(), - resolverComparator, - mProfiles.getQueryIntentsHandle(userHandle)); - } - - /** - * Finishing procedures to be performed after the list has been rebuilt. - *

Subclasses must call postRebuildListInternal at the end of postRebuildList. - * - * @return true if the activity is finishing and creation should halt. - */ - protected boolean postRebuildList(boolean rebuildCompleted) { - return postRebuildListInternal(rebuildCompleted); - } - - /** - * Callback called when user changes the profile tab. - */ - /* 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) { - 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); - } - } - - protected void resetButtonBar() { - 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, - mProfileAvailability.getWaitingToEnableProfile())) { - // We no longer have any items... just finish the activity. - finish(); - } - } - - protected void maybeLogProfileChange() {} - - @VisibleForTesting - protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() { - return new CrossProfileIntentsChecker(getContentResolver()); - } - - private void onWorkProfileStatusUpdated() { - if (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_WORK) { - mMultiProfilePagerAdapter.rebuildActiveTab(true); - } else { - mMultiProfilePagerAdapter.clearInactiveProfileCache(); - } - } - - // @NonFinalForTesting - @VisibleForTesting - protected ResolverListAdapter createResolverListAdapter( - Context context, - List payloadIntents, - Intent[] initialIntents, - List resolutionList, - boolean filterLastUsed, - UserHandle userHandle) { - UserHandle initialIntentsUserSpace = mProfiles.getQueryIntentsHandle(userHandle); - return new ResolverListAdapter( - context, - payloadIntents, - initialIntents, - resolutionList, - filterLastUsed, - createListController(userHandle), - userHandle, - mRequest.getIntent(), - this, - initialIntentsUserSpace, - mTargetDataLoader); - } - - protected final EmptyStateProvider createEmptyStateProvider( - @Nullable UserHandle workProfileUserHandle) { - final EmptyStateProvider blockerEmptyStateProvider = createBlockerEmptyStateProvider(); - - final EmptyStateProvider workProfileOffEmptyStateProvider = - new WorkProfilePausedEmptyStateProvider( - this, - mProfiles, - mProfileAvailability, - /* onSwitchOnWorkSelectedListener= */ - () -> { - if (mOnSwitchOnWorkSelectedListener != null) { - mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected(); - } - }, - getMetricsCategory()); - - final EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider( - this, - workProfileUserHandle, - mProfiles.getPersonalHandle(), - getMetricsCategory(), - mProfiles.getTabOwnerUserHandleForLaunch() - ); - - // Return composite provider, the order matters (the higher, the more priority) - return new CompositeEmptyStateProvider( - blockerEmptyStateProvider, - workProfileOffEmptyStateProvider, - noAppsEmptyStateProvider - ); - } - - private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForOneProfile( - Intent[] initialIntents, - List resolutionList, - boolean filterLastUsed) { - ResolverListAdapter personalAdapter = createResolverListAdapter( - /* context */ this, - mRequest.getPayloadIntents(), - initialIntents, - resolutionList, - filterLastUsed, - /* userHandle */ mProfiles.getPersonalHandle() - ); - return new ResolverMultiProfilePagerAdapter( - /* context */ this, - ImmutableList.of( - new TabConfig<>( - PROFILE_PERSONAL, - mDevicePolicyResources.getPersonalTabLabel(), - mDevicePolicyResources.getPersonalTabAccessibilityLabel(), - TAB_TAG_PERSONAL, - personalAdapter)), - createEmptyStateProvider(/* workProfileUserHandle= */ null), - /* workProfileQuietModeChecker= */ () -> false, - /* defaultProfile= */ PROFILE_PERSONAL, - /* workProfileUserHandle= */ null, - mProfiles.getCloneHandle()); - } - - private UserHandle getIntentUser() { - return Objects.requireNonNullElse(mRequest.getCallingUser(), - mProfiles.getTabOwnerUserHandleForLaunch()); - } - - private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles( - Intent[] initialIntents, - List resolutionList, - 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 (!mProfiles.getTabOwnerUserHandleForLaunch().equals(intentUser)) { - if (mProfiles.getPersonalHandle().equals(intentUser)) { - selectedProfile = PROFILE_PERSONAL; - } else if (mProfiles.getWorkHandle().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, - mRequest.getPayloadIntents(), - selectedProfile == PROFILE_PERSONAL ? initialIntents : null, - resolutionList, - (filterLastUsed && UserHandle.myUserId() - == mProfiles.getPersonalHandle().getIdentifier()), - /* userHandle */ mProfiles.getPersonalHandle() - ); - UserHandle workProfileUserHandle = mProfiles.getWorkHandle(); - ResolverListAdapter workAdapter = createResolverListAdapter( - /* context */ this, - mRequest.getPayloadIntents(), - selectedProfile == PROFILE_WORK ? initialIntents : null, - resolutionList, - (filterLastUsed && UserHandle.myUserId() - == workProfileUserHandle.getIdentifier()), - /* userHandle */ workProfileUserHandle - ); - return new ResolverMultiProfilePagerAdapter( - /* context */ this, - 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), - /* Supplier (QuietMode enabled) == !(available) */ - () -> !(mProfiles.getWorkProfilePresent() - && mProfileAvailability.isAvailable( - requireNonNull(mProfiles.getWorkProfile()))), - selectedProfile, - workProfileUserHandle, - 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. - */ - final int getSelectedProfileExtra() { - 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; - } - } - - protected final @ProfileType int getCurrentProfile() { - UserHandle launchUser = mProfiles.getTabOwnerUserHandleForLaunch(); - UserHandle personalUser = mProfiles.getPersonalHandle(); - return launchUser.equals(personalUser) ? PROFILE_PERSONAL : PROFILE_WORK; - } - - 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) { - // 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( - mProfiles.getPersonalHandle())) - .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); - } - - protected final CharSequence getTitleForAction(Intent intent, int defaultTitleRes) { - final ActionTitle title = mResolvingHome - ? 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); - } - } - - private boolean hasManagedProfile() { - UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE); - if (userManager == null) { - return false; - } - - try { - List 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 = mPackageManager.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 (mProfiles.getCloneUserPresent() - && (mMultiProfilePagerAdapter.getActiveProfile() == 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 = mPackageManager - .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 = mViewModel.getRequest().getValue().isAudioCaptureDevice(); - enabled = !hasAudioCapture; - } - } - mAlwaysButton.setEnabled(enabled); - } - - @Override // ResolverListCommunicator - public final void onPostListReady(ResolverListAdapter listAdapter, boolean doPostProcessing, - boolean rebuildCompleted) { - if (isAutolaunching()) { - return; - } - 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(); - } - } - - 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 true 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(mProfiles.getWorkProfilePresent()); - - 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); - - 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); - } - - @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 true 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). - * 2. This profile only has web browser matches. - * 3. The other profile has a single non-browser match. - */ - private boolean shouldUseMiniResolver() { - 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; - } - - 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 (!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; - } - - private boolean isAutolaunching() { - return !mRegistered && isFinishing(); - } - - /** - * @return {@code true} if a resolved target is autolaunched, otherwise {@code false} - */ - private boolean maybeAutolaunchActivity() { - 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 (!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; - } - - private void maybeHideDivider() { - final View divider = findViewById(com.android.internal.R.id.divider); - if (divider == null) { - return; - } - divider.setVisibility(View.GONE); - } - - private void resetCheckedItem() { - mLastSelected = ListView.INVALID_POSITION; - ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter) - .clearCheckedItemsInInactiveProfiles(); - } - - private void setupViewVisibilities() { - ResolverListAdapter activeListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter(); - if (!mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)) { - addUseDifferentAppLabelIfNecessary(activeListAdapter); - } - } - - /** - * Updates the button bar container {@code ignoreOffset} layout param. - *

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); - 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 (!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); - } - } - 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); - 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. - return mMultiProfilePagerAdapter.getListAdapterForUserHandle( - mProfiles.getTabOwnerUserHandleForLaunch() - ).hasFilteredItem(); - } - - 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; - } - - } - - 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 { - 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 mProfiles.getQueryIntentsHandle(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 getResolverRankerServiceUserHandleList(UserHandle userHandle) { - return getResolverRankerServiceUserHandleListInternal(userHandle); - } - - @VisibleForTesting - protected List getResolverRankerServiceUserHandleListInternal( - UserHandle userHandle) { - List 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; - } - - private CharSequence getOrLoadDisplayLabel(TargetInfo info) { - if (info.isDisplayResolveInfo()) { - mTargetDataLoader.getOrLoadLabel((DisplayResolveInfo) info); - } - CharSequence displayLabel = info.getDisplayLabel(); - return displayLabel == null ? "" : displayLabel; - } -} diff --git a/java/src/com/android/intentresolver/v2/ResolverHelper.kt b/java/src/com/android/intentresolver/v2/ResolverHelper.kt deleted file mode 100644 index 388b30a7..00000000 --- a/java/src/com/android/intentresolver/v2/ResolverHelper.kt +++ /dev/null @@ -1,129 +0,0 @@ -/* - * 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.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.inject.Background -import com.android.intentresolver.v2.annotation.JavaInterop -import com.android.intentresolver.v2.domain.interactor.UserInteractor -import com.android.intentresolver.v2.ui.model.ResolverRequest -import com.android.intentresolver.v2.ui.viewmodel.ResolverViewModel -import com.android.intentresolver.v2.validation.Invalid -import com.android.intentresolver.v2.validation.Valid -import com.android.intentresolver.v2.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() - - 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) { - request.errors.forEach { it.log(TAG) } - activity.finish() - } - - private fun initializeActivity(request: Valid) { - Log.d(TAG, "initializeActivity") - request.warnings.forEach { it.log(TAG) } - - activityInitializer.run() - } -} diff --git a/java/src/com/android/intentresolver/v2/annotation/JavaInterop.kt b/java/src/com/android/intentresolver/v2/annotation/JavaInterop.kt deleted file mode 100644 index a813358e..00000000 --- a/java/src/com/android/intentresolver/v2/annotation/JavaInterop.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * 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.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/v2/data/BroadcastSubscriber.kt b/java/src/com/android/intentresolver/v2/data/BroadcastSubscriber.kt deleted file mode 100644 index f3013246..00000000 --- a/java/src/com/android/intentresolver/v2/data/BroadcastSubscriber.kt +++ /dev/null @@ -1,73 +0,0 @@ -/* - * 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.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 createFlow( - filter: IntentFilter, - user: UserHandle, - transform: (Intent) -> T?, - ): Flow = 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/v2/data/model/ChooserRequest.kt b/java/src/com/android/intentresolver/v2/data/model/ChooserRequest.kt deleted file mode 100644 index 7c9c8613..00000000 --- a/java/src/com/android/intentresolver/v2/data/model/ChooserRequest.kt +++ /dev/null @@ -1,209 +0,0 @@ -/* - * 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.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.v2.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?, - - /** - * 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, - - /** The top-level content type as retrieved using [Intent.getType]. */ - val targetType: String?, - - /** 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?, - - /** - * Choices to exclude from results. - * - * Any resolved intents with a component in this list will be omitted before presentation. - */ - val filteredComponentNames: List = 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 = 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 = 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 = 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 - */ - 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 = 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 - - /** Constructs an instance from only the required values. */ - constructor( - targetIntent: Intent, - launchedFromPackage: String, - referrer: Uri? - ) : this( - targetIntent = targetIntent, - targetAction = targetIntent.action, - isSendActionTarget = targetIntent.hasAction(ACTION_SEND, ACTION_SEND_MULTIPLE), - targetType = targetIntent.type, - launchedFromPackage = launchedFromPackage, - referrer = referrer - ) -} diff --git a/java/src/com/android/intentresolver/v2/data/repository/ChooserRequestRepository.kt b/java/src/com/android/intentresolver/v2/data/repository/ChooserRequestRepository.kt deleted file mode 100644 index d23e07ee..00000000 --- a/java/src/com/android/intentresolver/v2/data/repository/ChooserRequestRepository.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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.data.repository - -import com.android.intentresolver.contentpreview.payloadtoggle.data.model.CustomActionModel -import com.android.intentresolver.v2.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, -) { - /** All information from the sharing application pertaining to the chooser. */ - val chooserRequest: MutableStateFlow = 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> = MutableStateFlow(initialActions) -} 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 5719ff08..00000000 --- a/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt +++ /dev/null @@ -1,100 +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.FORWARD_INTENT_TO_PERSONAL -import android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK -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) - } - ) - } - - 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/v2/data/repository/UserInfoExt.kt b/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt deleted file mode 100644 index a61d6d0d..00000000 --- a/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.data.repository - -import android.content.pm.UserInfo -import com.android.intentresolver.v2.shared.model.User -import com.android.intentresolver.v2.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 = - * 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 56c84fcf..00000000 --- a/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt +++ /dev/null @@ -1,328 +0,0 @@ -/* - * 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.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.inject.Background -import com.android.intentresolver.inject.Main -import com.android.intentresolver.inject.ProfileParent -import com.android.intentresolver.v2.data.BroadcastSubscriber -import com.android.intentresolver.v2.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> - - /** - * A [Flow] of availability. Only profile users may become unavailable. - * - * Availability is currently defined as not being in [quietMode][UserInfo.isQuietModeEnabled]. - */ - val availability: Flow> - - /** - * 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 - -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, - 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 = - 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> = - usersWithState.map { userStates -> userStates.map { it.user } }.distinctUntilChanged() - - override val availability: Flow> = - 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.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 { - 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): IntentFilter { - return IntentFilter().apply { actions.forEach(::addAction) } -} - -internal fun UserInfo?.isAvailable(): Boolean { - return this?.isQuietModeEnabled != true -} 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 ad4faa17..00000000 --- a/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.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/v2/data/repository/UserScopedService.kt b/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt deleted file mode 100644 index 65a48a55..00000000 --- a/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * 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.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 { - * return UserScopedServiceImpl(ctx, UserManager::class) - * } - * - * class MyUserHelper @Inject constructor( - * private val userMgr: UserScopedService, - * ) { - * fun isPrivateProfile(user: UserHandle): UserManager { - * return userMgr.forUser(user).isPrivateProfile() - * } - * } - * ``` - */ -fun interface UserScopedService { - /** Create a service instance for the given user. */ - fun forUser(user: UserHandle): T -} - -class UserScopedServiceImpl( - @ApplicationContext private val context: Context, - private val serviceType: KClass, -) : UserScopedService { - 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/v2/domain/interactor/UserInteractor.kt b/java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt deleted file mode 100644 index 69374f88..00000000 --- a/java/src/com/android/intentresolver/v2/domain/interactor/UserInteractor.kt +++ /dev/null @@ -1,92 +0,0 @@ -/* - * 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.domain.interactor - -import android.os.UserHandle -import com.android.intentresolver.inject.ApplicationUser -import com.android.intentresolver.v2.data.repository.UserRepository -import com.android.intentresolver.v2.shared.model.Profile -import com.android.intentresolver.v2.shared.model.Profile.Type -import com.android.intentresolver.v2.shared.model.User -import com.android.intentresolver.v2.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> = - 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 = - 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> = - 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/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> 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> 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 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 dfc46697..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.R; -import com.android.intentresolver.ResolvedComponentInfo; -import com.android.intentresolver.ResolverListAdapter; -import com.android.intentresolver.emptystate.EmptyState; -import com.android.intentresolver.emptystate.EmptyStateProvider; - -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 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 d52015bf..00000000 --- a/java/src/com/android/intentresolver/v2/emptystate/NoCrossProfileEmptyStateProvider.java +++ /dev/null @@ -1,155 +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.content.Intent; -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; -import com.android.intentresolver.v2.ProfileHelper; -import com.android.intentresolver.v2.shared.model.Profile; -import com.android.intentresolver.v2.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. - */ -public class NoCrossProfileEmptyStateProvider implements EmptyStateProvider { - - private final ProfileHelper mProfileHelper; - private final EmptyState mNoWorkToPersonalEmptyState; - private final EmptyState mNoPersonalToWorkEmptyState; - private final CrossProfileIntentsChecker mCrossProfileIntentsChecker; - - public NoCrossProfileEmptyStateProvider( - ProfileHelper profileHelper, - EmptyState noWorkToPersonalEmptyState, - EmptyState noPersonalToWorkEmptyState, - CrossProfileIntentsChecker crossProfileIntentsChecker) { - mProfileHelper = profileHelper; - mNoWorkToPersonalEmptyState = noWorkToPersonalEmptyState; - mNoPersonalToWorkEmptyState = noPersonalToWorkEmptyState; - mCrossProfileIntentsChecker = crossProfileIntentsChecker; - } - - private boolean anyCrossProfileAllowedIntents(ResolverListAdapter selected, UserHandle source) { - List intents = selected.getIntents(); - UserHandle target = selected.getUserHandle(); - return mCrossProfileIntentsChecker.hasCrossProfileIntents(intents, - source.getIdentifier(), target.getIdentifier()); - } - - @Nullable - @Override - 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; - } - - // 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, tabOwnerHandle)) { - return null; - } - - switch (launchedAsProfile.getType()) { - case WORK: return mNoWorkToPersonalEmptyState; - case PERSONAL: return mNoPersonalToWorkEmptyState; - } - return null; - } - - /** - * 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 af13f8fe..00000000 --- a/java/src/com/android/intentresolver/v2/emptystate/WorkProfilePausedEmptyStateProvider.java +++ /dev/null @@ -1,131 +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 static java.util.Objects.requireNonNull; - -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.emptystate.EmptyState; -import com.android.intentresolver.emptystate.EmptyStateProvider; -import com.android.intentresolver.v2.ProfileAvailability; -import com.android.intentresolver.v2.ProfileHelper; -import com.android.intentresolver.v2.shared.model.Profile; - -/** - * 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 ProfileHelper mProfileHelper; - private final ProfileAvailability mProfileAvailability; - private final String mMetricsCategory; - private final OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener; - private final Context mContext; - - public WorkProfilePausedEmptyStateProvider(@NonNull Context context, - ProfileHelper profileHelper, - ProfileAvailability profileAvailability, - @Nullable OnSwitchOnWorkSelectedListener onSwitchOnWorkSelectedListener, - @NonNull String metricsCategory) { - mContext = context; - mProfileHelper = profileHelper; - mProfileAvailability = profileAvailability; - mMetricsCategory = metricsCategory; - mOnSwitchOnWorkSelectedListener = onSwitchOnWorkSelectedListener; - } - - @Nullable - @Override - public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { - 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; - } - - 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, /* EmptyState.ClickListener */ (tab) -> { - tab.showSpinner(); - if (mOnSwitchOnWorkSelectedListener != null) { - mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected(); - } - 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/v2/ext/CreationExtrasExt.kt b/java/src/com/android/intentresolver/v2/ext/CreationExtrasExt.kt deleted file mode 100644 index 6c36e6aa..00000000 --- a/java/src/com/android/intentresolver/v2/ext/CreationExtrasExt.kt +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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.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): 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/v2/ext/IntentExt.kt b/java/src/com/android/intentresolver/v2/ext/IntentExt.kt deleted file mode 100644 index 8c2d7277..00000000 --- a/java/src/com/android/intentresolver/v2/ext/IntentExt.kt +++ /dev/null @@ -1,45 +0,0 @@ -/* - * 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.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, - 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/v2/ext/ParcelExt.kt b/java/src/com/android/intentresolver/v2/ext/ParcelExt.kt deleted file mode 100644 index b0ec97f4..00000000 --- a/java/src/com/android/intentresolver/v2/ext/ParcelExt.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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.ext - -import android.os.Parcel - -inline fun Parcel.requireParcelable(): T { - return requireNotNull(readParcelable()) { "A non-value required from this parcel was null!" } -} - -inline fun Parcel.readParcelable(): T? { - return readParcelable(T::class.java.classLoader, T::class.java) -} diff --git a/java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt b/java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt deleted file mode 100644 index 4e8783f8..00000000 --- a/java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt +++ /dev/null @@ -1,40 +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.icons - -import android.content.Context -import androidx.lifecycle.Lifecycle -import com.android.intentresolver.icons.DefaultTargetDataLoader -import com.android.intentresolver.icons.TargetDataLoader -import com.android.intentresolver.inject.ActivityOwned -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.android.components.ActivityComponent -import dagger.hilt.android.qualifiers.ActivityContext -import dagger.hilt.android.scopes.ActivityScoped - -@Module -@InstallIn(ActivityComponent::class) -object TargetDataLoaderModule { - @Provides - @ActivityScoped - fun targetDataLoader( - @ActivityContext context: Context, - @ActivityOwned lifecycle: Lifecycle, - ): TargetDataLoader = DefaultTargetDataLoader(context, lifecycle, isAudioCaptureDevice = false) -} diff --git a/java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt b/java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt 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, - userHandle: UserHandle, - ): List -} - -/** 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, - userHandle: UserHandle, - ): List { - 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, - userHandle: UserHandle, - baseFlags: Int, - ): List = 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/ListController.kt b/java/src/com/android/intentresolver/v2/listcontroller/ListController.kt deleted file mode 100644 index 4ddab755..00000000 --- a/java/src/com/android/intentresolver/v2/listcontroller/ListController.kt +++ /dev/null @@ -1,21 +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 - -/** Controller for managing lists of [com.android.intentresolver.ResolvedComponentInfo]s. */ -interface ListController : - LastChosenManager, IntentResolver, ResolvedComponentFiltering, ResolvedComponentSorting diff --git a/java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt b/java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt 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, - intent: Intent, - from: List, - ) -} - -/** - * 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, - intent: Intent, - from: List, - ) { - 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, - ): List - - /** Filter out any low priority items. */ - fun filterLowPriority(inputList: List): List -} - -/** - * 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, - ): List = 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, - ): List { - 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?): List? - - /** 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?>(null) - - @Throws(InterruptedException::class) - private suspend fun computeIfNeeded(inputList: List) { - 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?, - ): List? { - 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/AppPredictionModule.kt b/java/src/com/android/intentresolver/v2/platform/AppPredictionModule.kt deleted file mode 100644 index 090fab6b..00000000 --- a/java/src/com/android/intentresolver/v2/platform/AppPredictionModule.kt +++ /dev/null @@ -1,42 +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.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, 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/v2/platform/ImageEditorModule.kt deleted file mode 100644 index efbf053e..00000000 --- a/java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt +++ /dev/null @@ -1,35 +0,0 @@ -package com.android.intentresolver.v2.platform - -import android.content.ComponentName -import android.content.res.Resources -import androidx.annotation.StringRes -import com.android.intentresolver.R -import com.android.intentresolver.inject.ApplicationOwned -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import java.util.Optional -import javax.inject.Qualifier -import javax.inject.Singleton - -internal fun Resources.componentName(@StringRes resId: Int): ComponentName? { - check(getResourceTypeName(resId) == "string") { "resId must be a string" } - return ComponentName.unflattenFromString(getString(resId)) -} - -@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ImageEditor - -@Module -@InstallIn(SingletonComponent::class) -object ImageEditorModule { - /** - * The name of the preferred Activity to launch for editing images. This is added to Intents to - * edit images using Intent.ACTION_EDIT. - */ - @Provides - @Singleton - @ImageEditor - fun imageEditorComponent(@ApplicationOwned resources: Resources) = - Optional.ofNullable(resources.componentName(R.string.config_systemImageEditor)) -} diff --git a/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt b/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt deleted file mode 100644 index 25ee9198..00000000 --- a/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt +++ /dev/null @@ -1,32 +0,0 @@ -package com.android.intentresolver.v2.platform - -import android.content.ComponentName -import android.content.res.Resources -import android.provider.Settings.Secure.NEARBY_SHARING_COMPONENT -import com.android.intentresolver.R -import com.android.intentresolver.inject.ApplicationOwned -import dagger.Module -import dagger.Provides -import dagger.hilt.InstallIn -import dagger.hilt.components.SingletonComponent -import java.util.Optional -import javax.inject.Qualifier -import javax.inject.Singleton - -@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class NearbyShare - -@Module -@InstallIn(SingletonComponent::class) -object NearbyShareModule { - - @Provides - @Singleton - @NearbyShare - fun nearbyShareComponent(@ApplicationOwned resources: Resources, settings: SecureSettings) = - Optional.ofNullable( - ComponentName.unflattenFromString( - settings.getString(NEARBY_SHARING_COMPONENT)?.ifEmpty { null } - ?: resources.getString(R.string.config_defaultNearbySharingComponent), - ) - ) -} diff --git a/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt b/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt deleted file mode 100644 index 531152ba..00000000 --- a/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt +++ /dev/null @@ -1,30 +0,0 @@ -package com.android.intentresolver.v2.platform - -import android.content.ContentResolver -import android.provider.Settings -import javax.inject.Inject - -/** - * Implements [SecureSettings] backed by Settings.Secure and a ContentResolver. - * - * These methods make Binder calls and may block, so use on the Main thread should be avoided. - */ -class PlatformSecureSettings @Inject constructor(private val resolver: ContentResolver) : - SecureSettings { - - override fun getString(name: String): String? { - return Settings.Secure.getString(resolver, name) - } - - override fun getInt(name: String): Int? { - return runCatching { Settings.Secure.getInt(resolver, name) }.getOrNull() - } - - override fun getLong(name: String): Long? { - return runCatching { Settings.Secure.getLong(resolver, name) }.getOrNull() - } - - override fun getFloat(name: String): Float? { - return runCatching { Settings.Secure.getFloat(resolver, name) }.getOrNull() - } -} diff --git a/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt b/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt deleted file mode 100644 index 62ee8ae9..00000000 --- a/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt +++ /dev/null @@ -1,25 +0,0 @@ -package com.android.intentresolver.v2.platform - -import android.provider.Settings.SettingNotFoundException - -/** - * A component which provides access to values from [android.provider.Settings.Secure]. - * - * All methods return nullable types instead of throwing [SettingNotFoundException] which yields - * cleaner, more idiomatic Kotlin code: - * - * // apply a default: val foo = settings.getInt(FOO) ?: DEFAULT_FOO - * - * // assert if missing: val required = settings.getInt(REQUIRED_VALUE) ?: error("required value - * missing") - */ -interface SecureSettings { - - fun getString(name: String): String? - - fun getInt(name: String): Int? - - fun getLong(name: String): Long? - - fun getFloat(name: String): Float? -} diff --git a/java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt b/java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt 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/profiles/AdapterBinder.java b/java/src/com/android/intentresolver/v2/profiles/AdapterBinder.java deleted file mode 100644 index c5b35273..00000000 --- a/java/src/com/android/intentresolver/v2/profiles/AdapterBinder.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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.profiles; - -/** - * Delegate to set up a given adapter and page view to be used together. - * - * @param (as in {@link MultiProfilePagerAdapter}). - * @param (as in {@link MultiProfilePagerAdapter}). - */ -public interface AdapterBinder { - /** - * 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/profiles/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/profiles/ChooserMultiProfilePagerAdapter.java deleted file mode 100644 index c078c43f..00000000 --- a/java/src/com/android/intentresolver/v2/profiles/ChooserMultiProfilePagerAdapter.java +++ /dev/null @@ -1,212 +0,0 @@ -/* - * 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.profiles; - -import android.content.Context; -import android.os.UserHandle; -import android.view.LayoutInflater; -import android.view.ViewGroup; - -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import androidx.viewpager.widget.PagerAdapter; - -import com.android.intentresolver.ChooserListAdapter; -import com.android.intentresolver.ChooserRecyclerViewAccessibilityDelegate; -import com.android.intentresolver.FeatureFlags; -import com.android.intentresolver.R; -import com.android.intentresolver.emptystate.EmptyStateProvider; -import com.android.intentresolver.grid.ChooserGridAdapter; -import com.android.intentresolver.measurements.Tracer; - -import com.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. - */ -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, - ImmutableList> tabs, - EmptyStateProvider emptyStateProvider, - Supplier workProfileQuietModeChecker, - @ProfileType int defaultProfile, - UserHandle workProfileUserHandle, - UserHandle cloneProfileUserHandle, - int maxTargetsPerRow, - FeatureFlags featureFlags) { - this( - context, - new ChooserProfileAdapterBinder(maxTargetsPerRow), - tabs, - emptyStateProvider, - workProfileQuietModeChecker, - defaultProfile, - workProfileUserHandle, - cloneProfileUserHandle, - new BottomPaddingOverrideSupplier(context), - featureFlags); - } - - private ChooserMultiProfilePagerAdapter( - Context context, - ChooserProfileAdapterBinder adapterBinder, - ImmutableList> tabs, - EmptyStateProvider emptyStateProvider, - Supplier workProfileQuietModeChecker, - @ProfileType int defaultProfile, - UserHandle workProfileUserHandle, - UserHandle cloneProfileUserHandle, - BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier, - FeatureFlags featureFlags) { - super( - gridAdapter -> gridAdapter.getListAdapter(), - adapterBinder, - tabs, - emptyStateProvider, - workProfileQuietModeChecker, - defaultProfile, - workProfileUserHandle, - cloneProfileUserHandle, - () -> makeProfileView(context, featureFlags), - bottomPaddingOverrideSupplier); - mAdapterBinder = adapterBinder; - mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier; - } - - public void setMaxTargetsPerRow(int maxTargetsPerRow) { - mAdapterBinder.setMaxTargetsPerRow(maxTargetsPerRow); - } - - public void setEmptyStateBottomOffset(int bottomOffset) { - mBottomPaddingOverrideSupplier.setEmptyStateBottomOffset(bottomOffset); - setupContainerPadding(); - } - - /** - * Notify adapter about the drawer's collapse state. This will affect the app divider's - * visibility. - */ - public void setIsCollapsed(boolean isCollapsed) { - for (int i = 0, size = getItemCount(); i < size; i++) { - getPageAdapterForIndex(i).setAzLabelVisibility(!isCollapsed); - } - } - - private static ViewGroup makeProfileView( - Context context, FeatureFlags featureFlags) { - LayoutInflater inflater = LayoutInflater.from(context); - ViewGroup rootView = featureFlags.scrollablePreview() - ? (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile_wrap, null, false) - : (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile, null, false); - RecyclerView recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list); - recyclerView.setAccessibilityDelegateCompat( - new ChooserRecyclerViewAccessibilityDelegate(recyclerView)); - return rootView; - } - - @Override - public boolean onHandlePackagesChanged( - ChooserListAdapter listAdapter, boolean waitingToEnableWorkProfile) { - // TODO: why do we need to do the extra `notifyDataSetChanged()` in (only) the Chooser case? - getActiveListAdapter().notifyDataSetChanged(); - return super.onHandlePackagesChanged(listAdapter, waitingToEnableWorkProfile); - } - - @Override - protected final boolean rebuildTab(ChooserListAdapter listAdapter, boolean doPostProcessing) { - if (doPostProcessing) { - Tracer.INSTANCE.beginAppTargetLoadingSection(listAdapter.getUserHandle()); - } - return super.rebuildTab(listAdapter, doPostProcessing); - } - - /** Apply the specified {@code height} as the footer in each tab's adapter. */ - public void setFooterHeightInEveryAdapter(int height) { - for (int i = 0; i < getItemCount(); ++i) { - 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(); - } - } - } - - private static class BottomPaddingOverrideSupplier implements Supplier> { - private final Context mContext; - private int mBottomOffset; - - BottomPaddingOverrideSupplier(Context context) { - mContext = context; - } - - public void setEmptyStateBottomOffset(int bottomOffset) { - mBottomOffset = bottomOffset; - } - - @Override - public Optional 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 { - private int mMaxTargetsPerRow; - - ChooserProfileAdapterBinder(int maxTargetsPerRow) { - mMaxTargetsPerRow = maxTargetsPerRow; - } - - public void setMaxTargetsPerRow(int maxTargetsPerRow) { - mMaxTargetsPerRow = maxTargetsPerRow; - } - - @Override - public void bind( - RecyclerView recyclerView, ChooserGridAdapter chooserGridAdapter) { - GridLayoutManager glm = (GridLayoutManager) recyclerView.getLayoutManager(); - glm.setSpanCount(mMaxTargetsPerRow); - glm.setSpanSizeLookup( - new GridLayoutManager.SpanSizeLookup() { - @Override - public int getSpanSize(int position) { - return chooserGridAdapter.shouldCellSpan(position) - ? SINGLE_CELL_SPAN_SIZE - : glm.getSpanCount(); - } - }); - } - } -} diff --git a/java/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapter.java deleted file mode 100644 index 341e7043..00000000 --- a/java/src/com/android/intentresolver/v2/profiles/MultiProfilePagerAdapter.java +++ /dev/null @@ -1,694 +0,0 @@ -/* - * 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.profiles; - -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; - -import com.android.intentresolver.ResolverListAdapter; -import com.android.intentresolver.emptystate.EmptyState; -import com.android.intentresolver.emptystate.EmptyStateProvider; -import com.android.intentresolver.v2.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). - * - * @param the type of the widget that represents the contents of a page in this adapter - * @param the type of a "root" adapter class to be instantiated and included in - * the per-profile records. - * @param 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 - * mListAdapterExtractor. - */ -public class MultiProfilePagerAdapter< - PageViewT extends ViewGroup, - SinglePageAdapterT, - ListAdapterT extends ResolverListAdapter> extends PagerAdapter { - - public static final int PROFILE_PERSONAL = Profile.Type.PERSONAL.ordinal(); - public static final int PROFILE_WORK = Profile.Type.WORK.ordinal(); - - // Removed, must be constants. This is only used for linting anyway. - // @IntDef({PROFILE_PERSONAL, PROFILE_WORK}) - public @interface ProfileType {} - - private final Function mListAdapterExtractor; - private final AdapterBinder mAdapterBinder; - private final Supplier mPageViewInflater; - - private final ImmutableList> mItems; - - private final EmptyStateProvider mEmptyStateProvider; - private final UserHandle mWorkProfileUserHandle; - private final UserHandle mCloneProfileUserHandle; - private final Supplier mWorkProfileQuietModeChecker; // True when work is quiet. - - private final Set mLoadedPages; - private int mCurrentPage; - private OnProfileSelectedListener mOnProfileSelectedListener; - - protected MultiProfilePagerAdapter( - Function listAdapterExtractor, - AdapterBinder adapterBinder, - ImmutableList> tabs, - EmptyStateProvider emptyStateProvider, - Supplier workProfileQuietModeChecker, - @ProfileType int defaultProfile, - UserHandle workProfileUserHandle, - UserHandle cloneProfileUserHandle, - Supplier pageViewInflater, - Supplier> containerBottomPaddingOverrideSupplier) { - mLoadedPages = new HashSet<>(); - mWorkProfileUserHandle = workProfileUserHandle; - mCloneProfileUserHandle = cloneProfileUserHandle; - mEmptyStateProvider = emptyStateProvider; - mWorkProfileQuietModeChecker = workProfileQuietModeChecker; - - mListAdapterExtractor = listAdapterExtractor; - mAdapterBinder = adapterBinder; - mPageViewInflater = pageViewInflater; - - ImmutableList.Builder> items = - new ImmutableList.Builder<>(); - for (TabConfig 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 createProfileDescriptor( - @ProfileType int profile, - String tabLabel, - String tabAccessibilityLabel, - String tabTag, - SinglePageAdapterT adapter, - Supplier> containerBottomPaddingOverrideSupplier) { - return new ProfileDescriptor<>( - profile, - tabLabel, - tabAccessibilityLabel, - tabTag, - mPageViewInflater.get(), - adapter, - containerBottomPaddingOverrideSupplier); - } - - 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 - * userHandle. If there is no such adapter for the specified - * userHandle, returns {@code null}. - *

For example, if there is a work profile on the device with user id 10, calling this method - * with UserHandle.of(10) returns the work profile {@link ListAdapterT}. - */ - @Nullable - public final ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) { - return getListAdapterForPageNumber(getPageNumberForUserHandle(userHandle)); - } - - @Nullable - private ProfileDescriptor 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 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); - } - }; - } - - /** - * 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.onProfilePageSelected( - getProfileForPageNumber(position), 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() { - forEachInactivePage(pageNumber -> mLoadedPages.remove(pageNumber)); - } - - @Override - public final ViewGroup instantiateItem(ViewGroup container, int position) { - setupListAdapter(position); - final ProfileDescriptor descriptor = getItem(position); - container.addView(descriptor.mRootView); - return descriptor.mRootView; - } - - @Override - public void destroyItem(ViewGroup container, int position, Object view) { - container.removeView((View) view); - } - - @Override - public int getCount() { - return getItemCount(); - } - - public int getCurrentPage() { - return mCurrentPage; - } - - public final @ProfileType int getActiveProfile() { - return getProfileForPageNumber(getCurrentPage()); - } - - @VisibleForTesting - public UserHandle getCurrentUserHandle() { - return getActiveListAdapter().getUserHandle(); - } - - @Override - public boolean isViewFromObject(View view, Object object) { - return view == object; - } - - @Override - public CharSequence getPageTitle(int position) { - return null; - } - - public UserHandle getCloneUserHandle() { - return mCloneProfileUserHandle; - } - - /** - * Returns the {@link ProfileDescriptor} relevant to the given pageIndex. - *

    - *
  • For a device with only one user, pageIndex value of - * 0 would return the personal profile {@link ProfileDescriptor}.
  • - *
  • For a device with a work profile, pageIndex value of 0 would - * return the personal profile {@link ProfileDescriptor}, and pageIndex value of - * 1 would return the work profile {@link ProfileDescriptor}.
  • - *
- */ - @Nullable - private ProfileDescriptor getItem(int pageIndex) { - if (!hasPageForIndex(pageIndex)) { - return null; - } - return mItems.get(pageIndex); - } - - private ViewGroup getEmptyStateView(int pageIndex) { - return getItem(pageIndex).getEmptyStateView(); - } - - public ViewGroup getActiveEmptyStateView() { - return getEmptyStateView(getCurrentPage()); - } - - /** - * Returns the number of {@link ProfileDescriptor} objects. - *

For a normal consumer device with only one user returns 1. - *

For a device with a work profile returns 2. - */ - public final int getItemCount() { - return mItems.size(); - } - - public final PageViewT getListViewForIndex(int index) { - return getItem(index).getView(); - } - - /** - * Returns the adapter of the list view for the relevant page specified by - * pageIndex. - *

This method is meant to be implemented with an implementation-specific return type - * depending on the adapter type. - */ - @VisibleForTesting - public final SinglePageAdapterT getPageAdapterForIndex(int index) { - if (!hasPageForIndex(index)) { - return null; - } - return getItem(index).getAdapter(); - } - - /** - * Performs view-related initialization procedures for the adapter specified - * by pageIndex. - */ - public final void setupListAdapter(int pageIndex) { - mAdapterBinder.bind(getListViewForIndex(pageIndex), getPageAdapterForIndex(pageIndex)); - } - - /** - * Returns the {@link ListAdapterT} instance of the profile that is currently visible - * to the user. - *

For example, if the user is viewing the work tab in the share sheet, this method returns - * the work profile {@link ListAdapterT}. - */ - @VisibleForTesting - public final ListAdapterT getActiveListAdapter() { - return getListAdapterForPageNumber(getCurrentPage()); - } - - public final ListAdapterT getPersonalListAdapter() { - return getListAdapterForPageNumber(getPageNumberForProfile(PROFILE_PERSONAL)); - } - - @Nullable - public final ListAdapterT getWorkListAdapter() { - if (!hasPageForProfile(PROFILE_WORK)) { - return null; - } - return getListAdapterForPageNumber(getPageNumberForProfile(PROFILE_WORK)); - } - - public final SinglePageAdapterT getCurrentRootAdapter() { - return getPageAdapterForIndex(getCurrentPage()); - } - - public final PageViewT getActiveAdapterView() { - return getListViewForIndex(getCurrentPage()); - } - - private boolean anyAdapterHasItems() { - for (int i = 0; i < mItems.size(); ++i) { - ListAdapterT listAdapter = getListAdapterForPageNumber(i); - if (listAdapter.getCount() > 0) { - return true; - } - } - return false; - } - - public void refreshPackagesInAllTabs() { - // 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(); - forEachInactivePage(page -> getListAdapterForPageNumber(page).handlePackagesChanged()); - } - - /** - * Notify that there has been a package change which could potentially modify the set of targets - * that should be shown in the specified {@code listAdapter}. This may result in - * "rebuilding" the target list for that adapter. - * - * @param listAdapter an adapter that may need to be updated after the package-change event. - * @param waitingToEnableWorkProfile whether we've turned on the work profile, but haven't yet - * seen an {@code ACTION_USER_UNLOCKED} broadcast. In this case we skip the rebuild of any - * work-profile adapter because we wouldn't expect meaningful results -- but another rebuild - * will be prompted when we eventually get the broadcast. - * - * @return whether we're able to proceed with a Sharesheet session after processing this - * package-change event. If false, we were able to rebuild the targets but determined that there - * aren't any we could present in the UI without the app looking broken, so we should just quit. - */ - public boolean onHandlePackagesChanged( - ListAdapterT listAdapter, boolean waitingToEnableWorkProfile) { - if (listAdapter == getActiveListAdapter()) { - if (listAdapter.getUserHandle().equals(mWorkProfileUserHandle) - && waitingToEnableWorkProfile) { - // We have just turned on the work profile and entered the passcode to start it, - // now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no - // point in reloading the list now, since the work profile user is still turning on. - return true; - } - - boolean listRebuilt = rebuildActiveTab(true); - if (listRebuilt) { - listAdapter.notifyDataSetChanged(); - } - - // TODO: shouldn't we check that the inactive tabs are built before declaring that we - // have to quit for lack of items? - return anyAdapterHasItems(); - } else { - clearInactiveProfileCache(); - return true; - } - } - - /** - * Fully-rebuild the active tab and, if specified, partially-rebuild any other inactive tabs. - */ - public boolean rebuildTabs(boolean includePartialRebuildOfInactiveTabs) { - // TODO: we may be able to determine `includePartialRebuildOfInactiveTabs` ourselves as - // a function of our own instance state. OTOH the purpose of this "partial rebuild" is to - // be able to evaluate the intermediate state of one particular profile tab (i.e. work - // profile) that may not generalize well when we have other "inactive tabs." I.e., either we - // rebuild *all* the inactive tabs just to evaluate some auto-launch conditions that only - // depend on personal and/or work tabs, or we have to explicitly specify the ones we care - // about. It's not the pager-adapter's business to know "which ones we care about," so maybe - // they should be rebuilt lazily when-and-if it comes up (e.g. during the evaluation of - // autolaunch conditions). - boolean rebuildCompleted = rebuildActiveTab(true) || getActiveListAdapter().isTabLoaded(); - if (includePartialRebuildOfInactiveTabs) { - // 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; - } - - /** - * Rebuilds the tab that is currently visible to the user. - *

Returns {@code true} if rebuild has completed. - */ - public final boolean rebuildActiveTab(boolean doPostProcessing) { - Trace.beginSection("MultiProfilePagerAdapter#rebuildActiveTab"); - boolean result = rebuildTab(getActiveListAdapter(), doPostProcessing); - Trace.endSection(); - return result; - } - - /** - * Rebuilds any tabs that are not currently visible to the user. - *

Returns {@code true} if rebuild has completed in all inactive tabs. - */ - private boolean rebuildInactiveTabs(boolean doPostProcessing) { - Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab"); - 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 allRebuildsComplete.get(); - } - - protected void forEachPage(Consumer pageNumberHandler) { - for (int pageNumber = 0; pageNumber < getItemCount(); ++pageNumber) { - pageNumberHandler.accept(pageNumber); - } - } - - protected void forEachInactivePage(Consumer inactivePageNumberHandler) { - forEachPage(pageNumber -> { - if (pageNumber != getCurrentPage()) { - inactivePageNumberHandler.accept(pageNumber); - } - }); - } - - protected boolean rebuildTab(ListAdapterT activeListAdapter, boolean doPostProcessing) { - if (shouldSkipRebuild(activeListAdapter)) { - activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true); - return false; - } - return activeListAdapter.rebuildList(doPostProcessing); - } - - private boolean shouldSkipRebuild(ListAdapterT activeListAdapter) { - EmptyState emptyState = mEmptyStateProvider.getEmptyState(activeListAdapter); - return emptyState != null && emptyState.shouldSkipDataRebuild(); - } - - /** - * The empty state screens are shown according to their priority: - *

    - *
  1. (highest priority) cross-profile disabled by policy (handled in - * {@link #rebuildTab(ListAdapterT, boolean)})
  2. - *
  3. no apps available
  4. - *
  5. (least priority) work is off
  6. - *
- * - * The intention is to prevent the user from having to turn - * the work profile on if there will not be any apps resolved - * anyway. - * - * TODO: move this comment to the place where we configure our composite provider. - */ - public void showEmptyResolverListEmptyState(ListAdapterT listAdapter) { - final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter); - - if (emptyState == null) { - return; - } - - emptyState.onEmptyStateShown(); - - View.OnClickListener clickListener = null; - - if (emptyState.getButtonClickListener() != null) { - clickListener = v -> emptyState.getButtonClickListener().onClick(() -> { - ProfileDescriptor descriptor = - getDescriptorForUserHandle(listAdapter.getUserHandle()); - descriptor.mEmptyStateUi.showSpinner(); - }); - } - - showEmptyState(listAdapter, emptyState, clickListener); - } - - private void showEmptyState( - ListAdapterT activeListAdapter, - EmptyState emptyState, - View.OnClickListener buttonOnClick) { - ProfileDescriptor descriptor = - getDescriptorForUserHandle(activeListAdapter.getUserHandle()); - descriptor.mEmptyStateUi.showEmptyState(emptyState, buttonOnClick); - activeListAdapter.markTabLoaded(); - } - - /** - * Sets up the padding of the view containing the empty state screens for the current adapter - * view. - */ - protected final void setupContainerPadding() { - getItem(getCurrentPage()).setupContainerPadding(); - } - - public void showListView(ListAdapterT activeListAdapter) { - ProfileDescriptor descriptor = - getDescriptorForUserHandle(activeListAdapter.getUserHandle()); - descriptor.mEmptyStateUi.hide(); - } - - /** - * @return whether any "inactive" tab's adapter would show an empty-state screen in our current - * application state. - */ - public final boolean shouldShowEmptyStateScreenInAnyInactiveAdapter() { - 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) { - int count = listAdapter.getUnfilteredCount(); - return (count == 0 && listAdapter.getPlaceholderCount() == 0) - || (listAdapter.getUserHandle().equals(mWorkProfileUserHandle) - && mWorkProfileQuietModeChecker.get()); - } - -} diff --git a/java/src/com/android/intentresolver/v2/profiles/OnProfileSelectedListener.java b/java/src/com/android/intentresolver/v2/profiles/OnProfileSelectedListener.java deleted file mode 100644 index 7bdbec4c..00000000 --- a/java/src/com/android/intentresolver/v2/profiles/OnProfileSelectedListener.java +++ /dev/null @@ -1,46 +0,0 @@ -/* - * 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.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. - *

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/v2/profiles/OnSwitchOnWorkSelectedListener.java b/java/src/com/android/intentresolver/v2/profiles/OnSwitchOnWorkSelectedListener.java deleted file mode 100644 index 3dbbd4d0..00000000 --- a/java/src/com/android/intentresolver/v2/profiles/OnSwitchOnWorkSelectedListener.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * 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.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/v2/profiles/ProfileDescriptor.java b/java/src/com/android/intentresolver/v2/profiles/ProfileDescriptor.java deleted file mode 100644 index e2e9c19d..00000000 --- a/java/src/com/android/intentresolver/v2/profiles/ProfileDescriptor.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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.profiles; - -import android.view.ViewGroup; - -import com.android.intentresolver.v2.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 { - 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> 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/v2/profiles/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/profiles/ResolverMultiProfilePagerAdapter.java deleted file mode 100644 index e44cf8da..00000000 --- a/java/src/com/android/intentresolver/v2/profiles/ResolverMultiProfilePagerAdapter.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * 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.profiles; - -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.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. - */ -public class ResolverMultiProfilePagerAdapter extends - MultiProfilePagerAdapter { - private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier; - - public ResolverMultiProfilePagerAdapter(Context context, - ImmutableList> tabs, - EmptyStateProvider emptyStateProvider, - Supplier workProfileQuietModeChecker, - @ProfileType int defaultProfile, - UserHandle workProfileUserHandle, - UserHandle cloneProfileUserHandle) { - this( - context, - tabs, - emptyStateProvider, - workProfileQuietModeChecker, - defaultProfile, - workProfileUserHandle, - cloneProfileUserHandle, - new BottomPaddingOverrideSupplier()); - } - - private ResolverMultiProfilePagerAdapter( - Context context, - ImmutableList> tabs, - EmptyStateProvider emptyStateProvider, - Supplier workProfileQuietModeChecker, - @ProfileType int defaultProfile, - UserHandle workProfileUserHandle, - UserHandle cloneProfileUserHandle, - BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) { - super( - listAdapter -> listAdapter, - (listView, bindAdapter) -> listView.setAdapter(bindAdapter), - tabs, - 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: 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> { - private boolean mUseLayoutWithDefault; - - public void setUseLayoutWithDefault(boolean useLayoutWithDefault) { - mUseLayoutWithDefault = useLayoutWithDefault; - } - - @Override - public Optional get() { - return mUseLayoutWithDefault ? Optional.empty() : Optional.of(0); - } - } -} diff --git a/java/src/com/android/intentresolver/v2/profiles/TabConfig.java b/java/src/com/android/intentresolver/v2/profiles/TabConfig.java deleted file mode 100644 index 994f8aff..00000000 --- a/java/src/com/android/intentresolver/v2/profiles/TabConfig.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * 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.profiles; - -public class TabConfig { - 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/v2/shared/model/Profile.kt b/java/src/com/android/intentresolver/v2/shared/model/Profile.kt deleted file mode 100644 index 6e37174c..00000000 --- a/java/src/com/android/intentresolver/v2/shared/model/Profile.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.shared.model - -import com.android.intentresolver.v2.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/shared/model/User.kt b/java/src/com/android/intentresolver/v2/shared/model/User.kt deleted file mode 100644 index 46279ad0..00000000 --- a/java/src/com/android/intentresolver/v2/shared/model/User.kt +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.shared.model - -import android.annotation.UserIdInt -import android.os.UserHandle - -/** - * A User represents the owner of a distinct set of content. - * * maps 1:1 to a UserHandle or UserId (Int) value. - * * refers to either [Full][Type.FULL], or a [Profile][Type.PROFILE] user, as indicated by the - * [type] property. - * - * See - * [Users for system developers](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/Users.md) - * - * ``` - * val users = listOf( - * User(id = 0, role = PERSONAL), - * User(id = 10, role = WORK), - * User(id = 11, role = CLONE), - * User(id = 12, role = PRIVATE), - * ) - * ``` - */ -data class User( - @UserIdInt val id: Int, - val role: Role, -) { - val handle: UserHandle = UserHandle.of(id) - - enum class Role { - PERSONAL, - PRIVATE, - WORK, - CLONE - } -} diff --git a/java/src/com/android/intentresolver/v2/ui/ActionTitle.java b/java/src/com/android/intentresolver/v2/ui/ActionTitle.java deleted file mode 100644 index a1e1c7fa..00000000 --- a/java/src/com/android/intentresolver/v2/ui/ActionTitle.java +++ /dev/null @@ -1,88 +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.ui; - -import android.content.Intent; -import android.provider.MediaStore; - -import androidx.annotation.StringRes; - -import com.android.intentresolver.R; - -/** - * Provides a set of related resources for different use cases. - */ -public enum ActionTitle { - VIEW(Intent.ACTION_VIEW, - R.string.whichViewApplication, - R.string.whichViewApplicationNamed, - R.string.whichViewApplicationLabel), - EDIT(Intent.ACTION_EDIT, - R.string.whichEditApplication, - R.string.whichEditApplicationNamed, - R.string.whichEditApplicationLabel), - SEND(Intent.ACTION_SEND, - R.string.whichSendApplication, - R.string.whichSendApplicationNamed, - R.string.whichSendApplicationLabel), - SENDTO(Intent.ACTION_SENDTO, - R.string.whichSendToApplication, - R.string.whichSendToApplicationNamed, - R.string.whichSendToApplicationLabel), - SEND_MULTIPLE(Intent.ACTION_SEND_MULTIPLE, - R.string.whichSendApplication, - R.string.whichSendApplicationNamed, - R.string.whichSendApplicationLabel), - CAPTURE_IMAGE(MediaStore.ACTION_IMAGE_CAPTURE, - R.string.whichImageCaptureApplication, - R.string.whichImageCaptureApplicationNamed, - R.string.whichImageCaptureApplicationLabel), - DEFAULT(null, - R.string.whichApplication, - R.string.whichApplicationNamed, - R.string.whichApplicationLabel), - HOME(Intent.ACTION_MAIN, - R.string.whichHomeApplication, - R.string.whichHomeApplicationNamed, - R.string.whichHomeApplicationLabel); - - // titles for layout that deals with http(s) intents - public static final int BROWSABLE_TITLE_RES = R.string.whichOpenLinksWith; - public static final int BROWSABLE_HOST_TITLE_RES = R.string.whichOpenHostLinksWith; - public static final int BROWSABLE_HOST_APP_TITLE_RES = R.string.whichOpenHostLinksWithApp; - public static final int BROWSABLE_APP_TITLE_RES = R.string.whichOpenLinksWithApp; - - public final String action; - public final int titleRes; - public final int namedTitleRes; - public final @StringRes int labelRes; - - ActionTitle(String action, int titleRes, int namedTitleRes, @StringRes int labelRes) { - this.action = action; - this.titleRes = titleRes; - this.namedTitleRes = namedTitleRes; - this.labelRes = labelRes; - } - - public static ActionTitle forAction(String action) { - for (ActionTitle title : values()) { - if (title != HOME && action != null && action.equals(title.action)) { - return title; - } - } - return DEFAULT; - } -} diff --git a/java/src/com/android/intentresolver/v2/ui/ProfilePagerResources.kt b/java/src/com/android/intentresolver/v2/ui/ProfilePagerResources.kt deleted file mode 100644 index ca7ae0fc..00000000 --- a/java/src/com/android/intentresolver/v2/ui/ProfilePagerResources.kt +++ /dev/null @@ -1,53 +0,0 @@ -/* - * 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.ui - -import android.content.res.Resources -import com.android.intentresolver.R -import com.android.intentresolver.inject.ApplicationOwned -import com.android.intentresolver.v2.data.repository.DevicePolicyResources -import com.android.intentresolver.v2.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 - } - } -} diff --git a/java/src/com/android/intentresolver/v2/ui/ShareResultSender.kt b/java/src/com/android/intentresolver/v2/ui/ShareResultSender.kt deleted file mode 100644 index 2b01b5e7..00000000 --- a/java/src/com/android/intentresolver/v2/ui/ShareResultSender.kt +++ /dev/null @@ -1,163 +0,0 @@ -/* - * 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.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.v2.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/v2/ui/ShortcutPolicyModule.kt b/java/src/com/android/intentresolver/v2/ui/ShortcutPolicyModule.kt deleted file mode 100644 index 5e098cd5..00000000 --- a/java/src/com/android/intentresolver/v2/ui/ShortcutPolicyModule.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.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/v2/ui/model/ActivityModel.kt b/java/src/com/android/intentresolver/v2/ui/model/ActivityModel.kt deleted file mode 100644 index 67c2a25e..00000000 --- a/java/src/com/android/intentresolver/v2/ui/model/ActivityModel.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.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.v2.data.model.ANDROID_APP_SCHEME -import com.android.intentresolver.v2.ext.readParcelable -import com.android.intentresolver.v2.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 { - override fun newArray(size: Int) = arrayOfNulls(size) - override fun createFromParcel(source: Parcel) = ActivityModel(source) - } - - @JvmStatic - fun createFrom(activity: Activity): ActivityModel { - return ActivityModel( - activity.intent, - activity.launchedFromUid, - Objects.requireNonNull(activity.launchedFromPackage), - activity.referrer - ) - } - } -} diff --git a/java/src/com/android/intentresolver/v2/ui/model/ResolverRequest.kt b/java/src/com/android/intentresolver/v2/ui/model/ResolverRequest.kt deleted file mode 100644 index 44010caf..00000000 --- a/java/src/com/android/intentresolver/v2/ui/model/ResolverRequest.kt +++ /dev/null @@ -1,68 +0,0 @@ -/* - * 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.ui.model - -import android.content.Intent -import android.content.pm.ResolveInfo -import android.os.UserHandle -import com.android.intentresolver.v2.ext.isHomeIntent -import com.android.intentresolver.v2.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? = 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 = listOf(intent) -} diff --git a/java/src/com/android/intentresolver/v2/ui/model/ShareAction.kt b/java/src/com/android/intentresolver/v2/ui/model/ShareAction.kt deleted file mode 100644 index e13ef101..00000000 --- a/java/src/com/android/intentresolver/v2/ui/model/ShareAction.kt +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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.ui.model - -enum class ShareAction { - SYSTEM_COPY, - SYSTEM_EDIT, - APPLICATION_DEFINED -} diff --git a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt deleted file mode 100644 index a25fcbea..00000000 --- a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserRequestReader.kt +++ /dev/null @@ -1,198 +0,0 @@ -/* - * 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.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.inject.ChooserServiceFlags -import com.android.intentresolver.util.hasValidIcon -import com.android.intentresolver.v2.data.model.ChooserRequest -import com.android.intentresolver.v2.ext.hasSendAction -import com.android.intentresolver.v2.ext.ifMatch -import com.android.intentresolver.v2.ui.model.ActivityModel -import com.android.intentresolver.v2.validation.Validation -import com.android.intentresolver.v2.validation.ValidationResult -import com.android.intentresolver.v2.validation.types.IntentOrUri -import com.android.intentresolver.v2.validation.types.array -import com.android.intentresolver.v2.validation.types.value -import com.android.intentresolver.v2.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 { - 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(EXTRA_REPLACEMENT_EXTRAS)) - - val (customTitle, defaultTitleResource) = - if (isSendAction) { - ignored( - value(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(EXTRA_TITLE)) - custom to (custom?.let { 0 } ?: R.string.chooseActivity) - } - - val initialIntents = - optional(array(EXTRA_INITIAL_INTENTS))?.take(MAX_INITIAL_INTENTS)?.map { - it.maybeAddSendActionFlags() - } - ?: emptyList() - - val chosenComponentSender = - optional(value(EXTRA_CHOOSER_RESULT_INTENT_SENDER)) - ?: optional(value(EXTRA_CHOSEN_COMPONENT_INTENT_SENDER)) - - val refinementIntentSender = - optional(value(EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER)) - - val filteredComponents = - optional(array(EXTRA_EXCLUDE_COMPONENTS)) ?: emptyList() - - @Suppress("DEPRECATION") - val callerChooserTargets = - optional(array(EXTRA_CHOOSER_TARGETS)) ?: emptyList() - - val retainInOnStop = - optional(value(ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP)) ?: false - - val sharedText = optional(value(EXTRA_TEXT)) - - val chooserActions = readChooserActions() ?: emptyList() - - val modifyShareAction = optional(value(EXTRA_CHOOSER_MODIFY_SHARE_ACTION)) - - val additionalContentUri: Uri? - val focusedItemPos: Int - if (isSendAction && flags.chooserPayloadToggling()) { - additionalContentUri = optional(value(Intent.EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI)) - focusedItemPos = optional(value(Intent.EXTRA_CHOOSER_FOCUSED_ITEM_POSITION)) ?: 0 - } else { - additionalContentUri = null - focusedItemPos = 0 - } - - val contentTypeHint = - if (flags.chooserAlbumText()) { - when (optional(value(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(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? = - optional(array(EXTRA_ALTERNATE_INTENTS))?.map { it.maybeAddSendActionFlags() } - -fun Validation.readChooserActions(): List? = - optional(array(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/v2/ui/viewmodel/ChooserViewModel.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt deleted file mode 100644 index e39329b1..00000000 --- a/java/src/com/android/intentresolver/v2/ui/viewmodel/ChooserViewModel.kt +++ /dev/null @@ -1,94 +0,0 @@ -/* - * 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.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.inject.Background -import com.android.intentresolver.inject.ChooserServiceFlags -import com.android.intentresolver.v2.data.model.ChooserRequest -import com.android.intentresolver.v2.data.repository.ChooserRequestRepository -import com.android.intentresolver.v2.ui.model.ActivityModel -import com.android.intentresolver.v2.ui.model.ActivityModel.Companion.ACTIVITY_MODEL_KEY -import com.android.intentresolver.v2.validation.Invalid -import com.android.intentresolver.v2.validation.Valid -import com.android.intentresolver.v2.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, - private val processUpdatesInteractor: Lazy, - private val fetchPreviewsInteractor: Lazy, - @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, - private val chooserRequestRepository: Lazy, -) : 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 - 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/v2/ui/viewmodel/ResolverRequestReader.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestReader.kt deleted file mode 100644 index bbc376ea..00000000 --- a/java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverRequestReader.kt +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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.ui.viewmodel - -import android.os.Bundle -import android.os.UserHandle -import com.android.intentresolver.v2.ResolverActivity.PROFILE_PERSONAL -import com.android.intentresolver.v2.ResolverActivity.PROFILE_WORK -import com.android.intentresolver.v2.shared.model.Profile -import com.android.intentresolver.v2.ui.model.ActivityModel -import com.android.intentresolver.v2.ui.model.ResolverRequest -import com.android.intentresolver.v2.validation.Validation -import com.android.intentresolver.v2.validation.ValidationResult -import com.android.intentresolver.v2.validation.types.value -import com.android.intentresolver.v2.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 { - @Suppress("DEPRECATION") - return validateFrom((launch.intent.extras ?: Bundle())::get) { - val callingUser = optional(value(EXTRA_CALLING_USER)) - val selectedProfile = checkSelectedProfile() - val audioDevice = optional(value(EXTRA_IS_AUDIO_CAPTURE_DEVICE)) ?: false - ResolverRequest(launch.intent, selectedProfile, callingUser, audioDevice) - } -} - -private fun Validation.checkSelectedProfile(): Profile.Type? { - return when (val selected = optional(value(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/v2/ui/viewmodel/ResolverViewModel.kt b/java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverViewModel.kt deleted file mode 100644 index eb6a1b96..00000000 --- a/java/src/com/android/intentresolver/v2/ui/viewmodel/ResolverViewModel.kt +++ /dev/null @@ -1,70 +0,0 @@ -/* - * 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.ui.viewmodel - -import android.util.Log -import androidx.lifecycle.SavedStateHandle -import androidx.lifecycle.ViewModel -import com.android.intentresolver.v2.ui.model.ActivityModel -import com.android.intentresolver.v2.ui.model.ActivityModel.Companion.ACTIVITY_MODEL_KEY -import com.android.intentresolver.v2.ui.model.ResolverRequest -import com.android.intentresolver.v2.validation.Invalid -import com.android.intentresolver.v2.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 - - /** - * A [StateFlow] of [ResolverRequest]. - * - * Note: Only safe to access after checking if [initialRequest] is [Valid]. - */ - lateinit var request: StateFlow - 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/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(initializer: () -> T?) : Lazy { - - override val value: T? - get() = lazy.get()?.value - - private var lazy: AtomicReference?> = 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?) { - 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 mutableLazy(initializer: () -> T?) = MutableLazy(initializer) diff --git a/java/src/com/android/intentresolver/v2/validation/Findings.kt b/java/src/com/android/intentresolver/v2/validation/Findings.kt deleted file mode 100644 index bdf2f00a..00000000 --- a/java/src/com/android/intentresolver/v2/validation/Findings.kt +++ /dev/null @@ -1,120 +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 -import com.android.intentresolver.v2.validation.Importance.CRITICAL -import com.android.intentresolver.v2.validation.Importance.WARNING -import kotlin.reflect.KClass - -sealed interface Finding { - val importance: Importance - val message: String -} - -enum class Importance { - CRITICAL, - WARNING, -} - -val Finding.logcatPriority - get() = - when (importance) { - CRITICAL -> Log.ERROR - 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) -} - -data class IgnoredValue( - val key: String, - val reason: String, -) : Finding { - override val importance = WARNING - - override val message: String - get() = formatMessage(key, "Ignored. $reason") -} - -data class NoValue( - val key: String, - override val importance: Importance, - val allowedType: KClass<*>, -) : Finding { - - override val message: String - get() = - formatMessage( - key, - if (importance == CRITICAL) { - "expected value of ${allowedType.simpleName}, " + "but no value was present" - } else { - "no ${allowedType.simpleName} value present" - } - ) -} - -data class WrongElementType( - val key: String, - override val importance: Importance, - val container: KClass<*>, - val actualType: KClass<*>, - val expectedType: KClass<*> -) : Finding { - override val message: String - get() = - formatMessage( - key, - "${container.simpleName} expected with elements of " + - "${expectedType.simpleName} " + - "but found ${actualType.simpleName} values instead" - ) -} - -data class ValueIsWrongType( - val key: String, - override val importance: Importance, - val actualType: KClass<*>, - val allowedTypes: List>, -) : Finding { - - override val message: String - get() = - formatMessage( - key, - "expected value of ${allowedTypes.map(KClass<*>::simpleName)} " + - "but was ${actualType.simpleName}" - ) -} - -data class UncaughtException(val thrown: Throwable, val key: String? = null) : Finding { - override val importance: Importance - get() = CRITICAL - override val message: String - get() = - formatMessage( - key, - "An unhandled exception was caught during validation: " + - thrown.stackTraceToString() - ) -} diff --git a/java/src/com/android/intentresolver/v2/validation/Validation.kt b/java/src/com/android/intentresolver/v2/validation/Validation.kt deleted file mode 100644 index 6072ec9f..00000000 --- a/java/src/com/android/intentresolver/v2/validation/Validation.kt +++ /dev/null @@ -1,137 +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 com.android.intentresolver.v2.validation.Importance.CRITICAL -import com.android.intentresolver.v2.validation.Importance.WARNING - -/** - * Provides a mechanism for validating a result from a set of properties. - * - * The results of validation are provided as [findings]. - */ -interface Validation { - val findings: List - - /** - * Require a valid property. - * - * If [property] is not valid, this [Validation] will be immediately completed as [Invalid]. - * - * @param property the required property - * @return a valid **T** - */ - @Throws(InvalidResultError::class) fun required(property: Validator): T - - /** - * Request an optional value for a property. - * - * If [property] is not valid, this [Validation] will be immediately completed as [Invalid]. - * - * @param property the required property - * @return a valid **T** - */ - fun optional(property: Validator): T? - - /** - * Report a property as __ignored__. - * - * The presence of any value will report a warning citing [reason]. - */ - fun ignored(property: Validator, reason: String) -} - -/** Performs validation for a specific key -> value pair. */ -interface Validator { - val key: String - - /** - * Performs validation on a specific value from [source]. - * - * @param source a source for reading the property value. Values are intentionally untyped - * (Any?) to avoid upstream code from making type assertions through type inference. Types are - * asserted later using a [Validator]. - * @param importance the importance of any findings - */ - fun validate(source: (String) -> Any?, importance: Importance): ValidationResult -} - -internal class InvalidResultError internal constructor() : Error() - -/** - * Perform a number of validations on the source, assembling and returning a Result. - * - * When an exception is thrown by [validate], it is caught here. In response, a failed - * [ValidationResult] is returned containing a [CRITICAL] [Finding] for the exception. - * - * @param validate perform validations and return a [ValidationResult] - */ -fun validateFrom(source: (String) -> Any?, validate: Validation.() -> T): ValidationResult { - val validation = ValidationImpl(source) - return runCatching { validate(validation) } - .fold( - onSuccess = { result -> Valid(result, validation.findings) }, - onFailure = { - when (it) { - // A validator has interrupted validation. Return the findings. - is InvalidResultError -> Invalid(validation.findings) - - // Some other exception was thrown from [validate], - else -> Invalid(error = UncaughtException(it)) - } - } - ) -} - -private class ValidationImpl(val source: (String) -> Any?) : Validation { - override val findings = mutableListOf() - - override fun optional(property: Validator): T? = validate(property, WARNING) - - override fun required(property: Validator): T { - return validate(property, CRITICAL) ?: throw InvalidResultError() - } - - override fun ignored(property: Validator, reason: String) { - val result = property.validate(source, WARNING) - if (result is Valid) { - // Note: Any warnings about the value itself (result.findings) are ignored. - findings += IgnoredValue(property.key, reason) - } - } - - private fun validate(property: Validator, importance: Importance): T? { - return runCatching { property.validate(source, importance) } - .fold( - onSuccess = { result -> - return when (result) { - is Valid -> { - findings += result.warnings - result.value - } - is Invalid -> { - findings += result.errors - null - } - } - }, - onFailure = { - findings += UncaughtException(it, property.key) - null - } - ) - } -} diff --git a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt deleted file mode 100644 index f5c467dc..00000000 --- a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt +++ /dev/null @@ -1,26 +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 - -sealed interface ValidationResult - -data class Valid(val value: T, val warnings: List = emptyList()) : ValidationResult { - constructor(value: T, warning: Finding) : this(value, listOf(warning)) -} - -data class Invalid(val errors: List = emptyList()) : ValidationResult { - 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/v2/validation/types/IntentOrUri.kt deleted file mode 100644 index fc51ba1e..00000000 --- a/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt +++ /dev/null @@ -1,62 +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 android.content.Intent -import android.net.Uri -import com.android.intentresolver.v2.validation.Importance -import com.android.intentresolver.v2.validation.Invalid -import com.android.intentresolver.v2.validation.NoValue -import com.android.intentresolver.v2.validation.Valid -import com.android.intentresolver.v2.validation.ValidationResult -import com.android.intentresolver.v2.validation.Validator -import com.android.intentresolver.v2.validation.ValueIsWrongType - -class IntentOrUri(override val key: String) : Validator { - - override fun validate( - source: (String) -> Any?, - importance: Importance - ): ValidationResult { - return when (val value = source(key)) { - // An intent, return it. - is Intent -> Valid(value) - - // A Uri was supplied. - // Unfortunately, converting Uri -> Intent requires a toString(). - is Uri -> Valid(Intent.parseUri(value.toString(), Intent.URI_INTENT_SCHEME)) - - // No value present. - null -> - when (importance) { - Importance.WARNING -> Invalid() // No warnings if optional, but missing - Importance.CRITICAL -> Invalid(NoValue(key, importance, Intent::class)) - } - - // Some other type. - else -> { - return Invalid( - ValueIsWrongType( - key, - importance, - actualType = value::class, - allowedTypes = listOf(Intent::class, Uri::class) - ) - ) - } - } - } -} diff --git a/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt deleted file mode 100644 index b68d972f..00000000 --- a/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt +++ /dev/null @@ -1,84 +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.Importance -import com.android.intentresolver.v2.validation.Invalid -import com.android.intentresolver.v2.validation.NoValue -import com.android.intentresolver.v2.validation.Valid -import com.android.intentresolver.v2.validation.ValidationResult -import com.android.intentresolver.v2.validation.Validator -import com.android.intentresolver.v2.validation.ValueIsWrongType -import com.android.intentresolver.v2.validation.WrongElementType -import kotlin.reflect.KClass -import kotlin.reflect.cast - -class ParceledArray( - override val key: String, - private val elementType: KClass, -) : Validator> { - - override fun validate( - source: (String) -> Any?, - importance: Importance - ): ValidationResult> { - return when (val value: Any? = source(key)) { - // No value present. - 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, which is - // incompatible with Array. - - // To handle this safely, treat as Array<*>, assert contents of the expected - // parcelable type, and return as a list. - - is Array<*> -> { - val invalid = value.filterNotNull().firstOrNull { !elementType.isInstance(it) } - when (invalid) { - // No invalid elements, result is ok. - null -> Valid(value.map { elementType.cast(it) }) - - // At least one incorrect element type found. - else -> - Invalid( - WrongElementType( - key, - importance, - actualType = invalid::class, - container = Array::class, - expectedType = elementType - ) - ) - } - } - - // The value is not an Array at all. - else -> - Invalid( - ValueIsWrongType( - key, - importance, - actualType = value::class, - allowedTypes = listOf(elementType) - ) - ) - } - } -} diff --git a/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt deleted file mode 100644 index 0badebc4..00000000 --- a/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt +++ /dev/null @@ -1,60 +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.Importance -import com.android.intentresolver.v2.validation.Invalid -import com.android.intentresolver.v2.validation.NoValue -import com.android.intentresolver.v2.validation.Valid -import com.android.intentresolver.v2.validation.ValidationResult -import com.android.intentresolver.v2.validation.Validator -import com.android.intentresolver.v2.validation.ValueIsWrongType -import kotlin.reflect.KClass -import kotlin.reflect.cast - -class SimpleValue( - override val key: String, - private val expected: KClass, -) : Validator { - - override fun validate(source: (String) -> Any?, importance: Importance): ValidationResult { - val value: Any? = source(key) - return when { - // The value is present and of the expected type. - expected.isInstance(value) -> return Valid(expected.cast(value)) - - // No value is present. - value == null -> - 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 -> - Invalid( - listOf( - ValueIsWrongType( - key, - importance, - actualType = value::class, - allowedTypes = listOf(expected) - ) - ) - ) - } - } -} diff --git a/java/src/com/android/intentresolver/v2/validation/types/Validators.kt b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt deleted file mode 100644 index 70993b4d..00000000 --- a/java/src/com/android/intentresolver/v2/validation/types/Validators.kt +++ /dev/null @@ -1,26 +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.Validator - -inline fun value(key: String): Validator { - return SimpleValue(key, T::class) -} - -inline fun array(key: String): Validator> { - return ParceledArray(key, T::class) -} diff --git a/java/src/com/android/intentresolver/validation/Findings.kt b/java/src/com/android/intentresolver/validation/Findings.kt new file mode 100644 index 00000000..0d62017f --- /dev/null +++ b/java/src/com/android/intentresolver/validation/Findings.kt @@ -0,0 +1,120 @@ +/* + * 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 + +import android.util.Log +import com.android.intentresolver.validation.Importance.CRITICAL +import com.android.intentresolver.validation.Importance.WARNING +import kotlin.reflect.KClass + +sealed interface Finding { + val importance: Importance + val message: String +} + +enum class Importance { + CRITICAL, + WARNING, +} + +val Finding.logcatPriority + get() = + when (importance) { + CRITICAL -> Log.ERROR + 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) +} + +data class IgnoredValue( + val key: String, + val reason: String, +) : Finding { + override val importance = WARNING + + override val message: String + get() = formatMessage(key, "Ignored. $reason") +} + +data class NoValue( + val key: String, + override val importance: Importance, + val allowedType: KClass<*>, +) : Finding { + + override val message: String + get() = + formatMessage( + key, + if (importance == CRITICAL) { + "expected value of ${allowedType.simpleName}, " + "but no value was present" + } else { + "no ${allowedType.simpleName} value present" + } + ) +} + +data class WrongElementType( + val key: String, + override val importance: Importance, + val container: KClass<*>, + val actualType: KClass<*>, + val expectedType: KClass<*> +) : Finding { + override val message: String + get() = + formatMessage( + key, + "${container.simpleName} expected with elements of " + + "${expectedType.simpleName} " + + "but found ${actualType.simpleName} values instead" + ) +} + +data class ValueIsWrongType( + val key: String, + override val importance: Importance, + val actualType: KClass<*>, + val allowedTypes: List>, +) : Finding { + + override val message: String + get() = + formatMessage( + key, + "expected value of ${allowedTypes.map(KClass<*>::simpleName)} " + + "but was ${actualType.simpleName}" + ) +} + +data class UncaughtException(val thrown: Throwable, val key: String? = null) : Finding { + override val importance: Importance + get() = CRITICAL + override val message: String + get() = + formatMessage( + key, + "An unhandled exception was caught during validation: " + + thrown.stackTraceToString() + ) +} diff --git a/java/src/com/android/intentresolver/validation/Validation.kt b/java/src/com/android/intentresolver/validation/Validation.kt new file mode 100644 index 00000000..6ba62e57 --- /dev/null +++ b/java/src/com/android/intentresolver/validation/Validation.kt @@ -0,0 +1,137 @@ +/* + * 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 + +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. + * + * The results of validation are provided as [findings]. + */ +interface Validation { + val findings: List + + /** + * Require a valid property. + * + * If [property] is not valid, this [Validation] will be immediately completed as [Invalid]. + * + * @param property the required property + * @return a valid **T** + */ + @Throws(InvalidResultError::class) fun required(property: Validator): T + + /** + * Request an optional value for a property. + * + * If [property] is not valid, this [Validation] will be immediately completed as [Invalid]. + * + * @param property the required property + * @return a valid **T** + */ + fun optional(property: Validator): T? + + /** + * Report a property as __ignored__. + * + * The presence of any value will report a warning citing [reason]. + */ + fun ignored(property: Validator, reason: String) +} + +/** Performs validation for a specific key -> value pair. */ +interface Validator { + val key: String + + /** + * Performs validation on a specific value from [source]. + * + * @param source a source for reading the property value. Values are intentionally untyped + * (Any?) to avoid upstream code from making type assertions through type inference. Types are + * asserted later using a [Validator]. + * @param importance the importance of any findings + */ + fun validate(source: (String) -> Any?, importance: Importance): ValidationResult +} + +internal class InvalidResultError internal constructor() : Error() + +/** + * Perform a number of validations on the source, assembling and returning a Result. + * + * When an exception is thrown by [validate], it is caught here. In response, a failed + * [ValidationResult] is returned containing a [CRITICAL] [Finding] for the exception. + * + * @param validate perform validations and return a [ValidationResult] + */ +fun validateFrom(source: (String) -> Any?, validate: Validation.() -> T): ValidationResult { + val validation = ValidationImpl(source) + return runCatching { validate(validation) } + .fold( + onSuccess = { result -> Valid(result, validation.findings) }, + onFailure = { + when (it) { + // A validator has interrupted validation. Return the findings. + is InvalidResultError -> Invalid(validation.findings) + + // Some other exception was thrown from [validate], + else -> Invalid(error = UncaughtException(it)) + } + } + ) +} + +private class ValidationImpl(val source: (String) -> Any?) : Validation { + override val findings = mutableListOf() + + override fun optional(property: Validator): T? = validate(property, WARNING) + + override fun required(property: Validator): T { + return validate(property, CRITICAL) ?: throw InvalidResultError() + } + + override fun ignored(property: Validator, reason: String) { + val result = property.validate(source, WARNING) + if (result is Valid) { + // Note: Any warnings about the value itself (result.findings) are ignored. + findings += IgnoredValue(property.key, reason) + } + } + + private fun validate(property: Validator, importance: Importance): T? { + return runCatching { property.validate(source, importance) } + .fold( + onSuccess = { result -> + return when (result) { + is Valid -> { + findings += result.warnings + result.value + } + is Invalid -> { + findings += result.errors + null + } + } + }, + onFailure = { + findings += UncaughtException(it, property.key) + null + } + ) + } +} 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 + +data class Valid(val value: T, val warnings: List = emptyList()) : ValidationResult { + constructor(value: T, warning: Finding) : this(value, listOf(warning)) +} + +data class Invalid(val errors: List = emptyList()) : ValidationResult { + constructor(error: Finding) : this(listOf(error)) +} diff --git a/java/src/com/android/intentresolver/validation/types/IntentOrUri.kt b/java/src/com/android/intentresolver/validation/types/IntentOrUri.kt new file mode 100644 index 00000000..74c48a23 --- /dev/null +++ b/java/src/com/android/intentresolver/validation/types/IntentOrUri.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.validation.types + +import android.content.Intent +import android.net.Uri +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 { + + override fun validate( + source: (String) -> Any?, + importance: Importance + ): ValidationResult { + return when (val value = source(key)) { + // An intent, return it. + is Intent -> Valid(value) + + // A Uri was supplied. + // Unfortunately, converting Uri -> Intent requires a toString(). + is Uri -> Valid(Intent.parseUri(value.toString(), Intent.URI_INTENT_SCHEME)) + + // No value present. + null -> + when (importance) { + Importance.WARNING -> Invalid() // No warnings if optional, but missing + Importance.CRITICAL -> Invalid(NoValue(key, importance, Intent::class)) + } + + // Some other type. + else -> { + return Invalid( + ValueIsWrongType( + key, + importance, + actualType = value::class, + allowedTypes = listOf(Intent::class, Uri::class) + ) + ) + } + } + } +} diff --git a/java/src/com/android/intentresolver/validation/types/ParceledArray.kt b/java/src/com/android/intentresolver/validation/types/ParceledArray.kt new file mode 100644 index 00000000..5150ec5e --- /dev/null +++ b/java/src/com/android/intentresolver/validation/types/ParceledArray.kt @@ -0,0 +1,84 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.intentresolver.validation.types + +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 + +class ParceledArray( + override val key: String, + private val elementType: KClass, +) : Validator> { + + override fun validate( + source: (String) -> Any?, + importance: Importance + ): ValidationResult> { + return when (val value: Any? = source(key)) { + // No value present. + 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, which is + // incompatible with Array. + + // To handle this safely, treat as Array<*>, assert contents of the expected + // parcelable type, and return as a list. + + is Array<*> -> { + val invalid = value.filterNotNull().firstOrNull { !elementType.isInstance(it) } + when (invalid) { + // No invalid elements, result is ok. + null -> Valid(value.map { elementType.cast(it) }) + + // At least one incorrect element type found. + else -> + Invalid( + WrongElementType( + key, + importance, + actualType = invalid::class, + container = Array::class, + expectedType = elementType + ) + ) + } + } + + // The value is not an Array at all. + else -> + Invalid( + ValueIsWrongType( + key, + importance, + actualType = value::class, + allowedTypes = listOf(elementType) + ) + ) + } + } +} diff --git a/java/src/com/android/intentresolver/validation/types/SimpleValue.kt b/java/src/com/android/intentresolver/validation/types/SimpleValue.kt new file mode 100644 index 00000000..64299e11 --- /dev/null +++ b/java/src/com/android/intentresolver/validation/types/SimpleValue.kt @@ -0,0 +1,60 @@ +/* + * 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.types + +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 + +class SimpleValue( + override val key: String, + private val expected: KClass, +) : Validator { + + override fun validate(source: (String) -> Any?, importance: Importance): ValidationResult { + val value: Any? = source(key) + return when { + // The value is present and of the expected type. + expected.isInstance(value) -> return Valid(expected.cast(value)) + + // No value is present. + value == null -> + 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 -> + Invalid( + listOf( + ValueIsWrongType( + key, + importance, + actualType = value::class, + allowedTypes = listOf(expected) + ) + ) + ) + } + } +} diff --git a/java/src/com/android/intentresolver/validation/types/Validators.kt b/java/src/com/android/intentresolver/validation/types/Validators.kt new file mode 100644 index 00000000..1049f045 --- /dev/null +++ b/java/src/com/android/intentresolver/validation/types/Validators.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.types + +import com.android.intentresolver.validation.Validator + +inline fun value(key: String): Validator { + return SimpleValue(key, T::class) +} + +inline fun array(key: String): Validator> { + return ParceledArray(key, T::class) +} -- cgit v1.2.3-59-g8ed1b