summaryrefslogtreecommitdiff
path: root/java/src
diff options
context:
space:
mode:
author Xin Li <delphij@google.com> 2024-01-17 22:14:31 -0800
committer Xin Li <delphij@google.com> 2024-01-17 22:14:31 -0800
commitefee97bcc526928fb7168072e0305f5a72324fbc (patch)
tree7edfc23366f90cdca5852209a6ac207b7de884a4 /java/src
parent3e303554182e65402022ecd079d63b94ce80ffe4 (diff)
parent3007d9f481e92ed57ca9e3783719b3d84797ef2c (diff)
Merge Android 24Q1 Release (ab/11220357)
Bug: 319669529 Merged-In: I95e383e2822917198425acf9ba8bfbea76fdf948 Change-Id: Ibd7bfe1c21d32e1d0cc3023971afb779ed14c3a9
Diffstat (limited to 'java/src')
-rw-r--r--java/src/com/android/intentresolver/AnnotatedUserHandles.java18
-rw-r--r--java/src/com/android/intentresolver/ChooserActionFactory.java43
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java270
-rw-r--r--java/src/com/android/intentresolver/ChooserGridLayoutManager.java2
-rw-r--r--java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java6
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java249
-rw-r--r--java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java37
-rw-r--r--java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java6
-rw-r--r--java/src/com/android/intentresolver/ChooserRefinementManager.java15
-rw-r--r--java/src/com/android/intentresolver/ChooserRequestParameters.java17
-rw-r--r--java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java2
-rw-r--r--java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java4
-rw-r--r--java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java235
-rw-r--r--java/src/com/android/intentresolver/IntentForwarderActivity.java5
-rw-r--r--java/src/com/android/intentresolver/MainApplication.kt (renamed from java/src/com/android/intentresolver/flags/FeatureFlagRepository.kt)13
-rw-r--r--java/src/com/android/intentresolver/MultiProfilePagerAdapter.java (renamed from java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java)457
-rw-r--r--java/src/com/android/intentresolver/ResolvedComponentInfo.java4
-rw-r--r--java/src/com/android/intentresolver/ResolverActivity.java231
-rw-r--r--java/src/com/android/intentresolver/ResolverInfoHelpers.kt34
-rw-r--r--java/src/com/android/intentresolver/ResolverListAdapter.java227
-rw-r--r--java/src/com/android/intentresolver/ResolverListController.java7
-rw-r--r--java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java22
-rw-r--r--java/src/com/android/intentresolver/ResolverViewPager.java2
-rw-r--r--java/src/com/android/intentresolver/ShortcutSelectionLogic.java3
-rw-r--r--java/src/com/android/intentresolver/SimpleIconFactory.java19
-rw-r--r--java/src/com/android/intentresolver/TargetPresentationGetter.java5
-rw-r--r--java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java2
-rw-r--r--java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java41
-rw-r--r--java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java19
-rw-r--r--java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java3
-rw-r--r--java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java3
-rw-r--r--java/src/com/android/intentresolver/chooser/TargetInfo.java19
-rw-r--r--java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt3
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java36
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java2
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java39
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java22
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java54
-rw-r--r--java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt45
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ImageLoader.kt4
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt6
-rw-r--r--java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt6
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt74
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt15
-rw-r--r--java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java54
-rw-r--r--java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java31
-rw-r--r--java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.java46
-rw-r--r--java/src/com/android/intentresolver/emptystate/CrossProfileIntentsChecker.java59
-rw-r--r--java/src/com/android/intentresolver/emptystate/EmptyState.java78
-rw-r--r--java/src/com/android/intentresolver/emptystate/EmptyStateProvider.java37
-rw-r--r--java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java63
-rw-r--r--java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java (renamed from java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java)29
-rw-r--r--java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java (renamed from java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java)23
-rw-r--r--java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java (renamed from java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java)16
-rw-r--r--java/src/com/android/intentresolver/flags/DeviceConfigProxy.kt33
-rw-r--r--java/src/com/android/intentresolver/flags/Flags.kt30
-rw-r--r--java/src/com/android/intentresolver/grid/ChooserGridAdapter.java39
-rw-r--r--java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt13
-rw-r--r--java/src/com/android/intentresolver/icons/LabelInfo.kt19
-rw-r--r--java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java2
-rw-r--r--java/src/com/android/intentresolver/icons/LoadLabelTask.java39
-rw-r--r--java/src/com/android/intentresolver/icons/TargetDataLoader.kt10
-rw-r--r--java/src/com/android/intentresolver/inject/ActivityModule.kt46
-rw-r--r--java/src/com/android/intentresolver/inject/ConcurrencyModule.kt43
-rw-r--r--java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt15
-rw-r--r--java/src/com/android/intentresolver/inject/FrameworkModule.kt76
-rw-r--r--java/src/com/android/intentresolver/inject/Qualifiers.kt39
-rw-r--r--java/src/com/android/intentresolver/inject/SingletonModule.kt22
-rw-r--r--java/src/com/android/intentresolver/logging/EventLog.kt74
-rw-r--r--java/src/com/android/intentresolver/logging/EventLogImpl.java (renamed from java/src/com/android/intentresolver/logging/EventLog.java)169
-rw-r--r--java/src/com/android/intentresolver/logging/EventLogModule.kt46
-rw-r--r--java/src/com/android/intentresolver/logging/FrameworkStatsLogger.kt75
-rw-r--r--java/src/com/android/intentresolver/model/AbstractResolverComparator.java15
-rw-r--r--java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java67
-rw-r--r--java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java16
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ScopedAppTargetListCallback.kt58
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt24
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java5
-rw-r--r--java/src/com/android/intentresolver/v2/ActivityLogic.kt156
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserActionFactory.java395
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserActivity.java1845
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt87
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java227
-rw-r--r--java/src/com/android/intentresolver/v2/ChooserSelector.kt36
-rw-r--r--java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java666
-rw-r--r--java/src/com/android/intentresolver/v2/ResolverActivity.java2181
-rw-r--r--java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt81
-rw-r--r--java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java131
-rw-r--r--java/src/com/android/intentresolver/v2/data/BroadcastFlow.kt46
-rw-r--r--java/src/com/android/intentresolver/v2/data/model/User.kt50
-rw-r--r--java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt68
-rw-r--r--java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt29
-rw-r--r--java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt261
-rw-r--r--java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt34
-rw-r--r--java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt46
-rw-r--r--java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java141
-rw-r--r--java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java157
-rw-r--r--java/src/com/android/intentresolver/v2/emptystate/NoCrossProfileEmptyStateProvider.java138
-rw-r--r--java/src/com/android/intentresolver/v2/emptystate/WorkProfilePausedEmptyStateProvider.java116
-rw-r--r--java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt40
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt39
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt70
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt77
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/ListController.kt21
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt34
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt39
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt69
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt121
-rw-r--r--java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt108
-rw-r--r--java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt35
-rw-r--r--java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt32
-rw-r--r--java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt30
-rw-r--r--java/src/com/android/intentresolver/v2/platform/SecureSettings.kt25
-rw-r--r--java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt14
-rw-r--r--java/src/com/android/intentresolver/v2/ui/ActionTitle.java89
-rw-r--r--java/src/com/android/intentresolver/v2/util/MutableLazy.kt36
-rw-r--r--java/src/com/android/intentresolver/v2/validation/Findings.kt113
-rw-r--r--java/src/com/android/intentresolver/v2/validation/Validation.kt129
-rw-r--r--java/src/com/android/intentresolver/v2/validation/ValidationResult.kt39
-rw-r--r--java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt59
-rw-r--r--java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt83
-rw-r--r--java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt54
-rw-r--r--java/src/com/android/intentresolver/v2/validation/types/Validators.kt45
-rw-r--r--java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt90
-rw-r--r--java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java101
-rw-r--r--java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt137
126 files changed, 10870 insertions, 1487 deletions
diff --git a/java/src/com/android/intentresolver/AnnotatedUserHandles.java b/java/src/com/android/intentresolver/AnnotatedUserHandles.java
index 168f36d6..3565e757 100644
--- a/java/src/com/android/intentresolver/AnnotatedUserHandles.java
+++ b/java/src/com/android/intentresolver/AnnotatedUserHandles.java
@@ -16,12 +16,12 @@
package com.android.intentresolver;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.os.UserHandle;
import android.os.UserManager;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
/**
@@ -35,7 +35,7 @@ public final class AnnotatedUserHandles {
/**
* The {@link UserHandle} that launched Sharesheet.
* TODO: I believe this would always be the handle corresponding to {@code userIdOfCallingApp}
- * except possibly if the caller used {@link Activity#startActivityAsUser()} to launch
+ * except possibly if the caller used {@link Activity#startActivityAsUser} to launch
* Sharesheet as a different user than they themselves were running as. Verify and document.
*/
public final UserHandle userHandleSharesheetLaunchedAs;
@@ -57,21 +57,21 @@ public final class AnnotatedUserHandles {
/**
* The {@link UserHandle} that owns the "work tab" in a tabbed share UI. This is (an arbitrary)
- * one of the "managed" profiles associated with {@link personalProfileUserHandle}.
+ * one of the "managed" profiles associated with {@link #personalProfileUserHandle}.
*/
@Nullable
public final UserHandle workProfileUserHandle;
/**
- * The {@link UserHandle} of the clone profile belonging to {@link personalProfileUserHandle}.
+ * The {@link UserHandle} of the clone profile belonging to {@link #personalProfileUserHandle}.
*/
@Nullable
public final UserHandle cloneProfileUserHandle;
/**
- * The "tab owner" user handle (i.e., either {@link personalProfileUserHandle} or
- * {@link workProfileUserHandle}) that either matches or owns the profile of the
- * {@link userHandleSharesheetLaunchedAs}.
+ * The "tab owner" user handle (i.e., either {@link #personalProfileUserHandle} or
+ * {@link #workProfileUserHandle}) that either matches or owns the profile of the
+ * {@link #userHandleSharesheetLaunchedAs}.
*
* In the current implementation, we can assert that this is the same as
* `userHandleSharesheetLaunchedAs` except when the latter is the clone profile; then this is
@@ -105,7 +105,7 @@ public final class AnnotatedUserHandles {
.build();
}
- @VisibleForTesting static Builder newBuilder() {
+ @VisibleForTesting public static Builder newBuilder() {
return new Builder();
}
@@ -173,7 +173,7 @@ public final class AnnotatedUserHandles {
}
@VisibleForTesting
- static class Builder {
+ public static class Builder {
private int mUserIdOfCallingApp;
private UserHandle mUserHandleSharesheetLaunchedAs;
private UserHandle mPersonalProfileUserHandle;
diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java
index a54e8c62..310fcc27 100644
--- a/java/src/com/android/intentresolver/ChooserActionFactory.java
+++ b/java/src/com/android/intentresolver/ChooserActionFactory.java
@@ -16,7 +16,6 @@
package com.android.intentresolver;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityOptions;
import android.app.PendingIntent;
@@ -34,6 +33,8 @@ import android.text.TextUtils;
import android.util.Log;
import android.view.View;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
@@ -98,12 +99,11 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
private final @Nullable ChooserAction mModifyShareAction;
private final Consumer<Boolean> mExcludeSharedTextAction;
private final Consumer</* @Nullable */ Integer> mFinishCallback;
- private final EventLog mLogger;
+ private final EventLog mLog;
/**
* @param context
* @param chooserRequest data about the invocation of the current Sharesheet session.
- * @param integratedDeviceComponents info about other components that are available on this
* device to implement the supported action types.
* @param onUpdateSharedTextIsExcluded a delegate to be invoked when the "exclude shared text"
* setting is updated. The argument is whether the shared text is to be excluded.
@@ -117,7 +117,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
Context context,
ChooserRequestParameters chooserRequest,
ChooserIntegratedDeviceComponents integratedDeviceComponents,
- EventLog logger,
+ EventLog log,
Consumer<Boolean> onUpdateSharedTextIsExcluded,
Callable</* @Nullable */ View> firstVisibleImageQuery,
ActionActivityStarter activityStarter,
@@ -129,7 +129,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
chooserRequest.getTargetIntent(),
chooserRequest.getReferrerPackageName(),
finishCallback,
- logger),
+ log),
makeEditButtonRunnable(
getEditSharingTarget(
context,
@@ -137,11 +137,11 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
integratedDeviceComponents),
firstVisibleImageQuery,
activityStarter,
- logger),
+ log),
chooserRequest.getChooserActions(),
chooserRequest.getModifyShareAction(),
onUpdateSharedTextIsExcluded,
- logger,
+ log,
finishCallback);
}
@@ -153,7 +153,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
List<ChooserAction> customActions,
@Nullable ChooserAction modifyShareAction,
Consumer<Boolean> onUpdateSharedTextIsExcluded,
- EventLog logger,
+ EventLog log,
Consumer</* @Nullable */ Integer> finishCallback) {
mContext = context;
mCopyButtonRunnable = copyButtonRunnable;
@@ -161,7 +161,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
mCustomActions = ImmutableList.copyOf(customActions);
mModifyShareAction = modifyShareAction;
mExcludeSharedTextAction = onUpdateSharedTextIsExcluded;
- mLogger = logger;
+ mLog = log;
mFinishCallback = finishCallback;
}
@@ -188,7 +188,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
mCustomActions.get(i),
mFinishCallback,
() -> {
- mLogger.logCustomActionSelected(position);
+ mLog.logCustomActionSelected(position);
}
);
if (actionRow != null) {
@@ -209,7 +209,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
mModifyShareAction,
mFinishCallback,
() -> {
- mLogger.logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE);
+ mLog.logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE);
});
}
@@ -233,13 +233,13 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
Intent targetIntent,
String referrerPackageName,
Consumer<Integer> finishCallback,
- EventLog logger) {
+ EventLog log) {
final ClipData clipData;
try {
clipData = extractTextToCopy(targetIntent);
} catch (Throwable t) {
Log.e(TAG, "Failed to extract data to copy", t);
- return null;
+ return null;
}
if (clipData == null) {
return null;
@@ -249,7 +249,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
Context.CLIPBOARD_SERVICE);
clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName);
- logger.logActionSelected(EventLog.SELECTION_TYPE_COPY);
+ log.logActionSelected(EventLog.SELECTION_TYPE_COPY);
finishCallback.accept(Activity.RESULT_OK);
};
}
@@ -317,8 +317,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
ri,
context.getString(R.string.screenshot_edit),
"",
- resolveIntent,
- null);
+ resolveIntent);
dri.getDisplayIconHolder().setDisplayIcon(
context.getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));
return dri;
@@ -328,10 +327,10 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
TargetInfo editSharingTarget,
Callable</* @Nullable */ View> firstVisibleImageQuery,
ActionActivityStarter activityStarter,
- EventLog logger) {
+ EventLog log) {
return () -> {
// Log share completion via edit.
- logger.logActionSelected(EventLog.SELECTION_TYPE_EDIT);
+ log.logActionSelected(EventLog.SELECTION_TYPE_EDIT);
View firstImageView = null;
try {
@@ -373,10 +372,10 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
null,
null,
ActivityOptions.makeCustomAnimation(
- context,
- R.anim.slide_in_right,
- R.anim.slide_out_left)
- .toBundle());
+ context,
+ R.anim.slide_in_right,
+ R.anim.slide_out_left)
+ .toBundle());
} catch (PendingIntent.CanceledException e) {
Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled");
}
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index b27f054e..9000ab3a 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -24,10 +24,10 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROS
import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
+import static androidx.lifecycle.LifecycleKt.getCoroutineScope;
+
import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
-import android.annotation.IntDef;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityOptions;
@@ -51,11 +51,9 @@ import android.database.Cursor;
import android.graphics.Insets;
import android.net.Uri;
import android.os.Bundle;
-import android.os.Environment;
import android.os.SystemClock;
import android.os.UserHandle;
import android.os.UserManager;
-import android.os.storage.StorageManager;
import android.service.chooser.ChooserTarget;
import android.util.Log;
import android.util.Slog;
@@ -67,15 +65,15 @@ import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.TextView;
+import androidx.annotation.IntDef;
import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.ViewPager;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
-import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
@@ -83,8 +81,10 @@ import com.android.intentresolver.contentpreview.BasePreviewViewModel;
import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl;
import com.android.intentresolver.contentpreview.PreviewViewModel;
-import com.android.intentresolver.flags.FeatureFlagRepository;
-import com.android.intentresolver.flags.FeatureFlagRepositoryFactory;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider;
+import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
import com.android.intentresolver.grid.ChooserGridAdapter;
import com.android.intentresolver.icons.DefaultTargetDataLoader;
import com.android.intentresolver.icons.TargetDataLoader;
@@ -100,7 +100,8 @@ import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import java.io.File;
+import dagger.hilt.android.AndroidEntryPoint;
+
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.Collator;
@@ -115,12 +116,15 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
+import javax.inject.Inject;
+
/**
* The Chooser Activity handles intent resolution specifically for sharing intents -
* for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}.
*
*/
-public class ChooserActivity extends ResolverActivity implements
+@AndroidEntryPoint(ResolverActivity.class)
+public class ChooserActivity extends Hilt_ChooserActivity implements
ResolverListAdapter.ResolverListCommunicator {
private static final String TAG = "ChooserActivity";
@@ -161,7 +165,7 @@ public class ChooserActivity extends ResolverActivity implements
private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1;
private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2;
- @IntDef(flag = false, prefix = { "TARGET_TYPE_" }, value = {
+ @IntDef({
TARGET_TYPE_DEFAULT,
TARGET_TYPE_CHOOSER_TARGET,
TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER,
@@ -170,6 +174,9 @@ public class ChooserActivity extends ResolverActivity implements
@Retention(RetentionPolicy.SOURCE)
public @interface ShareTargetType {}
+ @Inject public FeatureFlags mFeatureFlags;
+ @Inject public EventLog mEventLog;
+
private ChooserIntegratedDeviceComponents mIntegratedDeviceComponents;
/* TODO: this is `nullable` because we have to defer the assignment til onCreate(). We make the
@@ -183,13 +190,9 @@ public class ChooserActivity extends ResolverActivity implements
private ChooserRefinementManager mRefinementManager;
- private FeatureFlagRepository mFeatureFlagRepository;
private ChooserContentPreviewUi mChooserContentPreviewUi;
private boolean mShouldDisplayLandscape;
- // statsd logger wrapper
- protected EventLog mEventLog;
-
private long mChooserShownTime;
protected boolean mIsSuccessfullySelected;
@@ -229,31 +232,52 @@ public class ChooserActivity extends ResolverActivity implements
*/
private boolean mFinishWhenStopped = false;
- public ChooserActivity() {}
-
@Override
protected void onCreate(Bundle savedInstanceState) {
Tracer.INSTANCE.markLaunched();
final long intentReceivedTime = System.currentTimeMillis();
mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
- getEventLog().logSharesheetTriggered();
-
- mFeatureFlagRepository = createFeatureFlagRepository();
- mIntegratedDeviceComponents = getIntegratedDeviceComponents();
-
try {
mChooserRequest = new ChooserRequestParameters(
getIntent(),
getReferrerPackageName(),
- getReferrer(),
- mFeatureFlagRepository);
+ getReferrer());
} catch (IllegalArgumentException e) {
Log.e(TAG, "Caller provided invalid Chooser request parameters", e);
finish();
super_onCreate(null);
return;
}
+ mPinnedSharedPrefs = getPinnedSharedPrefs(this);
+ mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
+ mShouldDisplayLandscape =
+ shouldDisplayLandscape(getResources().getConfiguration().orientation);
+ setRetainInOnStop(mChooserRequest.shouldRetainInOnStop());
+
+ createProfileRecords(
+ new AppPredictorFactory(
+ this,
+ mChooserRequest.getSharedText(),
+ mChooserRequest.getTargetIntentFilter()),
+ mChooserRequest.getTargetIntentFilter());
+
+
+ super.onCreate(
+ savedInstanceState,
+ mChooserRequest.getTargetIntent(),
+ mChooserRequest.getAdditionalTargets(),
+ mChooserRequest.getTitle(),
+ mChooserRequest.getDefaultTitleResource(),
+ mChooserRequest.getInitialIntents(),
+ /* resolutionList= */ null,
+ /* supportsAlwaysUseOption= */ false,
+ new DefaultTargetDataLoader(this, getLifecycle(), false),
+ /* safeForwardingMode= */ true);
+
+ getEventLog().logSharesheetTriggered();
+
+ mIntegratedDeviceComponents = getIntegratedDeviceComponents();
mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class);
@@ -279,39 +303,21 @@ public class ChooserActivity extends ResolverActivity implements
new ViewModelProvider(this, createPreviewViewModelFactory())
.get(BasePreviewViewModel.class);
mChooserContentPreviewUi = new ChooserContentPreviewUi(
- getLifecycle(),
- previewViewModel.createOrReuseProvider(mChooserRequest),
+ getCoroutineScope(getLifecycle()),
+ previewViewModel.createOrReuseProvider(mChooserRequest.getTargetIntent()),
mChooserRequest.getTargetIntent(),
previewViewModel.createOrReuseImageLoader(),
createChooserActionFactory(),
mEnterTransitionAnimationDelegate,
new HeadlineGeneratorImpl(this));
- mPinnedSharedPrefs = getPinnedSharedPrefs(this);
-
- mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
- mShouldDisplayLandscape =
- shouldDisplayLandscape(getResources().getConfiguration().orientation);
- setRetainInOnStop(mChooserRequest.shouldRetainInOnStop());
-
- createProfileRecords(
- new AppPredictorFactory(
- getApplicationContext(),
- mChooserRequest.getSharedText(),
- mChooserRequest.getTargetIntentFilter()),
- mChooserRequest.getTargetIntentFilter());
-
- super.onCreate(
- savedInstanceState,
- mChooserRequest.getTargetIntent(),
- mChooserRequest.getAdditionalTargets(),
- mChooserRequest.getTitle(),
- mChooserRequest.getDefaultTitleResource(),
- mChooserRequest.getInitialIntents(),
- /* resolutionList= */ null,
- /* supportsAlwaysUseOption= */ false,
- new DefaultTargetDataLoader(this, getLifecycle(), false),
- /* safeForwardingMode= */ true);
+ updateStickyContentPreview();
+ if (shouldShowStickyContentPreview()
+ || mChooserMultiProfilePagerAdapter
+ .getCurrentRootAdapter().getSystemRowCount() != 0) {
+ getEventLog().logActionShareWithPreview(
+ mChooserContentPreviewUi.getPreferredContentPreview());
+ }
mChooserShownTime = System.currentTimeMillis();
final long systemCost = mChooserShownTime - intentReceivedTime;
@@ -358,19 +364,15 @@ public class ChooserActivity extends ResolverActivity implements
return R.style.Theme_DeviceDefault_Chooser;
}
- protected FeatureFlagRepository createFeatureFlagRepository() {
- return new FeatureFlagRepositoryFactory().create(getApplicationContext());
- }
-
private void createProfileRecords(
AppPredictorFactory factory, IntentFilter targetIntentFilter) {
- UserHandle mainUserHandle = getPersonalProfileUserHandle();
+ UserHandle mainUserHandle = getAnnotatedUserHandles().personalProfileUserHandle;
ProfileRecord record = createProfileRecord(mainUserHandle, targetIntentFilter, factory);
if (record.shortcutLoader == null) {
Tracer.INSTANCE.endLaunchToShortcutTrace();
}
- UserHandle workUserHandle = getWorkProfileUserHandle();
+ UserHandle workUserHandle = getAnnotatedUserHandles().workProfileUserHandle;
if (workUserHandle != null) {
createProfileRecord(workUserHandle, targetIntentFilter, factory);
}
@@ -382,7 +384,7 @@ public class ChooserActivity extends ResolverActivity implements
ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic()
? null
: createShortcutLoader(
- getApplicationContext(),
+ this,
appPredictor,
userHandle,
targetIntentFilter,
@@ -406,7 +408,7 @@ public class ChooserActivity extends ResolverActivity implements
Consumer<ShortcutLoader.Result> callback) {
return new ShortcutLoader(
context,
- getLifecycle(),
+ getCoroutineScope(getLifecycle()),
appPredictor,
userHandle,
targetIntentFilter,
@@ -414,23 +416,11 @@ public class ChooserActivity extends ResolverActivity implements
}
static SharedPreferences getPinnedSharedPrefs(Context context) {
- // The code below is because in the android:ui process, no one can hear you scream.
- // The package info in the context isn't initialized in the way it is for normal apps,
- // so the standard, name-based context.getSharedPreferences doesn't work. Instead, we
- // build the path manually below using the same policy that appears in ContextImpl.
- // This fails silently under the hood if there's a problem, so if we find ourselves in
- // the case where we don't have access to credential encrypted storage we just won't
- // have our pinned target info.
- final File prefsFile = new File(new File(
- Environment.getDataUserCePackageDirectory(StorageManager.UUID_PRIVATE_INTERNAL,
- context.getUserId(), context.getPackageName()),
- "shared_prefs"),
- PINNED_SHARED_PREFS_NAME + ".xml");
- return context.getSharedPreferences(prefsFile, MODE_PRIVATE);
+ return context.getSharedPreferences(PINNED_SHARED_PREFS_NAME, MODE_PRIVATE);
}
@Override
- protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter(
+ protected ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter(
Intent[] initialIntents,
List<ResolveInfo> rList,
boolean filterLastUsed,
@@ -475,9 +465,12 @@ public class ChooserActivity extends ResolverActivity implements
/* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK,
/* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER);
- return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(),
- noWorkToPersonalEmptyState, noPersonalToWorkEmptyState,
- createCrossProfileIntentsChecker(), getTabOwnerUserHandleForLaunch());
+ return new NoCrossProfileEmptyStateProvider(
+ getAnnotatedUserHandles().personalProfileUserHandle,
+ noWorkToPersonalEmptyState,
+ noPersonalToWorkEmptyState,
+ createCrossProfileIntentsChecker(),
+ getAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
}
private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile(
@@ -491,7 +484,7 @@ public class ChooserActivity extends ResolverActivity implements
initialIntents,
rList,
filterLastUsed,
- /* userHandle */ getPersonalProfileUserHandle(),
+ /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,
targetDataLoader);
return new ChooserMultiProfilePagerAdapter(
/* context */ this,
@@ -499,8 +492,9 @@ public class ChooserActivity extends ResolverActivity implements
createEmptyStateProvider(/* workProfileUserHandle= */ null),
/* workProfileQuietModeChecker= */ () -> false,
/* workProfileUserHandle= */ null,
- getCloneProfileUserHandle(),
- mMaxTargetsPerRow);
+ getAnnotatedUserHandles().cloneProfileUserHandle,
+ mMaxTargetsPerRow,
+ mFeatureFlags);
}
private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles(
@@ -515,7 +509,7 @@ public class ChooserActivity extends ResolverActivity implements
selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
rList,
filterLastUsed,
- /* userHandle */ getPersonalProfileUserHandle(),
+ /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,
targetDataLoader);
ChooserGridAdapter workAdapter = createChooserGridAdapter(
/* context */ this,
@@ -523,40 +517,30 @@ public class ChooserActivity extends ResolverActivity implements
selectedProfile == PROFILE_WORK ? initialIntents : null,
rList,
filterLastUsed,
- /* userHandle */ getWorkProfileUserHandle(),
+ /* userHandle */ getAnnotatedUserHandles().workProfileUserHandle,
targetDataLoader);
return new ChooserMultiProfilePagerAdapter(
/* context */ this,
personalAdapter,
workAdapter,
- createEmptyStateProvider(/* workProfileUserHandle= */ getWorkProfileUserHandle()),
+ createEmptyStateProvider(getAnnotatedUserHandles().workProfileUserHandle),
() -> mWorkProfileAvailability.isQuietModeEnabled(),
selectedProfile,
- getWorkProfileUserHandle(),
- getCloneProfileUserHandle(),
- mMaxTargetsPerRow);
+ getAnnotatedUserHandles().workProfileUserHandle,
+ getAnnotatedUserHandles().cloneProfileUserHandle,
+ mMaxTargetsPerRow,
+ mFeatureFlags);
}
private int findSelectedProfile() {
int selectedProfile = getSelectedProfileExtra();
if (selectedProfile == -1) {
- selectedProfile = getProfileForUser(getTabOwnerUserHandleForLaunch());
+ selectedProfile = getProfileForUser(
+ getAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
}
return selectedProfile;
}
- @Override
- protected boolean postRebuildList(boolean rebuildCompleted) {
- updateStickyContentPreview();
- if (shouldShowStickyContentPreview()
- || mChooserMultiProfilePagerAdapter
- .getCurrentRootAdapter().getSystemRowCount() != 0) {
- getEventLog().logActionShareWithPreview(
- mChooserContentPreviewUi.getPreferredContentPreview());
- }
- return postRebuildListInternal(rebuildCompleted);
- }
-
/**
* Check if the profile currently used is a work profile.
* @return true if it is work profile, false if it is parent profile (or no work profile is
@@ -621,7 +605,7 @@ public class ChooserActivity extends ResolverActivity implements
}
@Override
- public void onConfigurationChanged(Configuration newConfig) {
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
if (viewPager.isLayoutRtl()) {
@@ -686,7 +670,10 @@ public class ChooserActivity extends ResolverActivity implements
ViewGroup layout = mChooserContentPreviewUi.displayContentPreview(
getResources(),
getLayoutInflater(),
- parent);
+ parent,
+ mFeatureFlags.scrollablePreview()
+ ? findViewById(R.id.chooser_headline_row_container)
+ : null);
if (layout != null) {
adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
@@ -807,7 +794,9 @@ public class ChooserActivity extends ResolverActivity implements
@Override
public int getLayoutResource() {
- return R.layout.chooser_grid;
+ return mFeatureFlags.scrollablePreview()
+ ? R.layout.chooser_grid_scrollable_preview
+ : R.layout.chooser_grid;
}
@Override // ResolverListCommunicator
@@ -1030,7 +1019,7 @@ public class ChooserActivity extends ResolverActivity implements
mIsSuccessfullySelected = true;
}
- private void maybeRemoveSharedText(@androidx.annotation.NonNull TargetInfo targetInfo) {
+ private void maybeRemoveSharedText(@NonNull TargetInfo targetInfo) {
Intent targetIntent = targetInfo.getTargetIntent();
if (targetIntent == null) {
return;
@@ -1105,7 +1094,8 @@ public class ChooserActivity extends ResolverActivity implements
ProfileRecord record = getProfileRecord(userHandle);
// We cannot use APS service when clone profile is present as APS service cannot sort
// cross profile targets as of now.
- return (record == null || getCloneProfileUserHandle() != null) ? null : record.appPredictor;
+ return ((record == null) || (getAnnotatedUserHandles().cloneProfileUserHandle != null))
+ ? null : record.appPredictor;
}
/**
@@ -1130,9 +1120,6 @@ public class ChooserActivity extends ResolverActivity implements
}
protected EventLog getEventLog() {
- if (mEventLog == null) {
- mEventLog = new EventLog();
- }
return mEventLog;
}
@@ -1156,7 +1143,7 @@ public class ChooserActivity extends ResolverActivity implements
}
@Override
- boolean isComponentFiltered(ComponentName name) {
+ public boolean isComponentFiltered(ComponentName name) {
return mChooserRequest.getFilteredComponentNames().contains(name);
}
@@ -1184,7 +1171,7 @@ public class ChooserActivity extends ResolverActivity implements
createListController(userHandle),
userHandle,
getTargetIntent(),
- mChooserRequest,
+ mChooserRequest.getReferrerFillInIntent(),
mMaxTargetsPerRow,
targetDataLoader);
@@ -1229,7 +1216,8 @@ public class ChooserActivity extends ResolverActivity implements
},
chooserListAdapter,
shouldShowContentPreview(),
- mMaxTargetsPerRow);
+ mMaxTargetsPerRow,
+ mFeatureFlags);
}
@VisibleForTesting
@@ -1242,12 +1230,12 @@ public class ChooserActivity extends ResolverActivity implements
ResolverListController resolverListController,
UserHandle userHandle,
Intent targetIntent,
- ChooserRequestParameters chooserRequest,
+ Intent referrerFillInIntent,
int maxTargetsPerRow,
TargetDataLoader targetDataLoader) {
UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()
- && userHandle.equals(getPersonalProfileUserHandle())
- ? getCloneProfileUserHandle() : userHandle;
+ && userHandle.equals(getAnnotatedUserHandles().personalProfileUserHandle)
+ ? getAnnotatedUserHandles().cloneProfileUserHandle : userHandle;
return new ChooserListAdapter(
context,
payloadIntents,
@@ -1257,18 +1245,19 @@ public class ChooserActivity extends ResolverActivity implements
createListController(userHandle),
userHandle,
targetIntent,
+ referrerFillInIntent,
this,
context.getPackageManager(),
getEventLog(),
- chooserRequest,
maxTargetsPerRow,
initialIntentsUserSpace,
- targetDataLoader);
+ targetDataLoader,
+ null);
}
@Override
protected void onWorkProfileStatusUpdated() {
- UserHandle workUser = getWorkProfileUserHandle();
+ UserHandle workUser = getAnnotatedUserHandles().workProfileUserHandle;
ProfileRecord record = workUser == null ? null : getProfileRecord(workUser);
if (record != null && record.shortcutLoader != null) {
record.shortcutLoader.reset();
@@ -1323,7 +1312,8 @@ public class ChooserActivity extends ResolverActivity implements
new ChooserActionFactory.ActionActivityStarter() {
@Override
public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) {
- safelyStartActivityAsUser(targetInfo, getPersonalProfileUserHandle());
+ safelyStartActivityAsUser(
+ targetInfo, getAnnotatedUserHandles().personalProfileUserHandle);
finish();
}
@@ -1333,11 +1323,12 @@ public class ChooserActivity extends ResolverActivity implements
ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
ChooserActivity.this, sharedElement, sharedElementName);
safelyStartActivityAsUser(
- targetInfo, getPersonalProfileUserHandle(), options.toBundle());
+ targetInfo,
+ getAnnotatedUserHandles().personalProfileUserHandle,
+ options.toBundle());
// Can't finish right away because the shared element transition may not
// be ready to start.
mFinishWhenStopped = true;
-
}
},
(status) -> {
@@ -1490,7 +1481,7 @@ public class ChooserActivity extends ResolverActivity implements
* Returns {@link #PROFILE_PERSONAL}, otherwise.
**/
private int getProfileForUser(UserHandle currentUserHandle) {
- if (currentUserHandle.equals(getWorkProfileUserHandle())) {
+ if (currentUserHandle.equals(getAnnotatedUserHandles().workProfileUserHandle)) {
return PROFILE_WORK;
}
// We return personal profile, as it is the default when there is no work profile, personal
@@ -1510,19 +1501,21 @@ public class ChooserActivity extends ResolverActivity implements
}
@Override
- public void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) {
+ protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) {
setupScrollListener();
maybeSetupGlobalLayoutListener();
ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter;
- if (chooserListAdapter.getUserHandle()
- .equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) {
+ UserHandle listProfileUserHandle = chooserListAdapter.getUserHandle();
+ if (listProfileUserHandle.equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) {
mChooserMultiProfilePagerAdapter.getActiveAdapterView()
.setAdapter(mChooserMultiProfilePagerAdapter.getCurrentRootAdapter());
mChooserMultiProfilePagerAdapter
.setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage());
}
+ //TODO: move this block inside ChooserListAdapter (should be called when
+ // ResolverListAdapter#mPostListReadyRunnable is executed.
if (chooserListAdapter.getDisplayResolveInfoCount() == 0) {
chooserListAdapter.notifyDataSetChanged();
} else {
@@ -1530,25 +1523,28 @@ public class ChooserActivity extends ResolverActivity implements
}
if (rebuildComplete) {
- long duration = Tracer.INSTANCE.endAppTargetLoadingSection(listAdapter.getUserHandle());
+ long duration = Tracer.INSTANCE.endAppTargetLoadingSection(listProfileUserHandle);
if (duration >= 0) {
Log.d(TAG, "app target loading time " + duration + " ms");
}
addCallerChooserTargets();
getEventLog().logSharesheetAppLoadComplete();
- maybeQueryAdditionalPostProcessingTargets(chooserListAdapter);
+ maybeQueryAdditionalPostProcessingTargets(
+ listProfileUserHandle,
+ chooserListAdapter.getDisplayResolveInfos());
mLatencyTracker.onActionEnd(ACTION_LOAD_SHARE_SHEET);
}
}
- private void maybeQueryAdditionalPostProcessingTargets(ChooserListAdapter chooserListAdapter) {
- UserHandle userHandle = chooserListAdapter.getUserHandle();
+ private void maybeQueryAdditionalPostProcessingTargets(
+ UserHandle userHandle,
+ DisplayResolveInfo[] displayResolveInfos) {
ProfileRecord record = getProfileRecord(userHandle);
if (record == null || record.shortcutLoader == null) {
return;
}
record.loadingStartTime = SystemClock.elapsedRealtime();
- record.shortcutLoader.updateAppTargets(chooserListAdapter.getDisplayResolveInfos());
+ record.shortcutLoader.updateAppTargets(displayResolveInfos);
}
@MainThread
@@ -1596,7 +1592,8 @@ public class ChooserActivity extends ResolverActivity implements
getResources().getDimensionPixelSize(R.dimen.chooser_header_scroll_elevation);
mChooserMultiProfilePagerAdapter.getActiveAdapterView().addOnScrollListener(
new RecyclerView.OnScrollListener() {
- public void onScrollStateChanged(RecyclerView view, int scrollState) {
+ @Override
+ public void onScrollStateChanged(@NonNull RecyclerView view, int scrollState) {
if (scrollState == RecyclerView.SCROLL_STATE_IDLE) {
if (mScrollStatus == SCROLL_STATUS_SCROLLING_VERTICAL) {
mScrollStatus = SCROLL_STATUS_IDLE;
@@ -1610,7 +1607,8 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- public void onScrolled(RecyclerView view, int dx, int dy) {
+ @Override
+ public void onScrolled(@NonNull RecyclerView view, int dx, int dy) {
if (view.getChildCount() > 0) {
View child = view.getLayoutManager().findViewByPosition(0);
if (child == null || child.getTop() < 0) {
@@ -1656,11 +1654,13 @@ public class ChooserActivity extends ResolverActivity implements
}
private boolean shouldShowStickyContentPreviewNoOrientationCheck() {
- return shouldShowTabs()
- && (mMultiProfilePagerAdapter.getListAdapterForUserHandle(
- UserHandle.of(UserHandle.myUserId())).getCount() > 0
- || shouldShowContentPreviewWhenEmpty())
- && shouldShowContentPreview();
+ if (!shouldShowContentPreview()) {
+ return false;
+ }
+ boolean isEmpty = mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+ UserHandle.of(UserHandle.myUserId())).getCount() == 0;
+ return (mFeatureFlags.scrollablePreview() || shouldShowTabs())
+ && (!isEmpty || shouldShowContentPreviewWhenEmpty());
}
/**
diff --git a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
index 5f373525..aaa7554c 100644
--- a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
+++ b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
@@ -70,7 +70,7 @@ public class ChooserGridLayoutManager extends GridLayoutManager {
return super.getRowCountForAccessibility(recycler, state) - 1;
}
- void setVerticalScrollEnabled(boolean verticalScrollEnabled) {
+ public void setVerticalScrollEnabled(boolean verticalScrollEnabled) {
mVerticalScrollEnabled = verticalScrollEnabled;
}
diff --git a/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
index 5fbf03a0..7cd86bf4 100644
--- a/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
+++ b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
@@ -16,12 +16,13 @@
package com.android.intentresolver;
-import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.provider.Settings;
import android.text.TextUtils;
+import androidx.annotation.Nullable;
+
import com.android.internal.annotations.VisibleForTesting;
/**
@@ -50,7 +51,8 @@ public class ChooserIntegratedDeviceComponents {
@VisibleForTesting
ChooserIntegratedDeviceComponents(
- ComponentName editSharingComponent, ComponentName nearbySharingComponent) {
+ @Nullable ComponentName editSharingComponent,
+ @Nullable ComponentName nearbySharingComponent) {
mEditSharingComponent = editSharingComponent;
mNearbySharingComponent = nearbySharingComponent;
}
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java
index e6d6dbf4..876ad5c3 100644
--- a/java/src/com/android/intentresolver/ChooserListAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -19,7 +19,6 @@ package com.android.intentresolver;
import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
-import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.prediction.AppTarget;
import android.content.ComponentName;
@@ -38,11 +37,16 @@ import android.os.UserManager;
import android.provider.DeviceConfig;
import android.service.chooser.ChooserTarget;
import android.text.Layout;
+import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.NotSelectableTargetInfo;
@@ -57,10 +61,23 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Set;
+import java.util.concurrent.Executor;
import java.util.stream.Collectors;
public class ChooserListAdapter extends ResolverListAdapter {
+
+ /**
+ * Delegate interface for injecting a chooser-specific operation to be performed before handling
+ * a package-change event. This allows the "driver" invoking the package-change to be generic,
+ * with no knowledge specific to the chooser implementation.
+ */
+ public interface PackageChangeCallback {
+ /** Perform any steps necessary before processing the package-change event. */
+ void beforeHandlingPackagesChanged();
+ }
+
private static final String TAG = "ChooserListAdapter";
private static final boolean DEBUG = false;
@@ -78,13 +95,17 @@ public class ChooserListAdapter extends ResolverListAdapter {
/** {@link #getBaseScore} */
public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f;
- private final ChooserRequestParameters mChooserRequest;
+ private final Intent mReferrerFillInIntent;
+
private final int mMaxRankedTargets;
private final EventLog mEventLog;
private final Set<TargetInfo> mRequestedIcons = new HashSet<>();
+ @Nullable
+ private final PackageChangeCallback mPackageChangeCallback;
+
// Reserve spots for incoming direct share targets by adding placeholders
private final TargetInfo mPlaceHolderTargetInfo;
private final TargetDataLoader mTargetDataLoader;
@@ -94,7 +115,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
private final ShortcutSelectionLogic mShortcutSelectionLogic;
// Sorted list of DisplayResolveInfos for the alphabetical app section.
- private List<DisplayResolveInfo> mSortedList = new ArrayList<>();
+ private final List<DisplayResolveInfo> mSortedList = new ArrayList<>();
private final ItemRevealAnimationTracker mAnimationTracker = new ItemRevealAnimationTracker();
@@ -138,13 +159,55 @@ public class ChooserListAdapter extends ResolverListAdapter {
ResolverListController resolverListController,
UserHandle userHandle,
Intent targetIntent,
+ Intent referrerFillInIntent,
+ ResolverListCommunicator resolverListCommunicator,
+ PackageManager packageManager,
+ EventLog eventLog,
+ int maxRankedTargets,
+ UserHandle initialIntentsUserSpace,
+ TargetDataLoader targetDataLoader,
+ @Nullable PackageChangeCallback packageChangeCallback) {
+ this(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ referrerFillInIntent,
+ resolverListCommunicator,
+ packageManager,
+ eventLog,
+ maxRankedTargets,
+ initialIntentsUserSpace,
+ targetDataLoader,
+ packageChangeCallback,
+ AsyncTask.SERIAL_EXECUTOR,
+ context.getMainExecutor());
+ }
+
+ @VisibleForTesting
+ public ChooserListAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ ResolverListController resolverListController,
+ UserHandle userHandle,
+ Intent targetIntent,
+ Intent referrerFillInIntent,
ResolverListCommunicator resolverListCommunicator,
PackageManager packageManager,
EventLog eventLog,
- ChooserRequestParameters chooserRequest,
int maxRankedTargets,
UserHandle initialIntentsUserSpace,
- TargetDataLoader targetDataLoader) {
+ TargetDataLoader targetDataLoader,
+ @Nullable PackageChangeCallback packageChangeCallback,
+ Executor bgExecutor,
+ Executor mainExecutor) {
// Don't send the initial intents through the shared ResolverActivity path,
// we want to separate them into a different section.
super(
@@ -158,13 +221,16 @@ public class ChooserListAdapter extends ResolverListAdapter {
targetIntent,
resolverListCommunicator,
initialIntentsUserSpace,
- targetDataLoader);
+ targetDataLoader,
+ bgExecutor,
+ mainExecutor);
- mChooserRequest = chooserRequest;
mMaxRankedTargets = maxRankedTargets;
+ mReferrerFillInIntent = referrerFillInIntent;
mPlaceHolderTargetInfo = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context);
mTargetDataLoader = targetDataLoader;
+ mPackageChangeCallback = packageChangeCallback;
createPlaceHolders();
mEventLog = eventLog;
mShortcutSelectionLogic = new ShortcutSelectionLogic(
@@ -227,9 +293,8 @@ public class ChooserListAdapter extends ResolverListAdapter {
ri.icon = 0;
}
ri.userHandle = initialIntentsUserSpace;
- // TODO: remove DisplayResolveInfo dependency on presentation getter
- DisplayResolveInfo displayResolveInfo = DisplayResolveInfo.newDisplayResolveInfo(
- ii, ri, ii, mTargetDataLoader.createPresentationGetter(ri));
+ DisplayResolveInfo displayResolveInfo =
+ DisplayResolveInfo.newDisplayResolveInfo(ii, ri, ii);
mCallerTargets.add(displayResolveInfo);
if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break;
}
@@ -238,6 +303,9 @@ public class ChooserListAdapter extends ResolverListAdapter {
@Override
public void handlePackagesChanged() {
+ if (mPackageChangeCallback != null) {
+ mPackageChangeCallback.beforeHandlingPackagesChanged();
+ }
if (DEBUG) {
Log.d(TAG, "clearing queryTargets on package change");
}
@@ -247,7 +315,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
@Override
- protected boolean rebuildList(boolean doPostProcessing) {
+ public boolean rebuildList(boolean doPostProcessing) {
mAnimationTracker.reset();
mSortedList.clear();
boolean result = super.rebuildList(doPostProcessing);
@@ -272,75 +340,77 @@ public class ChooserListAdapter extends ResolverListAdapter {
public void onBindView(View view, TargetInfo info, int position) {
final ViewHolder holder = (ViewHolder) view.getTag();
+ holder.reset();
+ // Always remove the spacing listener, attach as needed to direct share targets below.
+ holder.text.removeOnLayoutChangeListener(mPinTextSpacingListener);
+
if (info == null) {
holder.icon.setImageDrawable(loadIconPlaceholder());
return;
}
- holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo());
- mAnimationTracker.animateLabel(holder.text, info);
- if (holder.text2.getVisibility() == View.VISIBLE) {
+ final CharSequence displayLabel = Objects.requireNonNullElse(info.getDisplayLabel(), "");
+ final CharSequence extendedInfo = Objects.requireNonNullElse(info.getExtendedInfo(), "");
+ holder.bindLabel(displayLabel, extendedInfo);
+ if (!TextUtils.isEmpty(displayLabel)) {
+ mAnimationTracker.animateLabel(holder.text, info);
+ }
+ if (!TextUtils.isEmpty(extendedInfo) && holder.text2.getVisibility() == View.VISIBLE) {
mAnimationTracker.animateLabel(holder.text2, info);
}
+
holder.bindIcon(info);
- if (info.getDisplayIconHolder().getDisplayIcon() != null) {
+ if (info.hasDisplayIcon()) {
mAnimationTracker.animateIcon(holder.icon, info);
- } else {
- holder.icon.clearAnimation();
}
if (info.isSelectableTargetInfo()) {
// direct share targets should append the application name for a better readout
DisplayResolveInfo rInfo = info.getDisplayResolveInfo();
- CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
- CharSequence extendedInfo = info.getExtendedInfo();
- String contentDescription = String.join(" ", info.getDisplayLabel(),
- extendedInfo != null ? extendedInfo : "", appName);
+ CharSequence appName =
+ Objects.requireNonNullElse(rInfo == null ? null : rInfo.getDisplayLabel(), "");
+ String contentDescription =
+ String.join(" ", info.getDisplayLabel(), extendedInfo, appName);
+ if (info.isPinned()) {
+ contentDescription = String.join(
+ ". ",
+ contentDescription,
+ mContext.getResources().getString(R.string.pinned));
+ }
holder.updateContentDescription(contentDescription);
if (!info.hasDisplayIcon()) {
loadDirectShareIcon((SelectableTargetInfo) info);
}
} else if (info.isDisplayResolveInfo()) {
+ if (info.isPinned()) {
+ holder.updateContentDescription(String.join(
+ ". ",
+ info.getDisplayLabel(),
+ mContext.getResources().getString(R.string.pinned)));
+ }
DisplayResolveInfo dri = (DisplayResolveInfo) info;
if (!dri.hasDisplayIcon()) {
loadIcon(dri);
}
+ if (!dri.hasDisplayLabel()) {
+ loadLabel(dri);
+ }
}
- // If target is loading, show a special placeholder shape in the label, make unclickable
if (info.isPlaceHolderTargetInfo()) {
- final int maxWidth = mContext.getResources().getDimensionPixelSize(
- R.dimen.chooser_direct_share_label_placeholder_max_width);
- holder.text.setMaxWidth(maxWidth);
- holder.text.setBackground(mContext.getResources().getDrawable(
- R.drawable.chooser_direct_share_label_placeholder, mContext.getTheme()));
- // Prevent rippling by removing background containing ripple
- holder.itemView.setBackground(null);
- } else {
- holder.text.setMaxWidth(Integer.MAX_VALUE);
- holder.text.setBackground(null);
- holder.itemView.setBackground(holder.defaultItemViewBackground);
+ holder.bindPlaceholder();
}
- // Always remove the spacing listener, attach as needed to direct share targets below.
- holder.text.removeOnLayoutChangeListener(mPinTextSpacingListener);
-
if (info.isMultiDisplayResolveInfo()) {
// If the target is grouped show an indicator
- Drawable bkg = mContext.getDrawable(R.drawable.chooser_group_background);
- holder.text.setPaddingRelative(0, 0, bkg.getIntrinsicWidth() /* end */, 0);
- holder.text.setBackground(bkg);
+ holder.bindGroupIndicator(
+ mContext.getDrawable(R.drawable.chooser_group_background));
} else if (info.isPinned() && (getPositionTargetType(position) == TARGET_STANDARD
|| getPositionTargetType(position) == TARGET_SERVICE)) {
// If the appShare or directShare target is pinned and in the suggested row show a
// pinned indicator
- Drawable bkg = mContext.getDrawable(R.drawable.chooser_pinned_background);
- holder.text.setPaddingRelative(bkg.getIntrinsicWidth() /* start */, 0, 0, 0);
- holder.text.setBackground(bkg);
+ holder.bindPinnedIndicator(mContext.getDrawable(R.drawable.chooser_pinned_background));
holder.text.addOnLayoutChangeListener(mPinTextSpacingListener);
- } else {
- holder.text.setBackground(null);
- holder.text.setPaddingRelative(0, 0, 0, 0);
}
}
@@ -360,9 +430,13 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
}
- void updateAlphabeticalList() {
- // TODO: this procedure seems like it should be relatively lightweight. Why does it need to
- // run in an `AsyncTask`?
+ public void updateAlphabeticalList() {
+ final ChooserActivity.AzInfoComparator comparator =
+ new ChooserActivity.AzInfoComparator(mContext);
+ final List<DisplayResolveInfo> allTargets = new ArrayList<>();
+ allTargets.addAll(getTargetsInCurrentDisplayList());
+ allTargets.addAll(mCallerTargets);
+
new AsyncTask<Void, Void, List<DisplayResolveInfo>>() {
@Override
protected List<DisplayResolveInfo> doInBackground(Void... voids) {
@@ -375,32 +449,39 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
private List<DisplayResolveInfo> updateList() {
- List<DisplayResolveInfo> allTargets = new ArrayList<>();
- allTargets.addAll(getTargetsInCurrentDisplayList());
- allTargets.addAll(mCallerTargets);
+ loadMissingLabels(allTargets);
// Consolidate multiple targets from same app.
return allTargets
.stream()
.collect(Collectors.groupingBy(target ->
target.getResolvedComponentName().getPackageName()
- + "#" + target.getDisplayLabel()
- + '#' + target.getResolveInfo().userHandle.getIdentifier()
+ + "#" + target.getDisplayLabel()
+ + '#' + target.getResolveInfo().userHandle.getIdentifier()
))
.values()
.stream()
.map(appTargets ->
(appTargets.size() == 1)
- ? appTargets.get(0)
- : MultiDisplayResolveInfo.newMultiDisplayResolveInfo(appTargets))
- .sorted(new ChooserActivity.AzInfoComparator(mContext))
+ ? appTargets.get(0)
+ : MultiDisplayResolveInfo.newMultiDisplayResolveInfo(
+ appTargets))
+ .sorted(comparator)
.collect(Collectors.toList());
}
+
@Override
protected void onPostExecute(List<DisplayResolveInfo> newList) {
- mSortedList = newList;
+ mSortedList.clear();
+ mSortedList.addAll(newList);
notifyDataSetChanged();
}
+
+ private void loadMissingLabels(List<DisplayResolveInfo> targets) {
+ for (DisplayResolveInfo target: targets) {
+ mTargetDataLoader.getOrLoadLabel(target);
+ }
+ }
}.execute();
}
@@ -438,8 +519,14 @@ public class ChooserListAdapter extends ResolverListAdapter {
return count;
}
+ private static boolean hasSendAction(Intent intent) {
+ String action = intent.getAction();
+ return Intent.ACTION_SEND.equals(action)
+ || Intent.ACTION_SEND_MULTIPLE.equals(action);
+ }
+
public int getServiceTargetCount() {
- if (mChooserRequest.isSendActionTarget() && !ActivityManager.isLowRamDeviceStatic()) {
+ if (hasSendAction(getTargetIntent()) && !ActivityManager.isLowRamDeviceStatic()) {
return Math.min(mServiceTargets.size(), mMaxRankedTargets);
}
@@ -553,7 +640,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {
// Checks if this info is already listed in callerTargets.
for (TargetInfo existingInfo : mCallerTargets) {
- if (mResolverListCommunicator.resolveInfoMatch(
+ if (ResolveInfoHelpers.resolveInfoMatch(
dri.getResolveInfo(), existingInfo.getResolveInfo())) {
return false;
}
@@ -594,8 +681,8 @@ public class ChooserListAdapter extends ResolverListAdapter {
directShareToShortcutInfos,
directShareToAppTargets,
mContext.createContextAsUser(getUserHandle(), 0),
- mChooserRequest.getTargetIntent(),
- mChooserRequest.getReferrerFillInIntent(),
+ getTargetIntent(),
+ mReferrerFillInIntent,
mMaxRankedTargets,
mServiceTargets);
if (isUpdated) {
@@ -644,29 +731,23 @@ public class ChooserListAdapter extends ResolverListAdapter {
* in the head of input list and fill the tail with other elements in undetermined order.
*/
@Override
- AsyncTask<List<ResolvedComponentInfo>,
- Void,
- List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) {
- return new AsyncTask<List<ResolvedComponentInfo>,
- Void,
- List<ResolvedComponentInfo>>() {
- @Override
- protected List<ResolvedComponentInfo> doInBackground(
- List<ResolvedComponentInfo>... params) {
- Trace.beginSection("ChooserListAdapter#SortingTask");
- mResolverListController.topK(params[0], mMaxRankedTargets);
- Trace.endSection();
- return params[0];
- }
- @Override
- protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
- processSortedList(sortedComponents, doPostProcessing);
- if (doPostProcessing) {
- mResolverListCommunicator.updateProfileViewButton();
- notifyDataSetChanged();
- }
- }
- };
+ @WorkerThread
+ protected void sortComponents(List<ResolvedComponentInfo> components) {
+ Trace.beginSection("ChooserListAdapter#SortingTask");
+ mResolverListController.topK(components, mMaxRankedTargets);
+ Trace.endSection();
}
+ @Override
+ @MainThread
+ protected void onComponentsSorted(
+ @Nullable List<ResolvedComponentInfo> sortedComponents, boolean doPostProcessing) {
+ processSortedList(sortedComponents, doPostProcessing);
+ if (doPostProcessing) {
+ mResolverListCommunicator.updateProfileViewButton();
+ //TODO: this method is different from super's only in that `notifyDataSetChanged` is
+ // called conditionally here; is it really important?
+ notifyDataSetChanged();
+ }
+ }
}
diff --git a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
index c159243e..080f9d24 100644
--- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
@@ -25,6 +25,7 @@ import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.viewpager.widget.PagerAdapter;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
import com.android.intentresolver.grid.ChooserGridAdapter;
import com.android.intentresolver.measurements.Tracer;
import com.android.internal.annotations.VisibleForTesting;
@@ -38,21 +39,22 @@ import java.util.function.Supplier;
* A {@link PagerAdapter} which describes the work and personal profile share sheet screens.
*/
@VisibleForTesting
-public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAdapter<
+public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<
RecyclerView, ChooserGridAdapter, ChooserListAdapter> {
private static final int SINGLE_CELL_SPAN_SIZE = 1;
private final ChooserProfileAdapterBinder mAdapterBinder;
private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier;
- ChooserMultiProfilePagerAdapter(
+ public ChooserMultiProfilePagerAdapter(
Context context,
ChooserGridAdapter adapter,
EmptyStateProvider emptyStateProvider,
Supplier<Boolean> workProfileQuietModeChecker,
UserHandle workProfileUserHandle,
UserHandle cloneProfileUserHandle,
- int maxTargetsPerRow) {
+ int maxTargetsPerRow,
+ FeatureFlags featureFlags) {
this(
context,
new ChooserProfileAdapterBinder(maxTargetsPerRow),
@@ -62,10 +64,11 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
/* defaultProfile= */ 0,
workProfileUserHandle,
cloneProfileUserHandle,
- new BottomPaddingOverrideSupplier(context));
+ new BottomPaddingOverrideSupplier(context),
+ featureFlags);
}
- ChooserMultiProfilePagerAdapter(
+ public ChooserMultiProfilePagerAdapter(
Context context,
ChooserGridAdapter personalAdapter,
ChooserGridAdapter workAdapter,
@@ -74,7 +77,8 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
@Profile int defaultProfile,
UserHandle workProfileUserHandle,
UserHandle cloneProfileUserHandle,
- int maxTargetsPerRow) {
+ int maxTargetsPerRow,
+ FeatureFlags featureFlags) {
this(
context,
new ChooserProfileAdapterBinder(maxTargetsPerRow),
@@ -84,7 +88,8 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
defaultProfile,
workProfileUserHandle,
cloneProfileUserHandle,
- new BottomPaddingOverrideSupplier(context));
+ new BottomPaddingOverrideSupplier(context),
+ featureFlags);
}
private ChooserMultiProfilePagerAdapter(
@@ -96,9 +101,9 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
@Profile int defaultProfile,
UserHandle workProfileUserHandle,
UserHandle cloneProfileUserHandle,
- BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {
+ BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier,
+ FeatureFlags featureFlags) {
super(
- context,
gridAdapter -> gridAdapter.getListAdapter(),
adapterBinder,
gridAdapters,
@@ -107,7 +112,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
defaultProfile,
workProfileUserHandle,
cloneProfileUserHandle,
- () -> makeProfileView(context),
+ () -> makeProfileView(context, featureFlags),
bottomPaddingOverrideSupplier);
mAdapterBinder = adapterBinder;
mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier;
@@ -131,10 +136,12 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
}
}
- private static ViewGroup makeProfileView(Context context) {
+ private static ViewGroup makeProfileView(
+ Context context, FeatureFlags featureFlags) {
LayoutInflater inflater = LayoutInflater.from(context);
- ViewGroup rootView = (ViewGroup) inflater.inflate(
- R.layout.chooser_list_per_profile, null, false);
+ ViewGroup rootView = featureFlags.scrollablePreview()
+ ? (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile_wrap, null, false)
+ : (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile, null, false);
RecyclerView recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list);
recyclerView.setAccessibilityDelegateCompat(
new ChooserRecyclerViewAccessibilityDelegate(recyclerView));
@@ -142,7 +149,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
}
@Override
- boolean rebuildActiveTab(boolean doPostProcessing) {
+ public boolean rebuildActiveTab(boolean doPostProcessing) {
if (doPostProcessing) {
Tracer.INSTANCE.beginAppTargetLoadingSection(getActiveListAdapter().getUserHandle());
}
@@ -150,7 +157,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
}
@Override
- boolean rebuildInactiveTab(boolean doPostProcessing) {
+ public boolean rebuildInactiveTab(boolean doPostProcessing) {
if (getItemCount() != 1 && doPostProcessing) {
Tracer.INSTANCE.beginAppTargetLoadingSection(getInactiveListAdapter().getUserHandle());
}
diff --git a/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
index 250b6827..d6688d90 100644
--- a/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
+++ b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
@@ -16,20 +16,20 @@
package com.android.intentresolver;
-import android.annotation.NonNull;
import android.graphics.Rect;
import android.view.View;
import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
+import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
-class ChooserRecyclerViewAccessibilityDelegate extends RecyclerViewAccessibilityDelegate {
+public class ChooserRecyclerViewAccessibilityDelegate extends RecyclerViewAccessibilityDelegate {
private final Rect mTempRect = new Rect();
private final int[] mConsumed = new int[2];
- ChooserRecyclerViewAccessibilityDelegate(RecyclerView recyclerView) {
+ public ChooserRecyclerViewAccessibilityDelegate(RecyclerView recyclerView) {
super(recyclerView);
}
diff --git a/java/src/com/android/intentresolver/ChooserRefinementManager.java b/java/src/com/android/intentresolver/ChooserRefinementManager.java
index 2ebe48a6..474b240f 100644
--- a/java/src/com/android/intentresolver/ChooserRefinementManager.java
+++ b/java/src/com/android/intentresolver/ChooserRefinementManager.java
@@ -16,8 +16,6 @@
package com.android.intentresolver;
-import android.annotation.Nullable;
-import android.annotation.UiThread;
import android.app.Activity;
import android.app.Application;
import android.content.Intent;
@@ -28,22 +26,30 @@ import android.os.Parcel;
import android.os.ResultReceiver;
import android.util.Log;
+import androidx.annotation.Nullable;
+import androidx.annotation.UiThread;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import com.android.intentresolver.chooser.TargetInfo;
+import dagger.hilt.android.lifecycle.HiltViewModel;
+
import java.util.List;
import java.util.function.Consumer;
+import javax.inject.Inject;
+
+
/**
* Helper class to manage Sharesheet's "refinement" flow, where callers supply a "refinement
* activity" that will be invoked when a target is selected, allowing the calling app to add
- * additional extras and other refinements (subject to {@link Intent#filterEquals()}), e.g., to
+ * additional extras and other refinements (subject to {@link Intent#filterEquals}), e.g., to
* convert the format of the payload, or lazy-download some data that was deferred in the original
* call).
*/
+@HiltViewModel
@UiThread
public final class ChooserRefinementManager extends ViewModel {
private static final String TAG = "ChooserRefinement";
@@ -88,6 +94,9 @@ public final class ChooserRefinementManager extends ViewModel {
private MutableLiveData<RefinementCompletion> mRefinementCompletion = new MutableLiveData<>();
+ @Inject
+ public ChooserRefinementManager() {}
+
public LiveData<RefinementCompletion> getRefinementCompletion() {
return mRefinementCompletion;
}
diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java
index 5157986b..7ad809e9 100644
--- a/java/src/com/android/intentresolver/ChooserRequestParameters.java
+++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java
@@ -16,8 +16,6 @@
package com.android.intentresolver;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
@@ -32,7 +30,9 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
-import com.android.intentresolver.flags.FeatureFlagRepository;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.util.UriFilters;
import com.google.common.collect.ImmutableList;
@@ -104,8 +104,7 @@ public class ChooserRequestParameters {
public ChooserRequestParameters(
final Intent clientIntent,
String referrerPackageName,
- final Uri referrer,
- FeatureFlagRepository featureFlags) {
+ final Uri referrer) {
final Intent requestedTarget = parseTargetIntentExtra(
clientIntent.getParcelableExtra(Intent.EXTRA_INTENT));
mTarget = intentWithModifiedLaunchFlags(requestedTarget);
@@ -212,7 +211,7 @@ public class ChooserRequestParameters {
/**
* TODO: this returns a nullable array for convenience, but if the legacy APIs can be
- * refactored, returning {@link mAdditionalTargets} directly is simpler and safer.
+ * refactored, returning {@link #mAdditionalTargets} directly is simpler and safer.
*/
@Nullable
public Intent[] getAdditionalTargets() {
@@ -226,7 +225,7 @@ public class ChooserRequestParameters {
/**
* TODO: this returns a nullable array for convenience, but if the legacy APIs can be
- * refactored, returning {@link mInitialIntents} directly is simpler and safer.
+ * refactored, returning {@link #mInitialIntents} directly is simpler and safer.
*/
@Nullable
public Intent[] getInitialIntents() {
@@ -288,7 +287,7 @@ public class ChooserRequestParameters {
* requested target <em>wasn't</em> a send action; otherwise it is null. The second value is
* the resource ID of a default title string; this is nonzero only if the first value is null.
*
- * TODO: change the API for how these are passed up to {@link ResolverActivity#onCreate()}, or
+ * TODO: change the API for how these are passed up to {@link ResolverActivity#onCreate}, or
* create a real type (not {@link Pair}) to express the semantics described in this comment.
*/
private static Pair<CharSequence, Integer> makeTitleSpec(
@@ -371,7 +370,7 @@ public class ChooserRequestParameters {
* the required type. If false, throw an {@link IllegalArgumentException} if the extra is
* non-null but can't be assigned to variables of type {@code T}.
* @param streamEmptyIfNull Whether to return an empty stream if the optional extra isn't
- * present in the intent (or if it had the wrong type, but {@link warnOnTypeError} is true).
+ * present in the intent (or if it had the wrong type, but <em>warnOnTypeError</em> is true).
* If false, return null in these cases, and only return an empty stream if the intent
* explicitly provided an empty array for the specified extra.
*/
diff --git a/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
index 2cfceeae..f0fcd149 100644
--- a/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
+++ b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
@@ -22,6 +22,7 @@ import android.content.pm.PackageManager;
import android.graphics.drawable.Drawable;
import android.os.UserHandle;
+import androidx.annotation.NonNull;
import androidx.fragment.app.FragmentManager;
import com.android.intentresolver.chooser.DisplayResolveInfo;
@@ -66,6 +67,7 @@ public class ChooserStackedAppDialogFragment extends ChooserTargetActionsDialogF
dismiss();
}
+ @NonNull
@Override
protected CharSequence getItemLabel(DisplayResolveInfo dri) {
final PackageManager pm = getContext().getPackageManager();
diff --git a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
index 4bfb21aa..b6b7de96 100644
--- a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
+++ b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
@@ -21,8 +21,6 @@ import static android.content.Context.ACTIVITY_SERVICE;
import static java.util.stream.Collectors.toList;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.Dialog;
import android.content.ComponentName;
@@ -46,6 +44,8 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentManager;
import androidx.recyclerview.widget.RecyclerView;
diff --git a/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java
deleted file mode 100644
index a1c53402..00000000
--- a/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java
+++ /dev/null
@@ -1,235 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver;
-
-import android.annotation.Nullable;
-import android.content.Context;
-import android.os.UserHandle;
-import android.util.Log;
-import android.view.View;
-import android.view.ViewGroup;
-
-import com.android.internal.annotations.VisibleForTesting;
-
-import com.google.common.collect.ImmutableList;
-
-import java.util.Optional;
-import java.util.function.Function;
-import java.util.function.Supplier;
-
-/**
- * Implementation of {@link AbstractMultiProfilePagerAdapter} that consolidates the variation in
- * existing implementations; most overrides were only to vary type signatures (which are better
- * represented via generic types), and a few minor behavioral customizations are now implemented
- * through small injectable delegate classes.
- * TODO: now that the existing implementations are shown to be expressible in terms of this new
- * generic type, merge up into the base class and simplify the public APIs.
- * TODO: attempt to further restrict visibility in the methods we expose.
- * TODO: deprecate and audit/fix usages of any methods that refer to the "active" or "inactive"
- * adapters; these were marked {@link VisibleForTesting} and their usage seems like an accident
- * waiting to happen since clients seem to make assumptions about which adapter will be "active" in
- * a particular context, and more explicit APIs would make sure those were valid.
- * TODO: consider renaming legacy methods (e.g. why do we know it's a "list", not just a "page"?)
- *
- * @param <PageViewT> the type of the widget that represents the contents of a page in this adapter
- * @param <SinglePageAdapterT> the type of a "root" adapter class to be instantiated and included in
- * the per-profile records.
- * @param <ListAdapterT> the concrete type of a {@link ResolverListAdapter} implementation to
- * control the contents of a given per-profile list. This is provided for convenience, since it must
- * be possible to get the list adapter from the page adapter via our {@link mListAdapterExtractor}.
- *
- * TODO: this class doesn't make any explicit usage of the {@link ResolverListAdapter} API, so the
- * type constraint can probably be dropped once the API is merged upwards and cleaned.
- */
-class GenericMultiProfilePagerAdapter<
- PageViewT extends ViewGroup,
- SinglePageAdapterT,
- ListAdapterT extends ResolverListAdapter> extends AbstractMultiProfilePagerAdapter {
-
- /** Delegate to set up a given adapter and page view to be used together. */
- public interface AdapterBinder<PageViewT, SinglePageAdapterT> {
- /**
- * The given {@code view} will be associated with the given {@code adapter}. Do any work
- * necessary to configure them compatibly, introduce them to each other, etc.
- */
- void bind(PageViewT view, SinglePageAdapterT adapter);
- }
-
- private final Function<SinglePageAdapterT, ListAdapterT> mListAdapterExtractor;
- private final AdapterBinder<PageViewT, SinglePageAdapterT> mAdapterBinder;
- private final Supplier<ViewGroup> mPageViewInflater;
- private final Supplier<Optional<Integer>> mContainerBottomPaddingOverrideSupplier;
-
- private final ImmutableList<GenericProfileDescriptor<PageViewT, SinglePageAdapterT>> mItems;
-
- GenericMultiProfilePagerAdapter(
- Context context,
- Function<SinglePageAdapterT, ListAdapterT> listAdapterExtractor,
- AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder,
- ImmutableList<SinglePageAdapterT> adapters,
- EmptyStateProvider emptyStateProvider,
- Supplier<Boolean> workProfileQuietModeChecker,
- @Profile int defaultProfile,
- UserHandle workProfileUserHandle,
- UserHandle cloneProfileUserHandle,
- Supplier<ViewGroup> pageViewInflater,
- Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
- super(
- context,
- /* currentPage= */ defaultProfile,
- emptyStateProvider,
- workProfileQuietModeChecker,
- workProfileUserHandle,
- cloneProfileUserHandle);
-
- mListAdapterExtractor = listAdapterExtractor;
- mAdapterBinder = adapterBinder;
- mPageViewInflater = pageViewInflater;
- mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier;
-
- ImmutableList.Builder<GenericProfileDescriptor<PageViewT, SinglePageAdapterT>> items =
- new ImmutableList.Builder<>();
- for (SinglePageAdapterT adapter : adapters) {
- items.add(createProfileDescriptor(adapter));
- }
- mItems = items.build();
- }
-
- private GenericProfileDescriptor<PageViewT, SinglePageAdapterT>
- createProfileDescriptor(SinglePageAdapterT adapter) {
- return new GenericProfileDescriptor<>(mPageViewInflater.get(), adapter);
- }
-
- @Override
- protected GenericProfileDescriptor<PageViewT, SinglePageAdapterT> getItem(int pageIndex) {
- return mItems.get(pageIndex);
- }
-
- @Override
- public int getItemCount() {
- return mItems.size();
- }
-
- public PageViewT getListViewForIndex(int index) {
- return getItem(index).mView;
- }
-
- @Override
- @VisibleForTesting
- public SinglePageAdapterT getAdapterForIndex(int index) {
- return getItem(index).mAdapter;
- }
-
- @Override
- protected void setupListAdapter(int pageIndex) {
- mAdapterBinder.bind(getListViewForIndex(pageIndex), getAdapterForIndex(pageIndex));
- }
-
- @Override
- public ViewGroup instantiateItem(ViewGroup container, int position) {
- setupListAdapter(position);
- return super.instantiateItem(container, position);
- }
-
- @Override
- @Nullable
- protected ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) {
- if (getPersonalListAdapter().getUserHandle().equals(userHandle)
- || userHandle.equals(getCloneUserHandle())) {
- return getPersonalListAdapter();
- } else if (getWorkListAdapter() != null
- && getWorkListAdapter().getUserHandle().equals(userHandle)) {
- return getWorkListAdapter();
- }
- return null;
- }
-
- @Override
- @VisibleForTesting
- public ListAdapterT getActiveListAdapter() {
- return mListAdapterExtractor.apply(getAdapterForIndex(getCurrentPage()));
- }
-
- @Override
- @VisibleForTesting
- public ListAdapterT getInactiveListAdapter() {
- if (getCount() < 2) {
- return null;
- }
- return mListAdapterExtractor.apply(getAdapterForIndex(1 - getCurrentPage()));
- }
-
- @Override
- public ListAdapterT getPersonalListAdapter() {
- return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_PERSONAL));
- }
-
- @Override
- public ListAdapterT getWorkListAdapter() {
- if (!hasAdapterForIndex(PROFILE_WORK)) {
- return null;
- }
- return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_WORK));
- }
-
- @Override
- protected SinglePageAdapterT getCurrentRootAdapter() {
- return getAdapterForIndex(getCurrentPage());
- }
-
- @Override
- protected PageViewT getActiveAdapterView() {
- return getListViewForIndex(getCurrentPage());
- }
-
- @Override
- protected PageViewT getInactiveAdapterView() {
- if (getCount() < 2) {
- return null;
- }
- return getListViewForIndex(1 - getCurrentPage());
- }
-
- @Override
- protected void setupContainerPadding(View container) {
- Optional<Integer> bottomPaddingOverride = mContainerBottomPaddingOverrideSupplier.get();
- bottomPaddingOverride.ifPresent(paddingBottom ->
- container.setPadding(
- container.getPaddingLeft(),
- container.getPaddingTop(),
- container.getPaddingRight(),
- paddingBottom));
- }
-
- private boolean hasAdapterForIndex(int pageIndex) {
- return (pageIndex < getCount());
- }
-
- // TODO: `ChooserActivity` also has a per-profile record type. Maybe the "multi-profile pager"
- // should be the owner of all per-profile data (especially now that the API is generic)?
- private static class GenericProfileDescriptor<PageViewT, SinglePageAdapterT> extends
- ProfileDescriptor {
- private final SinglePageAdapterT mAdapter;
- private final PageViewT mView;
-
- GenericProfileDescriptor(ViewGroup rootView, SinglePageAdapterT adapter) {
- super(rootView);
- mAdapter = adapter;
- mView = (PageViewT) rootView.findViewById(com.android.internal.R.id.resolver_list);
- }
- }
-}
diff --git a/java/src/com/android/intentresolver/IntentForwarderActivity.java b/java/src/com/android/intentresolver/IntentForwarderActivity.java
index 5e8945f1..15996d00 100644
--- a/java/src/com/android/intentresolver/IntentForwarderActivity.java
+++ b/java/src/com/android/intentresolver/IntentForwarderActivity.java
@@ -23,7 +23,6 @@ import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
import static com.android.intentresolver.ResolverActivity.EXTRA_CALLING_USER;
import static com.android.intentresolver.ResolverActivity.EXTRA_SELECTED_PROFILE;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityThread;
import android.app.AppGlobals;
@@ -45,6 +44,8 @@ import android.provider.Settings;
import android.util.Slog;
import android.widget.Toast;
+import androidx.annotation.Nullable;
+
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -309,7 +310,7 @@ public class IntentForwarderActivity extends Activity {
* Check whether the intent can be forwarded to target user. Return the intent used for
* forwarding if it can be forwarded, {@code null} otherwise.
*/
- static Intent canForward(Intent incomingIntent, int sourceUserId, int targetUserId,
+ public static Intent canForward(Intent incomingIntent, int sourceUserId, int targetUserId,
IPackageManager packageManager, ContentResolver contentResolver) {
Intent forwardIntent = new Intent(incomingIntent);
forwardIntent.addFlags(
diff --git a/java/src/com/android/intentresolver/flags/FeatureFlagRepository.kt b/java/src/com/android/intentresolver/MainApplication.kt
index 5b5d769c..0a826629 100644
--- a/java/src/com/android/intentresolver/flags/FeatureFlagRepository.kt
+++ b/java/src/com/android/intentresolver/MainApplication.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,9 @@
* limitations under the License.
*/
-package com.android.intentresolver.flags
+package com.android.intentresolver
-import com.android.systemui.flags.ReleasedFlag
-import com.android.systemui.flags.UnreleasedFlag
+import android.app.Application
+import dagger.hilt.android.HiltAndroidApp
-interface FeatureFlagRepository {
- fun isEnabled(flag: UnreleasedFlag): Boolean
- fun isEnabled(flag: ReleasedFlag): Boolean
-}
+@HiltAndroidApp(Application::class) open class MainApplication : Hilt_MainApplication()
diff --git a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java
index 4b06db3b..42a29e55 100644
--- a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/MultiProfilePagerAdapter.java
@@ -15,15 +15,6 @@
*/
package com.android.intentresolver;
-import android.annotation.IntDef;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.UserIdInt;
-import android.app.AppGlobals;
-import android.content.ContentResolver;
-import android.content.Context;
-import android.content.Intent;
-import android.content.pm.IPackageManager;
import android.os.Trace;
import android.os.UserHandle;
import android.view.View;
@@ -31,62 +22,124 @@ import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.viewpager.widget.PagerAdapter;
import androidx.viewpager.widget.ViewPager;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.emptystate.EmptyStateUiHelper;
import com.android.internal.annotations.VisibleForTesting;
+import com.google.common.collect.ImmutableList;
+
import java.util.HashSet;
-import java.util.List;
-import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
+import java.util.function.Function;
import java.util.function.Supplier;
/**
- * Skeletal {@link PagerAdapter} implementation of a work or personal profile page for
- * intent resolution (including share sheet).
+ * Skeletal {@link PagerAdapter} implementation for a UI with per-profile tabs (as in Sharesheet).
+ *
+ * TODO: attempt to further restrict visibility/improve encapsulation in the methods we expose.
+ * TODO: deprecate and audit/fix usages of any methods that refer to the "active" or "inactive"
+ * adapters; these were marked {@link VisibleForTesting} and their usage seems like an accident
+ * waiting to happen since clients seem to make assumptions about which adapter will be "active" in
+ * a particular context, and more explicit APIs would make sure those were valid.
+ * TODO: consider renaming legacy methods (e.g. why do we know it's a "list", not just a "page"?)
+ *
+ * @param <PageViewT> the type of the widget that represents the contents of a page in this adapter
+ * @param <SinglePageAdapterT> the type of a "root" adapter class to be instantiated and included in
+ * the per-profile records.
+ * @param <ListAdapterT> the concrete type of a {@link ResolverListAdapter} implementation to
+ * control the contents of a given per-profile list. This is provided for convenience, since it must
+ * be possible to get the list adapter from the page adapter via our {@link mListAdapterExtractor}.
+ *
+ * TODO: this is part of an in-progress refactor to merge with `GenericMultiProfilePagerAdapter`.
+ * As originally noted there, we've reduced explicit references to the `ResolverListAdapter` base
+ * type and may be able to drop the type constraint.
*/
-public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
+public class MultiProfilePagerAdapter<
+ PageViewT extends ViewGroup,
+ SinglePageAdapterT,
+ ListAdapterT extends ResolverListAdapter> extends PagerAdapter {
+
+ /**
+ * Delegate to set up a given adapter and page view to be used together.
+ * @param <PageViewT> (as in {@link MultiProfilePagerAdapter}).
+ * @param <SinglePageAdapterT> (as in {@link MultiProfilePagerAdapter}).
+ */
+ public interface AdapterBinder<PageViewT, SinglePageAdapterT> {
+ /**
+ * The given {@code view} will be associated with the given {@code adapter}. Do any work
+ * necessary to configure them compatibly, introduce them to each other, etc.
+ */
+ void bind(PageViewT view, SinglePageAdapterT adapter);
+ }
- private static final String TAG = "AbstractMultiProfilePagerAdapter";
- static final int PROFILE_PERSONAL = 0;
- static final int PROFILE_WORK = 1;
+ public static final int PROFILE_PERSONAL = 0;
+ public static final int PROFILE_WORK = 1;
@IntDef({PROFILE_PERSONAL, PROFILE_WORK})
- @interface Profile {}
+ public @interface Profile {}
- private final Context mContext;
- private int mCurrentPage;
- private OnProfileSelectedListener mOnProfileSelectedListener;
+ private final Function<SinglePageAdapterT, ListAdapterT> mListAdapterExtractor;
+ private final AdapterBinder<PageViewT, SinglePageAdapterT> mAdapterBinder;
+ private final Supplier<ViewGroup> mPageViewInflater;
+ private final Supplier<Optional<Integer>> mContainerBottomPaddingOverrideSupplier;
+
+ private final ImmutableList<ProfileDescriptor<PageViewT, SinglePageAdapterT>> mItems;
- private Set<Integer> mLoadedPages;
private final EmptyStateProvider mEmptyStateProvider;
private final UserHandle mWorkProfileUserHandle;
private final UserHandle mCloneProfileUserHandle;
private final Supplier<Boolean> mWorkProfileQuietModeChecker; // True when work is quiet.
- AbstractMultiProfilePagerAdapter(
- Context context,
- int currentPage,
+ private Set<Integer> mLoadedPages;
+ private int mCurrentPage;
+ private OnProfileSelectedListener mOnProfileSelectedListener;
+
+ protected MultiProfilePagerAdapter(
+ Function<SinglePageAdapterT, ListAdapterT> listAdapterExtractor,
+ AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder,
+ ImmutableList<SinglePageAdapterT> adapters,
EmptyStateProvider emptyStateProvider,
Supplier<Boolean> workProfileQuietModeChecker,
+ @Profile int defaultProfile,
UserHandle workProfileUserHandle,
- UserHandle cloneProfileUserHandle) {
- mContext = Objects.requireNonNull(context);
- mCurrentPage = currentPage;
+ UserHandle cloneProfileUserHandle,
+ Supplier<ViewGroup> pageViewInflater,
+ Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
+ mCurrentPage = defaultProfile;
mLoadedPages = new HashSet<>();
mWorkProfileUserHandle = workProfileUserHandle;
mCloneProfileUserHandle = cloneProfileUserHandle;
mEmptyStateProvider = emptyStateProvider;
mWorkProfileQuietModeChecker = workProfileQuietModeChecker;
+
+ mListAdapterExtractor = listAdapterExtractor;
+ mAdapterBinder = adapterBinder;
+ mPageViewInflater = pageViewInflater;
+ mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier;
+
+ ImmutableList.Builder<ProfileDescriptor<PageViewT, SinglePageAdapterT>> items =
+ new ImmutableList.Builder<>();
+ for (SinglePageAdapterT adapter : adapters) {
+ items.add(createProfileDescriptor(adapter));
+ }
+ mItems = items.build();
}
- void setOnProfileSelectedListener(OnProfileSelectedListener listener) {
- mOnProfileSelectedListener = listener;
+ private ProfileDescriptor<PageViewT, SinglePageAdapterT> createProfileDescriptor(
+ SinglePageAdapterT adapter) {
+ return new ProfileDescriptor<>(mPageViewInflater.get(), adapter);
}
- Context getContext() {
- return mContext;
+ public void setOnProfileSelectedListener(OnProfileSelectedListener listener) {
+ mOnProfileSelectedListener = listener;
}
/**
@@ -94,7 +147,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* an {@link ViewPager.OnPageChangeListener} where it keeps track of the currently displayed
* page and rebuilds the list.
*/
- void setupViewPager(ViewPager viewPager) {
+ public void setupViewPager(ViewPager viewPager) {
viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
@Override
public void onPageSelected(int position) {
@@ -120,22 +173,24 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
mLoadedPages.add(mCurrentPage);
}
- void clearInactiveProfileCache() {
+ public void clearInactiveProfileCache() {
if (mLoadedPages.size() == 1) {
return;
}
mLoadedPages.remove(1 - mCurrentPage);
}
+ @NonNull
@Override
- public ViewGroup instantiateItem(ViewGroup container, int position) {
- final ProfileDescriptor profileDescriptor = getItem(position);
- container.addView(profileDescriptor.rootView);
- return profileDescriptor.rootView;
+ public final ViewGroup instantiateItem(ViewGroup container, int position) {
+ setupListAdapter(position);
+ final ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(position);
+ container.addView(descriptor.mRootView);
+ return descriptor.mRootView;
}
@Override
- public void destroyItem(ViewGroup container, int position, Object view) {
+ public void destroyItem(ViewGroup container, int position, @NonNull Object view) {
container.removeView((View) view);
}
@@ -144,7 +199,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
return getItemCount();
}
- protected int getCurrentPage() {
+ public int getCurrentPage() {
return mCurrentPage;
}
@@ -154,7 +209,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
@Override
- public boolean isViewFromObject(View view, Object object) {
+ public boolean isViewFromObject(@NonNull View view, @NonNull Object object) {
return view == object;
}
@@ -177,9 +232,11 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* <code>1</code> would return the work profile {@link ProfileDescriptor}.</li>
* </ul>
*/
- abstract ProfileDescriptor getItem(int pageIndex);
+ private ProfileDescriptor<PageViewT, SinglePageAdapterT> getItem(int pageIndex) {
+ return mItems.get(pageIndex);
+ }
- protected ViewGroup getEmptyStateView(int pageIndex) {
+ public ViewGroup getEmptyStateView(int pageIndex) {
return getItem(pageIndex).getEmptyStateView();
}
@@ -188,13 +245,13 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* <p>For a normal consumer device with only one user returns <code>1</code>.
* <p>For a device with a work profile returns <code>2</code>.
*/
- abstract int getItemCount();
+ public final int getItemCount() {
+ return mItems.size();
+ }
- /**
- * Performs view-related initialization procedures for the adapter specified
- * by <code>pageIndex</code>.
- */
- abstract void setupListAdapter(int pageIndex);
+ public final PageViewT getListViewForIndex(int index) {
+ return getItem(index).mView;
+ }
/**
* Returns the adapter of the list view for the relevant page specified by
@@ -203,54 +260,99 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* depending on the adapter type.
*/
@VisibleForTesting
- public abstract Object getAdapterForIndex(int pageIndex);
+ public final SinglePageAdapterT getAdapterForIndex(int index) {
+ return getItem(index).mAdapter;
+ }
/**
- * Returns the {@link ResolverListAdapter} instance of the profile that represents
+ * Performs view-related initialization procedures for the adapter specified
+ * by <code>pageIndex</code>.
+ */
+ public final void setupListAdapter(int pageIndex) {
+ mAdapterBinder.bind(getListViewForIndex(pageIndex), getAdapterForIndex(pageIndex));
+ }
+
+ /**
+ * Returns the {@link ListAdapterT} instance of the profile that represents
* <code>userHandle</code>. If there is no such adapter for the specified
* <code>userHandle</code>, returns {@code null}.
* <p>For example, if there is a work profile on the device with user id 10, calling this method
- * with <code>UserHandle.of(10)</code> returns the work profile {@link ResolverListAdapter}.
+ * with <code>UserHandle.of(10)</code> returns the work profile {@link ListAdapterT}.
*/
@Nullable
- abstract ResolverListAdapter getListAdapterForUserHandle(UserHandle userHandle);
+ public final ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) {
+ if (getPersonalListAdapter().getUserHandle().equals(userHandle)
+ || userHandle.equals(getCloneUserHandle())) {
+ return getPersonalListAdapter();
+ } else if ((getWorkListAdapter() != null)
+ && getWorkListAdapter().getUserHandle().equals(userHandle)) {
+ return getWorkListAdapter();
+ }
+ return null;
+ }
/**
- * Returns the {@link ResolverListAdapter} instance of the profile that is currently visible
+ * Returns the {@link ListAdapterT} instance of the profile that is currently visible
* to the user.
* <p>For example, if the user is viewing the work tab in the share sheet, this method returns
- * the work profile {@link ResolverListAdapter}.
+ * the work profile {@link ListAdapterT}.
* @see #getInactiveListAdapter()
*/
@VisibleForTesting
- public abstract ResolverListAdapter getActiveListAdapter();
+ public final ListAdapterT getActiveListAdapter() {
+ return mListAdapterExtractor.apply(getAdapterForIndex(getCurrentPage()));
+ }
/**
- * If this is a device with a work profile, returns the {@link ResolverListAdapter} instance
+ * If this is a device with a work profile, returns the {@link ListAdapterT} instance
* of the profile that is <b><i>not</i></b> currently visible to the user. Otherwise returns
* {@code null}.
* <p>For example, if the user is viewing the work tab in the share sheet, this method returns
- * the personal profile {@link ResolverListAdapter}.
+ * the personal profile {@link ListAdapterT}.
* @see #getActiveListAdapter()
*/
@VisibleForTesting
- public abstract @Nullable ResolverListAdapter getInactiveListAdapter();
+ @Nullable
+ public final ListAdapterT getInactiveListAdapter() {
+ if (getCount() < 2) {
+ return null;
+ }
+ return mListAdapterExtractor.apply(getAdapterForIndex(1 - getCurrentPage()));
+ }
- public abstract ResolverListAdapter getPersonalListAdapter();
+ public final ListAdapterT getPersonalListAdapter() {
+ return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_PERSONAL));
+ }
- public abstract @Nullable ResolverListAdapter getWorkListAdapter();
+ @Nullable
+ public final ListAdapterT getWorkListAdapter() {
+ if (!hasAdapterForIndex(PROFILE_WORK)) {
+ return null;
+ }
+ return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_WORK));
+ }
- abstract Object getCurrentRootAdapter();
+ public final SinglePageAdapterT getCurrentRootAdapter() {
+ return getAdapterForIndex(getCurrentPage());
+ }
- abstract ViewGroup getActiveAdapterView();
+ public final PageViewT getActiveAdapterView() {
+ return getListViewForIndex(getCurrentPage());
+ }
- abstract @Nullable ViewGroup getInactiveAdapterView();
+ @Nullable
+ public final PageViewT getInactiveAdapterView() {
+ if (getCount() < 2) {
+ return null;
+ }
+ return getListViewForIndex(1 - getCurrentPage());
+ }
/**
* Rebuilds the tab that is currently visible to the user.
* <p>Returns {@code true} if rebuild has completed.
*/
- boolean rebuildActiveTab(boolean doPostProcessing) {
+ public boolean rebuildActiveTab(boolean doPostProcessing) {
Trace.beginSection("MultiProfilePagerAdapter#rebuildActiveTab");
boolean result = rebuildTab(getActiveListAdapter(), doPostProcessing);
Trace.endSection();
@@ -261,7 +363,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* Rebuilds the tab that is not currently visible to the user, if such one exists.
* <p>Returns {@code true} if rebuild has completed.
*/
- boolean rebuildInactiveTab(boolean doPostProcessing) {
+ public boolean rebuildInactiveTab(boolean doPostProcessing) {
Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab");
if (getItemCount() == 1) {
Trace.endSection();
@@ -280,7 +382,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
}
- private boolean rebuildTab(ResolverListAdapter activeListAdapter, boolean doPostProcessing) {
+ private boolean rebuildTab(ListAdapterT activeListAdapter, boolean doPostProcessing) {
if (shouldSkipRebuild(activeListAdapter)) {
activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true);
return false;
@@ -288,16 +390,20 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
return activeListAdapter.rebuildList(doPostProcessing);
}
- private boolean shouldSkipRebuild(ResolverListAdapter activeListAdapter) {
+ private boolean shouldSkipRebuild(ListAdapterT activeListAdapter) {
EmptyState emptyState = mEmptyStateProvider.getEmptyState(activeListAdapter);
return emptyState != null && emptyState.shouldSkipDataRebuild();
}
+ private boolean hasAdapterForIndex(int pageIndex) {
+ return (pageIndex < getCount());
+ }
+
/**
* The empty state screens are shown according to their priority:
* <ol>
* <li>(highest priority) cross-profile disabled by policy (handled in
- * {@link #rebuildTab(ResolverListAdapter, boolean)})</li>
+ * {@link #rebuildTab(ListAdapterT, boolean)})</li>
* <li>no apps available</li>
* <li>(least priority) work is off</li>
* </ol>
@@ -306,7 +412,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* the work profile on if there will not be any apps resolved
* anyway.
*/
- void showEmptyResolverListEmptyState(ResolverListAdapter listAdapter) {
+ public void showEmptyResolverListEmptyState(ListAdapterT listAdapter) {
final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter);
if (emptyState == null) {
@@ -319,9 +425,9 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
if (emptyState.getButtonClickListener() != null) {
clickListener = v -> emptyState.getButtonClickListener().onClick(() -> {
- ProfileDescriptor descriptor = getItem(
+ ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
userHandleToPageIndex(listAdapter.getUserHandle()));
- AbstractMultiProfilePagerAdapter.this.showSpinner(descriptor.getEmptyStateView());
+ descriptor.mEmptyStateUi.showSpinner();
});
}
@@ -340,45 +446,24 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
}
- /**
- * Utility class to check if there are cross profile intents, it is in a separate class so
- * it could be mocked in tests
- */
- public static class CrossProfileIntentsChecker {
-
- private final ContentResolver mContentResolver;
-
- public CrossProfileIntentsChecker(@NonNull ContentResolver contentResolver) {
- mContentResolver = contentResolver;
- }
-
- /**
- * Returns {@code true} if at least one of the provided {@code intents} can be forwarded
- * from {@code source} (user id) to {@code target} (user id).
- */
- public boolean hasCrossProfileIntents(List<Intent> intents, @UserIdInt int source,
- @UserIdInt int target) {
- IPackageManager packageManager = AppGlobals.getPackageManager();
-
- return intents.stream().anyMatch(intent ->
- null != IntentForwarderActivity.canForward(intent, source, target,
- packageManager, mContentResolver));
- }
- }
-
- protected void showEmptyState(ResolverListAdapter activeListAdapter, EmptyState emptyState,
+ protected void showEmptyState(
+ ListAdapterT activeListAdapter,
+ EmptyState emptyState,
View.OnClickListener buttonOnClick) {
- ProfileDescriptor descriptor = getItem(
+ ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
userHandleToPageIndex(activeListAdapter.getUserHandle()));
- descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.GONE);
+ descriptor.mRootView.findViewById(
+ com.android.internal.R.id.resolver_list).setVisibility(View.GONE);
+ descriptor.mEmptyStateUi.resetViewVisibilities();
+
ViewGroup emptyStateView = descriptor.getEmptyStateView();
- resetViewVisibilitiesForEmptyState(emptyStateView);
- emptyStateView.setVisibility(View.VISIBLE);
- View container = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_container);
+ View container = emptyStateView.findViewById(
+ com.android.internal.R.id.resolver_empty_state_container);
setupContainerPadding(container);
- TextView titleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title);
+ TextView titleView = emptyStateView.findViewById(
+ com.android.internal.R.id.resolver_empty_state_title);
String title = emptyState.getTitle();
if (title != null) {
titleView.setVisibility(View.VISIBLE);
@@ -387,7 +472,8 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
titleView.setVisibility(View.GONE);
}
- TextView subtitleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle);
+ TextView subtitleView = emptyStateView.findViewById(
+ com.android.internal.R.id.resolver_empty_state_subtitle);
String subtitle = emptyState.getSubtitle();
if (subtitle != null) {
subtitleView.setVisibility(View.VISIBLE);
@@ -399,7 +485,8 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
View defaultEmptyText = emptyStateView.findViewById(com.android.internal.R.id.empty);
defaultEmptyText.setVisibility(emptyState.useDefaultEmptyView() ? View.VISIBLE : View.GONE);
- Button button = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button);
+ Button button = emptyStateView.findViewById(
+ com.android.internal.R.id.resolver_empty_state_button);
button.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE);
button.setOnClickListener(buttonOnClick);
@@ -410,44 +497,50 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* Sets up the padding of the view containing the empty state screens.
* <p>This method is meant to be overridden so that subclasses can customize the padding.
*/
- protected void setupContainerPadding(View container) {}
-
- private void showSpinner(View emptyStateView) {
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.INVISIBLE);
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.INVISIBLE);
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).setVisibility(View.VISIBLE);
- emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE);
- }
-
- private void resetViewVisibilitiesForEmptyState(View emptyStateView) {
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.VISIBLE);
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle).setVisibility(View.VISIBLE);
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.INVISIBLE);
- emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).setVisibility(View.GONE);
- emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE);
- }
-
- protected void showListView(ResolverListAdapter activeListAdapter) {
- ProfileDescriptor descriptor = getItem(
+ public void setupContainerPadding(View container) {
+ Optional<Integer> bottomPaddingOverride = mContainerBottomPaddingOverrideSupplier.get();
+ bottomPaddingOverride.ifPresent(paddingBottom ->
+ container.setPadding(
+ container.getPaddingLeft(),
+ container.getPaddingTop(),
+ container.getPaddingRight(),
+ paddingBottom));
+ }
+
+ public void showListView(ListAdapterT activeListAdapter) {
+ ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
userHandleToPageIndex(activeListAdapter.getUserHandle()));
- descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.VISIBLE);
- View emptyStateView = descriptor.rootView.findViewById(com.android.internal.R.id.resolver_empty_state);
- emptyStateView.setVisibility(View.GONE);
+ descriptor.mRootView.findViewById(
+ com.android.internal.R.id.resolver_list).setVisibility(View.VISIBLE);
+ descriptor.mEmptyStateUi.hide();
}
- boolean shouldShowEmptyStateScreen(ResolverListAdapter listAdapter) {
+ public boolean shouldShowEmptyStateScreen(ListAdapterT listAdapter) {
int count = listAdapter.getUnfilteredCount();
return (count == 0 && listAdapter.getPlaceholderCount() == 0)
|| (listAdapter.getUserHandle().equals(mWorkProfileUserHandle)
&& mWorkProfileQuietModeChecker.get());
}
- protected static class ProfileDescriptor {
- final ViewGroup rootView;
+ // TODO: `ChooserActivity` also has a per-profile record type. Maybe the "multi-profile pager"
+ // should be the owner of all per-profile data (especially now that the API is generic)?
+ private static class ProfileDescriptor<PageViewT, SinglePageAdapterT> {
+ final ViewGroup mRootView;
+ final EmptyStateUiHelper mEmptyStateUi;
+
+ // TODO: post-refactoring, we may not need to retain these ivars directly (since they may
+ // be encapsulated within the `EmptyStateUiHelper`?).
private final ViewGroup mEmptyStateView;
- ProfileDescriptor(ViewGroup rootView) {
- this.rootView = rootView;
+
+ private final SinglePageAdapterT mAdapter;
+ private final PageViewT mView;
+
+ ProfileDescriptor(ViewGroup rootView, SinglePageAdapterT adapter) {
+ mRootView = rootView;
+ mAdapter = adapter;
mEmptyStateView = rootView.findViewById(com.android.internal.R.id.resolver_empty_state);
+ mView = (PageViewT) rootView.findViewById(com.android.internal.R.id.resolver_list);
+ mEmptyStateUi = new EmptyStateUiHelper(rootView);
}
protected ViewGroup getEmptyStateView() {
@@ -455,6 +548,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
}
+ /** Listener interface for changes between the per-profile UI tabs. */
public interface OnProfileSelectedListener {
/**
* Callback for when the user changes the active tab from personal to work or vice versa.
@@ -478,102 +572,9 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
/**
- * Returns an empty state to show for the current profile page (tab) if necessary.
- * This could be used e.g. to show a blocker on a tab if device management policy doesn't
- * allow to use it or there are no apps available.
- */
- public interface EmptyStateProvider {
- /**
- * When a non-null empty state is returned the corresponding profile page will show
- * this empty state
- * @param resolverListAdapter the current adapter
- */
- @Nullable
- default EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
- return null;
- }
- }
-
- /**
- * Empty state provider that combines multiple providers. Providers earlier in the list have
- * priority, that is if there is a provider that returns non-null empty state then all further
- * providers will be ignored.
- */
- public static class CompositeEmptyStateProvider implements EmptyStateProvider {
-
- private final EmptyStateProvider[] mProviders;
-
- public CompositeEmptyStateProvider(EmptyStateProvider... providers) {
- mProviders = providers;
- }
-
- @Nullable
- @Override
- public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
- for (EmptyStateProvider provider : mProviders) {
- EmptyState emptyState = provider.getEmptyState(resolverListAdapter);
- if (emptyState != null) {
- return emptyState;
- }
- }
- return null;
- }
- }
-
- /**
- * Describes how the blocked empty state should look like for a profile tab
- */
- public interface EmptyState {
- /**
- * Title that will be shown on the empty state
- */
- @Nullable
- default String getTitle() { return null; }
-
- /**
- * Subtitle that will be shown underneath the title on the empty state
- */
- @Nullable
- default String getSubtitle() { return null; }
-
- /**
- * If non-null then a button will be shown and this listener will be called
- * when the button is clicked
- */
- @Nullable
- default ClickListener getButtonClickListener() { return null; }
-
- /**
- * If true then default text ('No apps can perform this action') and style for the empty
- * state will be applied, title and subtitle will be ignored.
- */
- default boolean useDefaultEmptyView() { return false; }
-
- /**
- * Returns true if for this empty state we should skip rebuilding of the apps list
- * for this tab.
- */
- default boolean shouldSkipDataRebuild() { return false; }
-
- /**
- * Called when empty state is shown, could be used e.g. to track analytics events
- */
- default void onEmptyStateShown() {}
-
- interface ClickListener {
- void onClick(TabControl currentTab);
- }
-
- interface TabControl {
- void showSpinner();
- }
- }
-
-
- /**
* Listener for when the user switches on the work profile from the work tab.
*/
- interface OnSwitchOnWorkSelectedListener {
+ public interface OnSwitchOnWorkSelectedListener {
/**
* Callback for when the user switches on the work profile from the work tab.
*/
diff --git a/java/src/com/android/intentresolver/ResolvedComponentInfo.java b/java/src/com/android/intentresolver/ResolvedComponentInfo.java
index ecb72cbf..aaa97c42 100644
--- a/java/src/com/android/intentresolver/ResolvedComponentInfo.java
+++ b/java/src/com/android/intentresolver/ResolvedComponentInfo.java
@@ -20,6 +20,8 @@ import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.ResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+
import java.util.ArrayList;
import java.util.List;
@@ -86,7 +88,7 @@ public final class ResolvedComponentInfo {
}
/**
- * @return whether this component was pinned by a call to {@link #setPinned()}.
+ * @return whether this component was pinned by a call to {@link #setPinned}.
* TODO: consolidate sources of pinning data and/or document how this differs from other places
* we make a "pinning" determination.
*/
diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java
index 35c7e897..0331c33e 100644
--- a/java/src/com/android/intentresolver/ResolverActivity.java
+++ b/java/src/com/android/intentresolver/ResolverActivity.java
@@ -36,9 +36,6 @@ import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTE
import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED;
-import android.annotation.Nullable;
-import android.annotation.StringRes;
-import android.annotation.UiThread;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.ActivityThread;
@@ -96,18 +93,26 @@ import android.widget.TabWidget;
import android.widget.TextView;
import android.widget.Toast;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.annotation.UiThread;
import androidx.fragment.app.FragmentActivity;
import androidx.viewpager.widget.ViewPager;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CompositeEmptyStateProvider;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.Profile;
-import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
+import com.android.intentresolver.MultiProfilePagerAdapter.MyUserIdProvider;
+import com.android.intentresolver.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener;
+import com.android.intentresolver.MultiProfilePagerAdapter.Profile;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.emptystate.CompositeEmptyStateProvider;
+import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.emptystate.NoAppsAvailableEmptyStateProvider;
+import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider;
+import com.android.intentresolver.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
+import com.android.intentresolver.emptystate.WorkProfilePausedEmptyStateProvider;
import com.android.intentresolver.icons.DefaultTargetDataLoader;
import com.android.intentresolver.icons.TargetDataLoader;
import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
@@ -199,8 +204,10 @@ public class ResolverActivity extends FragmentActivity implements
private PackageMonitor mPersonalPackageMonitor;
private PackageMonitor mWorkPackageMonitor;
+ private TargetDataLoader mTargetDataLoader;
+
@VisibleForTesting
- protected AbstractMultiProfilePagerAdapter mMultiProfilePagerAdapter;
+ protected MultiProfilePagerAdapter mMultiProfilePagerAdapter;
protected WorkProfileAvailabilityManager mWorkProfileAvailability;
@@ -227,8 +234,8 @@ public class ResolverActivity extends FragmentActivity implements
static final String EXTRA_CALLING_USER =
"com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER";
- protected static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL;
- protected static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK;
+ protected static final int PROFILE_PERSONAL = MultiProfilePagerAdapter.PROFILE_PERSONAL;
+ protected static final int PROFILE_WORK = MultiProfilePagerAdapter.PROFILE_WORK;
private UserHandle mHeaderCreatorUser;
@@ -239,11 +246,20 @@ public class ResolverActivity extends FragmentActivity implements
// new component whose lifecycle is limited to the "created" Activity (so that we can just hold
// the annotations as a `final` ivar, which is a better way to show immutability).
private Supplier<AnnotatedUserHandles> mLazyAnnotatedUserHandles = () -> {
- final AnnotatedUserHandles result = AnnotatedUserHandles.forShareActivity(this);
+ final AnnotatedUserHandles result = computeAnnotatedUserHandles();
mLazyAnnotatedUserHandles = () -> result;
return result;
};
+ // This method is called exactly once during creation to compute the immutable annotations
+ // accessible through the lazy supplier {@link mLazyAnnotatedUserHandles}.
+ // TODO: this is only defined so that tests can provide an override that injects fake
+ // annotations. Dagger could provide a cleaner model for our testing/injection requirements.
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
+ protected AnnotatedUserHandles computeAnnotatedUserHandles() {
+ return AnnotatedUserHandles.forShareActivity(this);
+ }
+
@Nullable
private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
@@ -418,6 +434,7 @@ public class ResolverActivity extends FragmentActivity implements
mSupportsAlwaysUseOption = supportsAlwaysUseOption;
mSafeForwardingMode = safeForwardingMode;
+ mTargetDataLoader = targetDataLoader;
// The last argument of createResolverListAdapter is whether to do special handling
// of the last used choice to highlight it in the list. We need to always
@@ -438,11 +455,12 @@ public class ResolverActivity extends FragmentActivity implements
mPersonalPackageMonitor = createPackageMonitor(
mMultiProfilePagerAdapter.getPersonalListAdapter());
mPersonalPackageMonitor.register(
- this, getMainLooper(), getPersonalProfileUserHandle(), false);
+ this, getMainLooper(), getAnnotatedUserHandles().personalProfileUserHandle, false);
if (shouldShowTabs()) {
mWorkPackageMonitor = createPackageMonitor(
mMultiProfilePagerAdapter.getWorkListAdapter());
- mWorkPackageMonitor.register(this, getMainLooper(), getWorkProfileUserHandle(), false);
+ mWorkPackageMonitor.register(
+ this, getMainLooper(), getAnnotatedUserHandles().workProfileUserHandle, false);
}
mRegistered = true;
@@ -484,12 +502,12 @@ public class ResolverActivity extends FragmentActivity implements
+ (categories != null ? Arrays.toString(categories.toArray()) : ""));
}
- protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter(
+ protected MultiProfilePagerAdapter createMultiProfilePagerAdapter(
Intent[] initialIntents,
List<ResolveInfo> resolutionList,
boolean filterLastUsed,
TargetDataLoader targetDataLoader) {
- AbstractMultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null;
+ MultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null;
if (shouldShowTabs()) {
resolverMultiProfilePagerAdapter =
createResolverMultiProfilePagerAdapterForTwoProfiles(
@@ -509,9 +527,9 @@ public class ResolverActivity extends FragmentActivity implements
return new EmptyStateProvider() {};
}
- final AbstractMultiProfilePagerAdapter.EmptyState
- noWorkToPersonalEmptyState =
- new DevicePolicyBlockerEmptyState(/* context= */ this,
+ final EmptyState noWorkToPersonalEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
/* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
/* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
/* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_PERSONAL,
@@ -521,8 +539,9 @@ public class ResolverActivity extends FragmentActivity implements
/* devicePolicyEventCategory= */
ResolverActivity.METRICS_CATEGORY_RESOLVER);
- final AbstractMultiProfilePagerAdapter.EmptyState noPersonalToWorkEmptyState =
- new DevicePolicyBlockerEmptyState(/* context= */ this,
+ final EmptyState noPersonalToWorkEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
/* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
/* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
/* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_WORK,
@@ -532,9 +551,12 @@ public class ResolverActivity extends FragmentActivity implements
/* devicePolicyEventCategory= */
ResolverActivity.METRICS_CATEGORY_RESOLVER);
- return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(),
- noWorkToPersonalEmptyState, noPersonalToWorkEmptyState,
- createCrossProfileIntentsChecker(), getTabOwnerUserHandleForLaunch());
+ return new NoCrossProfileEmptyStateProvider(
+ getAnnotatedUserHandles().personalProfileUserHandle,
+ noWorkToPersonalEmptyState,
+ noPersonalToWorkEmptyState,
+ createCrossProfileIntentsChecker(),
+ getAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
}
protected int appliedThemeResId() {
@@ -591,7 +613,7 @@ public class ResolverActivity extends FragmentActivity implements
}
@Override
- public void onConfigurationChanged(Configuration newConfig) {
+ public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
if (mIsIntentPicker && shouldShowTabs() && !useLayoutWithDefault()
@@ -1014,7 +1036,7 @@ public class ResolverActivity extends FragmentActivity implements
@Override // ResolverListCommunicator
public void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
if (listAdapter == mMultiProfilePagerAdapter.getActiveListAdapter()) {
- if (listAdapter.getUserHandle().equals(getWorkProfileUserHandle())
+ if (listAdapter.getUserHandle().equals(getAnnotatedUserHandles().workProfileUserHandle)
&& mWorkProfileAvailability.isWaitingToEnableWorkProfile()) {
// We have just turned on the work profile and entered the pass code to start it,
// now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no
@@ -1052,16 +1074,15 @@ public class ResolverActivity extends FragmentActivity implements
}
protected WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() {
- final UserHandle workUser = getWorkProfileUserHandle();
-
return new WorkProfileAvailabilityManager(
getSystemService(UserManager.class),
- workUser,
+ getAnnotatedUserHandles().workProfileUserHandle,
this::onWorkProfileStatusUpdated);
}
protected void onWorkProfileStatusUpdated() {
- if (mMultiProfilePagerAdapter.getCurrentUserHandle().equals(getWorkProfileUserHandle())) {
+ if (mMultiProfilePagerAdapter.getCurrentUserHandle().equals(
+ getAnnotatedUserHandles().workProfileUserHandle)) {
mMultiProfilePagerAdapter.rebuildActiveTab(true);
} else {
mMultiProfilePagerAdapter.clearInactiveProfileCache();
@@ -1079,8 +1100,8 @@ public class ResolverActivity extends FragmentActivity implements
UserHandle userHandle,
TargetDataLoader targetDataLoader) {
UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()
- && userHandle.equals(getPersonalProfileUserHandle())
- ? getCloneProfileUserHandle() : userHandle;
+ && userHandle.equals(getAnnotatedUserHandles().personalProfileUserHandle)
+ ? getAnnotatedUserHandles().cloneProfileUserHandle : userHandle;
return new ResolverListAdapter(
context,
payloadIntents,
@@ -1136,9 +1157,9 @@ public class ResolverActivity extends FragmentActivity implements
final EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider(
this,
workProfileUserHandle,
- getPersonalProfileUserHandle(),
+ getAnnotatedUserHandles().personalProfileUserHandle,
getMetricsCategory(),
- getTabOwnerUserHandleForLaunch()
+ getAnnotatedUserHandles().tabOwnerUserHandleForLaunch
);
// Return composite provider, the order matters (the higher, the more priority)
@@ -1188,7 +1209,7 @@ public class ResolverActivity extends FragmentActivity implements
initialIntents,
resolutionList,
filterLastUsed,
- /* userHandle */ getPersonalProfileUserHandle(),
+ /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,
targetDataLoader);
return new ResolverMultiProfilePagerAdapter(
/* context */ this,
@@ -1196,13 +1217,13 @@ public class ResolverActivity extends FragmentActivity implements
createEmptyStateProvider(/* workProfileUserHandle= */ null),
/* workProfileQuietModeChecker= */ () -> false,
/* workProfileUserHandle= */ null,
- getCloneProfileUserHandle());
+ getAnnotatedUserHandles().cloneProfileUserHandle);
}
private UserHandle getIntentUser() {
return getIntent().hasExtra(EXTRA_CALLING_USER)
? getIntent().getParcelableExtra(EXTRA_CALLING_USER)
- : getTabOwnerUserHandleForLaunch();
+ : getAnnotatedUserHandles().tabOwnerUserHandleForLaunch;
}
private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles(
@@ -1215,10 +1236,10 @@ public class ResolverActivity extends FragmentActivity implements
// this happens, we check for it here and set the current profile's tab.
int selectedProfile = getCurrentProfile();
UserHandle intentUser = getIntentUser();
- if (!getTabOwnerUserHandleForLaunch().equals(intentUser)) {
- if (getPersonalProfileUserHandle().equals(intentUser)) {
+ if (!getAnnotatedUserHandles().tabOwnerUserHandleForLaunch.equals(intentUser)) {
+ if (getAnnotatedUserHandles().personalProfileUserHandle.equals(intentUser)) {
selectedProfile = PROFILE_PERSONAL;
- } else if (getWorkProfileUserHandle().equals(intentUser)) {
+ } else if (getAnnotatedUserHandles().workProfileUserHandle.equals(intentUser)) {
selectedProfile = PROFILE_WORK;
}
} else {
@@ -1236,10 +1257,10 @@ public class ResolverActivity extends FragmentActivity implements
selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
resolutionList,
(filterLastUsed && UserHandle.myUserId()
- == getPersonalProfileUserHandle().getIdentifier()),
- /* userHandle */ getPersonalProfileUserHandle(),
+ == getAnnotatedUserHandles().personalProfileUserHandle.getIdentifier()),
+ /* userHandle */ getAnnotatedUserHandles().personalProfileUserHandle,
targetDataLoader);
- UserHandle workProfileUserHandle = getWorkProfileUserHandle();
+ UserHandle workProfileUserHandle = getAnnotatedUserHandles().workProfileUserHandle;
ResolverListAdapter workAdapter = createResolverListAdapter(
/* context */ this,
/* payloadIntents */ mIntents,
@@ -1253,11 +1274,11 @@ public class ResolverActivity extends FragmentActivity implements
/* context */ this,
personalAdapter,
workAdapter,
- createEmptyStateProvider(getWorkProfileUserHandle()),
+ createEmptyStateProvider(workProfileUserHandle),
() -> mWorkProfileAvailability.isQuietModeEnabled(),
selectedProfile,
- getWorkProfileUserHandle(),
- getCloneProfileUserHandle());
+ workProfileUserHandle,
+ getAnnotatedUserHandles().cloneProfileUserHandle);
}
/**
@@ -1280,55 +1301,29 @@ public class ResolverActivity extends FragmentActivity implements
}
protected final @Profile int getCurrentProfile() {
- return (getTabOwnerUserHandleForLaunch().equals(getPersonalProfileUserHandle())
- ? PROFILE_PERSONAL : PROFILE_WORK);
+ UserHandle launchUser = getAnnotatedUserHandles().tabOwnerUserHandleForLaunch;
+ UserHandle personalUser = getAnnotatedUserHandles().personalProfileUserHandle;
+ return launchUser.equals(personalUser) ? PROFILE_PERSONAL : PROFILE_WORK;
}
protected final AnnotatedUserHandles getAnnotatedUserHandles() {
return mLazyAnnotatedUserHandles.get();
}
- protected final UserHandle getPersonalProfileUserHandle() {
- return getAnnotatedUserHandles().personalProfileUserHandle;
- }
-
- // TODO: have tests override `getAnnotatedUserHandles()`, and make this method `final`.
- // @NonFinalForTesting
- @Nullable
- protected UserHandle getWorkProfileUserHandle() {
- return getAnnotatedUserHandles().workProfileUserHandle;
- }
-
- // TODO: have tests override `getAnnotatedUserHandles()`, and make this method `final`.
- @Nullable
- protected UserHandle getCloneProfileUserHandle() {
- return getAnnotatedUserHandles().cloneProfileUserHandle;
- }
-
- // TODO: have tests override `getAnnotatedUserHandles()`, and make this method `final`.
- protected UserHandle getTabOwnerUserHandleForLaunch() {
- return getAnnotatedUserHandles().tabOwnerUserHandleForLaunch;
- }
-
- protected UserHandle getUserHandleSharesheetLaunchedAs() {
- return getAnnotatedUserHandles().userHandleSharesheetLaunchedAs;
- }
-
-
private boolean hasWorkProfile() {
- return getWorkProfileUserHandle() != null;
+ return getAnnotatedUserHandles().workProfileUserHandle != null;
}
private boolean hasCloneProfile() {
- return getCloneProfileUserHandle() != null;
+ return getAnnotatedUserHandles().cloneProfileUserHandle != null;
}
protected final boolean isLaunchedAsCloneProfile() {
- return hasCloneProfile()
- && getUserHandleSharesheetLaunchedAs().equals(getCloneProfileUserHandle());
+ UserHandle launchUser = getAnnotatedUserHandles().userHandleSharesheetLaunchedAs;
+ UserHandle cloneUser = getAnnotatedUserHandles().cloneProfileUserHandle;
+ return hasCloneProfile() && launchUser.equals(cloneUser);
}
-
protected final boolean shouldShowTabs() {
return hasWorkProfile();
}
@@ -1368,7 +1363,9 @@ public class ResolverActivity extends FragmentActivity implements
}
DevicePolicyEventLogger
.createEvent(DevicePolicyEnums.RESOLVER_CROSS_PROFILE_TARGET_OPENED)
- .setBoolean(currentUserHandle.equals(getPersonalProfileUserHandle()))
+ .setBoolean(
+ currentUserHandle.equals(
+ getAnnotatedUserHandles().personalProfileUserHandle))
.setStrings(getMetricsCategory(),
cti.isInDirectShareMetricsCategory() ? "direct_share" : "other_target")
.write();
@@ -1399,7 +1396,7 @@ public class ResolverActivity extends FragmentActivity implements
}
final Option optionForChooserTarget(TargetInfo target, int index) {
- return new Option(target.getDisplayLabel(), index);
+ return new Option(getOrLoadDisplayLabel(target), index);
}
public final Intent getTargetIntent() {
@@ -1475,8 +1472,11 @@ public class ResolverActivity extends FragmentActivity implements
return getString(defaultTitleRes);
} else {
return named
- ? getString(title.namedTitleRes, mMultiProfilePagerAdapter
- .getActiveListAdapter().getFilteredItem().getDisplayLabel())
+ ? getString(
+ title.namedTitleRes,
+ getOrLoadDisplayLabel(
+ mMultiProfilePagerAdapter
+ .getActiveListAdapter().getFilteredItem()))
: getString(title.titleRes);
}
}
@@ -1491,15 +1491,21 @@ public class ResolverActivity extends FragmentActivity implements
protected final void onRestart() {
super.onRestart();
if (!mRegistered) {
- mPersonalPackageMonitor.register(this, getMainLooper(),
- getPersonalProfileUserHandle(), false);
+ mPersonalPackageMonitor.register(
+ this,
+ getMainLooper(),
+ getAnnotatedUserHandles().personalProfileUserHandle,
+ false);
if (shouldShowTabs()) {
if (mWorkPackageMonitor == null) {
mWorkPackageMonitor = createPackageMonitor(
mMultiProfilePagerAdapter.getWorkListAdapter());
}
- mWorkPackageMonitor.register(this, getMainLooper(),
- getWorkProfileUserHandle(), false);
+ mWorkPackageMonitor.register(
+ this,
+ getMainLooper(),
+ getAnnotatedUserHandles().workProfileUserHandle,
+ false);
}
mRegistered = true;
}
@@ -1523,7 +1529,7 @@ public class ResolverActivity extends FragmentActivity implements
}
@Override
- protected final void onSaveInstanceState(Bundle outState) {
+ protected final void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
if (viewPager != null) {
@@ -1532,7 +1538,7 @@ public class ResolverActivity extends FragmentActivity implements
}
@Override
- protected final void onRestoreInstanceState(Bundle savedInstanceState) {
+ protected final void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
resetButtonBar();
ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
@@ -1807,9 +1813,10 @@ public class ResolverActivity extends FragmentActivity implements
((TextView) findViewById(com.android.internal.R.id.open_cross_profile)).setText(
getResources().getString(
- inWorkProfile ? R.string.miniresolver_open_in_personal
+ inWorkProfile
+ ? R.string.miniresolver_open_in_personal
: R.string.miniresolver_open_in_work,
- otherProfileResolveInfo.getDisplayLabel()));
+ getOrLoadDisplayLabel(otherProfileResolveInfo)));
((Button) findViewById(com.android.internal.R.id.use_same_profile_browser)).setText(
inWorkProfile ? R.string.miniresolver_use_work_browser
: R.string.miniresolver_use_personal_browser);
@@ -1973,7 +1980,7 @@ public class ResolverActivity extends FragmentActivity implements
DevicePolicyEventLogger
.createEvent(DevicePolicyEnums.RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET)
.setBoolean(activeListAdapter.getUserHandle()
- .equals(getPersonalProfileUserHandle()))
+ .equals(getAnnotatedUserHandles().personalProfileUserHandle))
.setStrings(getMetricsCategory())
.write();
safelyStartActivity(activeProfileTarget);
@@ -2080,7 +2087,7 @@ public class ResolverActivity extends FragmentActivity implements
viewPager.setVisibility(View.VISIBLE);
tabHost.setCurrentTab(mMultiProfilePagerAdapter.getCurrentPage());
mMultiProfilePagerAdapter.setOnProfileSelectedListener(
- new AbstractMultiProfilePagerAdapter.OnProfileSelectedListener() {
+ new MultiProfilePagerAdapter.OnProfileSelectedListener() {
@Override
public void onProfileSelected(int index) {
tabHost.setCurrentTab(index);
@@ -2256,7 +2263,7 @@ public class ResolverActivity extends FragmentActivity implements
// filtered item. We always show the same default app even in the inactive user profile.
boolean adapterForCurrentUserHasFilteredItem =
mMultiProfilePagerAdapter.getListAdapterForUserHandle(
- getTabOwnerUserHandleForLaunch()).hasFilteredItem();
+ getAnnotatedUserHandles().tabOwnerUserHandleForLaunch).hasFilteredItem();
return mSupportsAlwaysUseOption && adapterForCurrentUserHasFilteredItem;
}
@@ -2268,20 +2275,6 @@ public class ResolverActivity extends FragmentActivity implements
mRetainInOnStop = retainInOnStop;
}
- /**
- * Check a simple match for the component of two ResolveInfos.
- */
- @Override // ResolverListCommunicator
- public final boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs) {
- return lhs == null ? rhs == null
- : lhs.activityInfo == null ? rhs.activityInfo == null
- : Objects.equals(lhs.activityInfo.name, rhs.activityInfo.name)
- && Objects.equals(lhs.activityInfo.packageName, rhs.activityInfo.packageName)
- // Comparing against resolveInfo.userHandle in case cloned apps are present,
- // as they will have the same activityInfo.
- && Objects.equals(lhs.userHandle, rhs.userHandle);
- }
-
private boolean inactiveListAdapterHasItems() {
if (!shouldShowTabs()) {
return false;
@@ -2391,7 +2384,7 @@ public class ResolverActivity extends FragmentActivity implements
* {@link ResolverListController} configured for the provided {@code userHandle}.
*/
protected final UserHandle getQueryIntentsUser(UserHandle userHandle) {
- return mLazyAnnotatedUserHandles.get().getQueryIntentsUser(userHandle);
+ return getAnnotatedUserHandles().getQueryIntentsUser(userHandle);
}
/**
@@ -2411,10 +2404,18 @@ public class ResolverActivity extends FragmentActivity implements
// Add clonedProfileUserHandle to the list only if we are:
// a. Building the Personal Tab.
// b. CloneProfile exists on the device.
- if (userHandle.equals(getPersonalProfileUserHandle())
- && getCloneProfileUserHandle() != null) {
- userList.add(getCloneProfileUserHandle());
+ if (userHandle.equals(getAnnotatedUserHandles().personalProfileUserHandle)
+ && hasCloneProfile()) {
+ userList.add(getAnnotatedUserHandles().cloneProfileUserHandle);
}
return userList;
}
+
+ private CharSequence getOrLoadDisplayLabel(TargetInfo info) {
+ if (info.isDisplayResolveInfo()) {
+ mTargetDataLoader.getOrLoadLabel((DisplayResolveInfo) info);
+ }
+ CharSequence displayLabel = info.getDisplayLabel();
+ return displayLabel == null ? "" : displayLabel;
+ }
}
diff --git a/java/src/com/android/intentresolver/ResolverInfoHelpers.kt b/java/src/com/android/intentresolver/ResolverInfoHelpers.kt
new file mode 100644
index 00000000..8d1d8658
--- /dev/null
+++ b/java/src/com/android/intentresolver/ResolverInfoHelpers.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("ResolveInfoHelpers")
+
+package com.android.intentresolver
+
+import android.content.pm.ActivityInfo
+import android.content.pm.ResolveInfo
+
+fun resolveInfoMatch(lhs: ResolveInfo?, rhs: ResolveInfo?): Boolean =
+ (lhs === rhs) ||
+ ((lhs != null && rhs != null) &&
+ activityInfoMatch(lhs.activityInfo, rhs.activityInfo) &&
+ // Comparing against resolveInfo.userHandle in case cloned apps are present,
+ // as they will have the same activityInfo.
+ lhs.userHandle == rhs.userHandle)
+
+private fun activityInfoMatch(lhs: ActivityInfo?, rhs: ActivityInfo?): Boolean =
+ (lhs === rhs) ||
+ (lhs != null && rhs != null && lhs.name == rhs.name && lhs.packageName == rhs.packageName)
diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java
index 282a672f..564d8d19 100644
--- a/java/src/com/android/intentresolver/ResolverListAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverListAdapter.java
@@ -16,8 +16,6 @@
package com.android.intentresolver;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
@@ -27,6 +25,7 @@ import android.content.pm.ResolveInfo;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
import android.graphics.drawable.Drawable;
+import android.net.Uri;
import android.os.AsyncTask;
import android.os.RemoteException;
import android.os.Trace;
@@ -42,8 +41,14 @@ import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.WorkerThread;
+
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.icons.LabelInfo;
import com.android.intentresolver.icons.TargetDataLoader;
import com.android.internal.annotations.VisibleForTesting;
@@ -53,6 +58,8 @@ import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicBoolean;
public class ResolverListAdapter extends BaseAdapter {
private static final String TAG = "ResolverListAdapter";
@@ -63,7 +70,7 @@ public class ResolverListAdapter extends BaseAdapter {
protected final Context mContext;
protected final LayoutInflater mInflater;
protected final ResolverListCommunicator mResolverListCommunicator;
- protected final ResolverListController mResolverListController;
+ public final ResolverListController mResolverListController;
private final List<Intent> mIntents;
private final Intent[] mInitialIntents;
@@ -75,6 +82,9 @@ public class ResolverListAdapter extends BaseAdapter {
private final Set<DisplayResolveInfo> mRequestedIcons = new HashSet<>();
private final Set<DisplayResolveInfo> mRequestedLabels = new HashSet<>();
+ private final Executor mBgExecutor;
+ private final Executor mCallbackExecutor;
+ private final AtomicBoolean mDestroyed = new AtomicBoolean();
private ResolveInfo mLastChosen;
private DisplayResolveInfo mOtherProfile;
@@ -86,7 +96,6 @@ public class ResolverListAdapter extends BaseAdapter {
private int mLastChosenPosition = -1;
private final boolean mFilterLastUsed;
- private Runnable mPostListReadyRunnable;
private boolean mIsTabLoaded;
// Represents the UserSpace in which the Initial Intents should be resolved.
private final UserHandle mInitialIntentsUserSpace;
@@ -103,6 +112,37 @@ public class ResolverListAdapter extends BaseAdapter {
ResolverListCommunicator resolverListCommunicator,
UserHandle initialIntentsUserSpace,
TargetDataLoader targetDataLoader) {
+ this(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ initialIntentsUserSpace,
+ targetDataLoader,
+ AsyncTask.SERIAL_EXECUTOR,
+ runnable -> context.getMainThreadHandler().post(runnable));
+ }
+
+ @VisibleForTesting
+ public ResolverListAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ ResolverListController resolverListController,
+ UserHandle userHandle,
+ Intent targetIntent,
+ ResolverListCommunicator resolverListCommunicator,
+ UserHandle initialIntentsUserSpace,
+ TargetDataLoader targetDataLoader,
+ Executor bgExecutor,
+ Executor callbackExecutor) {
mContext = context;
mIntents = payloadIntents;
mInitialIntents = initialIntents;
@@ -117,6 +157,12 @@ public class ResolverListAdapter extends BaseAdapter {
mTargetIntent = targetIntent;
mResolverListCommunicator = resolverListCommunicator;
mInitialIntentsUserSpace = initialIntentsUserSpace;
+ mBgExecutor = bgExecutor;
+ mCallbackExecutor = callbackExecutor;
+ }
+
+ protected Intent getTargetIntent() {
+ return mTargetIntent;
}
public final DisplayResolveInfo getFirstDisplayResolveInfo() {
@@ -189,18 +235,18 @@ public class ResolverListAdapter extends BaseAdapter {
packageName, userHandle, action);
}
- List<ResolvedComponentInfo> getUnfilteredResolveList() {
+ public List<ResolvedComponentInfo> getUnfilteredResolveList() {
return mUnfilteredResolveList;
}
/**
* Rebuild the list of resolvers. When rebuilding is complete, queue the {@code onPostListReady}
- * callback on the main handler with {@code rebuildCompleted} true.
+ * callback on the callback executor with {@code rebuildCompleted} true.
*
* In some cases some parts will need some asynchronous work to complete. Then this will first
- * immediately queue {@code onPostListReady} (on the main handler) with {@code rebuildCompleted}
- * false; only when the asynchronous work completes will this then go on to queue another
- * {@code onPostListReady} callback with {@code rebuildCompleted} true.
+ * immediately queue {@code onPostListReady} (on the callback executor) with
+ * {@code rebuildCompleted} false; only when the asynchronous work completes will this then go
+ * on to queue another {@code onPostListReady} callback with {@code rebuildCompleted} true.
*
* The {@code doPostProcessing} parameter is used to specify whether to update the UI and
* load additional targets (e.g. direct share) after the list has been rebuilt. We may choose
@@ -212,7 +258,7 @@ public class ResolverListAdapter extends BaseAdapter {
* with {@code rebuildCompleted} true at the end of some newly-launched asynchronous work.
* Otherwise the callback is only queued once, with {@code rebuildCompleted} true.
*/
- protected boolean rebuildList(boolean doPostProcessing) {
+ public boolean rebuildList(boolean doPostProcessing) {
Trace.beginSection("ResolverListAdapter#rebuildList");
mDisplayList.clear();
mIsTabLoaded = false;
@@ -357,8 +403,8 @@ public class ResolverListAdapter extends BaseAdapter {
otherProfileInfo,
mPm,
mTargetIntent,
- mResolverListCommunicator,
- mTargetDataLoader);
+ mResolverListCommunicator
+ );
} else {
mOtherProfile = null;
try {
@@ -402,35 +448,42 @@ public class ResolverListAdapter extends BaseAdapter {
// Send an "incomplete" list-ready while the async task is running.
postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ false);
- createSortingTask(doPostProcessing).execute(filteredResolveList);
+ mBgExecutor.execute(() -> {
+ List<ResolvedComponentInfo> sortedComponents = null;
+ //TODO: the try-catch logic here is to formally match the AsyncTask's behavior.
+ // Empirically, we don't need it as in the case on an exception, the app will crash and
+ // `onComponentsSorted` won't be invoked.
+ try {
+ sortComponents(filteredResolveList);
+ sortedComponents = filteredResolveList;
+ } catch (Throwable t) {
+ Log.e(TAG, "Failed to sort components", t);
+ throw t;
+ } finally {
+ final List<ResolvedComponentInfo> result = sortedComponents;
+ mCallbackExecutor.execute(() -> onComponentsSorted(result, doPostProcessing));
+ }
+ });
return false;
}
- AsyncTask<List<ResolvedComponentInfo>,
- Void,
- List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) {
- return new AsyncTask<List<ResolvedComponentInfo>,
- Void,
- List<ResolvedComponentInfo>>() {
- @Override
- protected List<ResolvedComponentInfo> doInBackground(
- List<ResolvedComponentInfo>... params) {
- mResolverListController.sort(params[0]);
- return params[0];
- }
- @Override
- protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
- processSortedList(sortedComponents, doPostProcessing);
- notifyDataSetChanged();
- if (doPostProcessing) {
- mResolverListCommunicator.updateProfileViewButton();
- }
- }
- };
+ @WorkerThread
+ protected void sortComponents(List<ResolvedComponentInfo> components) {
+ mResolverListController.sort(components);
}
- protected void processSortedList(List<ResolvedComponentInfo> sortedComponents,
- boolean doPostProcessing) {
+ @MainThread
+ protected void onComponentsSorted(
+ @Nullable List<ResolvedComponentInfo> sortedComponents, boolean doPostProcessing) {
+ processSortedList(sortedComponents, doPostProcessing);
+ notifyDataSetChanged();
+ if (doPostProcessing) {
+ mResolverListCommunicator.updateProfileViewButton();
+ }
+ }
+
+ protected void processSortedList(
+ @Nullable List<ResolvedComponentInfo> sortedComponents, boolean doPostProcessing) {
final int n = sortedComponents != null ? sortedComponents.size() : 0;
Trace.beginSection("ResolverListAdapter#processSortedList:" + n);
if (n != 0) {
@@ -471,8 +524,7 @@ public class ResolverListAdapter extends BaseAdapter {
ri,
ri.loadLabel(mPm),
null,
- ii,
- mTargetDataLoader.createPresentationGetter(ri)));
+ ii));
}
}
@@ -494,23 +546,23 @@ public class ResolverListAdapter extends BaseAdapter {
/**
* Some necessary methods for creating the list are initiated in onCreate and will also
* determine the layout known. We therefore can't update the UI inline and post to the
- * handler thread to update after the current task is finished.
+ * callback executor to update after the current task is finished.
* @param doPostProcessing Whether to update the UI and load additional direct share targets
* after the list has been rebuilt
* @param rebuildCompleted Whether the list has been completely rebuilt
*/
- void postListReadyRunnable(boolean doPostProcessing, boolean rebuildCompleted) {
- if (mPostListReadyRunnable == null) {
- mPostListReadyRunnable = new Runnable() {
- @Override
- public void run() {
- mResolverListCommunicator.onPostListReady(ResolverListAdapter.this,
- doPostProcessing, rebuildCompleted);
- mPostListReadyRunnable = null;
+ public void postListReadyRunnable(boolean doPostProcessing, boolean rebuildCompleted) {
+ Runnable listReadyRunnable = new Runnable() {
+ @Override
+ public void run() {
+ if (mDestroyed.get()) {
+ return;
}
- };
- mContext.getMainThreadHandler().post(mPostListReadyRunnable);
- }
+ mResolverListCommunicator.onPostListReady(ResolverListAdapter.this,
+ doPostProcessing, rebuildCompleted);
+ }
+ };
+ mCallbackExecutor.execute(listReadyRunnable);
}
private void addResolveInfoWithAlternates(ResolvedComponentInfo rci) {
@@ -524,8 +576,7 @@ public class ResolverListAdapter extends BaseAdapter {
final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
intent,
add,
- (replaceIntent != null) ? replaceIntent : defaultIntent,
- mTargetDataLoader.createPresentationGetter(add));
+ (replaceIntent != null) ? replaceIntent : defaultIntent);
dri.setPinned(rci.isPinned());
if (rci.isPinned()) {
Log.i(TAG, "Pinned item: " + rci.name);
@@ -572,7 +623,7 @@ public class ResolverListAdapter extends BaseAdapter {
protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {
// Checks if this info is already listed in display.
for (DisplayResolveInfo existingInfo : mDisplayList) {
- if (mResolverListCommunicator
+ if (ResolveInfoHelpers
.resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) {
return false;
}
@@ -710,27 +761,25 @@ public class ResolverListAdapter extends BaseAdapter {
}
}
- private void loadLabel(DisplayResolveInfo info) {
+ protected final void loadLabel(DisplayResolveInfo info) {
if (mRequestedLabels.add(info)) {
mTargetDataLoader.loadLabel(info, (result) -> onLabelLoaded(info, result));
}
}
protected final void onLabelLoaded(
- DisplayResolveInfo displayResolveInfo, CharSequence[] result) {
+ DisplayResolveInfo displayResolveInfo, LabelInfo result) {
if (displayResolveInfo.hasDisplayLabel()) {
return;
}
- displayResolveInfo.setDisplayLabel(result[0]);
- displayResolveInfo.setExtendedInfo(result[1]);
+ displayResolveInfo.setDisplayLabel(result.getLabel());
+ displayResolveInfo.setExtendedInfo(result.getSubLabel());
notifyDataSetChanged();
}
public void onDestroy() {
- if (mPostListReadyRunnable != null) {
- mContext.getMainThreadHandler().removeCallbacks(mPostListReadyRunnable);
- mPostListReadyRunnable = null;
- }
+ mDestroyed.set(true);
+
if (mResolverListController != null) {
mResolverListController.destroy();
}
@@ -765,7 +814,7 @@ public class ResolverListAdapter extends BaseAdapter {
return mContext.getDrawable(R.drawable.resolver_icon_placeholder);
}
- void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) {
+ public void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) {
final DisplayResolveInfo iconInfo = getFilteredItem();
if (iconInfo != null) {
mTargetDataLoader.loadAppTargetIcon(
@@ -777,7 +826,7 @@ public class ResolverListAdapter extends BaseAdapter {
return mUserHandle;
}
- protected List<ResolvedComponentInfo> getResolversForUser(UserHandle userHandle) {
+ public final List<ResolvedComponentInfo> getResolversForUser(UserHandle userHandle) {
return mResolverListController.getResolversForIntentAsUser(
/* shouldGetResolvedFilter= */ true,
mResolverListCommunicator.shouldGetActivityMetadata(),
@@ -786,15 +835,16 @@ public class ResolverListAdapter extends BaseAdapter {
userHandle);
}
- protected List<Intent> getIntents() {
+ public final List<Intent> getIntents() {
+ // TODO: immutable copy?
return mIntents;
}
- protected boolean isTabLoaded() {
+ public boolean isTabLoaded() {
return mIsTabLoaded;
}
- protected void markTabLoaded() {
+ public void markTabLoaded() {
mIsTabLoaded = true;
}
@@ -828,8 +878,7 @@ public class ResolverListAdapter extends BaseAdapter {
ResolvedComponentInfo resolvedComponentInfo,
PackageManager pm,
Intent targetIntent,
- ResolverListCommunicator resolverListCommunicator,
- TargetDataLoader targetDataLoader) {
+ ResolverListCommunicator resolverListCommunicator) {
ResolveInfo resolveInfo = resolvedComponentInfo.getResolveInfoAt(0);
Intent pOrigIntent = resolverListCommunicator.getReplacementIntent(
@@ -838,25 +887,19 @@ public class ResolverListAdapter extends BaseAdapter {
Intent replacementIntent = resolverListCommunicator.getReplacementIntent(
resolveInfo.activityInfo, targetIntent);
- TargetPresentationGetter presentationGetter =
- targetDataLoader.createPresentationGetter(resolveInfo);
-
return DisplayResolveInfo.newDisplayResolveInfo(
resolvedComponentInfo.getIntentAt(0),
resolveInfo,
resolveInfo.loadLabel(pm),
resolveInfo.loadLabel(pm),
- pOrigIntent != null ? pOrigIntent : replacementIntent,
- presentationGetter);
+ pOrigIntent != null ? pOrigIntent : replacementIntent);
}
/**
* Necessary methods to communicate between {@link ResolverListAdapter}
* and {@link ResolverActivity}.
*/
- interface ResolverListCommunicator {
-
- boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs);
+ public interface ResolverListCommunicator {
Intent getReplacementIntent(ActivityInfo activityInfo, Intent defIntent);
@@ -893,6 +936,24 @@ public class ResolverListAdapter extends BaseAdapter {
public TextView text2;
public ImageView icon;
+ public final void reset() {
+ text.setText("");
+ text.setMaxLines(2);
+ text.setMaxWidth(Integer.MAX_VALUE);
+ text.setBackground(null);
+ text.setPaddingRelative(0, 0, 0, 0);
+
+ text2.setVisibility(View.GONE);
+ text2.setText("");
+
+ itemView.setContentDescription(null);
+ itemView.setBackground(defaultItemViewBackground);
+
+ icon.setImageDrawable(null);
+ icon.setColorFilter(null);
+ icon.clearAnimation();
+ }
+
@VisibleForTesting
public ViewHolder(View view) {
itemView = view;
@@ -937,5 +998,19 @@ public class ResolverListAdapter extends BaseAdapter {
icon.setColorFilter(null);
}
}
+
+ public void bindPlaceholder() {
+ itemView.setBackground(null);
+ }
+
+ public void bindGroupIndicator(Drawable indicator) {
+ text.setPaddingRelative(0, 0, /*end = */indicator.getIntrinsicWidth(), 0);
+ text.setBackground(indicator);
+ }
+
+ public void bindPinnedIndicator(Drawable indicator) {
+ text.setPaddingRelative(/*start = */indicator.getIntrinsicWidth(), 0, 0, 0);
+ text.setBackground(indicator);
+ }
}
}
diff --git a/java/src/com/android/intentresolver/ResolverListController.java b/java/src/com/android/intentresolver/ResolverListController.java
index d5a5fedf..e88d766d 100644
--- a/java/src/com/android/intentresolver/ResolverListController.java
+++ b/java/src/com/android/intentresolver/ResolverListController.java
@@ -17,7 +17,6 @@
package com.android.intentresolver;
-import android.annotation.WorkerThread;
import android.app.ActivityManager;
import android.app.AppGlobals;
import android.content.ComponentName;
@@ -31,6 +30,8 @@ import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Log;
+import androidx.annotation.WorkerThread;
+
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.model.AbstractResolverComparator;
@@ -254,7 +255,6 @@ public class ResolverListController {
isComputed = true;
}
- @VisibleForTesting
@WorkerThread
public void sort(List<ResolvedComponentInfo> inputList) {
try {
@@ -273,7 +273,6 @@ public class ResolverListController {
}
}
- @VisibleForTesting
@WorkerThread
public void topK(List<ResolvedComponentInfo> inputList, int k) {
if (inputList == null || inputList.isEmpty() || k <= 0) {
@@ -335,7 +334,7 @@ public class ResolverListController {
&& ai.name.equals(b.name.getClassName());
}
- boolean isComponentFiltered(ComponentName componentName) {
+ public boolean isComponentFiltered(ComponentName componentName) {
return false;
}
diff --git a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
index 85d97ad5..591c23b7 100644
--- a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
@@ -24,6 +24,7 @@ import android.widget.ListView;
import androidx.viewpager.widget.PagerAdapter;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
import com.android.internal.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
@@ -36,10 +37,10 @@ import java.util.function.Supplier;
*/
@VisibleForTesting
public class ResolverMultiProfilePagerAdapter extends
- GenericMultiProfilePagerAdapter<ListView, ResolverListAdapter, ResolverListAdapter> {
+ MultiProfilePagerAdapter<ListView, ResolverListAdapter, ResolverListAdapter> {
private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier;
- ResolverMultiProfilePagerAdapter(
+ public ResolverMultiProfilePagerAdapter(
Context context,
ResolverListAdapter adapter,
EmptyStateProvider emptyStateProvider,
@@ -57,14 +58,14 @@ public class ResolverMultiProfilePagerAdapter extends
new BottomPaddingOverrideSupplier());
}
- ResolverMultiProfilePagerAdapter(Context context,
- ResolverListAdapter personalAdapter,
- ResolverListAdapter workAdapter,
- EmptyStateProvider emptyStateProvider,
- Supplier<Boolean> workProfileQuietModeChecker,
- @Profile int defaultProfile,
- UserHandle workProfileUserHandle,
- UserHandle cloneProfileUserHandle) {
+ public ResolverMultiProfilePagerAdapter(Context context,
+ ResolverListAdapter personalAdapter,
+ ResolverListAdapter workAdapter,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ @Profile int defaultProfile,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle) {
this(
context,
ImmutableList.of(personalAdapter, workAdapter),
@@ -86,7 +87,6 @@ public class ResolverMultiProfilePagerAdapter extends
UserHandle cloneProfileUserHandle,
BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {
super(
- context,
listAdapter -> listAdapter,
(listView, bindAdapter) -> listView.setAdapter(bindAdapter),
listAdapters,
diff --git a/java/src/com/android/intentresolver/ResolverViewPager.java b/java/src/com/android/intentresolver/ResolverViewPager.java
index 0804a2b8..0496579d 100644
--- a/java/src/com/android/intentresolver/ResolverViewPager.java
+++ b/java/src/com/android/intentresolver/ResolverViewPager.java
@@ -69,7 +69,7 @@ public class ResolverViewPager extends ViewPager {
* Sets whether swiping sideways should happen.
* <p>Note that swiping is always disabled for RTL layouts (b/159110029 for context).
*/
- void setSwipingEnabled(boolean swipingEnabled) {
+ public void setSwipingEnabled(boolean swipingEnabled) {
mSwipingEnabled = swipingEnabled;
}
diff --git a/java/src/com/android/intentresolver/ShortcutSelectionLogic.java b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java
index 645b9391..efaaf894 100644
--- a/java/src/com/android/intentresolver/ShortcutSelectionLogic.java
+++ b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java
@@ -16,7 +16,6 @@
package com.android.intentresolver;
-import android.annotation.Nullable;
import android.app.prediction.AppTarget;
import android.content.Context;
import android.content.Intent;
@@ -26,6 +25,8 @@ import android.content.pm.ShortcutInfo;
import android.service.chooser.ChooserTarget;
import android.util.Log;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.SelectableTargetInfo;
import com.android.intentresolver.chooser.TargetInfo;
diff --git a/java/src/com/android/intentresolver/SimpleIconFactory.java b/java/src/com/android/intentresolver/SimpleIconFactory.java
index ec5179ac..750b24ac 100644
--- a/java/src/com/android/intentresolver/SimpleIconFactory.java
+++ b/java/src/com/android/intentresolver/SimpleIconFactory.java
@@ -21,9 +21,6 @@ import static android.graphics.Paint.DITHER_FLAG;
import static android.graphics.Paint.FILTER_BITMAP_FLAG;
import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction;
-import android.annotation.AttrRes;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.ActivityManager;
import android.content.Context;
import android.content.pm.PackageManager;
@@ -50,6 +47,10 @@ import android.util.AttributeSet;
import android.util.Pools.SynchronizedPool;
import android.util.TypedValue;
+import androidx.annotation.AttrRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import com.android.internal.annotations.VisibleForTesting;
import org.xmlpull.v1.XmlPullParser;
@@ -719,10 +720,18 @@ public class SimpleIconFactory {
}
@Override
- public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) { }
+ public void inflate(
+ @NonNull Resources r,
+ @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs) {
+ }
@Override
- public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) { }
+ public void inflate(
+ @NonNull Resources r,
+ @NonNull XmlPullParser parser,
+ @NonNull AttributeSet attrs, Theme theme) {
+ }
/**
* Sets the scale associated with this drawable
diff --git a/java/src/com/android/intentresolver/TargetPresentationGetter.java b/java/src/com/android/intentresolver/TargetPresentationGetter.java
index f8b36566..910c65c9 100644
--- a/java/src/com/android/intentresolver/TargetPresentationGetter.java
+++ b/java/src/com/android/intentresolver/TargetPresentationGetter.java
@@ -16,7 +16,6 @@
package com.android.intentresolver;
-import android.annotation.Nullable;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
@@ -30,6 +29,8 @@ import android.os.UserHandle;
import android.text.TextUtils;
import android.util.Log;
+import androidx.annotation.Nullable;
+
/**
* Loads the icon and label for the provided ApplicationInfo. Defaults to using the application icon
* and label over any IntentFilter or Activity icon to increase user understanding, with an
@@ -37,7 +38,7 @@ import android.util.Log;
* resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses
* Strings to strip creative formatting.
*
- * Use one of the {@link TargetPresentationGetter#Factory} methods to create an instance of the
+ * Use one of the {@link TargetPresentationGetter.Factory} methods to create an instance of the
* appropriate concrete type.
*
* TODO: once this component (and its tests) are merged, it should be possible to refactor and
diff --git a/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java b/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
index 8b9bfb32..074537ef 100644
--- a/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
@@ -16,6 +16,8 @@
package com.android.intentresolver.chooser;
+import android.service.chooser.ChooserTarget;
+
import java.util.ArrayList;
import java.util.Arrays;
diff --git a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
index 09cf319f..536f11ce 100644
--- a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
+++ b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
@@ -16,8 +16,6 @@
package com.android.intentresolver.chooser;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Intent;
@@ -27,10 +25,10 @@ import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.UserHandle;
-import com.android.intentresolver.TargetPresentationGetter;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.List;
/**
@@ -39,12 +37,11 @@ import java.util.List;
*/
public class DisplayResolveInfo implements TargetInfo {
private final ResolveInfo mResolveInfo;
- private CharSequence mDisplayLabel;
- private CharSequence mExtendedInfo;
+ private volatile CharSequence mDisplayLabel;
+ private volatile CharSequence mExtendedInfo;
private final Intent mResolvedIntent;
private final List<Intent> mSourceIntents = new ArrayList<>();
private final boolean mIsSuspended;
- private TargetPresentationGetter mPresentationGetter;
private boolean mPinned = false;
private final IconHolder mDisplayIconHolder = new SettableIconHolder();
@@ -52,15 +49,13 @@ public class DisplayResolveInfo implements TargetInfo {
public static DisplayResolveInfo newDisplayResolveInfo(
Intent originalIntent,
ResolveInfo resolveInfo,
- @NonNull Intent resolvedIntent,
- @Nullable TargetPresentationGetter presentationGetter) {
+ @NonNull Intent resolvedIntent) {
return newDisplayResolveInfo(
originalIntent,
resolveInfo,
/* displayLabel=*/ null,
/* extendedInfo=*/ null,
- resolvedIntent,
- presentationGetter);
+ resolvedIntent);
}
/** Create a new {@code DisplayResolveInfo} instance. */
@@ -69,15 +64,13 @@ public class DisplayResolveInfo implements TargetInfo {
ResolveInfo resolveInfo,
CharSequence displayLabel,
CharSequence extendedInfo,
- @NonNull Intent resolvedIntent,
- @Nullable TargetPresentationGetter presentationGetter) {
+ @NonNull Intent resolvedIntent) {
return new DisplayResolveInfo(
originalIntent,
resolveInfo,
displayLabel,
extendedInfo,
- resolvedIntent,
- presentationGetter);
+ resolvedIntent);
}
private DisplayResolveInfo(
@@ -85,13 +78,11 @@ public class DisplayResolveInfo implements TargetInfo {
ResolveInfo resolveInfo,
CharSequence displayLabel,
CharSequence extendedInfo,
- @NonNull Intent resolvedIntent,
- @Nullable TargetPresentationGetter presentationGetter) {
+ @NonNull Intent resolvedIntent) {
mSourceIntents.add(originalIntent);
mResolveInfo = resolveInfo;
mDisplayLabel = displayLabel;
mExtendedInfo = extendedInfo;
- mPresentationGetter = presentationGetter;
final ActivityInfo ai = mResolveInfo.activityInfo;
mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
@@ -101,8 +92,7 @@ public class DisplayResolveInfo implements TargetInfo {
private DisplayResolveInfo(
DisplayResolveInfo other,
- @Nullable Intent baseIntentToSend,
- TargetPresentationGetter presentationGetter) {
+ @Nullable Intent baseIntentToSend) {
mSourceIntents.addAll(other.getAllSourceIntents());
mResolveInfo = other.mResolveInfo;
mIsSuspended = other.mIsSuspended;
@@ -112,7 +102,6 @@ public class DisplayResolveInfo implements TargetInfo {
mResolvedIntent = createResolvedIntent(
baseIntentToSend == null ? other.mResolvedIntent : baseIntentToSend,
mResolveInfo.activityInfo);
- mPresentationGetter = presentationGetter;
mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());
}
@@ -124,7 +113,6 @@ public class DisplayResolveInfo implements TargetInfo {
mDisplayLabel = other.mDisplayLabel;
mExtendedInfo = other.mExtendedInfo;
mResolvedIntent = other.mResolvedIntent;
- mPresentationGetter = other.mPresentationGetter;
mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());
}
@@ -147,10 +135,6 @@ public class DisplayResolveInfo implements TargetInfo {
}
public CharSequence getDisplayLabel() {
- if (mDisplayLabel == null && mPresentationGetter != null) {
- mDisplayLabel = mPresentationGetter.getLabel();
- mExtendedInfo = mPresentationGetter.getSubLabel();
- }
return mDisplayLabel;
}
@@ -186,8 +170,7 @@ public class DisplayResolveInfo implements TargetInfo {
return new DisplayResolveInfo(
this,
- TargetInfo.mergeRefinementIntoMatchingBaseIntent(matchingBase, proposedRefinement),
- mPresentationGetter);
+ TargetInfo.mergeRefinementIntoMatchingBaseIntent(matchingBase, proposedRefinement));
}
@Override
@@ -197,7 +180,7 @@ public class DisplayResolveInfo implements TargetInfo {
@Override
public ArrayList<DisplayResolveInfo> getAllDisplayTargets() {
- return new ArrayList<>(Arrays.asList(this));
+ return new ArrayList<>(List.of(this));
}
public void addAlternateSourceIntent(Intent alt) {
diff --git a/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java b/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java
index 10d4415a..50aaec0b 100644
--- a/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java
@@ -16,8 +16,6 @@
package com.android.intentresolver.chooser;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.prediction.AppTarget;
import android.content.ComponentName;
@@ -27,8 +25,11 @@ import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.os.Bundle;
import android.os.UserHandle;
+import android.service.chooser.ChooserTarget;
import android.util.HashedStringCache;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import com.google.common.collect.ImmutableList;
@@ -43,7 +44,7 @@ import java.util.List;
public final class ImmutableTargetInfo implements TargetInfo {
private static final String TAG = "TargetInfo";
- /** Delegate interface to implement {@link TargetInfo#getHashedTargetIdForMetrics()}. */
+ /** Delegate interface to implement {@link TargetInfo#getHashedTargetIdForMetrics}. */
public interface TargetHashProvider {
/** Request a hash for the specified {@code target}. */
HashedStringCache.HashResult getHashedTargetIdForMetrics(
@@ -53,15 +54,15 @@ public final class ImmutableTargetInfo implements TargetInfo {
/** Delegate interface to request that the target be launched by a particular API. */
public interface TargetActivityStarter {
/**
- * Request that the delegate use the {@link Activity#startAsCaller()} API to launch the
- * specified {@code target}.
+ * Request that the delegate use the {@link Activity#startActivityAsCaller} API to launch
+ * the specified {@code target}.
*
* @return true if the target was launched successfully.
*/
boolean startAsCaller(TargetInfo target, Activity activity, Bundle options, int userId);
/**
- * Request that the delegate use the {@link Activity#startAsUser()} API to launch the
+ * Request that the delegate use the {@link Activity#startActivityAsUser} API to launch the
* specified {@code target}.
*
* @return true if the target was launched successfully.
@@ -145,7 +146,7 @@ public final class ImmutableTargetInfo implements TargetInfo {
/**
* Configure an {@link Intent} to be built in to the output target as the "base intent to
* send," which may be a refinement of any of our source targets. This is private because
- * it's only used internally by {@link #tryToCloneWithAppliedRefinement()}; if it's ever
+ * it's only used internally by {@link #tryToCloneWithAppliedRefinement}; if it's ever
* expanded, the builder should probably be responsible for enforcing the refinement check.
*/
private Builder setBaseIntentToSend(Intent baseIntent) {
@@ -229,8 +230,8 @@ public final class ImmutableTargetInfo implements TargetInfo {
/**
* Configure the full list of source intents we could resolve for this target. This is
- * effectively the same as calling {@link #setResolvedIntent()} with the first element of
- * the list, and {@link #setAlternateSourceIntents()} with the remainder (or clearing those
+ * effectively the same as calling {@link #setResolvedIntent} with the first element of
+ * the list, and {@link #setAlternateSourceIntents} with the remainder (or clearing those
* fields on the builder if there are no corresponding elements in the list).
*/
public Builder setAllSourceIntents(List<Intent> sourceIntents) {
diff --git a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
index 6444e13b..46803a04 100644
--- a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
@@ -16,7 +16,6 @@
package com.android.intentresolver.chooser;
-import android.annotation.Nullable;
import android.app.Activity;
import android.content.Context;
import android.graphics.drawable.AnimatedVectorDrawable;
@@ -24,6 +23,8 @@ import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.R;
import java.util.function.Supplier;
diff --git a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
index 5766db0e..c4aa9021 100644
--- a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
@@ -16,7 +16,6 @@
package com.android.intentresolver.chooser;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.prediction.AppTarget;
import android.content.ComponentName;
@@ -33,6 +32,8 @@ import android.text.SpannableStringBuilder;
import android.util.HashedStringCache;
import android.util.Log;
+import androidx.annotation.Nullable;
+
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import java.util.ArrayList;
diff --git a/java/src/com/android/intentresolver/chooser/TargetInfo.java b/java/src/com/android/intentresolver/chooser/TargetInfo.java
index 9d793994..ba6c3c05 100644
--- a/java/src/com/android/intentresolver/chooser/TargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/TargetInfo.java
@@ -17,14 +17,15 @@
package com.android.intentresolver.chooser;
-import android.annotation.Nullable;
import android.app.Activity;
import android.app.prediction.AppTarget;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.SharedPreferences;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.UserHandle;
@@ -32,6 +33,12 @@ import android.service.chooser.ChooserTarget;
import android.text.TextUtils;
import android.util.HashedStringCache;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.ChooserRefinementManager;
+import com.android.intentresolver.ResolverActivity;
+
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
@@ -187,9 +194,9 @@ public interface TargetInfo {
* Attempt to apply a {@code proposedRefinement} that the {@link ChooserRefinementManager}
* received from the caller's refinement flow. This may succeed only if the target has a source
* intent that matches the filtering parameters of the proposed refinement (according to
- * {@link Intent#filterEquals()}). Then the first such match is the "base intent," and the
- * proposed refinement is merged into that base (via {@link Intent#fillIn()}; this can never
- * result in a change to the {@link Intent#filterEquals()} status of the base, but may e.g. add
+ * {@link Intent#filterEquals}). Then the first such match is the "base intent," and the
+ * proposed refinement is merged into that base (via {@link Intent#fillIn}; this can never
+ * result in a change to the {@link Intent#filterEquals} status of the base, but may e.g. add
* new "extras" that weren't previously given in the base intent).
*
* @return a copy of this {@link TargetInfo} where the "base intent to send" is the result of
@@ -280,7 +287,7 @@ public interface TargetInfo {
}
/**
- * @return the {@link ShortcutManager} data for any shortcut associated with this target.
+ * @return the {@link ShortcutInfo} for any shortcut associated with this target.
*/
@Nullable
default ShortcutInfo getDirectShareShortcutInfo() {
@@ -422,7 +429,7 @@ public interface TargetInfo {
/**
* @return true if this target should be logged with the "direct_share" metrics category in
- * {@link ResolverActivity#maybeLogCrossProfileTargetLaunch()}. This is defined for legacy
+ * {@link ResolverActivity#maybeLogCrossProfileTargetLaunch}. This is defined for legacy
* compatibility and is <em>not</em> likely to be a good indicator of whether this is actually a
* "direct share" target (e.g. because it historically also applies to "empty" and "placeholder"
* targets).
diff --git a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt
index 103e8bf4..10ee5af1 100644
--- a/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt
+++ b/java/src/com/android/intentresolver/contentpreview/BasePreviewViewModel.kt
@@ -16,6 +16,7 @@
package com.android.intentresolver.contentpreview
+import android.content.Intent
import androidx.annotation.MainThread
import androidx.lifecycle.ViewModel
import com.android.intentresolver.ChooserRequestParameters
@@ -24,7 +25,7 @@ import com.android.intentresolver.ChooserRequestParameters
abstract class BasePreviewViewModel : ViewModel() {
@MainThread
abstract fun createOrReuseProvider(
- chooserRequest: ChooserRequestParameters
+ targetIntent: Intent
): PreviewDataProvider
@MainThread abstract fun createOrReuseImageLoader(): ImageLoader
diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
index d279f11f..a015147d 100644
--- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
@@ -16,8 +16,6 @@
package com.android.intentresolver.contentpreview;
-import static androidx.lifecycle.LifecycleKt.getCoroutineScope;
-
import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE;
import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE;
import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT;
@@ -28,11 +26,11 @@ import android.content.res.Resources;
import android.net.Uri;
import android.text.TextUtils;
import android.view.LayoutInflater;
+import android.view.View;
import android.view.ViewGroup;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
-import androidx.lifecycle.Lifecycle;
import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback;
@@ -40,6 +38,8 @@ import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatu
import java.util.List;
import java.util.function.Consumer;
+import kotlinx.coroutines.CoroutineScope;
+
/**
* Collection of helpers for building the content preview UI displayed in
* {@link com.android.intentresolver.ChooserActivity}.
@@ -47,7 +47,7 @@ import java.util.function.Consumer;
*/
public final class ChooserContentPreviewUi {
- private final Lifecycle mLifecycle;
+ private final CoroutineScope mScope;
/**
* Delegate to build the default system action buttons to display in the preview layout, if/when
@@ -92,14 +92,14 @@ public final class ChooserContentPreviewUi {
final ContentPreviewUi mContentPreviewUi;
public ChooserContentPreviewUi(
- Lifecycle lifecycle,
+ CoroutineScope scope,
PreviewDataProvider previewData,
Intent targetIntent,
ImageLoader imageLoader,
ActionFactory actionFactory,
TransitionElementStatusCallback transitionElementStatusCallback,
HeadlineGenerator headlineGenerator) {
- mLifecycle = lifecycle;
+ mScope = scope;
mContentPreviewUi = createContentPreview(
previewData,
targetIntent,
@@ -125,7 +125,7 @@ public final class ChooserContentPreviewUi {
int previewType = previewData.getPreviewType();
if (previewType == CONTENT_PREVIEW_TEXT) {
return createTextPreview(
- mLifecycle,
+ mScope,
targetIntent,
actionFactory,
imageLoader,
@@ -137,8 +137,7 @@ public final class ChooserContentPreviewUi {
actionFactory,
headlineGenerator);
if (previewData.getUriCount() > 0) {
- previewData.getFirstFileName(
- mLifecycle, fileContentPreviewUi::setFirstFileName);
+ previewData.getFirstFileName(mScope, fileContentPreviewUi::setFirstFileName);
}
return fileContentPreviewUi;
}
@@ -148,7 +147,7 @@ public final class ChooserContentPreviewUi {
if (!TextUtils.isEmpty(text)) {
FilesPlusTextContentPreviewUi previewUi =
new FilesPlusTextContentPreviewUi(
- mLifecycle,
+ mScope,
isSingleImageShare,
previewData.getUriCount(),
targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT),
@@ -159,7 +158,7 @@ public final class ChooserContentPreviewUi {
headlineGenerator);
if (previewData.getUriCount() > 0) {
JavaFlowHelper.collectToList(
- getCoroutineScope(mLifecycle),
+ mScope,
previewData.getImagePreviewFileInfoFlow(),
previewUi::updatePreviewMetadata);
}
@@ -167,7 +166,7 @@ public final class ChooserContentPreviewUi {
}
return new UnifiedContentPreviewUi(
- getCoroutineScope(mLifecycle),
+ mScope,
isSingleImageShare,
targetIntent.getType(),
actionFactory,
@@ -188,19 +187,22 @@ public final class ChooserContentPreviewUi {
* specified {@code intent}.
*/
public ViewGroup displayContentPreview(
- Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ViewGroup parent,
+ @Nullable View headlineViewParent) {
- return mContentPreviewUi.display(resources, layoutInflater, parent);
+ return mContentPreviewUi.display(resources, layoutInflater, parent, headlineViewParent);
}
private static TextContentPreviewUi createTextPreview(
- Lifecycle lifecycle,
+ CoroutineScope scope,
Intent targetIntent,
ChooserContentPreviewUi.ActionFactory actionFactory,
ImageLoader imageLoader,
HeadlineGenerator headlineGenerator) {
CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
- String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE);
+ CharSequence previewTitle = targetIntent.getCharSequenceExtra(Intent.EXTRA_TITLE);
ClipData previewData = targetIntent.getClipData();
Uri previewThumbnail = null;
if (previewData != null) {
@@ -210,7 +212,7 @@ public final class ChooserContentPreviewUi {
}
}
return new TextContentPreviewUi(
- lifecycle,
+ scope,
sharingText,
previewTitle,
previewThumbnail,
diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java
index ebab147d..ad1c6c01 100644
--- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java
+++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java
@@ -18,7 +18,7 @@ package com.android.intentresolver.contentpreview;
import static java.lang.annotation.RetentionPolicy.SOURCE;
-import android.annotation.IntDef;
+import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
index 2d81794e..dce146b0 100644
--- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
@@ -24,10 +24,13 @@ import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.ViewStub;
import android.view.animation.DecelerateInterpolator;
import android.widget.ImageView;
import android.widget.TextView;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.R;
import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.ScrollableImagePreviewView;
@@ -40,7 +43,10 @@ abstract class ContentPreviewUi {
public abstract int getType();
public abstract ViewGroup display(
- Resources resources, LayoutInflater layoutInflater, ViewGroup parent);
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ViewGroup parent,
+ @Nullable View headlineViewParent);
protected static void updateViewWithImage(ImageView imageView, Bitmap image) {
if (image == null) {
@@ -57,23 +63,28 @@ abstract class ContentPreviewUi {
fadeAnim.start();
}
- protected static void displayHeadline(ViewGroup layout, String headline) {
- if (layout != null) {
- TextView titleView = layout.findViewById(R.id.headline);
- if (titleView != null) {
- if (!TextUtils.isEmpty(headline)) {
- titleView.setText(headline);
- titleView.setVisibility(View.VISIBLE);
- } else {
- titleView.setVisibility(View.GONE);
- }
- }
+ protected static void inflateHeadline(View layout) {
+ ViewStub stub = layout.findViewById(R.id.chooser_headline_row_stub);
+ if (stub != null) {
+ stub.inflate();
+ }
+ }
+
+ protected static void displayHeadline(View layout, String headline) {
+ TextView titleView = layout == null ? null : layout.findViewById(R.id.headline);
+ if (titleView == null) {
+ return;
+ }
+ if (!TextUtils.isEmpty(headline)) {
+ titleView.setText(headline);
+ titleView.setVisibility(View.VISIBLE);
+ } else {
+ titleView.setVisibility(View.GONE);
}
}
protected static void displayModifyShareAction(
- ViewGroup layout,
- ChooserContentPreviewUi.ActionFactory actionFactory) {
+ View layout, ChooserContentPreviewUi.ActionFactory actionFactory) {
ActionRow.Action modifyShareAction = actionFactory.getModifyShareAction();
if (modifyShareAction != null && layout != null) {
TextView modifyShareView = layout.findViewById(R.id.reselection_action);
diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java
index 20758189..89e7e528 100644
--- a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java
@@ -67,18 +67,30 @@ class FileContentPreviewUi extends ContentPreviewUi {
}
@Override
- public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
- ViewGroup layout = displayInternal(resources, layoutInflater, parent);
- displayModifyShareAction(layout, mActionFactory);
+ public ViewGroup display(
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ViewGroup parent,
+ @Nullable View headlineViewParent) {
+ ViewGroup layout = displayInternal(resources, layoutInflater, parent, headlineViewParent);
+ displayModifyShareAction(
+ headlineViewParent == null ? layout : headlineViewParent, mActionFactory);
return layout;
}
private ViewGroup displayInternal(
- Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ViewGroup parent,
+ @Nullable View headlineViewParent) {
mContentPreview = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_file, parent, false);
+ if (headlineViewParent == null) {
+ headlineViewParent = mContentPreview;
+ }
+ inflateHeadline(headlineViewParent);
- displayHeadline(mContentPreview, mHeadlineGenerator.getFilesHeadline(mFileCount));
+ displayHeadline(headlineViewParent, mHeadlineGenerator.getFilesHeadline(mFileCount));
if (mFileCount == 0) {
mContentPreview.setVisibility(View.GONE);
diff --git a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java
index 6e1212e9..78fc6586 100644
--- a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java
@@ -31,7 +31,6 @@ import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
-import androidx.lifecycle.Lifecycle;
import com.android.intentresolver.R;
import com.android.intentresolver.widget.ActionRow;
@@ -41,6 +40,8 @@ import java.util.HashMap;
import java.util.List;
import java.util.function.Consumer;
+import kotlinx.coroutines.CoroutineScope;
+
/**
* FilesPlusTextContentPreviewUi is shown when the user is sending 1 or more files along with
* non-empty EXTRA_TEXT. The text can be toggled with a checkbox. If a single image file is being
@@ -48,7 +49,7 @@ import java.util.function.Consumer;
* file content).
*/
class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
- private final Lifecycle mLifecycle;
+ private final CoroutineScope mScope;
@Nullable
private final String mIntentMimeType;
private final CharSequence mText;
@@ -59,6 +60,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
private final boolean mIsSingleImage;
private final int mFileCount;
private ViewGroup mContentPreviewView;
+ private View mHeadliveView;
private boolean mIsMetadataUpdated = false;
@Nullable
private Uri mFirstFilePreviewUri;
@@ -68,7 +70,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
private static final boolean SHOW_TOGGLE_CHECKMARK = false;
FilesPlusTextContentPreviewUi(
- Lifecycle lifecycle,
+ CoroutineScope scope,
boolean isSingleImage,
int fileCount,
CharSequence text,
@@ -81,7 +83,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
throw new IllegalArgumentException(
"fileCount = " + fileCount + " and isSingleImage = true");
}
- mLifecycle = lifecycle;
+ mScope = scope;
mIntentMimeType = intentMimeType;
mFileCount = fileCount;
mIsSingleImage = isSingleImage;
@@ -98,9 +100,14 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
}
@Override
- public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
- ViewGroup layout = displayInternal(layoutInflater, parent);
- displayModifyShareAction(layout, mActionFactory);
+ public ViewGroup display(
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ViewGroup parent,
+ @Nullable View headlineViewParent) {
+ ViewGroup layout = displayInternal(layoutInflater, parent, headlineViewParent);
+ displayModifyShareAction(
+ headlineViewParent == null ? layout : headlineViewParent, mActionFactory);
return layout;
}
@@ -118,13 +125,18 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
mFirstFilePreviewUri = files.isEmpty() ? null : files.get(0).getPreviewUri();
mIsMetadataUpdated = true;
if (mContentPreviewView != null) {
- updateUiWithMetadata(mContentPreviewView);
+ updateUiWithMetadata(mContentPreviewView, mHeadliveView);
}
}
- private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) {
+ private ViewGroup displayInternal(
+ LayoutInflater layoutInflater,
+ ViewGroup parent,
+ @Nullable View headlineViewParent) {
mContentPreviewView = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_files_text, parent, false);
+ mHeadliveView = headlineViewParent == null ? mContentPreviewView : headlineViewParent;
+ inflateHeadline(mHeadliveView);
final ActionRow actionRow =
mContentPreviewView.findViewById(com.android.internal.R.id.chooser_action_row);
@@ -134,12 +146,12 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
if (!mIsSingleImage) {
mContentPreviewView.requireViewById(R.id.image_view).setVisibility(View.GONE);
}
- prepareTextPreview(mContentPreviewView, mActionFactory);
+ prepareTextPreview(mContentPreviewView, mHeadliveView, mActionFactory);
if (mIsMetadataUpdated) {
- updateUiWithMetadata(mContentPreviewView);
+ updateUiWithMetadata(mContentPreviewView, mHeadliveView);
} else {
updateHeadline(
- mContentPreviewView,
+ mHeadliveView,
mFileCount,
mTypeClassifier.isImageType(mIntentMimeType),
mTypeClassifier.isVideoType(mIntentMimeType));
@@ -148,13 +160,14 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
return mContentPreviewView;
}
- private void updateUiWithMetadata(ViewGroup contentPreviewView) {
- updateHeadline(contentPreviewView, mFileCount, mAllImages, mAllVideos);
+ private void updateUiWithMetadata(ViewGroup contentPreviewView, View headlineView) {
+ prepareTextPreview(contentPreviewView, headlineView, mActionFactory);
+ updateHeadline(headlineView, mFileCount, mAllImages, mAllVideos);
ImageView imagePreview = mContentPreviewView.requireViewById(R.id.image_view);
if (mIsSingleImage && mFirstFilePreviewUri != null) {
mImageLoader.loadImage(
- mLifecycle,
+ mScope,
mFirstFilePreviewUri,
bitmap -> {
if (bitmap == null) {
@@ -169,8 +182,8 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
}
private void updateHeadline(
- ViewGroup contentPreview, int fileCount, boolean allImages, boolean allVideos) {
- CheckBox includeText = contentPreview.requireViewById(R.id.include_text_action);
+ View headlineView, int fileCount, boolean allImages, boolean allVideos) {
+ CheckBox includeText = headlineView.requireViewById(R.id.include_text_action);
String headline;
if (includeText.getVisibility() == View.VISIBLE && includeText.isChecked()) {
if (allImages) {
@@ -190,14 +203,15 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
}
}
- displayHeadline(contentPreview, headline);
+ displayHeadline(headlineView, headline);
}
private void prepareTextPreview(
ViewGroup contentPreview,
+ View headlineView,
ChooserContentPreviewUi.ActionFactory actionFactory) {
final TextView textView = contentPreview.requireViewById(R.id.content_preview_text);
- CheckBox includeText = contentPreview.requireViewById(R.id.include_text_action);
+ CheckBox includeText = headlineView.requireViewById(R.id.include_text_action);
boolean isLink = HttpUriMatcher.isHttpUri(mText.toString());
textView.setAutoLinkMask(isLink ? Linkify.WEB_URLS : 0);
textView.setText(mText);
@@ -213,7 +227,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
textView.setText(getNoTextString(contentPreview.getResources()));
}
shareTextAction.accept(!isChecked);
- updateHeadline(contentPreview, mFileCount, mAllImages, mAllVideos);
+ updateHeadline(headlineView, mFileCount, mAllImages, mAllVideos);
});
if (SHOW_TOGGLE_CHECKMARK) {
includeText.setVisibility(View.VISIBLE);
diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt
index 1aace8c3..ef1e55d8 100644
--- a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt
+++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt
@@ -16,36 +16,55 @@
package com.android.intentresolver.contentpreview
-import android.annotation.StringRes
import android.content.Context
-import com.android.intentresolver.R
import android.util.PluralsMessageFormatter
+import androidx.annotation.StringRes
+import com.android.intentresolver.R
private const val PLURALS_COUNT = "count"
/**
- * HeadlineGenerator generates the text to show at the top of the sharesheet as a brief
- * description of the content being shared.
+ * HeadlineGenerator generates the text to show at the top of the sharesheet as a brief description
+ * of the content being shared.
*/
class HeadlineGeneratorImpl(private val context: Context) : HeadlineGenerator {
override fun getTextHeadline(text: CharSequence): String {
return context.getString(
- getTemplateResource(text, R.string.sharing_link, R.string.sharing_text))
+ getTemplateResource(text, R.string.sharing_link, R.string.sharing_text)
+ )
}
override fun getImagesWithTextHeadline(text: CharSequence, count: Int): String {
- return getPluralString(getTemplateResource(
- text, R.string.sharing_images_with_link, R.string.sharing_images_with_text), count)
+ return getPluralString(
+ getTemplateResource(
+ text,
+ R.string.sharing_images_with_link,
+ R.string.sharing_images_with_text
+ ),
+ count
+ )
}
override fun getVideosWithTextHeadline(text: CharSequence, count: Int): String {
- return getPluralString(getTemplateResource(
- text, R.string.sharing_videos_with_link, R.string.sharing_videos_with_text), count)
+ return getPluralString(
+ getTemplateResource(
+ text,
+ R.string.sharing_videos_with_link,
+ R.string.sharing_videos_with_text
+ ),
+ count
+ )
}
override fun getFilesWithTextHeadline(text: CharSequence, count: Int): String {
- return getPluralString(getTemplateResource(
- text, R.string.sharing_files_with_link, R.string.sharing_files_with_text), count)
+ return getPluralString(
+ getTemplateResource(
+ text,
+ R.string.sharing_files_with_link,
+ R.string.sharing_files_with_text
+ ),
+ count
+ )
}
override fun getImagesHeadline(count: Int): String {
@@ -70,7 +89,9 @@ class HeadlineGeneratorImpl(private val context: Context) : HeadlineGenerator {
@StringRes
private fun getTemplateResource(
- text: CharSequence, @StringRes linkResource: Int, @StringRes nonLinkResource: Int
+ text: CharSequence,
+ @StringRes linkResource: Int,
+ @StringRes nonLinkResource: Int
): Int {
return if (text.toString().isHttpUri()) linkResource else nonLinkResource
}
diff --git a/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt
index 8d0fb84b..629651a3 100644
--- a/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt
+++ b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt
@@ -18,8 +18,8 @@ package com.android.intentresolver.contentpreview
import android.graphics.Bitmap
import android.net.Uri
-import androidx.lifecycle.Lifecycle
import java.util.function.Consumer
+import kotlinx.coroutines.CoroutineScope
/** A content preview image loader. */
interface ImageLoader : suspend (Uri) -> Bitmap?, suspend (Uri, Boolean) -> Bitmap? {
@@ -30,7 +30,7 @@ interface ImageLoader : suspend (Uri) -> Bitmap?, suspend (Uri, Boolean) -> Bitm
* @param callback a callback that will be invoked with the loaded image or null if loading has
* failed.
*/
- fun loadImage(callerLifecycle: Lifecycle, uri: Uri, callback: Consumer<Bitmap?>)
+ fun loadImage(callerScope: CoroutineScope, uri: Uri, callback: Consumer<Bitmap?>)
/** Prepopulate the image loader cache. */
fun prePopulate(uris: List<Uri>)
diff --git a/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt
index 22dd1125..572ccf0b 100644
--- a/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt
+++ b/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt
@@ -24,8 +24,6 @@ import android.util.Size
import androidx.annotation.GuardedBy
import androidx.annotation.VisibleForTesting
import androidx.collection.LruCache
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.coroutineScope
import java.util.function.Consumer
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
@@ -70,8 +68,8 @@ constructor(
override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = loadImageAsync(uri, caching)
- override fun loadImage(callerLifecycle: Lifecycle, uri: Uri, callback: Consumer<Bitmap?>) {
- callerLifecycle.coroutineScope.launch {
+ override fun loadImage(callerScope: CoroutineScope, uri: Uri, callback: Consumer<Bitmap?>) {
+ callerScope.launch {
val image = loadImageAsync(uri, caching = true)
if (isActive) {
callback.accept(image)
diff --git a/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt b/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt
index 90016932..31a7006c 100644
--- a/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt
+++ b/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt
@@ -19,13 +19,17 @@ package com.android.intentresolver.contentpreview
import android.content.res.Resources
import android.util.Log
import android.view.LayoutInflater
+import android.view.View
import android.view.ViewGroup
internal class NoContextPreviewUi(private val type: Int) : ContentPreviewUi() {
override fun getType(): Int = type
override fun display(
- resources: Resources?, layoutInflater: LayoutInflater?, parent: ViewGroup?
+ resources: Resources?,
+ layoutInflater: LayoutInflater?,
+ parent: ViewGroup?,
+ headlineViewParent: View?,
): ViewGroup? {
Log.e(TAG, "Unexpected content preview type: $type")
return null
diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt
index 9f1cc6c1..38918d79 100644
--- a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt
+++ b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt
@@ -29,8 +29,6 @@ import android.text.TextUtils
import android.util.Log
import androidx.annotation.OpenForTesting
import androidx.annotation.VisibleForTesting
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.coroutineScope
import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE
import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE
import com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT
@@ -185,11 +183,11 @@ constructor(
* is not provided, derived from the URI.
*/
@Throws(IndexOutOfBoundsException::class)
- fun getFirstFileName(callerLifecycle: Lifecycle, callback: Consumer<String>) {
+ fun getFirstFileName(callerScope: CoroutineScope, callback: Consumer<String>) {
if (records.isEmpty()) {
throw IndexOutOfBoundsException("There are no shared URIs")
}
- callerLifecycle.coroutineScope.launch {
+ callerScope.launch {
val result = scope.async { getFirstFileName() }.await()
callback.accept(result)
}
@@ -264,44 +262,46 @@ constructor(
private val query by lazy { readQueryResult() }
- private fun readQueryResult(): QueryResult {
- val cursor =
- contentResolver.querySafe(uri)?.takeIf { it.moveToFirst() } ?: return QueryResult()
-
- var flagColIdx = -1
- var displayIconUriColIdx = -1
- var nameColIndex = -1
- var titleColIndex = -1
- // TODO: double-check why Cursor#getColumnInded didn't work
- cursor.columnNames.forEachIndexed { i, columnName ->
- when (columnName) {
- DocumentsContract.Document.COLUMN_FLAGS -> flagColIdx = i
- MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI -> displayIconUriColIdx = i
- OpenableColumns.DISPLAY_NAME -> nameColIndex = i
- Downloads.Impl.COLUMN_TITLE -> titleColIndex = i
+ private fun readQueryResult(): QueryResult =
+ contentResolver.querySafe(uri)?.use { cursor ->
+ if (!cursor.moveToFirst()) return@use null
+
+ var flagColIdx = -1
+ var displayIconUriColIdx = -1
+ var nameColIndex = -1
+ var titleColIndex = -1
+ // TODO: double-check why Cursor#getColumnInded didn't work
+ cursor.columnNames.forEachIndexed { i, columnName ->
+ when (columnName) {
+ DocumentsContract.Document.COLUMN_FLAGS -> flagColIdx = i
+ MediaMetadata.METADATA_KEY_DISPLAY_ICON_URI -> displayIconUriColIdx = i
+ OpenableColumns.DISPLAY_NAME -> nameColIndex = i
+ Downloads.Impl.COLUMN_TITLE -> titleColIndex = i
+ }
}
- }
-
- val supportsThumbnail =
- flagColIdx >= 0 && ((cursor.getInt(flagColIdx) and FLAG_SUPPORTS_THUMBNAIL) != 0)
- var title = ""
- if (nameColIndex >= 0) {
- title = cursor.getString(nameColIndex) ?: ""
- }
- if (TextUtils.isEmpty(title) && titleColIndex >= 0) {
- title = cursor.getString(titleColIndex) ?: ""
- }
+ val supportsThumbnail =
+ flagColIdx >= 0 &&
+ ((cursor.getInt(flagColIdx) and FLAG_SUPPORTS_THUMBNAIL) != 0)
- val iconUri =
- if (displayIconUriColIdx >= 0) {
- cursor.getString(displayIconUriColIdx)?.let(Uri::parse)
- } else {
- null
+ var title = ""
+ if (nameColIndex >= 0) {
+ title = cursor.getString(nameColIndex) ?: ""
+ }
+ if (TextUtils.isEmpty(title) && titleColIndex >= 0) {
+ title = cursor.getString(titleColIndex) ?: ""
}
- return QueryResult(supportsThumbnail, title, iconUri)
- }
+ val iconUri =
+ if (displayIconUriColIdx >= 0) {
+ cursor.getString(displayIconUriColIdx)?.let(Uri::parse)
+ } else {
+ null
+ }
+
+ QueryResult(supportsThumbnail, title, iconUri)
+ }
+ ?: QueryResult()
}
private class QueryResult(
diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
index 6013f5a0..6350756e 100644
--- a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
+++ b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
@@ -17,6 +17,7 @@
package com.android.intentresolver.contentpreview
import android.app.Application
+import android.content.Intent
import androidx.annotation.MainThread
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
@@ -25,26 +26,32 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.android.intentresolver.ChooserRequestParameters
import com.android.intentresolver.R
+import com.android.intentresolver.inject.Background
+import dagger.hilt.android.lifecycle.HiltViewModel
+import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.plus
/** A trivial view model to keep a [PreviewDataProvider] instance over a configuration change */
-class PreviewViewModel(
+@HiltViewModel
+class PreviewViewModel
+@Inject
+constructor(
private val application: Application,
- private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
+ @Background private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
) : BasePreviewViewModel() {
private var previewDataProvider: PreviewDataProvider? = null
private var imageLoader: ImagePreviewImageLoader? = null
@MainThread
override fun createOrReuseProvider(
- chooserRequest: ChooserRequestParameters
+ targetIntent: Intent
): PreviewDataProvider =
previewDataProvider
?: PreviewDataProvider(
viewModelScope + dispatcher,
- chooserRequest.targetIntent,
+ targetIntent,
application.contentResolver
)
.also { previewDataProvider = it }
diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java
index c38ed03a..b0dc3c58 100644
--- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java
@@ -20,6 +20,7 @@ import static com.android.intentresolver.util.UriFilters.isOwnedByCurrentUser;
import android.content.res.Resources;
import android.net.Uri;
+import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
@@ -28,13 +29,14 @@ import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.Nullable;
-import androidx.lifecycle.Lifecycle;
import com.android.intentresolver.R;
import com.android.intentresolver.widget.ActionRow;
+import kotlinx.coroutines.CoroutineScope;
+
class TextContentPreviewUi extends ContentPreviewUi {
- private final Lifecycle mLifecycle;
+ private final CoroutineScope mScope;
@Nullable
private final CharSequence mSharingText;
@Nullable
@@ -46,14 +48,14 @@ class TextContentPreviewUi extends ContentPreviewUi {
private final HeadlineGenerator mHeadlineGenerator;
TextContentPreviewUi(
- Lifecycle lifecycle,
+ CoroutineScope scope,
@Nullable CharSequence sharingText,
@Nullable CharSequence previewTitle,
@Nullable Uri previewThumbnail,
ChooserContentPreviewUi.ActionFactory actionFactory,
ImageLoader imageLoader,
HeadlineGenerator headlineGenerator) {
- mLifecycle = lifecycle;
+ mScope = scope;
mSharingText = sharingText;
mPreviewTitle = previewTitle;
mPreviewThumbnail = previewThumbnail;
@@ -68,17 +70,27 @@ class TextContentPreviewUi extends ContentPreviewUi {
}
@Override
- public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
- ViewGroup layout = displayInternal(layoutInflater, parent);
- displayModifyShareAction(layout, mActionFactory);
+ public ViewGroup display(
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ViewGroup parent,
+ @Nullable View headlineViewParent) {
+ ViewGroup layout = displayInternal(layoutInflater, parent, headlineViewParent);
+ displayModifyShareAction(
+ headlineViewParent == null ? layout : headlineViewParent, mActionFactory);
return layout;
}
private ViewGroup displayInternal(
LayoutInflater layoutInflater,
- ViewGroup parent) {
+ ViewGroup parent,
+ @Nullable View headlineViewParent) {
ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_text, parent, false);
+ if (headlineViewParent == null) {
+ headlineViewParent = contentPreviewLayout;
+ }
+ inflateHeadline(headlineViewParent);
final ActionRow actionRow =
contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
@@ -93,13 +105,9 @@ class TextContentPreviewUi extends ContentPreviewUi {
TextView textView = contentPreviewLayout.findViewById(
com.android.internal.R.id.content_preview_text);
- String text = mSharingText.toString();
- // If we're only previewing one line, then strip out newlines.
- if (textView.getMaxLines() == 1) {
- text = text.replace("\n", " ");
- }
- textView.setText(text);
+ textView.setText(
+ textView.getMaxLines() == 1 ? replaceLineBreaks(mSharingText) : mSharingText);
TextView previewTitleView = contentPreviewLayout.findViewById(
com.android.internal.R.id.content_preview_title);
@@ -115,7 +123,7 @@ class TextContentPreviewUi extends ContentPreviewUi {
previewThumbnailView.setVisibility(View.GONE);
} else {
mImageLoader.loadImage(
- mLifecycle,
+ mScope,
mPreviewThumbnail,
(bitmap) -> updateViewWithImage(
contentPreviewLayout.findViewById(
@@ -131,8 +139,22 @@ class TextContentPreviewUi extends ContentPreviewUi {
copyButton.setVisibility(View.GONE);
}
- displayHeadline(contentPreviewLayout, mHeadlineGenerator.getTextHeadline(mSharingText));
+ displayHeadline(headlineViewParent, mHeadlineGenerator.getTextHeadline(mSharingText));
return contentPreviewLayout;
}
+
+ @Nullable
+ private static CharSequence replaceLineBreaks(@Nullable CharSequence text) {
+ if (text == null) {
+ return null;
+ }
+ SpannableStringBuilder string = new SpannableStringBuilder(text);
+ for (int i = 0, size = string.length(); i < size; i++) {
+ if (string.charAt(i) == '\n') {
+ string.replace(i, i + 1, " ");
+ }
+ }
+ return string;
+ }
}
diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
index 8e635aba..8ddd5273 100644
--- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
@@ -52,6 +52,8 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
private List<FileInfo> mFiles;
@Nullable
private ViewGroup mContentPreviewView;
+ @Nullable
+ private View mHeadlineView;
UnifiedContentPreviewUi(
CoroutineScope scope,
@@ -83,9 +85,14 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
}
@Override
- public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
- ViewGroup layout = displayInternal(layoutInflater, parent);
- displayModifyShareAction(layout, mActionFactory);
+ public ViewGroup display(
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ViewGroup parent,
+ @Nullable View headlineViewParent) {
+ ViewGroup layout = displayInternal(layoutInflater, parent, headlineViewParent);
+ displayModifyShareAction(
+ headlineViewParent == null ? layout : headlineViewParent, mActionFactory);
return layout;
}
@@ -96,13 +103,16 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
.toList());
mFiles = files;
if (mContentPreviewView != null) {
- updatePreviewWithFiles(mContentPreviewView, files);
+ updatePreviewWithFiles(mContentPreviewView, mHeadlineView, files);
}
}
- private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) {
+ private ViewGroup displayInternal(
+ LayoutInflater layoutInflater, ViewGroup parent, @Nullable View headlineViewParent) {
mContentPreviewView = (ViewGroup) layoutInflater.inflate(
R.layout.chooser_grid_preview_image, parent, false);
+ mHeadlineView = headlineViewParent == null ? mContentPreviewView : headlineViewParent;
+ inflateHeadline(mHeadlineView);
final ActionRow actionRow =
mContentPreviewView.findViewById(com.android.internal.R.id.chooser_action_row);
@@ -122,10 +132,10 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
mItemCount);
if (mFiles != null) {
- updatePreviewWithFiles(mContentPreviewView, mFiles);
+ updatePreviewWithFiles(mContentPreviewView, mHeadlineView, mFiles);
} else {
displayHeadline(
- mContentPreviewView,
+ mHeadlineView,
mItemCount,
mTypeClassifier.isImageType(mIntentMimeType),
mTypeClassifier.isVideoType(mIntentMimeType));
@@ -135,7 +145,8 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
return mContentPreviewView;
}
- private void updatePreviewWithFiles(ViewGroup contentPreviewView, List<FileInfo> files) {
+ private void updatePreviewWithFiles(
+ ViewGroup contentPreviewView, View headlineView, List<FileInfo> files) {
final int count = files.size();
ScrollableImagePreviewView imagePreview =
contentPreviewView.requireViewById(R.id.scrollable_image_preview);
@@ -158,11 +169,11 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
allVideos = allVideos && previewType == ScrollableImagePreviewView.PreviewType.Video;
}
- displayHeadline(contentPreviewView, count, allImages, allVideos);
+ displayHeadline(headlineView, count, allImages, allVideos);
}
private void displayHeadline(
- ViewGroup layout, int count, boolean allImages, boolean allVideos) {
+ View layout, int count, boolean allImages, boolean allVideos) {
if (allImages) {
displayHeadline(layout, mHeadlineGenerator.getImagesHeadline(count));
} else if (allVideos) {
diff --git a/java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.java
new file mode 100644
index 00000000..41422b66
--- /dev/null
+++ b/java/src/com/android/intentresolver/emptystate/CompositeEmptyStateProvider.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.emptystate;
+
+import android.annotation.Nullable;
+
+import com.android.intentresolver.ResolverListAdapter;
+
+/**
+ * Empty state provider that combines multiple providers. Providers earlier in the list have
+ * priority, that is if there is a provider that returns non-null empty state then all further
+ * providers will be ignored.
+ */
+public class CompositeEmptyStateProvider implements EmptyStateProvider {
+
+ private final EmptyStateProvider[] mProviders;
+
+ public CompositeEmptyStateProvider(EmptyStateProvider... providers) {
+ mProviders = providers;
+ }
+
+ @Nullable
+ @Override
+ public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ for (EmptyStateProvider provider : mProviders) {
+ EmptyState emptyState = provider.getEmptyState(resolverListAdapter);
+ if (emptyState != null) {
+ return emptyState;
+ }
+ }
+ return null;
+ }
+}
diff --git a/java/src/com/android/intentresolver/emptystate/CrossProfileIntentsChecker.java b/java/src/com/android/intentresolver/emptystate/CrossProfileIntentsChecker.java
new file mode 100644
index 00000000..2164e533
--- /dev/null
+++ b/java/src/com/android/intentresolver/emptystate/CrossProfileIntentsChecker.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.emptystate;
+
+import android.annotation.NonNull;
+import android.annotation.UserIdInt;
+import android.app.AppGlobals;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.pm.IPackageManager;
+
+import com.android.intentresolver.IntentForwarderActivity;
+
+import java.util.List;
+
+/**
+ * Utility class to check if there are cross profile intents, it is in a separate class so
+ * it could be mocked in tests
+ */
+public class CrossProfileIntentsChecker {
+
+ private final ContentResolver mContentResolver;
+ private final IPackageManager mPackageManager;
+
+ public CrossProfileIntentsChecker(@NonNull ContentResolver contentResolver) {
+ this(contentResolver, AppGlobals.getPackageManager());
+ }
+
+ CrossProfileIntentsChecker(
+ @NonNull ContentResolver contentResolver, IPackageManager packageManager) {
+ mContentResolver = contentResolver;
+ mPackageManager = packageManager;
+ }
+
+ /**
+ * Returns {@code true} if at least one of the provided {@code intents} can be forwarded
+ * from {@code source} (user id) to {@code target} (user id).
+ */
+ public boolean hasCrossProfileIntents(
+ List<Intent> intents, @UserIdInt int source, @UserIdInt int target) {
+ return intents.stream().anyMatch(intent ->
+ null != IntentForwarderActivity.canForward(intent, source, target,
+ mPackageManager, mContentResolver));
+ }
+}
+
diff --git a/java/src/com/android/intentresolver/emptystate/EmptyState.java b/java/src/com/android/intentresolver/emptystate/EmptyState.java
new file mode 100644
index 00000000..cde99fe1
--- /dev/null
+++ b/java/src/com/android/intentresolver/emptystate/EmptyState.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.emptystate;
+
+import android.annotation.Nullable;
+
+/**
+ * Model for the "empty state"/"blocker" UI to display instead of a profile tab's normal contents.
+ */
+public interface EmptyState {
+ /**
+ * Get the title to show on the empty state.
+ */
+ @Nullable
+ default String getTitle() {
+ return null;
+ }
+
+ /**
+ * Get the subtitle string to show underneath the title on the empty state.
+ */
+ @Nullable
+ default String getSubtitle() {
+ return null;
+ }
+
+ /**
+ * Get the handler for an optional button associated with this empty state. If the result is
+ * non-null, the empty-state UI will be built with a button that dispatches this handler.
+ */
+ @Nullable
+ default ClickListener getButtonClickListener() {
+ return null;
+ }
+
+ /**
+ * Get whether to show the default UI for the empty state. If true, the UI will show the default
+ * blocker text ('No apps can perform this action') and style; title and subtitle are ignored.
+ */
+ default boolean useDefaultEmptyView() {
+ return false;
+ }
+
+ /**
+ * Returns true if for this empty state we should skip rebuilding of the apps list
+ * for this tab.
+ */
+ default boolean shouldSkipDataRebuild() {
+ return false;
+ }
+
+ /**
+ * Called when empty state is shown, could be used e.g. to track analytics events.
+ */
+ default void onEmptyStateShown() {}
+
+ interface ClickListener {
+ void onClick(TabControl currentTab);
+ }
+
+ interface TabControl {
+ void showSpinner();
+ }
+}
diff --git a/java/src/com/android/intentresolver/emptystate/EmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/EmptyStateProvider.java
new file mode 100644
index 00000000..c3261287
--- /dev/null
+++ b/java/src/com/android/intentresolver/emptystate/EmptyStateProvider.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.emptystate;
+
+import android.annotation.Nullable;
+
+import com.android.intentresolver.ResolverListAdapter;
+
+/**
+ * Returns an empty state to show for the current profile page (tab) if necessary.
+ * This could be used e.g. to show a blocker on a tab if device management policy doesn't
+ * allow to use it or there are no apps available.
+ */
+public interface EmptyStateProvider {
+ /**
+ * When a non-null empty state is returned the corresponding profile page will show
+ * this empty state
+ * @param resolverListAdapter the current adapter
+ */
+ @Nullable
+ default EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ return null;
+ }
+}
diff --git a/java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java b/java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java
new file mode 100644
index 00000000..d7ef8c75
--- /dev/null
+++ b/java/src/com/android/intentresolver/emptystate/EmptyStateUiHelper.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.emptystate;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Helper for building `MultiProfilePagerAdapter` tab UIs for profile tabs that are "blocked" by
+ * some empty-state status.
+ */
+public class EmptyStateUiHelper {
+ private final View mEmptyStateView;
+
+ public EmptyStateUiHelper(ViewGroup rootView) {
+ mEmptyStateView =
+ rootView.requireViewById(com.android.internal.R.id.resolver_empty_state);
+ }
+
+ public void resetViewVisibilities() {
+ mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_title)
+ .setVisibility(View.VISIBLE);
+ mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_subtitle)
+ .setVisibility(View.VISIBLE);
+ mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_button)
+ .setVisibility(View.INVISIBLE);
+ mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_progress)
+ .setVisibility(View.GONE);
+ mEmptyStateView.requireViewById(com.android.internal.R.id.empty)
+ .setVisibility(View.GONE);
+ mEmptyStateView.setVisibility(View.VISIBLE);
+ }
+
+ public void showSpinner() {
+ mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_title)
+ .setVisibility(View.INVISIBLE);
+ // TODO: subtitle?
+ mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_button)
+ .setVisibility(View.INVISIBLE);
+ mEmptyStateView.requireViewById(com.android.internal.R.id.resolver_empty_state_progress)
+ .setVisibility(View.VISIBLE);
+ mEmptyStateView.requireViewById(com.android.internal.R.id.empty)
+ .setVisibility(View.GONE);
+ }
+
+ public void hide() {
+ mEmptyStateView.setVisibility(View.GONE);
+ }
+}
+
diff --git a/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java
index a7b50f38..2653c560 100644
--- a/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java
+++ b/java/src/com/android/intentresolver/emptystate/NoAppsAvailableEmptyStateProvider.java
@@ -14,13 +14,11 @@
* limitations under the License.
*/
-package com.android.intentresolver;
+package com.android.intentresolver.emptystate;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_PERSONAL_APPS;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.admin.DevicePolicyEventLogger;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
@@ -28,8 +26,11 @@ import android.content.pm.ResolveInfo;
import android.os.UserHandle;
import android.stats.devicepolicy.nano.DevicePolicyEnums;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.ResolvedComponentInfo;
+import com.android.intentresolver.ResolverListAdapter;
import com.android.internal.R;
import java.util.List;
@@ -51,9 +52,12 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider {
@NonNull
private final UserHandle mTabOwnerUserHandleForLaunch;
- public NoAppsAvailableEmptyStateProvider(Context context, UserHandle workProfileUserHandle,
- UserHandle personalProfileUserHandle, String metricsCategory,
- UserHandle tabOwnerUserHandleForLaunch) {
+ public NoAppsAvailableEmptyStateProvider(
+ @NonNull Context context,
+ @Nullable UserHandle workProfileUserHandle,
+ @Nullable UserHandle personalProfileUserHandle,
+ @NonNull String metricsCategory,
+ @NonNull UserHandle tabOwnerUserHandleForLaunch) {
mContext = context;
mWorkProfileUserHandle = workProfileUserHandle;
mPersonalProfileUserHandle = personalProfileUserHandle;
@@ -76,12 +80,12 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider {
title = mContext.getSystemService(
DevicePolicyManager.class).getResources().getString(
RESOLVER_NO_PERSONAL_APPS,
- () -> mContext.getString(R.string.resolver_no_personal_apps_available));
+ () -> mContext.getString(R.string.resolver_no_personal_apps_available));
} else {
title = mContext.getSystemService(
DevicePolicyManager.class).getResources().getString(
RESOLVER_NO_WORK_APPS,
- () -> mContext.getString(R.string.resolver_no_work_apps_available));
+ () -> mContext.getString(R.string.resolver_no_work_apps_available));
}
return new NoAppsAvailableEmptyState(
@@ -128,8 +132,9 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider {
private boolean mIsPersonalProfile;
- public NoAppsAvailableEmptyState(String title, String metricsCategory,
- boolean isPersonalProfile) {
+ public NoAppsAvailableEmptyState(@NonNull String title,
+ @NonNull String metricsCategory,
+ boolean isPersonalProfile) {
mTitle = title;
mMetricsCategory = metricsCategory;
mIsPersonalProfile = isPersonalProfile;
diff --git a/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java
index 6f72bb00..ce7bd8d9 100644
--- a/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java
+++ b/java/src/com/android/intentresolver/emptystate/NoCrossProfileEmptyStateProvider.java
@@ -14,19 +14,18 @@
* limitations under the License.
*/
-package com.android.intentresolver;
+package com.android.intentresolver.emptystate;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.annotation.StringRes;
import android.app.admin.DevicePolicyEventLogger;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.os.UserHandle;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+
+import com.android.intentresolver.ResolverListAdapter;
/**
* Empty state provider that does not allow cross profile sharing, it will return a blocker
@@ -92,10 +91,14 @@ public class NoCrossProfileEmptyStateProvider implements EmptyStateProvider {
@NonNull
private final String mEventCategory;
- public DevicePolicyBlockerEmptyState(Context context, String devicePolicyStringTitleId,
- @StringRes int defaultTitleResource, String devicePolicyStringSubtitleId,
+ public DevicePolicyBlockerEmptyState(
+ @NonNull Context context,
+ String devicePolicyStringTitleId,
+ @StringRes int defaultTitleResource,
+ String devicePolicyStringSubtitleId,
@StringRes int defaultSubtitleResource,
- int devicePolicyEventId, String devicePolicyEventCategory) {
+ int devicePolicyEventId,
+ @NonNull String devicePolicyEventCategory) {
mContext = context;
mDevicePolicyStringTitleId = devicePolicyStringTitleId;
mDefaultTitleResource = defaultTitleResource;
diff --git a/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java b/java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java
index 2f3dfbd5..612828e0 100644
--- a/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java
+++ b/java/src/com/android/intentresolver/emptystate/WorkProfilePausedEmptyStateProvider.java
@@ -14,21 +14,23 @@
* limitations under the License.
*/
-package com.android.intentresolver;
+package com.android.intentresolver.emptystate;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.admin.DevicePolicyEventLogger;
import android.app.admin.DevicePolicyManager;
import android.content.Context;
import android.os.UserHandle;
import android.stats.devicepolicy.nano.DevicePolicyEnums;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener;
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.WorkProfileAvailabilityManager;
/**
* Chooser/ResolverActivity empty state provider that returns empty state which is shown when
@@ -65,7 +67,7 @@ public class WorkProfilePausedEmptyStateProvider implements EmptyStateProvider {
final String title = mContext.getSystemService(DevicePolicyManager.class)
.getResources().getString(RESOLVER_WORK_PAUSED_TITLE,
- () -> mContext.getString(R.string.resolver_turn_on_work_apps));
+ () -> mContext.getString(R.string.resolver_turn_on_work_apps));
return new WorkProfileOffEmptyState(title, (tab) -> {
tab.showSpinner();
diff --git a/java/src/com/android/intentresolver/flags/DeviceConfigProxy.kt b/java/src/com/android/intentresolver/flags/DeviceConfigProxy.kt
deleted file mode 100644
index d1494fe7..00000000
--- a/java/src/com/android/intentresolver/flags/DeviceConfigProxy.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver.flags
-
-import android.provider.DeviceConfig
-import com.android.systemui.flags.ParcelableFlag
-
-internal class DeviceConfigProxy {
- fun isEnabled(flag: ParcelableFlag<Boolean>): Boolean? {
- return runCatching {
- val hasProperty = DeviceConfig.getProperty(flag.namespace, flag.name) != null
- if (hasProperty) {
- DeviceConfig.getBoolean(flag.namespace, flag.name, flag.default)
- } else {
- null
- }
- }.getOrDefault(null)
- }
-}
diff --git a/java/src/com/android/intentresolver/flags/Flags.kt b/java/src/com/android/intentresolver/flags/Flags.kt
deleted file mode 100644
index 2c20d341..00000000
--- a/java/src/com/android/intentresolver/flags/Flags.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver.flags
-
-import com.android.systemui.flags.ReleasedFlag
-import com.android.systemui.flags.UnreleasedFlag
-
-// Flag id, name and namespace should be kept in sync with [com.android.systemui.flags.Flags] to
-// make the flags available in the flag flipper app (see go/sysui-flags).
-// All flags added should be included in UnbundledChooserActivityTest.ALL_FLAGS.
-object Flags {
- private fun releasedFlag(name: String) = ReleasedFlag(name, "systemui")
-
- private fun unreleasedFlag(name: String, teamfood: Boolean = false) =
- UnreleasedFlag(name, "systemui", teamfood)
-}
diff --git a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
index fadea934..51d4e677 100644
--- a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
+++ b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
@@ -32,9 +32,12 @@ import android.view.animation.DecelerateInterpolator;
import android.widget.Space;
import android.widget.TextView;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import androidx.recyclerview.widget.RecyclerView;
import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.FeatureFlags;
import com.android.intentresolver.R;
import com.android.intentresolver.ResolverListAdapter.ViewHolder;
import com.android.internal.annotations.VisibleForTesting;
@@ -107,6 +110,9 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
private final boolean mShouldShowContentPreview;
private final int mChooserWidthPixels;
private final int mChooserRowTextOptionTranslatePixelSize;
+ private final FeatureFlags mFeatureFlags;
+ @Nullable
+ private RecyclerView mRecyclerView;
private int mChooserTargetWidth = 0;
@@ -119,7 +125,8 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
ChooserActivityDelegate chooserActivityDelegate,
ChooserListAdapter wrappedAdapter,
boolean shouldShowContentPreview,
- int maxTargetsPerRow) {
+ int maxTargetsPerRow,
+ FeatureFlags featureFlags) {
super();
mChooserActivityDelegate = chooserActivityDelegate;
@@ -133,6 +140,7 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
mChooserWidthPixels = context.getResources().getDimensionPixelSize(R.dimen.chooser_width);
mChooserRowTextOptionTranslatePixelSize = context.getResources().getDimensionPixelSize(
R.dimen.chooser_row_text_option_translate);
+ mFeatureFlags = featureFlags;
wrappedAdapter.registerDataSetObserver(new DataSetObserver() {
@Override
@@ -149,6 +157,18 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
});
}
+ @Override
+ public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
+ if (mFeatureFlags.scrollablePreview()) {
+ mRecyclerView = recyclerView;
+ }
+ }
+
+ @Override
+ public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
+ mRecyclerView = null;
+ }
+
public void setFooterHeight(int height) {
mFooterHeight = height;
}
@@ -198,7 +218,8 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
public int getSystemRowCount() {
// For the tabbed case we show the sticky content preview above the tabs,
// please refer to shouldShowStickyContentPreview
- if (mChooserActivityDelegate.shouldShowTabs()) {
+ if (mChooserActivityDelegate.shouldShowTabs()
+ || mFeatureFlags.scrollablePreview()) {
return 0;
}
@@ -267,8 +288,9 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
+ getFooterRowCount();
}
+ @NonNull
@Override
- public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
switch (viewType) {
case VIEW_TYPE_CONTENT_PREVIEW:
return new ItemViewHolder(
@@ -304,7 +326,7 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
return new FooterViewHolder(sp, viewType);
default:
// Since we catch all possible viewTypes above, no chance this is being called.
- return null;
+ throw new IllegalStateException("unmatched view type");
}
}
@@ -318,6 +340,15 @@ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.
mAzLabelVisibility = isVisible;
int azRowPos = getAzLabelRowPosition();
if (azRowPos >= 0) {
+ if (mRecyclerView != null) {
+ for (int i = 0, size = mRecyclerView.getChildCount(); i < size; i++) {
+ View child = mRecyclerView.getChildAt(i);
+ if (mRecyclerView.getChildAdapterPosition(child) == azRowPos) {
+ child.setVisibility(isVisible ? View.VISIBLE : View.GONE);
+ }
+ }
+ return;
+ }
notifyItemChanged(azRowPos);
}
}
diff --git a/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt b/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt
index 0e4d0209..054fbe71 100644
--- a/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt
+++ b/java/src/com/android/intentresolver/icons/DefaultTargetDataLoader.kt
@@ -18,7 +18,6 @@ package com.android.intentresolver.icons
import android.app.ActivityManager
import android.content.Context
-import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable
import android.os.AsyncTask
import android.os.UserHandle
@@ -95,7 +94,7 @@ class DefaultTargetDataLoader(
.executeOnExecutor(executor)
}
- override fun loadLabel(info: DisplayResolveInfo, callback: Consumer<Array<CharSequence?>>) {
+ override fun loadLabel(info: DisplayResolveInfo, callback: Consumer<LabelInfo>) {
val taskId = nextTaskId.getAndIncrement()
LoadLabelTask(context, info, isAudioCaptureDevice, presentationFactory) { result ->
removeTask(taskId)
@@ -105,8 +104,14 @@ class DefaultTargetDataLoader(
.executeOnExecutor(executor)
}
- override fun createPresentationGetter(info: ResolveInfo): TargetPresentationGetter =
- presentationFactory.makePresentationGetter(info)
+ override fun getOrLoadLabel(info: DisplayResolveInfo) {
+ if (!info.hasDisplayLabel()) {
+ val result =
+ LoadLabelTask.loadLabel(context, info, isAudioCaptureDevice, presentationFactory)
+ info.displayLabel = result.label
+ info.extendedInfo = result.subLabel
+ }
+ }
private fun addTask(id: Int, task: AsyncTask<*, *, *>) {
synchronized(activeTasks) { activeTasks.put(id, task) }
diff --git a/java/src/com/android/intentresolver/icons/LabelInfo.kt b/java/src/com/android/intentresolver/icons/LabelInfo.kt
new file mode 100644
index 00000000..a9c4cd77
--- /dev/null
+++ b/java/src/com/android/intentresolver/icons/LabelInfo.kt
@@ -0,0 +1,19 @@
+/*
+ * 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
+
+class LabelInfo(val label: CharSequence?, val subLabel: CharSequence?)
diff --git a/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java b/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java
index 6aee69b5..0f135d63 100644
--- a/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java
+++ b/java/src/com/android/intentresolver/icons/LoadDirectShareIconTask.java
@@ -16,7 +16,6 @@
package com.android.intentresolver.icons;
-import android.annotation.Nullable;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.ActivityInfo;
@@ -30,6 +29,7 @@ import android.graphics.drawable.Icon;
import android.os.Trace;
import android.util.Log;
+import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import com.android.intentresolver.SimpleIconFactory;
diff --git a/java/src/com/android/intentresolver/icons/LoadLabelTask.java b/java/src/com/android/intentresolver/icons/LoadLabelTask.java
index a0867b8e..6d443f78 100644
--- a/java/src/com/android/intentresolver/icons/LoadLabelTask.java
+++ b/java/src/com/android/intentresolver/icons/LoadLabelTask.java
@@ -28,16 +28,16 @@ import com.android.intentresolver.chooser.DisplayResolveInfo;
import java.util.function.Consumer;
-class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> {
+class LoadLabelTask extends AsyncTask<Void, Void, LabelInfo> {
private final Context mContext;
private final DisplayResolveInfo mDisplayResolveInfo;
private final boolean mIsAudioCaptureDevice;
protected final TargetPresentationGetter.Factory mPresentationFactory;
- private final Consumer<CharSequence[]> mCallback;
+ private final Consumer<LabelInfo> mCallback;
LoadLabelTask(Context context, DisplayResolveInfo dri,
boolean isAudioCaptureDevice, TargetPresentationGetter.Factory presentationFactory,
- Consumer<CharSequence[]> callback) {
+ Consumer<LabelInfo> callback) {
mContext = context;
mDisplayResolveInfo = dri;
mIsAudioCaptureDevice = isAudioCaptureDevice;
@@ -46,49 +46,52 @@ class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> {
}
@Override
- protected CharSequence[] doInBackground(Void... voids) {
+ protected LabelInfo doInBackground(Void... voids) {
try {
Trace.beginSection("app-label");
- return loadLabel();
+ return loadLabel(
+ mContext, mDisplayResolveInfo, mIsAudioCaptureDevice, mPresentationFactory);
} finally {
Trace.endSection();
}
}
- private CharSequence[] loadLabel() {
- TargetPresentationGetter pg = mPresentationFactory.makePresentationGetter(
- mDisplayResolveInfo.getResolveInfo());
+ static LabelInfo loadLabel(
+ Context context,
+ DisplayResolveInfo displayResolveInfo,
+ boolean isAudioCaptureDevice,
+ TargetPresentationGetter.Factory presentationFactory) {
+ TargetPresentationGetter pg = presentationFactory.makePresentationGetter(
+ displayResolveInfo.getResolveInfo());
- if (mIsAudioCaptureDevice) {
+ if (isAudioCaptureDevice) {
// This is an audio capture device, so check record permissions
- ActivityInfo activityInfo = mDisplayResolveInfo.getResolveInfo().activityInfo;
+ ActivityInfo activityInfo = displayResolveInfo.getResolveInfo().activityInfo;
String packageName = activityInfo.packageName;
int uid = activityInfo.applicationInfo.uid;
boolean hasRecordPermission =
PermissionChecker.checkPermissionForPreflight(
- mContext,
+ context,
android.Manifest.permission.RECORD_AUDIO, -1, uid,
packageName)
== android.content.pm.PackageManager.PERMISSION_GRANTED;
if (!hasRecordPermission) {
// Doesn't have record permission, so warn the user
- return new CharSequence[]{
+ return new LabelInfo(
pg.getLabel(),
- mContext.getString(R.string.usb_device_resolve_prompt_warn)
- };
+ context.getString(R.string.usb_device_resolve_prompt_warn));
}
}
- return new CharSequence[]{
+ return new LabelInfo(
pg.getLabel(),
- pg.getSubLabel()
- };
+ pg.getSubLabel());
}
@Override
- protected void onPostExecute(CharSequence[] result) {
+ protected void onPostExecute(LabelInfo result) {
mCallback.accept(result);
}
}
diff --git a/java/src/com/android/intentresolver/icons/TargetDataLoader.kt b/java/src/com/android/intentresolver/icons/TargetDataLoader.kt
index 50f731f8..07c62177 100644
--- a/java/src/com/android/intentresolver/icons/TargetDataLoader.kt
+++ b/java/src/com/android/intentresolver/icons/TargetDataLoader.kt
@@ -16,10 +16,8 @@
package com.android.intentresolver.icons
-import android.content.pm.ResolveInfo
import android.graphics.drawable.Drawable
import android.os.UserHandle
-import com.android.intentresolver.TargetPresentationGetter
import com.android.intentresolver.chooser.DisplayResolveInfo
import com.android.intentresolver.chooser.SelectableTargetInfo
import java.util.function.Consumer
@@ -41,10 +39,8 @@ abstract class TargetDataLoader {
)
/** Load target label */
- abstract fun loadLabel(info: DisplayResolveInfo, callback: Consumer<Array<CharSequence?>>)
+ abstract fun loadLabel(info: DisplayResolveInfo, callback: Consumer<LabelInfo>)
- /** Create a presentation getter to be used with a [DisplayResolveInfo] */
- // TODO: get rid of DisplayResolveInfo's dependency on the presentation getter and remove this
- // method.
- abstract fun createPresentationGetter(info: ResolveInfo): TargetPresentationGetter
+ /** Loads DisplayResolveInfo's display label synchronously, if needed */
+ abstract fun getOrLoadLabel(info: DisplayResolveInfo)
}
diff --git a/java/src/com/android/intentresolver/inject/ActivityModule.kt b/java/src/com/android/intentresolver/inject/ActivityModule.kt
new file mode 100644
index 00000000..21bfe4c6
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/ActivityModule.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.inject
+
+import android.app.Activity
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.lifecycleScope
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityComponent
+import kotlinx.coroutines.CoroutineScope
+
+@Module
+@InstallIn(ActivityComponent::class)
+object ActivityModule {
+
+ @Provides
+ @ActivityOwned
+ fun lifecycle(activity: Activity): Lifecycle {
+ check(activity is LifecycleOwner) { "activity must implement LifecycleOwner" }
+ return activity.lifecycle
+ }
+
+ @Provides
+ @ActivityOwned
+ fun activityScope(activity: Activity): CoroutineScope {
+ check(activity is LifecycleOwner) { "activity must implement LifecycleOwner" }
+ return activity.lifecycleScope
+ }
+}
diff --git a/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt b/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt
new file mode 100644
index 00000000..e0f8e88b
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/ConcurrencyModule.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.inject
+
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+
+@Module
+@InstallIn(SingletonComponent::class)
+object ConcurrencyModule {
+
+ @Provides @Main fun mainDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
+
+ /** Injectable alternative to [MainScope()][kotlinx.coroutines.MainScope] */
+ @Provides
+ @Singleton
+ @Main
+ fun mainCoroutineScope(@Main mainDispatcher: CoroutineDispatcher) =
+ CoroutineScope(SupervisorJob() + mainDispatcher)
+
+ @Provides @Background fun backgroundDispatcher(): CoroutineDispatcher = Dispatchers.IO
+}
diff --git a/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt b/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt
new file mode 100644
index 00000000..05cf2104
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/FeatureFlagsModule.kt
@@ -0,0 +1,15 @@
+package com.android.intentresolver.inject
+
+import com.android.intentresolver.FeatureFlags
+import com.android.intentresolver.FeatureFlagsImpl
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+object FeatureFlagsModule {
+
+ @Provides fun featureFlags(): FeatureFlags = FeatureFlagsImpl()
+}
diff --git a/java/src/com/android/intentresolver/inject/FrameworkModule.kt b/java/src/com/android/intentresolver/inject/FrameworkModule.kt
new file mode 100644
index 00000000..2f6cc6a0
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/FrameworkModule.kt
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.inject
+
+import android.app.ActivityManager
+import android.app.admin.DevicePolicyManager
+import android.content.ClipboardManager
+import android.content.Context
+import android.content.pm.LauncherApps
+import android.content.pm.ShortcutManager
+import android.os.UserManager
+import android.view.WindowManager
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+
+private fun <T> Context.requireSystemService(serviceClass: Class<T>): T {
+ return checkNotNull(getSystemService(serviceClass))
+}
+
+@Module
+@InstallIn(SingletonComponent::class)
+object FrameworkModule {
+
+ @Provides
+ fun contentResolver(@ApplicationContext ctx: Context) =
+ requireNotNull(ctx.contentResolver) { "ContentResolver is expected but missing" }
+
+ @Provides
+ fun activityManager(@ApplicationContext ctx: Context) =
+ ctx.requireSystemService(ActivityManager::class.java)
+
+ @Provides
+ fun clipboardManager(@ApplicationContext ctx: Context) =
+ ctx.requireSystemService(ClipboardManager::class.java)
+
+ @Provides
+ fun devicePolicyManager(@ApplicationContext ctx: Context) =
+ ctx.requireSystemService(DevicePolicyManager::class.java)
+
+ @Provides
+ fun launcherApps(@ApplicationContext ctx: Context) =
+ ctx.requireSystemService(LauncherApps::class.java)
+
+ @Provides
+ fun packageManager(@ApplicationContext ctx: Context) =
+ requireNotNull(ctx.packageManager) { "PackageManager is expected but missing" }
+
+ @Provides
+ fun shortcutManager(@ApplicationContext ctx: Context) =
+ ctx.requireSystemService(ShortcutManager::class.java)
+
+ @Provides
+ fun userManager(@ApplicationContext ctx: Context) =
+ ctx.requireSystemService(UserManager::class.java)
+
+ @Provides
+ fun windowManager(@ApplicationContext ctx: Context) =
+ ctx.requireSystemService(WindowManager::class.java)
+}
diff --git a/java/src/com/android/intentresolver/inject/Qualifiers.kt b/java/src/com/android/intentresolver/inject/Qualifiers.kt
new file mode 100644
index 00000000..157e8f76
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/Qualifiers.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.inject
+
+import javax.inject.Qualifier
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ActivityOwned
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ApplicationOwned
+
+@Qualifier
+@MustBeDocumented
+@Retention(AnnotationRetention.RUNTIME)
+annotation class ApplicationUser
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ProfileParent
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Background
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Default
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Main
diff --git a/java/src/com/android/intentresolver/inject/SingletonModule.kt b/java/src/com/android/intentresolver/inject/SingletonModule.kt
new file mode 100644
index 00000000..e517800d
--- /dev/null
+++ b/java/src/com/android/intentresolver/inject/SingletonModule.kt
@@ -0,0 +1,22 @@
+package com.android.intentresolver.inject
+
+import android.content.Context
+import com.android.intentresolver.logging.EventLogImpl
+import dagger.Module
+import dagger.Provides
+import dagger.Reusable
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@InstallIn(SingletonComponent::class)
+@Module
+object SingletonModule {
+ @Provides @Singleton fun instanceIdSequence() = EventLogImpl.newIdSequence()
+
+ @Provides
+ @Reusable
+ @ApplicationOwned
+ fun resources(@ApplicationContext context: Context) = context.resources
+}
diff --git a/java/src/com/android/intentresolver/logging/EventLog.kt b/java/src/com/android/intentresolver/logging/EventLog.kt
new file mode 100644
index 00000000..476bd4bf
--- /dev/null
+++ b/java/src/com/android/intentresolver/logging/EventLog.kt
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.logging
+
+import android.net.Uri
+import android.util.HashedStringCache
+
+/** Logs notable events during ShareSheet usage. */
+interface EventLog {
+
+ companion object {
+ const val SELECTION_TYPE_SERVICE = 1
+ const val SELECTION_TYPE_APP = 2
+ const val SELECTION_TYPE_STANDARD = 3
+ const val SELECTION_TYPE_COPY = 4
+ const val SELECTION_TYPE_NEARBY = 5
+ const val SELECTION_TYPE_EDIT = 6
+ const val SELECTION_TYPE_MODIFY_SHARE = 7
+ const val SELECTION_TYPE_CUSTOM_ACTION = 8
+ }
+
+ fun logChooserActivityShown(isWorkProfile: Boolean, targetMimeType: String?, systemCost: Long)
+
+ fun logShareStarted(
+ packageName: String?,
+ mimeType: String?,
+ appProvidedDirect: Int,
+ appProvidedApp: Int,
+ isWorkprofile: Boolean,
+ previewType: Int,
+ intent: String?,
+ customActionCount: Int,
+ modifyShareActionProvided: Boolean
+ )
+
+ fun logCustomActionSelected(positionPicked: Int)
+ fun logShareTargetSelected(
+ targetType: Int,
+ packageName: String?,
+ positionPicked: Int,
+ directTargetAlsoRanked: Int,
+ numCallerProvided: Int,
+ directTargetHashed: HashedStringCache.HashResult?,
+ isPinned: Boolean,
+ successfullySelected: Boolean,
+ selectionCost: Long
+ )
+
+ fun logDirectShareTargetReceived(category: Int, latency: Int)
+ fun logActionShareWithPreview(previewType: Int)
+ fun logActionSelected(targetType: Int)
+ fun logContentPreviewWarning(uri: Uri?)
+ fun logSharesheetTriggered()
+ fun logSharesheetAppLoadComplete()
+ fun logSharesheetDirectLoadComplete()
+ fun logSharesheetDirectLoadTimeout()
+ fun logSharesheetProfileChanged()
+ fun logSharesheetExpansionChanged(isCollapsed: Boolean)
+ fun logSharesheetAppShareRankingTimeout()
+ fun logSharesheetEmptyDirectShareRow()
+}
diff --git a/java/src/com/android/intentresolver/logging/EventLog.java b/java/src/com/android/intentresolver/logging/EventLogImpl.java
index b30e825b..84029e76 100644
--- a/java/src/com/android/intentresolver/logging/EventLog.java
+++ b/java/src/com/android/intentresolver/logging/EventLogImpl.java
@@ -16,7 +16,6 @@
package com.android.intentresolver.logging;
-import android.annotation.Nullable;
import android.content.Intent;
import android.metrics.LogMaker;
import android.net.Uri;
@@ -24,6 +23,8 @@ import android.provider.MediaStore;
import android.util.HashedStringCache;
import android.util.Log;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.ChooserActivity;
import com.android.intentresolver.contentpreview.ContentPreviewType;
import com.android.internal.annotations.VisibleForTesting;
@@ -32,84 +33,42 @@ import com.android.internal.logging.InstanceIdSequence;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
-import com.android.internal.logging.UiEventLoggerImpl;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.FrameworkStatsLog;
+import javax.inject.Inject;
+
/**
* Helper for writing Sharesheet atoms to statsd log.
- * @hide
*/
-public class EventLog {
+public class EventLogImpl implements EventLog {
private static final String TAG = "ChooserActivity";
private static final boolean DEBUG = true;
- public static final int SELECTION_TYPE_SERVICE = 1;
- public static final int SELECTION_TYPE_APP = 2;
- public static final int SELECTION_TYPE_STANDARD = 3;
- public static final int SELECTION_TYPE_COPY = 4;
- public static final int SELECTION_TYPE_NEARBY = 5;
- public static final int SELECTION_TYPE_EDIT = 6;
- public static final int SELECTION_TYPE_MODIFY_SHARE = 7;
- public static final int SELECTION_TYPE_CUSTOM_ACTION = 8;
-
- /**
- * This shim is provided only for testing. In production, clients will only ever use a
- * {@link DefaultFrameworkStatsLogger}.
- */
- @VisibleForTesting
- interface FrameworkStatsLogger {
- /** Overload to use for logging {@code FrameworkStatsLog.SHARESHEET_STARTED}. */
- void write(
- int frameworkEventId,
- int appEventId,
- String packageName,
- int instanceId,
- String mimeType,
- int numAppProvidedDirectTargets,
- int numAppProvidedAppTargets,
- boolean isWorkProfile,
- int previewType,
- int intentType,
- int numCustomActions,
- boolean modifyShareActionProvided);
-
- /** Overload to use for logging {@code FrameworkStatsLog.RANKING_SELECTED}. */
- void write(
- int frameworkEventId,
- int appEventId,
- String packageName,
- int instanceId,
- int positionPicked,
- boolean isPinned);
- }
-
private static final int SHARESHEET_INSTANCE_ID_MAX = (1 << 13);
- // A small per-notification ID, used for statsd logging.
- // TODO: consider precomputing and storing as final.
- private static InstanceIdSequence sInstanceIdSequence;
- private InstanceId mInstanceId;
+ private final InstanceId mInstanceId;
private final UiEventLogger mUiEventLogger;
private final FrameworkStatsLogger mFrameworkStatsLogger;
private final MetricsLogger mMetricsLogger;
- public EventLog() {
- this(new UiEventLoggerImpl(), new DefaultFrameworkStatsLogger(), new MetricsLogger());
+ public static InstanceIdSequence newIdSequence() {
+ return new InstanceIdSequence(SHARESHEET_INSTANCE_ID_MAX);
}
- @VisibleForTesting
- EventLog(
- UiEventLogger uiEventLogger,
- FrameworkStatsLogger frameworkLogger,
- MetricsLogger metricsLogger) {
+ @Inject
+ public EventLogImpl(UiEventLogger uiEventLogger, FrameworkStatsLogger frameworkLogger,
+ MetricsLogger metricsLogger, InstanceId instanceId) {
mUiEventLogger = uiEventLogger;
mFrameworkStatsLogger = frameworkLogger;
mMetricsLogger = metricsLogger;
+ mInstanceId = instanceId;
}
+
/** Records metrics for the start time of the {@link ChooserActivity}. */
+ @Override
public void logChooserActivityShown(
boolean isWorkProfile, String targetMimeType, long systemCost) {
mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN)
@@ -120,6 +79,7 @@ public class EventLog {
}
/** Logs a UiEventReported event for the system sharesheet completing initial start-up. */
+ @Override
public void logShareStarted(
String packageName,
String mimeType,
@@ -133,7 +93,7 @@ public class EventLog {
mFrameworkStatsLogger.write(FrameworkStatsLog.SHARESHEET_STARTED,
/* event_id = 1 */ SharesheetStartedEvent.SHARE_STARTED.getId(),
/* package_name = 2 */ packageName,
- /* instance_id = 3 */ getInstanceId().getId(),
+ /* instance_id = 3 */ mInstanceId.getId(),
/* mime_type = 4 */ mimeType,
/* num_app_provided_direct_targets = 5 */ appProvidedDirect,
/* num_app_provided_app_targets = 6 */ appProvidedApp,
@@ -149,12 +109,13 @@ public class EventLog {
*
* @param positionPicked index of the custom action within the list of custom actions.
*/
+ @Override
public void logCustomActionSelected(int positionPicked) {
mFrameworkStatsLogger.write(FrameworkStatsLog.RANKING_SELECTED,
/* event_id = 1 */
SharesheetTargetSelectedEvent.SHARESHEET_CUSTOM_ACTION_SELECTED.getId(),
/* package_name = 2 */ null,
- /* instance_id = 3 */ getInstanceId().getId(),
+ /* instance_id = 3 */ mInstanceId.getId(),
/* position_picked = 4 */ positionPicked,
/* is_pinned = 5 */ false);
}
@@ -164,6 +125,7 @@ public class EventLog {
* TODO: document parameters and/or consider breaking up by targetType so we don't have to
* support an overly-generic signature.
*/
+ @Override
public void logShareTargetSelected(
int targetType,
String packageName,
@@ -177,7 +139,7 @@ public class EventLog {
mFrameworkStatsLogger.write(FrameworkStatsLog.RANKING_SELECTED,
/* event_id = 1 */ SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(),
/* package_name = 2 */ packageName,
- /* instance_id = 3 */ getInstanceId().getId(),
+ /* instance_id = 3 */ mInstanceId.getId(),
/* position_picked = 4 */ positionPicked,
/* is_pinned = 5 */ isPinned);
@@ -209,6 +171,7 @@ public class EventLog {
}
/** Log when direct share targets were received. */
+ @Override
public void logDirectShareTargetReceived(int category, int latency) {
mMetricsLogger.write(new LogMaker(category).setSubtype(latency));
}
@@ -217,12 +180,14 @@ public class EventLog {
* Log when we display a preview UI of the specified {@code previewType} as part of our
* Sharesheet session.
*/
+ @Override
public void logActionShareWithPreview(int previewType) {
mMetricsLogger.write(
new LogMaker(MetricsEvent.ACTION_SHARE_WITH_PREVIEW).setSubtype(previewType));
}
/** Log when the user selects an action button with the specified {@code targetType}. */
+ @Override
public void logActionSelected(int targetType) {
if (targetType == SELECTION_TYPE_COPY) {
LogMaker targetLogMaker = new LogMaker(
@@ -232,12 +197,13 @@ public class EventLog {
mFrameworkStatsLogger.write(FrameworkStatsLog.RANKING_SELECTED,
/* event_id = 1 */ SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(),
/* package_name = 2 */ "",
- /* instance_id = 3 */ getInstanceId().getId(),
+ /* instance_id = 3 */ mInstanceId.getId(),
/* position_picked = 4 */ -1,
/* is_pinned = 5 */ false);
}
/** Log a warning that we couldn't display the content preview from the supplied {@code uri}. */
+ @Override
public void logContentPreviewWarning(Uri uri) {
// The ContentResolver already logs the exception. Log something more informative.
Log.w(TAG, "Could not load (" + uri.toString() + ") thumbnail/name for preview. If "
@@ -248,55 +214,63 @@ public class EventLog {
}
/** Logs a UiEventReported event for the system sharesheet being triggered by the user. */
+ @Override
public void logSharesheetTriggered() {
- log(SharesheetStandardEvent.SHARESHEET_TRIGGERED, getInstanceId());
+ log(SharesheetStandardEvent.SHARESHEET_TRIGGERED, mInstanceId);
}
/** Logs a UiEventReported event for the system sharesheet completing loading app targets. */
+ @Override
public void logSharesheetAppLoadComplete() {
- log(SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE, getInstanceId());
+ log(SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE, mInstanceId);
}
/**
* Logs a UiEventReported event for the system sharesheet completing loading service targets.
*/
+ @Override
public void logSharesheetDirectLoadComplete() {
- log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_COMPLETE, getInstanceId());
+ log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_COMPLETE, mInstanceId);
}
/**
* Logs a UiEventReported event for the system sharesheet timing out loading service targets.
*/
+ @Override
public void logSharesheetDirectLoadTimeout() {
- log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_TIMEOUT, getInstanceId());
+ log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_TIMEOUT, mInstanceId);
}
/**
* Logs a UiEventReported event for the system sharesheet switching
* between work and main profile.
*/
+ @Override
public void logSharesheetProfileChanged() {
- log(SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED, getInstanceId());
+ log(SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED, mInstanceId);
}
/** Logs a UiEventReported event for the system sharesheet getting expanded or collapsed. */
+ @Override
public void logSharesheetExpansionChanged(boolean isCollapsed) {
log(isCollapsed ? SharesheetStandardEvent.SHARESHEET_COLLAPSED :
- SharesheetStandardEvent.SHARESHEET_EXPANDED, getInstanceId());
+ SharesheetStandardEvent.SHARESHEET_EXPANDED, mInstanceId);
}
/**
* Logs a UiEventReported event for the system sharesheet app share ranking timing out.
*/
+ @Override
public void logSharesheetAppShareRankingTimeout() {
- log(SharesheetStandardEvent.SHARESHEET_APP_SHARE_RANKING_TIMEOUT, getInstanceId());
+ log(SharesheetStandardEvent.SHARESHEET_APP_SHARE_RANKING_TIMEOUT, mInstanceId);
}
/**
* Logs a UiEventReported event for the system sharesheet when direct share row is empty.
*/
+ @Override
public void logSharesheetEmptyDirectShareRow() {
- log(SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW, getInstanceId());
+ log(SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW, mInstanceId);
}
/**
@@ -313,19 +287,6 @@ public class EventLog {
}
/**
- * @return A unique {@link InstanceId} to join across events recorded by this logger instance.
- */
- private InstanceId getInstanceId() {
- if (mInstanceId == null) {
- if (sInstanceIdSequence == null) {
- sInstanceIdSequence = new InstanceIdSequence(SHARESHEET_INSTANCE_ID_MAX);
- }
- mInstanceId = sInstanceIdSequence.newInstanceId();
- }
- return mInstanceId;
- }
-
- /**
* The UiEvent enums that this class can log.
*/
enum SharesheetStartedEvent implements UiEventLogger.UiEventEnum {
@@ -488,52 +449,4 @@ public class EventLog {
return 0;
}
}
-
- private static class DefaultFrameworkStatsLogger implements FrameworkStatsLogger {
- @Override
- public void write(
- int frameworkEventId,
- int appEventId,
- String packageName,
- int instanceId,
- String mimeType,
- int numAppProvidedDirectTargets,
- int numAppProvidedAppTargets,
- boolean isWorkProfile,
- int previewType,
- int intentType,
- int numCustomActions,
- boolean modifyShareActionProvided) {
- FrameworkStatsLog.write(
- frameworkEventId,
- /* event_id = 1 */ appEventId,
- /* package_name = 2 */ packageName,
- /* instance_id = 3 */ instanceId,
- /* mime_type = 4 */ mimeType,
- /* num_app_provided_direct_targets */ numAppProvidedDirectTargets,
- /* num_app_provided_app_targets */ numAppProvidedAppTargets,
- /* is_workprofile */ isWorkProfile,
- /* previewType = 8 */ previewType,
- /* intentType = 9 */ intentType,
- /* num_provided_custom_actions = 10 */ numCustomActions,
- /* modify_share_action_provided = 11 */ modifyShareActionProvided);
- }
-
- @Override
- public void write(
- int frameworkEventId,
- int appEventId,
- String packageName,
- int instanceId,
- int positionPicked,
- boolean isPinned) {
- FrameworkStatsLog.write(
- frameworkEventId,
- /* event_id = 1 */ appEventId,
- /* package_name = 2 */ packageName,
- /* instance_id = 3 */ instanceId,
- /* position_picked = 4 */ positionPicked,
- /* is_pinned = 5 */ isPinned);
- }
- }
}
diff --git a/java/src/com/android/intentresolver/logging/EventLogModule.kt b/java/src/com/android/intentresolver/logging/EventLogModule.kt
new file mode 100644
index 00000000..eba8ecc8
--- /dev/null
+++ b/java/src/com/android/intentresolver/logging/EventLogModule.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.logging
+
+import com.android.internal.logging.InstanceId
+import com.android.internal.logging.InstanceIdSequence
+import com.android.internal.logging.MetricsLogger
+import com.android.internal.logging.UiEventLogger
+import com.android.internal.logging.UiEventLoggerImpl
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityComponent
+import dagger.hilt.android.scopes.ActivityScoped
+
+@Module
+@InstallIn(ActivityComponent::class)
+interface EventLogModule {
+
+ @Binds @ActivityScoped fun eventLog(value: EventLogImpl): EventLog
+
+ companion object {
+ @Provides
+ fun instanceId(sequence: InstanceIdSequence): InstanceId = sequence.newInstanceId()
+
+ @Provides fun uiEventLogger(): UiEventLogger = UiEventLoggerImpl()
+
+ @Provides fun frameworkLogger(): FrameworkStatsLogger = object : FrameworkStatsLogger {}
+
+ @Provides fun metricsLogger(): MetricsLogger = MetricsLogger()
+ }
+}
diff --git a/java/src/com/android/intentresolver/logging/FrameworkStatsLogger.kt b/java/src/com/android/intentresolver/logging/FrameworkStatsLogger.kt
new file mode 100644
index 00000000..6508d305
--- /dev/null
+++ b/java/src/com/android/intentresolver/logging/FrameworkStatsLogger.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.logging
+
+import com.android.internal.util.FrameworkStatsLog
+
+/** A documenting annotation for FrameworkStatsLog methods and their associated UiEvents. */
+internal annotation class ForUiEvent(vararg val uiEventId: Int)
+
+/** Isolates the specific method signatures to use for each of the logged UiEvents. */
+interface FrameworkStatsLogger {
+
+ @ForUiEvent(FrameworkStatsLog.SHARESHEET_STARTED)
+ fun write(
+ frameworkEventId: Int,
+ appEventId: Int,
+ packageName: String?,
+ instanceId: Int,
+ mimeType: String?,
+ numAppProvidedDirectTargets: Int,
+ numAppProvidedAppTargets: Int,
+ isWorkProfile: Boolean,
+ previewType: Int,
+ intentType: Int,
+ numCustomActions: Int,
+ modifyShareActionProvided: Boolean,
+ ) {
+ FrameworkStatsLog.write(
+ frameworkEventId, /* event_id = 1 */
+ appEventId, /* package_name = 2 */
+ packageName, /* instance_id = 3 */
+ instanceId, /* mime_type = 4 */
+ mimeType, /* num_app_provided_direct_targets */
+ numAppProvidedDirectTargets, /* num_app_provided_app_targets */
+ numAppProvidedAppTargets, /* is_workprofile */
+ isWorkProfile, /* previewType = 8 */
+ previewType, /* intentType = 9 */
+ intentType, /* num_provided_custom_actions = 10 */
+ numCustomActions, /* modify_share_action_provided = 11 */
+ modifyShareActionProvided
+ )
+ }
+
+ @ForUiEvent(FrameworkStatsLog.RANKING_SELECTED)
+ fun write(
+ frameworkEventId: Int,
+ appEventId: Int,
+ packageName: String?,
+ instanceId: Int,
+ positionPicked: Int,
+ isPinned: Boolean,
+ ) {
+ FrameworkStatsLog.write(
+ frameworkEventId, /* event_id = 1 */
+ appEventId, /* package_name = 2 */
+ packageName, /* instance_id = 3 */
+ instanceId, /* position_picked = 4 */
+ positionPicked, /* is_pinned = 5 */
+ isPinned
+ )
+ }
+}
diff --git a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
index ff2d6a0f..724fa849 100644
--- a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
@@ -16,7 +16,6 @@
package com.android.intentresolver.model;
-import android.annotation.Nullable;
import android.app.usage.UsageStatsManager;
import android.content.ComponentName;
import android.content.Context;
@@ -30,10 +29,13 @@ import android.os.Message;
import android.os.UserHandle;
import android.util.Log;
-import com.android.intentresolver.logging.EventLog;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.ResolvedComponentInfo;
import com.android.intentresolver.ResolverActivity;
+import com.android.intentresolver.ResolverListController;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.logging.EventLog;
import java.text.Collator;
import java.util.ArrayList;
@@ -75,6 +77,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
private EventLog mEventLog;
protected final Handler mHandler = new Handler(Looper.getMainLooper()) {
+ @Override
public void handleMessage(Message msg) {
switch (msg.what) {
case RANKER_SERVICE_RESULT:
@@ -229,7 +232,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
* {@link ResolvedComponentInfo#getResolveInfoAt(int)} from the parameters of {@link
* #compare(ResolvedComponentInfo, ResolvedComponentInfo)}
*/
- abstract int compare(ResolveInfo lhs, ResolveInfo rhs);
+ public abstract int compare(ResolveInfo lhs, ResolveInfo rhs);
/**
* Computes features for each target. This will be called before calls to {@link
@@ -245,7 +248,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
}
/** Implementation of compute called after {@link #beforeCompute()}. */
- abstract void doCompute(List<ResolvedComponentInfo> targets);
+ public abstract void doCompute(List<ResolvedComponentInfo> targets);
/**
* Returns the score that was calculated for the corresponding {@link ResolvedComponentInfo}
@@ -254,12 +257,12 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
public abstract float getScore(TargetInfo targetInfo);
/** Handles result message sent to mHandler. */
- abstract void handleResultMessage(Message message);
+ public abstract void handleResultMessage(Message message);
/**
* Reports to UsageStats what was chosen.
*/
- public final void updateChooserCounts(String packageName, UserHandle user, String action) {
+ public void updateChooserCounts(String packageName, UserHandle user, String action) {
if (mUsmMap.containsKey(user)) {
mUsmMap.get(user).reportChooserSelection(
packageName,
diff --git a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
index 621ae306..0651d26c 100644
--- a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
@@ -18,7 +18,6 @@ package com.android.intentresolver.model;
import static android.app.prediction.AppTargetEvent.ACTION_LAUNCH;
-import android.annotation.Nullable;
import android.app.prediction.AppPredictor;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
@@ -31,9 +30,12 @@ import android.os.Message;
import android.os.UserHandle;
import android.util.Log;
-import com.android.intentresolver.logging.EventLog;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.ResolvedComponentInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.logging.EventLog;
+import com.android.intentresolver.shortcuts.ScopedAppTargetListCallback;
import com.google.android.collect.Lists;
@@ -85,12 +87,12 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp
}
@Override
- int compare(ResolveInfo lhs, ResolveInfo rhs) {
+ public int compare(ResolveInfo lhs, ResolveInfo rhs) {
return mComparatorModel.getComparator().compare(lhs, rhs);
}
@Override
- void doCompute(List<ResolvedComponentInfo> targets) {
+ public void doCompute(List<ResolvedComponentInfo> targets) {
if (targets.isEmpty()) {
mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT);
return;
@@ -105,33 +107,44 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp
.setClassName(target.name.getClassName())
.build());
}
- mAppPredictor.sortTargets(appTargets, Executors.newSingleThreadExecutor(),
- sortedAppTargets -> {
- if (sortedAppTargets.isEmpty()) {
- Log.i(TAG, "AppPredictionService disabled. Using resolver.");
- // APS for chooser is disabled. Fallback to resolver.
- mResolverRankerService =
- new ResolverRankerServiceResolverComparator(
- mContext,
- mIntent,
- mReferrerPackage,
- () -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT),
- getEventLog(),
- mUser,
- mPromoteToFirst);
- mComparatorModel = buildUpdatedModel();
- mResolverRankerService.compute(targets);
- } else {
- Log.i(TAG, "AppPredictionService response received");
- // Skip sending to Handler which takes extra time to dispatch messages.
- handleResult(sortedAppTargets);
- }
- }
+ mAppPredictor.sortTargets(
+ appTargets,
+ Executors.newSingleThreadExecutor(),
+ new ScopedAppTargetListCallback(
+ mContext,
+ sortedAppTargets -> {
+ onAppTargetsSorted(targets, sortedAppTargets);
+ return kotlin.Unit.INSTANCE;
+ }).toConsumer()
);
}
+ private void onAppTargetsSorted(
+ List<ResolvedComponentInfo> targets, List<AppTarget> sortedAppTargets) {
+ if (sortedAppTargets.isEmpty()) {
+ Log.i(TAG, "AppPredictionService disabled. Using resolver.");
+ // APS for chooser is disabled. Fallback to resolver.
+ mResolverRankerService =
+ new ResolverRankerServiceResolverComparator(
+ mContext,
+ mIntent,
+ mReferrerPackage,
+ () -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT),
+ getEventLog(),
+ mUser,
+ mPromoteToFirst);
+ mComparatorModel = buildUpdatedModel();
+ mResolverRankerService.compute(targets);
+ } else {
+ Log.i(TAG, "AppPredictionService response received");
+ // Skip sending to Handler which takes extra time to dispatch
+ // messages.
+ handleResult(sortedAppTargets);
+ }
+ }
+
@Override
- void handleResultMessage(Message msg) {
+ public void handleResultMessage(Message msg) {
// Null value is okay if we have defaulted to the ResolverRankerService.
if (msg.what == RANKER_SERVICE_RESULT && msg.obj != null) {
final List<AppTarget> sortedAppTargets = (List<AppTarget>) msg.obj;
diff --git a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
index 7d473660..f3804154 100644
--- a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
@@ -17,7 +17,6 @@
package com.android.intentresolver.model;
-import android.annotation.Nullable;
import android.app.usage.UsageStats;
import android.content.ComponentName;
import android.content.Context;
@@ -39,9 +38,11 @@ import android.service.resolver.ResolverRankerService;
import android.service.resolver.ResolverTarget;
import android.util.Log;
-import com.android.intentresolver.logging.EventLog;
+import androidx.annotation.Nullable;
+
import com.android.intentresolver.ResolvedComponentInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.logging.EventLog;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -101,9 +102,9 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom
* the userSpace provided by context.
*/
public ResolverRankerServiceResolverComparator(Context launchedFromContext, Intent intent,
- String referrerPackage, Runnable afterCompute,
- EventLog eventLog, UserHandle targetUserSpace,
- ComponentName promoteToFirst) {
+ String referrerPackage, Runnable afterCompute,
+ EventLog eventLog, UserHandle targetUserSpace,
+ ComponentName promoteToFirst) {
this(launchedFromContext, intent, referrerPackage, afterCompute, eventLog,
Lists.newArrayList(targetUserSpace), promoteToFirst);
}
@@ -117,9 +118,8 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom
* different from the userSpace provided by context.
*/
public ResolverRankerServiceResolverComparator(Context launchedFromContext, Intent intent,
- String referrerPackage, Runnable afterCompute,
- EventLog eventLog, List<UserHandle> targetUserSpaceList,
- @Nullable ComponentName promoteToFirst) {
+ String referrerPackage, Runnable afterCompute, EventLog eventLog,
+ List<UserHandle> targetUserSpaceList, @Nullable ComponentName promoteToFirst) {
super(launchedFromContext, intent, targetUserSpaceList, promoteToFirst);
mCollator = Collator.getInstance(
launchedFromContext.getResources().getConfiguration().locale);
diff --git a/java/src/com/android/intentresolver/shortcuts/ScopedAppTargetListCallback.kt b/java/src/com/android/intentresolver/shortcuts/ScopedAppTargetListCallback.kt
new file mode 100644
index 00000000..9606a6a1
--- /dev/null
+++ b/java/src/com/android/intentresolver/shortcuts/ScopedAppTargetListCallback.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.shortcuts
+
+import android.app.prediction.AppPredictor
+import android.app.prediction.AppTarget
+import android.content.Context
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.coroutineScope
+import java.util.function.Consumer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.launch
+
+/**
+ * A memory leak workaround for b/290971946. Drops the references to the actual [callback] when the
+ * [scope] is cancelled allowing it to be garbage-collected (and only leaking this instance).
+ */
+class ScopedAppTargetListCallback(
+ scope: CoroutineScope?,
+ callback: (List<AppTarget>) -> Unit,
+) {
+
+ @Volatile private var callbackRef: ((List<AppTarget>) -> Unit)? = callback
+
+ constructor(
+ context: Context,
+ callback: (List<AppTarget>) -> Unit,
+ ) : this((context as? LifecycleOwner)?.lifecycle?.coroutineScope, callback)
+
+ init {
+ scope?.launch { awaitCancellation() }?.invokeOnCompletion { callbackRef = null }
+ }
+
+ private fun notifyCallback(result: List<AppTarget>) {
+ callbackRef?.invoke(result)
+ }
+
+ fun toConsumer(): Consumer<MutableList<AppTarget>?> =
+ Consumer<MutableList<AppTarget>?> { notifyCallback(it ?: emptyList()) }
+
+ fun toAppPredictorCallback(): AppPredictor.Callback =
+ AppPredictor.Callback { notifyCallback(it) }
+}
diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
index f05542e2..a8b59fb0 100644
--- a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
+++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
@@ -35,14 +35,13 @@ import androidx.annotation.MainThread
import androidx.annotation.OpenForTesting
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.coroutineScope
import com.android.intentresolver.chooser.DisplayResolveInfo
import com.android.intentresolver.measurements.Tracer
import com.android.intentresolver.measurements.runTracing
import java.util.concurrent.Executor
import java.util.function.Consumer
import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asExecutor
import kotlinx.coroutines.channels.BufferOverflow
@@ -50,6 +49,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
/**
@@ -58,14 +58,14 @@ import kotlinx.coroutines.launch
* A ShortcutLoader instance can be viewed as a per-profile singleton hot stream of shortcut
* updates. The shortcut loading is triggered in the constructor or by the [reset] method, the
* processing happens on the [dispatcher] and the result is delivered through the [callback] on the
- * default [lifecycle]'s dispatcher, the main thread.
+ * default [scope]'s dispatcher, the main thread.
*/
@OpenForTesting
open class ShortcutLoader
@VisibleForTesting
constructor(
private val context: Context,
- private val lifecycle: Lifecycle,
+ private val scope: CoroutineScope,
private val appPredictor: AppPredictorProxy?,
private val userHandle: UserHandle,
private val isPersonalProfile: Boolean,
@@ -75,7 +75,9 @@ constructor(
) {
private val shortcutToChooserTargetConverter = ShortcutToChooserTargetConverter()
private val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
- private val appPredictorCallback = AppPredictor.Callback { onAppPredictorCallback(it) }
+ private val appPredictorCallback =
+ ScopedAppTargetListCallback(scope) { onAppPredictorCallback(it) }.toAppPredictorCallback()
+
private val appTargetSource =
MutableSharedFlow<Array<DisplayResolveInfo>?>(
replay = 1,
@@ -84,19 +86,19 @@ constructor(
private val shortcutSource =
MutableSharedFlow<ShortcutData?>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
private val isDestroyed
- get() = !lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)
+ get() = !scope.isActive
@MainThread
constructor(
context: Context,
- lifecycle: Lifecycle,
+ scope: CoroutineScope,
appPredictor: AppPredictor?,
userHandle: UserHandle,
targetIntentFilter: IntentFilter?,
callback: Consumer<Result>
) : this(
context,
- lifecycle,
+ scope,
appPredictor?.let { AppPredictorProxy(it) },
userHandle,
userHandle == UserHandle.of(ActivityManager.getCurrentUser()),
@@ -107,7 +109,7 @@ constructor(
init {
appPredictor?.registerPredictionUpdates(dispatcher.asExecutor(), appPredictorCallback)
- lifecycle.coroutineScope
+ scope
.launch {
appTargetSource
.combine(shortcutSource) { appTargets, shortcutData ->
@@ -135,13 +137,13 @@ constructor(
reset()
}
- /** Clear application targets (see [updateAppTargets] and initiate shrtcuts loading. */
+ /** Clear application targets (see [updateAppTargets] and initiate shortcuts loading. */
@OpenForTesting
open fun reset() {
Log.d(TAG, "reset shortcut loader for user $userHandle")
appTargetSource.tryEmit(null)
shortcutSource.tryEmit(null)
- lifecycle.coroutineScope.launch(dispatcher) { loadShortcuts() }
+ scope.launch(dispatcher) { loadShortcuts() }
}
/**
diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java b/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java
index a37d6558..31929948 100644
--- a/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java
+++ b/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java
@@ -16,8 +16,6 @@
package com.android.intentresolver.shortcuts;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
import android.app.prediction.AppTarget;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
@@ -25,6 +23,9 @@ import android.content.pm.ShortcutManager;
import android.os.Bundle;
import android.service.chooser.ChooserTarget;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
diff --git a/java/src/com/android/intentresolver/v2/ActivityLogic.kt b/java/src/com/android/intentresolver/v2/ActivityLogic.kt
new file mode 100644
index 00000000..c81bed09
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ActivityLogic.kt
@@ -0,0 +1,156 @@
+package com.android.intentresolver.v2
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL
+import android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK
+import android.content.Intent
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.core.content.getSystemService
+import com.android.intentresolver.AnnotatedUserHandles
+import com.android.intentresolver.R
+import com.android.intentresolver.WorkProfileAvailabilityManager
+import com.android.intentresolver.icons.TargetDataLoader
+
+/**
+ * Logic for IntentResolver Activities. Anything that is not the same across activities (including
+ * test activities) should be in this interface. Expect there to be one implementation for each
+ * activity, including test activities, but all implementations should delegate to a
+ * CommonActivityLogic implementation.
+ */
+interface ActivityLogic : CommonActivityLogic {
+ /** The intent for the target. This will always come before additional targets, if any. */
+ val targetIntent: Intent
+ /** Whether the intent is for home. */
+ val resolvingHome: Boolean
+ /** Custom title to display. */
+ val title: CharSequence?
+ /** Resource ID for the title to display when there is no custom title. */
+ val defaultTitleResId: Int
+ /** Intents received to be processed. */
+ val initialIntents: List<Intent>?
+ /** Whether or not this activity supports choosing a default handler for the intent. */
+ val supportsAlwaysUseOption: Boolean
+ /** Fetches display info for processed candidates. */
+ val targetDataLoader: TargetDataLoader
+ /** The theme to use. */
+ val themeResId: Int
+ /**
+ * Message showing that intent is forwarded from managed profile to owner or other way around.
+ */
+ val profileSwitchMessage: String?
+ /** The intents for potential actual targets. [targetIntent] must be first. */
+ val payloadIntents: List<Intent>
+
+ /**
+ * Called after Activity superclass creation, but before any other onCreate logic is performed.
+ */
+ fun preInitialization()
+
+ /** Sets [profileSwitchMessage] to null */
+ fun clearProfileSwitchMessage()
+}
+
+/**
+ * Logic that is common to all IntentResolver activities. Anything that is the same across
+ * activities (including test activities), should live here.
+ */
+interface CommonActivityLogic {
+ /** The tag to use when logging. */
+ val tag: String
+ /** A reference to the activity owning, and used by, this logic. */
+ val activity: ComponentActivity
+ /** The name of the referring package. */
+ val referrerPackageName: String?
+ /** User manager system service. */
+ val userManager: UserManager
+ /** Device policy manager system service. */
+ val devicePolicyManager: DevicePolicyManager
+ /** Current [UserHandle]s retrievable by type. */
+ val annotatedUserHandles: AnnotatedUserHandles?
+ /** Monitors for changes to work profile availability. */
+ val workProfileAvailabilityManager: WorkProfileAvailabilityManager
+
+ /** Returns display message indicating intent forwarding or null if not intent forwarding. */
+ fun forwardMessageFor(intent: Intent): String?
+}
+
+/**
+ * Concrete implementation of the [CommonActivityLogic] interface meant to be delegated to by
+ * [ActivityLogic] implementations. Test implementations of [ActivityLogic] may need to create their
+ * own [CommonActivityLogic] implementation.
+ */
+class CommonActivityLogicImpl(
+ override val tag: String,
+ activityProvider: () -> ComponentActivity,
+ onWorkProfileStatusUpdated: () -> Unit,
+) : CommonActivityLogic {
+
+ override val activity: ComponentActivity by lazy { activityProvider() }
+
+ override val referrerPackageName: String? by lazy {
+ activity.referrer.let {
+ if (ANDROID_APP_URI_SCHEME == it?.scheme) {
+ it.host
+ } else {
+ null
+ }
+ }
+ }
+
+ override val userManager: UserManager by lazy { activity.getSystemService()!! }
+
+ override val devicePolicyManager: DevicePolicyManager by lazy { activity.getSystemService()!! }
+
+ override val annotatedUserHandles: AnnotatedUserHandles? by lazy {
+ try {
+ AnnotatedUserHandles.forShareActivity(activity)
+ } catch (e: SecurityException) {
+ Log.e(tag, "Request from UID without necessary permissions", e)
+ null
+ }
+ }
+
+ override val workProfileAvailabilityManager: WorkProfileAvailabilityManager by lazy {
+ WorkProfileAvailabilityManager(
+ userManager,
+ annotatedUserHandles?.workProfileUserHandle,
+ onWorkProfileStatusUpdated,
+ )
+ }
+
+ private val forwardToPersonalMessage: String? by lazy {
+ devicePolicyManager.resources.getString(FORWARD_INTENT_TO_PERSONAL) {
+ activity.getString(R.string.forward_intent_to_owner)
+ }
+ }
+
+ private val forwardToWorkMessage: String? by lazy {
+ devicePolicyManager.resources.getString(FORWARD_INTENT_TO_WORK) {
+ activity.getString(R.string.forward_intent_to_work)
+ }
+ }
+
+ override fun forwardMessageFor(intent: Intent): String? {
+ val contentUserHint = intent.contentUserHint
+ if (
+ contentUserHint != UserHandle.USER_CURRENT && contentUserHint != UserHandle.myUserId()
+ ) {
+ val originUserInfo = userManager.getUserInfo(contentUserHint)
+ val originIsManaged = originUserInfo?.isManagedProfile ?: false
+ val targetIsManaged = userManager.isManagedProfile
+ return when {
+ originIsManaged && !targetIsManaged -> forwardToPersonalMessage
+ !originIsManaged && targetIsManaged -> forwardToWorkMessage
+ else -> null
+ }
+ }
+ return null
+ }
+
+ companion object {
+ private const val ANDROID_APP_URI_SCHEME = "android-app"
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ChooserActionFactory.java b/java/src/com/android/intentresolver/v2/ChooserActionFactory.java
new file mode 100644
index 00000000..db840387
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ChooserActionFactory.java
@@ -0,0 +1,395 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2;
+
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.service.chooser.ChooserAction;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.R;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
+import com.android.intentresolver.logging.EventLog;
+import com.android.intentresolver.widget.ActionRow;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.concurrent.Callable;
+import java.util.function.Consumer;
+
+/**
+ * Implementation of {@link ChooserContentPreviewUi.ActionFactory} specialized to the application
+ * requirements of Sharesheet / {@link ChooserActivity}.
+ */
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+public final class ChooserActionFactory implements ChooserContentPreviewUi.ActionFactory {
+ /**
+ * Delegate interface to launch activities when the actions are selected.
+ */
+ public interface ActionActivityStarter {
+ /**
+ * Request an activity launch for the provided target. Implementations may choose to exit
+ * the current activity when the target is launched.
+ */
+ void safelyStartActivityAsPersonalProfileUser(TargetInfo info);
+
+ /**
+ * Request an activity launch for the provided target, optionally employing the specified
+ * shared element transition. Implementations may choose to exit the current activity when
+ * the target is launched.
+ */
+ default void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+ TargetInfo info, View sharedElement, String sharedElementName) {
+ safelyStartActivityAsPersonalProfileUser(info);
+ }
+ }
+
+ private static final String TAG = "ChooserActions";
+
+ private static final int URI_PERMISSION_INTENT_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
+
+ // Boolean extra used to inform the editor that it may want to customize the editing experience
+ // for the sharesheet editing flow.
+ private static final String EDIT_SOURCE = "edit_source";
+ private static final String EDIT_SOURCE_SHARESHEET = "sharesheet";
+
+ private static final String CHIP_LABEL_METADATA_KEY = "android.service.chooser.chip_label";
+ private static final String CHIP_ICON_METADATA_KEY = "android.service.chooser.chip_icon";
+
+ private static final String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image";
+
+ private final Context mContext;
+
+ @Nullable
+ private final Runnable mCopyButtonRunnable;
+ private final Runnable mEditButtonRunnable;
+ private final ImmutableList<ChooserAction> mCustomActions;
+ private final @Nullable ChooserAction mModifyShareAction;
+ private final Consumer<Boolean> mExcludeSharedTextAction;
+ private final Consumer</* @Nullable */ Integer> mFinishCallback;
+ private final EventLog mLog;
+
+ /**
+ * @param context
+ * @param imageEditor an explicit Activity to launch for editing images
+ * @param onUpdateSharedTextIsExcluded a delegate to be invoked when the "exclude shared text"
+ * setting is updated. The argument is whether the shared text is to be excluded.
+ * @param firstVisibleImageQuery a delegate that provides a reference to the first visible image
+ * View in the Sharesheet UI, if any, or null.
+ * @param activityStarter a delegate to launch activities when actions are selected.
+ * @param finishCallback a delegate to close the Sharesheet UI (e.g. because some action was
+ * completed).
+ */
+ public ChooserActionFactory(
+ Context context,
+ Intent targetIntent,
+ String referrerPackageName,
+ List<ChooserAction> chooserActions,
+ ChooserAction modifyShareAction,
+ Optional<ComponentName> imageEditor,
+ EventLog log,
+ Consumer<Boolean> onUpdateSharedTextIsExcluded,
+ Callable</* @Nullable */ View> firstVisibleImageQuery,
+ ActionActivityStarter activityStarter,
+ Consumer</* @Nullable */ Integer> finishCallback) {
+ this(
+ context,
+ makeCopyButtonRunnable(
+ context,
+ targetIntent,
+ referrerPackageName,
+ finishCallback,
+ log),
+ makeEditButtonRunnable(
+ getEditSharingTarget(
+ context,
+ targetIntent,
+ imageEditor),
+ firstVisibleImageQuery,
+ activityStarter,
+ log),
+ chooserActions,
+ modifyShareAction,
+ onUpdateSharedTextIsExcluded,
+ log,
+ finishCallback);
+ }
+
+ @VisibleForTesting
+ ChooserActionFactory(
+ Context context,
+ @Nullable Runnable copyButtonRunnable,
+ Runnable editButtonRunnable,
+ List<ChooserAction> customActions,
+ @Nullable ChooserAction modifyShareAction,
+ Consumer<Boolean> onUpdateSharedTextIsExcluded,
+ EventLog log,
+ Consumer</* @Nullable */ Integer> finishCallback) {
+ mContext = context;
+ mCopyButtonRunnable = copyButtonRunnable;
+ mEditButtonRunnable = editButtonRunnable;
+ mCustomActions = ImmutableList.copyOf(customActions);
+ mModifyShareAction = modifyShareAction;
+ mExcludeSharedTextAction = onUpdateSharedTextIsExcluded;
+ mLog = log;
+ mFinishCallback = finishCallback;
+ }
+
+ @Override
+ @Nullable
+ public Runnable getEditButtonRunnable() {
+ return mEditButtonRunnable;
+ }
+
+ @Override
+ @Nullable
+ public Runnable getCopyButtonRunnable() {
+ return mCopyButtonRunnable;
+ }
+
+ /** Create custom actions */
+ @Override
+ public List<ActionRow.Action> createCustomActions() {
+ List<ActionRow.Action> actions = new ArrayList<>();
+ for (int i = 0; i < mCustomActions.size(); i++) {
+ final int position = i;
+ ActionRow.Action actionRow = createCustomAction(
+ mContext,
+ mCustomActions.get(i),
+ mFinishCallback,
+ () -> {
+ mLog.logCustomActionSelected(position);
+ }
+ );
+ if (actionRow != null) {
+ actions.add(actionRow);
+ }
+ }
+ return actions;
+ }
+
+ /**
+ * Provides a share modification action, if any.
+ */
+ @Override
+ @Nullable
+ public ActionRow.Action getModifyShareAction() {
+ return createCustomAction(
+ mContext,
+ mModifyShareAction,
+ mFinishCallback,
+ () -> {
+ mLog.logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE);
+ });
+ }
+
+ /**
+ * <p>
+ * Creates an exclude-text action that can be called when the user changes shared text
+ * status in the Media + Text preview.
+ * </p>
+ * <p>
+ * <code>true</code> argument value indicates that the text should be excluded.
+ * </p>
+ */
+ @Override
+ public Consumer<Boolean> getExcludeSharedTextAction() {
+ return mExcludeSharedTextAction;
+ }
+
+ @Nullable
+ private static Runnable makeCopyButtonRunnable(
+ Context context,
+ Intent targetIntent,
+ String referrerPackageName,
+ Consumer<Integer> finishCallback,
+ EventLog log) {
+ final ClipData clipData;
+ try {
+ clipData = extractTextToCopy(targetIntent);
+ } catch (Throwable t) {
+ Log.e(TAG, "Failed to extract data to copy", t);
+ return null;
+ }
+ if (clipData == null) {
+ return null;
+ }
+ return () -> {
+ ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(
+ Context.CLIPBOARD_SERVICE);
+ clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName);
+
+ log.logActionSelected(EventLog.SELECTION_TYPE_COPY);
+ finishCallback.accept(Activity.RESULT_OK);
+ };
+ }
+
+ @Nullable
+ private static ClipData extractTextToCopy(Intent targetIntent) {
+ if (targetIntent == null) {
+ return null;
+ }
+
+ final String action = targetIntent.getAction();
+
+ ClipData clipData = null;
+ if (Intent.ACTION_SEND.equals(action)) {
+ String extraText = targetIntent.getStringExtra(Intent.EXTRA_TEXT);
+
+ if (extraText != null) {
+ clipData = ClipData.newPlainText(null, extraText);
+ } else {
+ Log.w(TAG, "No data available to copy to clipboard");
+ }
+ } else {
+ // expected to only be visible with ACTION_SEND (when a text is shared)
+ Log.d(TAG, "Action (" + action + ") not supported for copying to clipboard");
+ }
+ return clipData;
+ }
+
+ private static TargetInfo getEditSharingTarget(
+ Context context,
+ Intent originalIntent,
+ Optional<ComponentName> imageEditor) {
+
+ final Intent resolveIntent = new Intent(originalIntent);
+ // Retain only URI permission grant flags if present. Other flags may prevent the scene
+ // transition animation from running (i.e FLAG_ACTIVITY_NO_ANIMATION,
+ // FLAG_ACTIVITY_NEW_TASK, FLAG_ACTIVITY_NEW_DOCUMENT) but also not needed.
+ resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS);
+ imageEditor.ifPresent(resolveIntent::setComponent);
+ resolveIntent.setAction(Intent.ACTION_EDIT);
+ resolveIntent.putExtra(EDIT_SOURCE, EDIT_SOURCE_SHARESHEET);
+ String originalAction = originalIntent.getAction();
+ if (Intent.ACTION_SEND.equals(originalAction)) {
+ if (resolveIntent.getData() == null) {
+ Uri uri = resolveIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+ if (uri != null) {
+ String mimeType = context.getContentResolver().getType(uri);
+ resolveIntent.setDataAndType(uri, mimeType);
+ }
+ }
+ } else {
+ Log.e(TAG, originalAction + " is not supported.");
+ return null;
+ }
+ final ResolveInfo ri = context.getPackageManager().resolveActivity(
+ resolveIntent, PackageManager.GET_META_DATA);
+ if (ri == null || ri.activityInfo == null) {
+ Log.e(TAG, "Device-specified editor (" + imageEditor + ") not available");
+ return null;
+ }
+
+ final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent,
+ ri,
+ context.getString(R.string.screenshot_edit),
+ "",
+ resolveIntent);
+ dri.getDisplayIconHolder().setDisplayIcon(
+ context.getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));
+ return dri;
+ }
+
+ private static Runnable makeEditButtonRunnable(
+ TargetInfo editSharingTarget,
+ Callable</* @Nullable */ View> firstVisibleImageQuery,
+ ActionActivityStarter activityStarter,
+ EventLog log) {
+ return () -> {
+ // Log share completion via edit.
+ log.logActionSelected(EventLog.SELECTION_TYPE_EDIT);
+
+ View firstImageView = null;
+ try {
+ firstImageView = firstVisibleImageQuery.call();
+ } catch (Exception e) { /* ignore */ }
+ // Action bar is user-independent; always start as primary.
+ if (firstImageView == null) {
+ activityStarter.safelyStartActivityAsPersonalProfileUser(editSharingTarget);
+ } else {
+ activityStarter.safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+ editSharingTarget, firstImageView, IMAGE_EDITOR_SHARED_ELEMENT);
+ }
+ };
+ }
+
+ @Nullable
+ private static ActionRow.Action createCustomAction(
+ Context context,
+ ChooserAction action,
+ Consumer<Integer> finishCallback,
+ Runnable loggingRunnable) {
+ if (action == null || action.getAction() == null) {
+ return null;
+ }
+ Drawable icon = action.getIcon().loadDrawable(context);
+ if (icon == null && TextUtils.isEmpty(action.getLabel())) {
+ return null;
+ }
+ return new ActionRow.Action(
+ action.getLabel(),
+ icon,
+ () -> {
+ try {
+ action.getAction().send(
+ null,
+ 0,
+ null,
+ null,
+ null,
+ null,
+ ActivityOptions.makeCustomAnimation(
+ context,
+ R.anim.slide_in_right,
+ R.anim.slide_out_left)
+ .toBundle());
+ } catch (PendingIntent.CanceledException e) {
+ Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled");
+ }
+ if (loggingRunnable != null) {
+ loggingRunnable.run();
+ }
+ finishCallback.accept(Activity.RESULT_OK);
+ }
+ );
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java
new file mode 100644
index 00000000..70812642
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java
@@ -0,0 +1,1845 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2;
+
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
+import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
+import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
+
+import static androidx.lifecycle.LifecycleKt.getCoroutineScope;
+
+import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
+
+import static java.util.Objects.requireNonNull;
+
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.app.prediction.AppTargetId;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.content.SharedPreferences;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.content.res.Configuration;
+import android.database.Cursor;
+import android.graphics.Insets;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.service.chooser.ChooserTarget;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.widget.TextView;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.viewpager.widget.ViewPager;
+
+import com.android.intentresolver.AnnotatedUserHandles;
+import com.android.intentresolver.ChooserGridLayoutManager;
+import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.ChooserRefinementManager;
+import com.android.intentresolver.ChooserRequestParameters;
+import com.android.intentresolver.ChooserStackedAppDialogFragment;
+import com.android.intentresolver.ChooserTargetActionsDialogFragment;
+import com.android.intentresolver.EnterTransitionAnimationDelegate;
+import com.android.intentresolver.FeatureFlags;
+import com.android.intentresolver.IntentForwarderActivity;
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.ResolverListController;
+import com.android.intentresolver.ResolverViewPager;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.contentpreview.BasePreviewViewModel;
+import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
+import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl;
+import com.android.intentresolver.contentpreview.PreviewViewModel;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.grid.ChooserGridAdapter;
+import com.android.intentresolver.icons.TargetDataLoader;
+import com.android.intentresolver.logging.EventLog;
+import com.android.intentresolver.measurements.Tracer;
+import com.android.intentresolver.model.AbstractResolverComparator;
+import com.android.intentresolver.model.AppPredictionServiceResolverComparator;
+import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
+import com.android.intentresolver.shortcuts.AppPredictorFactory;
+import com.android.intentresolver.shortcuts.ShortcutLoader;
+import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider;
+import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
+import com.android.intentresolver.v2.platform.ImageEditor;
+import com.android.intentresolver.v2.platform.NearbyShare;
+import com.android.intentresolver.widget.ImagePreviewView;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import dagger.hilt.android.AndroidEntryPoint;
+
+import kotlin.Unit;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Consumer;
+
+import javax.inject.Inject;
+
+/**
+ * The Chooser Activity handles intent resolution specifically for sharing intents -
+ * for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}.
+ *
+ */
+@SuppressWarnings("OptionalUsedAsFieldOrParameterType")
+@AndroidEntryPoint(ResolverActivity.class)
+public class ChooserActivity extends Hilt_ChooserActivity implements
+ ResolverListAdapter.ResolverListCommunicator {
+ private static final String TAG = "ChooserActivity";
+
+ /**
+ * Boolean extra to change the following behavior: Normally, ChooserActivity finishes itself
+ * in onStop when launched in a new task. If this extra is set to true, we do not finish
+ * ourselves when onStop gets called.
+ */
+ public static final String EXTRA_PRIVATE_RETAIN_IN_ON_STOP
+ = "com.android.internal.app.ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP";
+
+ /**
+ * Transition name for the first image preview.
+ * To be used for shared element transition into this activity.
+ * @hide
+ */
+ public static final String FIRST_IMAGE_PREVIEW_TRANSITION_NAME = "screenshot_preview_image";
+
+ private static final boolean DEBUG = true;
+
+ public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share";
+ private static final String SHORTCUT_TARGET = "shortcut_target";
+
+ // TODO: these data structures are for one-time use in shuttling data from where they're
+ // populated in `ShortcutToChooserTargetConverter` to where they're consumed in
+ // `ShortcutSelectionLogic` which packs the appropriate elements into the final `TargetInfo`.
+ // That flow should be refactored so that `ChooserActivity` isn't responsible for holding their
+ // intermediate data, and then these members can be removed.
+ private final Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache = new HashMap<>();
+ private final Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache = new HashMap<>();
+
+ private static final int TARGET_TYPE_DEFAULT = 0;
+ private static final int TARGET_TYPE_CHOOSER_TARGET = 1;
+ private static final int TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER = 2;
+ private static final int TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE = 3;
+
+ private static final int SCROLL_STATUS_IDLE = 0;
+ private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1;
+ private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2;
+
+ @Inject public FeatureFlags mFeatureFlags;
+ @Inject public EventLog mEventLog;
+ @Inject @ImageEditor public Optional<ComponentName> mImageEditor;
+ @Inject @NearbyShare public Optional<ComponentName> mNearbyShare;
+ @Inject public TargetDataLoader mTargetDataLoader;
+
+ private ChooserRefinementManager mRefinementManager;
+
+ private ChooserContentPreviewUi mChooserContentPreviewUi;
+
+ private boolean mShouldDisplayLandscape;
+ private long mChooserShownTime;
+ protected boolean mIsSuccessfullySelected;
+
+ private int mCurrAvailableWidth = 0;
+ private Insets mLastAppliedInsets = null;
+ private int mLastNumberOfChildren = -1;
+ private int mMaxTargetsPerRow = 1;
+
+ private static final int MAX_LOG_RANK_POSITION = 12;
+
+ // TODO: are these used anywhere? They should probably be migrated to ChooserRequestParameters.
+ private static final int MAX_EXTRA_INITIAL_INTENTS = 2;
+ private static final int MAX_EXTRA_CHOOSER_TARGETS = 2;
+
+ private SharedPreferences mPinnedSharedPrefs;
+ private static final String PINNED_SHARED_PREFS_NAME = "chooser_pin_settings";
+
+ private final ExecutorService mBackgroundThreadPoolExecutor = Executors.newFixedThreadPool(5);
+
+ private int mScrollStatus = SCROLL_STATUS_IDLE;
+
+ @VisibleForTesting
+ protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter;
+ private final EnterTransitionAnimationDelegate mEnterTransitionAnimationDelegate =
+ new EnterTransitionAnimationDelegate(this, () -> mResolverDrawerLayout);
+
+ private View mContentView = null;
+
+ private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>();
+
+ private boolean mExcludeSharedText = false;
+ /**
+ * When we intend to finish the activity with a shared element transition, we can't immediately
+ * finish() when the transition is invoked, as the receiving end may not be able to start the
+ * animation and the UI breaks if this takes too long. Instead we defer finishing until onStop
+ * in order to wait for the transition to begin.
+ */
+ private boolean mFinishWhenStopped = false;
+
+ private final AtomicLong mIntentReceivedTime = new AtomicLong(-1);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ Tracer.INSTANCE.markLaunched();
+ super.onCreate(savedInstanceState);
+ setLogic(new ChooserActivityLogic(
+ TAG,
+ () -> this,
+ this::onWorkProfileStatusUpdated,
+ () -> mTargetDataLoader,
+ this::onPreinitialization));
+ addInitializer(this::init);
+ }
+
+ private void init() {
+ if (getChooserRequest() == null) {
+ finish();
+ return;
+ }
+ if (isFinishing()) {
+ // Performing a clean exit:
+ // Skip initializing any additional resources.
+ return;
+ }
+ setTheme(mLogic.getThemeResId());
+
+ getEventLog().logSharesheetTriggered();
+
+ mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class);
+
+ mRefinementManager.getRefinementCompletion().observe(this, completion -> {
+ if (completion.consume()) {
+ TargetInfo targetInfo = completion.getTargetInfo();
+ // targetInfo is non-null if the refinement process was successful.
+ if (targetInfo != null) {
+ maybeRemoveSharedText(targetInfo);
+
+ // We already block suspended targets from going to refinement, and we probably
+ // can't recover a Chooser session if that's the reason the refined target fails
+ // to launch now. Fire-and-forget the refined launch; ignore the return value
+ // and just make sure the Sharesheet session gets cleaned up regardless.
+ ChooserActivity.super.onTargetSelected(targetInfo, false);
+ }
+
+ finish();
+ }
+ });
+
+ BasePreviewViewModel previewViewModel =
+ new ViewModelProvider(this, createPreviewViewModelFactory())
+ .get(BasePreviewViewModel.class);
+ ChooserRequestParameters chooserRequest = requireChooserRequest();
+ mChooserContentPreviewUi = new ChooserContentPreviewUi(
+ getCoroutineScope(getLifecycle()),
+ previewViewModel.createOrReuseProvider(chooserRequest.getTargetIntent()),
+ chooserRequest.getTargetIntent(),
+ previewViewModel.createOrReuseImageLoader(),
+ createChooserActionFactory(),
+ mEnterTransitionAnimationDelegate,
+ new HeadlineGeneratorImpl(this));
+
+ updateStickyContentPreview();
+ if (shouldShowStickyContentPreview()
+ || mChooserMultiProfilePagerAdapter
+ .getCurrentRootAdapter().getSystemRowCount() != 0) {
+ getEventLog().logActionShareWithPreview(
+ mChooserContentPreviewUi.getPreferredContentPreview());
+ }
+
+ mChooserShownTime = System.currentTimeMillis();
+ final long systemCost = mChooserShownTime - mIntentReceivedTime.get();
+ getEventLog().logChooserActivityShown(
+ isWorkProfile(), chooserRequest.getTargetType(), systemCost);
+
+ if (mResolverDrawerLayout != null) {
+ mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange);
+
+ mResolverDrawerLayout.setOnCollapsedChangedListener(
+ isCollapsed -> {
+ mChooserMultiProfilePagerAdapter.setIsCollapsed(isCollapsed);
+ getEventLog().logSharesheetExpansionChanged(isCollapsed);
+ });
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "System Time Cost is " + systemCost);
+ }
+
+ getEventLog().logShareStarted(
+ mLogic.getReferrerPackageName(),
+ chooserRequest.getTargetType(),
+ chooserRequest.getCallerChooserTargets().size(),
+ (chooserRequest.getInitialIntents() == null)
+ ? 0 : chooserRequest.getInitialIntents().length,
+ isWorkProfile(),
+ mChooserContentPreviewUi.getPreferredContentPreview(),
+ chooserRequest.getTargetAction(),
+ chooserRequest.getChooserActions().size(),
+ chooserRequest.getModifyShareAction() != null
+ );
+
+ mEnterTransitionAnimationDelegate.postponeTransition();
+ }
+
+ protected final Unit onPreinitialization() {
+ mIntentReceivedTime.set(System.currentTimeMillis());
+ mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
+
+ mPinnedSharedPrefs = getPinnedSharedPrefs(this);
+ mMaxTargetsPerRow =
+ getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
+ mShouldDisplayLandscape =
+ shouldDisplayLandscape(getResources().getConfiguration().orientation);
+
+
+ ChooserRequestParameters chooserRequest = getChooserRequest();
+ if (chooserRequest == null) {
+ return Unit.INSTANCE;
+ }
+ setRetainInOnStop(chooserRequest.shouldRetainInOnStop());
+
+ createProfileRecords(
+ new AppPredictorFactory(
+ this,
+ chooserRequest.getSharedText(),
+ chooserRequest.getTargetIntentFilter()
+ ),
+ chooserRequest.getTargetIntentFilter()
+ );
+ return Unit.INSTANCE;
+ }
+
+ @Nullable
+ private ChooserRequestParameters getChooserRequest() {
+ return ((ChooserActivityLogic) mLogic).getChooserRequestParameters();
+ }
+
+ private ChooserRequestParameters requireChooserRequest() {
+ return requireNonNull(getChooserRequest());
+ }
+
+ private AnnotatedUserHandles requireAnnotatedUserHandles() {
+ return requireNonNull(mLogic.getAnnotatedUserHandles());
+ }
+
+ private void createProfileRecords(
+ AppPredictorFactory factory, IntentFilter targetIntentFilter) {
+ UserHandle mainUserHandle = requireAnnotatedUserHandles().personalProfileUserHandle;
+ ProfileRecord record = createProfileRecord(mainUserHandle, targetIntentFilter, factory);
+ if (record.shortcutLoader == null) {
+ Tracer.INSTANCE.endLaunchToShortcutTrace();
+ }
+
+ UserHandle workUserHandle = requireAnnotatedUserHandles().workProfileUserHandle;
+ if (workUserHandle != null) {
+ createProfileRecord(workUserHandle, targetIntentFilter, factory);
+ }
+ }
+
+ private ProfileRecord createProfileRecord(
+ UserHandle userHandle, IntentFilter targetIntentFilter, AppPredictorFactory factory) {
+ AppPredictor appPredictor = factory.create(userHandle);
+ ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic()
+ ? null
+ : createShortcutLoader(
+ this,
+ appPredictor,
+ userHandle,
+ targetIntentFilter,
+ shortcutsResult -> onShortcutsLoaded(userHandle, shortcutsResult));
+ ProfileRecord record = new ProfileRecord(appPredictor, shortcutLoader);
+ mProfileRecords.put(userHandle.getIdentifier(), record);
+ return record;
+ }
+
+ @Nullable
+ private ProfileRecord getProfileRecord(UserHandle userHandle) {
+ return mProfileRecords.get(userHandle.getIdentifier(), null);
+ }
+
+ @VisibleForTesting
+ protected ShortcutLoader createShortcutLoader(
+ Context context,
+ AppPredictor appPredictor,
+ UserHandle userHandle,
+ IntentFilter targetIntentFilter,
+ Consumer<ShortcutLoader.Result> callback) {
+ return new ShortcutLoader(
+ context,
+ getCoroutineScope(getLifecycle()),
+ appPredictor,
+ userHandle,
+ targetIntentFilter,
+ callback);
+ }
+
+ static SharedPreferences getPinnedSharedPrefs(Context context) {
+ return context.getSharedPreferences(PINNED_SHARED_PREFS_NAME, MODE_PRIVATE);
+ }
+
+ @Override
+ protected ChooserMultiProfilePagerAdapter createMultiProfilePagerAdapter(
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ if (shouldShowTabs()) {
+ mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForTwoProfiles(
+ initialIntents, rList, filterLastUsed, targetDataLoader);
+ } else {
+ mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForOneProfile(
+ initialIntents, rList, filterLastUsed, targetDataLoader);
+ }
+ return mChooserMultiProfilePagerAdapter;
+ }
+
+ @Override
+ protected EmptyStateProvider createBlockerEmptyStateProvider() {
+ final boolean isSendAction = requireChooserRequest().isSendActionTarget();
+
+ final EmptyState noWorkToPersonalEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
+ /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
+ /* devicePolicyStringSubtitleId= */
+ isSendAction ? RESOLVER_CANT_SHARE_WITH_PERSONAL : RESOLVER_CANT_ACCESS_PERSONAL,
+ /* defaultSubtitleResource= */
+ isSendAction ? R.string.resolver_cant_share_with_personal_apps_explanation
+ : R.string.resolver_cant_access_personal_apps_explanation,
+ /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL,
+ /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER);
+
+ final EmptyState noPersonalToWorkEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
+ /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
+ /* devicePolicyStringSubtitleId= */
+ isSendAction ? RESOLVER_CANT_SHARE_WITH_WORK : RESOLVER_CANT_ACCESS_WORK,
+ /* defaultSubtitleResource= */
+ isSendAction ? R.string.resolver_cant_share_with_work_apps_explanation
+ : R.string.resolver_cant_access_work_apps_explanation,
+ /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK,
+ /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_CHOOSER);
+
+ return new NoCrossProfileEmptyStateProvider(
+ requireAnnotatedUserHandles().personalProfileUserHandle,
+ noWorkToPersonalEmptyState,
+ noPersonalToWorkEmptyState,
+ createCrossProfileIntentsChecker(),
+ requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
+ }
+
+ private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile(
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ ChooserGridAdapter adapter = createChooserGridAdapter(
+ /* context */ this,
+ mLogic.getPayloadIntents(),
+ initialIntents,
+ rList,
+ filterLastUsed,
+ /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle,
+ targetDataLoader);
+ return new ChooserMultiProfilePagerAdapter(
+ /* context */ this,
+ adapter,
+ createEmptyStateProvider(/* workProfileUserHandle= */ null),
+ /* workProfileQuietModeChecker= */ () -> false,
+ /* workProfileUserHandle= */ null,
+ requireAnnotatedUserHandles().cloneProfileUserHandle,
+ mMaxTargetsPerRow,
+ mFeatureFlags);
+ }
+
+ private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles(
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ int selectedProfile = findSelectedProfile();
+ ChooserGridAdapter personalAdapter = createChooserGridAdapter(
+ /* context */ this,
+ mLogic.getPayloadIntents(),
+ selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
+ rList,
+ filterLastUsed,
+ /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle,
+ targetDataLoader);
+ ChooserGridAdapter workAdapter = createChooserGridAdapter(
+ /* context */ this,
+ mLogic.getPayloadIntents(),
+ selectedProfile == PROFILE_WORK ? initialIntents : null,
+ rList,
+ filterLastUsed,
+ /* userHandle */ requireAnnotatedUserHandles().workProfileUserHandle,
+ targetDataLoader);
+ return new ChooserMultiProfilePagerAdapter(
+ /* context */ this,
+ personalAdapter,
+ workAdapter,
+ createEmptyStateProvider(requireAnnotatedUserHandles().workProfileUserHandle),
+ () -> mLogic.getWorkProfileAvailabilityManager().isQuietModeEnabled(),
+ selectedProfile,
+ requireAnnotatedUserHandles().workProfileUserHandle,
+ requireAnnotatedUserHandles().cloneProfileUserHandle,
+ mMaxTargetsPerRow,
+ mFeatureFlags);
+ }
+
+ private int findSelectedProfile() {
+ int selectedProfile = getSelectedProfileExtra();
+ if (selectedProfile == -1) {
+ selectedProfile = getProfileForUser(
+ requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
+ }
+ return selectedProfile;
+ }
+
+ /**
+ * Check if the profile currently used is a work profile.
+ * @return true if it is work profile, false if it is parent profile (or no work profile is
+ * set up)
+ */
+ protected boolean isWorkProfile() {
+ return getSystemService(UserManager.class)
+ .getUserInfo(UserHandle.myUserId()).isManagedProfile();
+ }
+
+ @Override
+ protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) {
+ return new PackageMonitor() {
+ @Override
+ public void onSomePackagesChanged() {
+ handlePackagesChanged(listAdapter);
+ }
+ };
+ }
+
+ /**
+ * Update UI to reflect changes in data.
+ */
+ public void handlePackagesChanged() {
+ handlePackagesChanged(/* listAdapter */ null);
+ }
+
+ /**
+ * Update UI to reflect changes in data.
+ * <p>If {@code listAdapter} is {@code null}, both profile list adapters are updated if
+ * available.
+ */
+ private void handlePackagesChanged(@Nullable ResolverListAdapter listAdapter) {
+ // Refresh pinned items
+ mPinnedSharedPrefs = getPinnedSharedPrefs(this);
+ if (listAdapter == null) {
+ mChooserMultiProfilePagerAdapter.refreshPackagesInAllTabs();
+ } else {
+ listAdapter.handlePackagesChanged();
+ }
+ updateProfileViewButton();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.d(TAG, "onResume: " + getComponentName().flattenToShortString());
+ mFinishWhenStopped = false;
+ mRefinementManager.onActivityResume();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ if (viewPager.isLayoutRtl()) {
+ mMultiProfilePagerAdapter.setupViewPager(viewPager);
+ }
+
+ mShouldDisplayLandscape = shouldDisplayLandscape(newConfig.orientation);
+ mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
+ mChooserMultiProfilePagerAdapter.setMaxTargetsPerRow(mMaxTargetsPerRow);
+ adjustPreviewWidth(newConfig.orientation, null);
+ updateStickyContentPreview();
+ updateTabPadding();
+ }
+
+ private boolean shouldDisplayLandscape(int orientation) {
+ // Sharesheet fixes the # of items per row and therefore can not correctly lay out
+ // when in the restricted size of multi-window mode. In the future, would be nice
+ // to use minimum dp size requirements instead
+ return orientation == Configuration.ORIENTATION_LANDSCAPE && !isInMultiWindowMode();
+ }
+
+ private void adjustPreviewWidth(int orientation, View parent) {
+ int width = -1;
+ if (mShouldDisplayLandscape) {
+ width = getResources().getDimensionPixelSize(R.dimen.chooser_preview_width);
+ }
+
+ parent = parent == null ? getWindow().getDecorView() : parent;
+
+ updateLayoutWidth(com.android.internal.R.id.content_preview_file_layout, width, parent);
+ }
+
+ private void updateTabPadding() {
+ if (shouldShowTabs()) {
+ View tabs = findViewById(com.android.internal.R.id.tabs);
+ float iconSize = getResources().getDimension(R.dimen.chooser_icon_size);
+ // The entire width consists of icons or padding. Divide the item padding in half to get
+ // paddingHorizontal.
+ float padding = (tabs.getWidth() - mMaxTargetsPerRow * iconSize)
+ / mMaxTargetsPerRow / 2;
+ // Subtract the margin the buttons already have.
+ padding -= getResources().getDimension(R.dimen.resolver_profile_tab_margin);
+ tabs.setPadding((int) padding, 0, (int) padding, 0);
+ }
+ }
+
+ private void updateLayoutWidth(int layoutResourceId, int width, View parent) {
+ View view = parent.findViewById(layoutResourceId);
+ if (view != null && view.getLayoutParams() != null) {
+ LayoutParams params = view.getLayoutParams();
+ params.width = width;
+ view.setLayoutParams(params);
+ }
+ }
+
+ /**
+ * Create a view that will be shown in the content preview area
+ * @param parent reference to the parent container where the view should be attached to
+ * @return content preview view
+ */
+ protected ViewGroup createContentPreviewView(ViewGroup parent) {
+ ViewGroup layout = mChooserContentPreviewUi.displayContentPreview(
+ getResources(),
+ getLayoutInflater(),
+ parent,
+ mFeatureFlags.scrollablePreview()
+ ? findViewById(R.id.chooser_headline_row_container)
+ : null);
+
+ if (layout != null) {
+ adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
+ }
+
+ return layout;
+ }
+
+ @Nullable
+ private View getFirstVisibleImgPreviewView() {
+ View imagePreview = findViewById(R.id.scrollable_image_preview);
+ return imagePreview instanceof ImagePreviewView
+ ? ((ImagePreviewView) imagePreview).getTransitionView()
+ : null;
+ }
+
+ /**
+ * Wrapping the ContentResolver call to expose for easier mocking,
+ * and to avoid mocking Android core classes.
+ */
+ @VisibleForTesting
+ public Cursor queryResolver(ContentResolver resolver, Uri uri) {
+ return resolver.query(uri, null, null, null, null);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (mRefinementManager != null) {
+ mRefinementManager.onActivityStop(isChangingConfigurations());
+ }
+
+ if (mFinishWhenStopped) {
+ mFinishWhenStopped = false;
+ finish();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ if (isFinishing()) {
+ mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET);
+ }
+
+ mBackgroundThreadPoolExecutor.shutdownNow();
+
+ destroyProfileRecords();
+ }
+
+ private void destroyProfileRecords() {
+ for (int i = 0; i < mProfileRecords.size(); ++i) {
+ mProfileRecords.valueAt(i).destroy();
+ }
+ mProfileRecords.clear();
+ }
+
+ @Override // ResolverListCommunicator
+ public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
+ ChooserRequestParameters chooserRequest = getChooserRequest();
+ if (chooserRequest == null) {
+ return defIntent;
+ }
+
+ Intent result = defIntent;
+ if (chooserRequest.getReplacementExtras() != null) {
+ final Bundle replExtras =
+ chooserRequest.getReplacementExtras().getBundle(aInfo.packageName);
+ if (replExtras != null) {
+ result = new Intent(defIntent);
+ result.putExtras(replExtras);
+ }
+ }
+ if (aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_PARENT)
+ || aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE)) {
+ result = Intent.createChooser(result,
+ getIntent().getCharSequenceExtra(Intent.EXTRA_TITLE));
+
+ // Don't auto-launch single intents if the intent is being forwarded. This is done
+ // because automatically launching a resolving application as a response to the user
+ // action of switching accounts is pretty unexpected.
+ result.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false);
+ }
+ return result;
+ }
+
+ @Override
+ public void onActivityStarted(TargetInfo cti) {
+ ChooserRequestParameters chooserRequest = requireChooserRequest();
+ if (chooserRequest.getChosenComponentSender() != null) {
+ final ComponentName target = cti.getResolvedComponentName();
+ if (target != null) {
+ final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target);
+ try {
+ chooserRequest.getChosenComponentSender().sendIntent(
+ this, Activity.RESULT_OK, fillIn, null, null);
+ } catch (IntentSender.SendIntentException e) {
+ Slog.e(TAG, "Unable to launch supplied IntentSender to report "
+ + "the chosen component: " + e);
+ }
+ }
+ }
+ }
+
+ private void addCallerChooserTargets() {
+ ChooserRequestParameters chooserRequest = requireChooserRequest();
+ if (!chooserRequest.getCallerChooserTargets().isEmpty()) {
+ // Send the caller's chooser targets only to the default profile.
+ UserHandle defaultUser = (findSelectedProfile() == PROFILE_WORK)
+ ? requireAnnotatedUserHandles().workProfileUserHandle
+ : requireAnnotatedUserHandles().personalProfileUserHandle;
+ if (mChooserMultiProfilePagerAdapter.getCurrentUserHandle() == defaultUser) {
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults(
+ /* origTarget */ null,
+ new ArrayList<>(chooserRequest.getCallerChooserTargets()),
+ TARGET_TYPE_DEFAULT,
+ /* directShareShortcutInfoCache */ Collections.emptyMap(),
+ /* directShareAppTargetCache */ Collections.emptyMap());
+ }
+ }
+ }
+
+ @Override
+ public int getLayoutResource() {
+ return mFeatureFlags.scrollablePreview()
+ ? R.layout.chooser_grid_scrollable_preview
+ : R.layout.chooser_grid;
+ }
+
+ @Override // ResolverListCommunicator
+ public boolean shouldGetActivityMetadata() {
+ return true;
+ }
+
+ @Override
+ public boolean shouldAutoLaunchSingleChoice(TargetInfo target) {
+ // Note that this is only safe because the Intent handled by the ChooserActivity is
+ // guaranteed to contain no extras unknown to the local ClassLoader. That is why this
+ // method can not be replaced in the ResolverActivity whole hog.
+ if (!super.shouldAutoLaunchSingleChoice(target)) {
+ return false;
+ }
+
+ return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true);
+ }
+
+ private void showTargetDetails(TargetInfo targetInfo) {
+ if (targetInfo == null) return;
+
+ List<DisplayResolveInfo> targetList = targetInfo.getAllDisplayTargets();
+ if (targetList.isEmpty()) {
+ Log.e(TAG, "No displayable data to show target details");
+ return;
+ }
+
+ // TODO: implement these type-conditioned behaviors polymorphically, and consider moving
+ // the logic into `ChooserTargetActionsDialogFragment.show()`.
+ boolean isShortcutPinned = targetInfo.isSelectableTargetInfo() && targetInfo.isPinned();
+ IntentFilter intentFilter = targetInfo.isSelectableTargetInfo()
+ ? requireChooserRequest().getTargetIntentFilter() : null;
+ String shortcutTitle = targetInfo.isSelectableTargetInfo()
+ ? targetInfo.getDisplayLabel().toString() : null;
+ String shortcutIdKey = targetInfo.getDirectShareShortcutId();
+
+ ChooserTargetActionsDialogFragment.show(
+ getSupportFragmentManager(),
+ targetList,
+ // Adding userHandle from ResolveInfo allows the app icon in Dialog Box to be
+ // resolved correctly within the same tab.
+ targetInfo.getResolveInfo().userHandle,
+ shortcutIdKey,
+ shortcutTitle,
+ isShortcutPinned,
+ intentFilter);
+ }
+
+ @Override
+ protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) {
+ if (mRefinementManager.maybeHandleSelection(
+ target,
+ requireChooserRequest().getRefinementIntentSender(),
+ getApplication(),
+ getMainThreadHandler())) {
+ return false;
+ }
+ updateModelAndChooserCounts(target);
+ maybeRemoveSharedText(target);
+ return super.onTargetSelected(target, alwaysCheck);
+ }
+
+ @Override
+ public void startSelected(int which, boolean always, boolean filtered) {
+ ChooserListAdapter currentListAdapter =
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter();
+ TargetInfo targetInfo = currentListAdapter
+ .targetInfoForPosition(which, filtered);
+ if (targetInfo != null && targetInfo.isNotSelectableTargetInfo()) {
+ return;
+ }
+
+ final long selectionCost = System.currentTimeMillis() - mChooserShownTime;
+
+ if ((targetInfo != null) && targetInfo.isMultiDisplayResolveInfo()) {
+ MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo;
+ if (!mti.hasSelected()) {
+ // Add userHandle based badge to the stackedAppDialogBox.
+ ChooserStackedAppDialogFragment.show(
+ getSupportFragmentManager(),
+ mti,
+ which,
+ targetInfo.getResolveInfo().userHandle);
+ return;
+ }
+ }
+
+ super.startSelected(which, always, filtered);
+
+ // TODO: both of the conditions around this switch logic *should* be redundant, and
+ // can be removed if certain invariants can be guaranteed. In particular, it seems
+ // like targetInfo (from `ChooserListAdapter.targetInfoForPosition()`) is *probably*
+ // expected to be null only at out-of-bounds indexes where `getPositionTargetType()`
+ // returns TARGET_BAD; then the switch falls through to a default no-op, and we don't
+ // need to null-check targetInfo. We only need the null check if it's possible that
+ // the ChooserListAdapter contains null elements "in the middle" of its list data,
+ // such that they're classified as belonging to one of the real target types. That
+ // should probably never happen. But why would this method ever be invoked with a
+ // null target at all? Even an out-of-bounds index should never be "selected"...
+ if ((currentListAdapter.getCount() > 0) && (targetInfo != null)) {
+ switch (currentListAdapter.getPositionTargetType(which)) {
+ case ChooserListAdapter.TARGET_SERVICE:
+ getEventLog().logShareTargetSelected(
+ EventLog.SELECTION_TYPE_SERVICE,
+ targetInfo.getResolveInfo().activityInfo.processName,
+ which,
+ /* directTargetAlsoRanked= */ getRankedPosition(targetInfo),
+ requireChooserRequest().getCallerChooserTargets().size(),
+ targetInfo.getHashedTargetIdForMetrics(this),
+ targetInfo.isPinned(),
+ mIsSuccessfullySelected,
+ selectionCost
+ );
+ return;
+ case ChooserListAdapter.TARGET_CALLER:
+ case ChooserListAdapter.TARGET_STANDARD:
+ getEventLog().logShareTargetSelected(
+ EventLog.SELECTION_TYPE_APP,
+ targetInfo.getResolveInfo().activityInfo.processName,
+ (which - currentListAdapter.getSurfacedTargetInfo().size()),
+ /* directTargetAlsoRanked= */ -1,
+ currentListAdapter.getCallerTargetCount(),
+ /* directTargetHashed= */ null,
+ targetInfo.isPinned(),
+ mIsSuccessfullySelected,
+ selectionCost
+ );
+ return;
+ case ChooserListAdapter.TARGET_STANDARD_AZ:
+ // A-Z targets are unranked standard targets; we use a value of -1 to mark that
+ // they are from the alphabetical pool.
+ // TODO: why do we log a different selection type if the -1 value already
+ // designates the same condition?
+ getEventLog().logShareTargetSelected(
+ EventLog.SELECTION_TYPE_STANDARD,
+ targetInfo.getResolveInfo().activityInfo.processName,
+ /* value= */ -1,
+ /* directTargetAlsoRanked= */ -1,
+ /* numCallerProvided= */ 0,
+ /* directTargetHashed= */ null,
+ /* isPinned= */ false,
+ mIsSuccessfullySelected,
+ selectionCost
+ );
+ return;
+ }
+ }
+ }
+
+ private int getRankedPosition(TargetInfo targetInfo) {
+ String targetPackageName =
+ targetInfo.getChooserTargetComponentName().getPackageName();
+ ChooserListAdapter currentListAdapter =
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter();
+ int maxRankedResults = Math.min(
+ currentListAdapter.getDisplayResolveInfoCount(), MAX_LOG_RANK_POSITION);
+
+ for (int i = 0; i < maxRankedResults; i++) {
+ if (currentListAdapter.getDisplayResolveInfo(i)
+ .getResolveInfo().activityInfo.packageName.equals(targetPackageName)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ protected boolean shouldAddFooterView() {
+ // To accommodate for window insets
+ return true;
+ }
+
+ @Override
+ protected void applyFooterView(int height) {
+ mChooserMultiProfilePagerAdapter.setFooterHeightInEveryAdapter(height);
+ }
+
+ private void logDirectShareTargetReceived(UserHandle forUser) {
+ ProfileRecord profileRecord = getProfileRecord(forUser);
+ if (profileRecord == null) {
+ return;
+ }
+ getEventLog().logDirectShareTargetReceived(
+ MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER,
+ (int) (SystemClock.elapsedRealtime() - profileRecord.loadingStartTime));
+ }
+
+ void updateModelAndChooserCounts(TargetInfo info) {
+ if (info != null && info.isMultiDisplayResolveInfo()) {
+ info = ((MultiDisplayResolveInfo) info).getSelectedTarget();
+ }
+ if (info != null) {
+ sendClickToAppPredictor(info);
+ final ResolveInfo ri = info.getResolveInfo();
+ Intent targetIntent = mLogic.getTargetIntent();
+ if (ri != null && ri.activityInfo != null && targetIntent != null) {
+ ChooserListAdapter currentListAdapter =
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter();
+ if (currentListAdapter != null) {
+ sendImpressionToAppPredictor(info, currentListAdapter);
+ currentListAdapter.updateModel(info);
+ currentListAdapter.updateChooserCounts(
+ ri.activityInfo.packageName,
+ targetIntent.getAction(),
+ ri.userHandle);
+ }
+ if (DEBUG) {
+ Log.d(TAG, "ResolveInfo Package is " + ri.activityInfo.packageName);
+ Log.d(TAG, "Action to be updated is " + targetIntent.getAction());
+ }
+ } else if (DEBUG) {
+ Log.d(TAG, "Can not log Chooser Counts of null ResolveInfo");
+ }
+ }
+ mIsSuccessfullySelected = true;
+ }
+
+ private void maybeRemoveSharedText(@NonNull TargetInfo targetInfo) {
+ Intent targetIntent = targetInfo.getTargetIntent();
+ if (targetIntent == null) {
+ return;
+ }
+ Intent originalTargetIntent = new Intent(requireChooserRequest().getTargetIntent());
+ // Our TargetInfo implementations add associated component to the intent, let's do the same
+ // for the sake of the comparison below.
+ if (targetIntent.getComponent() != null) {
+ originalTargetIntent.setComponent(targetIntent.getComponent());
+ }
+ // Use filterEquals as a way to check that the primary intent is in use (and not an
+ // alternative one). For example, an app is sharing an image and a link with mime type
+ // "image/png" and provides an alternative intent to share only the link with mime type
+ // "text/uri". Should there be a target that accepts only the latter, the alternative intent
+ // will be used and we don't want to exclude the link from it.
+ if (mExcludeSharedText && originalTargetIntent.filterEquals(targetIntent)) {
+ targetIntent.removeExtra(Intent.EXTRA_TEXT);
+ }
+ }
+
+ private void sendImpressionToAppPredictor(TargetInfo targetInfo, ChooserListAdapter adapter) {
+ // Send DS target impression info to AppPredictor, only when user chooses app share.
+ if (targetInfo.isChooserTargetInfo()) {
+ return;
+ }
+
+ AppPredictor directShareAppPredictor = getAppPredictor(
+ mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
+ if (directShareAppPredictor == null) {
+ return;
+ }
+ List<TargetInfo> surfacedTargetInfo = adapter.getSurfacedTargetInfo();
+ List<AppTargetId> targetIds = new ArrayList<>();
+ for (TargetInfo chooserTargetInfo : surfacedTargetInfo) {
+ ShortcutInfo shortcutInfo = chooserTargetInfo.getDirectShareShortcutInfo();
+ if (shortcutInfo != null) {
+ ComponentName componentName =
+ chooserTargetInfo.getChooserTargetComponentName();
+ targetIds.add(new AppTargetId(
+ String.format(
+ "%s/%s/%s",
+ shortcutInfo.getId(),
+ componentName.flattenToString(),
+ SHORTCUT_TARGET)));
+ }
+ }
+ directShareAppPredictor.notifyLaunchLocationShown(LAUNCH_LOCATION_DIRECT_SHARE, targetIds);
+ }
+
+ private void sendClickToAppPredictor(TargetInfo targetInfo) {
+ if (!targetInfo.isChooserTargetInfo()) {
+ return;
+ }
+
+ AppPredictor directShareAppPredictor = getAppPredictor(
+ mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
+ if (directShareAppPredictor == null) {
+ return;
+ }
+ AppTarget appTarget = targetInfo.getDirectShareAppTarget();
+ if (appTarget != null) {
+ // This is a direct share click that was provided by the APS
+ directShareAppPredictor.notifyAppTargetEvent(
+ new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH)
+ .setLaunchLocation(LAUNCH_LOCATION_DIRECT_SHARE)
+ .build());
+ }
+ }
+
+ @Nullable
+ private AppPredictor getAppPredictor(UserHandle userHandle) {
+ ProfileRecord record = getProfileRecord(userHandle);
+ // We cannot use APS service when clone profile is present as APS service cannot sort
+ // cross profile targets as of now.
+ return ((record == null) || (requireAnnotatedUserHandles().cloneProfileUserHandle != null))
+ ? null : record.appPredictor;
+ }
+
+ /**
+ * Sort intents alphabetically based on display label.
+ */
+ static class AzInfoComparator implements Comparator<DisplayResolveInfo> {
+ Comparator<DisplayResolveInfo> mComparator;
+ AzInfoComparator(Context context) {
+ Collator collator = Collator
+ .getInstance(context.getResources().getConfiguration().locale);
+ // Adding two stage comparator, first stage compares using displayLabel, next stage
+ // compares using resolveInfo.userHandle
+ mComparator = Comparator.comparing(DisplayResolveInfo::getDisplayLabel, collator)
+ .thenComparingInt(target -> target.getResolveInfo().userHandle.getIdentifier());
+ }
+
+ @Override
+ public int compare(
+ DisplayResolveInfo lhsp, DisplayResolveInfo rhsp) {
+ return mComparator.compare(lhsp, rhsp);
+ }
+ }
+
+ protected EventLog getEventLog() {
+ return mEventLog;
+ }
+
+ public class ChooserListController extends ResolverListController {
+ public ChooserListController(
+ Context context,
+ PackageManager pm,
+ Intent targetIntent,
+ String referrerPackageName,
+ int launchedFromUid,
+ AbstractResolverComparator resolverComparator,
+ UserHandle queryIntentsAsUser) {
+ super(
+ context,
+ pm,
+ targetIntent,
+ referrerPackageName,
+ launchedFromUid,
+ resolverComparator,
+ queryIntentsAsUser);
+ }
+
+ @Override
+ public boolean isComponentFiltered(ComponentName name) {
+ return requireChooserRequest().getFilteredComponentNames().contains(name);
+ }
+
+ @Override
+ public boolean isComponentPinned(ComponentName name) {
+ return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
+ }
+ }
+
+ @VisibleForTesting
+ public ChooserGridAdapter createChooserGridAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ UserHandle userHandle,
+ TargetDataLoader targetDataLoader) {
+ ChooserRequestParameters parameters = requireChooserRequest();
+ ChooserListAdapter chooserListAdapter = createChooserListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ createListController(userHandle),
+ userHandle,
+ mLogic.getTargetIntent(),
+ parameters.getReferrerFillInIntent(),
+ mMaxTargetsPerRow,
+ targetDataLoader);
+
+ return new ChooserGridAdapter(
+ context,
+ new ChooserGridAdapter.ChooserActivityDelegate() {
+ @Override
+ public boolean shouldShowTabs() {
+ return ChooserActivity.this.shouldShowTabs();
+ }
+
+ @Override
+ public View buildContentPreview(ViewGroup parent) {
+ return createContentPreviewView(parent);
+ }
+
+ @Override
+ public void onTargetSelected(int itemIndex) {
+ startSelected(itemIndex, false, true);
+ }
+
+ @Override
+ public void onTargetLongPressed(int selectedPosition) {
+ final TargetInfo longPressedTargetInfo =
+ mChooserMultiProfilePagerAdapter
+ .getActiveListAdapter()
+ .targetInfoForPosition(
+ selectedPosition, /* filtered= */ true);
+ // Only a direct share target or an app target is expected
+ if (longPressedTargetInfo.isDisplayResolveInfo()
+ || longPressedTargetInfo.isSelectableTargetInfo()) {
+ showTargetDetails(longPressedTargetInfo);
+ }
+ }
+
+ @Override
+ public void updateProfileViewButton(View newButtonFromProfileRow) {
+ mProfileView = newButtonFromProfileRow;
+ mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick);
+ ChooserActivity.this.updateProfileViewButton();
+ }
+ },
+ chooserListAdapter,
+ shouldShowContentPreview(),
+ mMaxTargetsPerRow,
+ mFeatureFlags);
+ }
+
+ @VisibleForTesting
+ public ChooserListAdapter createChooserListAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ ResolverListController resolverListController,
+ UserHandle userHandle,
+ Intent targetIntent,
+ Intent referrerFillInIntent,
+ int maxTargetsPerRow,
+ TargetDataLoader targetDataLoader) {
+ UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()
+ && userHandle.equals(requireAnnotatedUserHandles().personalProfileUserHandle)
+ ? requireAnnotatedUserHandles().cloneProfileUserHandle : userHandle;
+ return new ChooserListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ createListController(userHandle),
+ userHandle,
+ targetIntent,
+ referrerFillInIntent,
+ this,
+ context.getPackageManager(),
+ getEventLog(),
+ maxTargetsPerRow,
+ initialIntentsUserSpace,
+ targetDataLoader,
+ () -> {
+ ProfileRecord record = getProfileRecord(userHandle);
+ if (record != null && record.shortcutLoader != null) {
+ record.shortcutLoader.reset();
+ }
+ });
+ }
+
+ @Override
+ protected Unit onWorkProfileStatusUpdated() {
+ UserHandle workUser = requireAnnotatedUserHandles().workProfileUserHandle;
+ ProfileRecord record = workUser == null ? null : getProfileRecord(workUser);
+ if (record != null && record.shortcutLoader != null) {
+ record.shortcutLoader.reset();
+ }
+ return super.onWorkProfileStatusUpdated();
+ }
+
+ @Override
+ @VisibleForTesting
+ protected ChooserListController createListController(UserHandle userHandle) {
+ AppPredictor appPredictor = getAppPredictor(userHandle);
+ AbstractResolverComparator resolverComparator;
+ if (appPredictor != null) {
+ resolverComparator = new AppPredictionServiceResolverComparator(
+ this,
+ mLogic.getTargetIntent(),
+ mLogic.getReferrerPackageName(),
+ appPredictor,
+ userHandle,
+ getEventLog(),
+ mNearbyShare.orElse(null)
+ );
+ } else {
+ resolverComparator =
+ new ResolverRankerServiceResolverComparator(
+ this,
+ mLogic.getTargetIntent(),
+ mLogic.getReferrerPackageName(),
+ null,
+ getEventLog(),
+ getResolverRankerServiceUserHandleList(userHandle),
+ mNearbyShare.orElse(null));
+ }
+
+ return new ChooserListController(
+ this,
+ mPm,
+ mLogic.getTargetIntent(),
+ mLogic.getReferrerPackageName(),
+ requireAnnotatedUserHandles().userIdOfCallingApp,
+ resolverComparator,
+ getQueryIntentsUser(userHandle));
+ }
+
+ @VisibleForTesting
+ protected ViewModelProvider.Factory createPreviewViewModelFactory() {
+ return PreviewViewModel.Companion.getFactory();
+ }
+
+ private ChooserActionFactory createChooserActionFactory() {
+ ChooserRequestParameters request = requireChooserRequest();
+ return new ChooserActionFactory(
+ this,
+ request.getTargetIntent(),
+ request.getReferrerPackageName(),
+ request.getChooserActions(),
+ request.getModifyShareAction(),
+ mImageEditor,
+ getEventLog(),
+ (isExcluded) -> mExcludeSharedText = isExcluded,
+ this::getFirstVisibleImgPreviewView,
+ new ChooserActionFactory.ActionActivityStarter() {
+ @Override
+ public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) {
+ safelyStartActivityAsUser(
+ targetInfo,
+ requireAnnotatedUserHandles().personalProfileUserHandle
+ );
+ finish();
+ }
+
+ @Override
+ public void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+ TargetInfo targetInfo, View sharedElement, String sharedElementName) {
+ ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
+ ChooserActivity.this, sharedElement, sharedElementName);
+ safelyStartActivityAsUser(
+ targetInfo,
+ requireAnnotatedUserHandles().personalProfileUserHandle,
+ options.toBundle());
+ // Can't finish right away because the shared element transition may not
+ // be ready to start.
+ mFinishWhenStopped = true;
+ }
+ },
+ (status) -> {
+ if (status != null) {
+ setResult(status);
+ }
+ finish();
+ });
+ }
+
+ /*
+ * Need to dynamically adjust how many icons can fit per row before we add them,
+ * which also means setting the correct offset to initially show the content
+ * preview area + 2 rows of targets
+ */
+ private void handleLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+ int oldTop, int oldRight, int oldBottom) {
+ if (mChooserMultiProfilePagerAdapter == null) {
+ return;
+ }
+ RecyclerView recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
+ ChooserGridAdapter gridAdapter = mChooserMultiProfilePagerAdapter.getCurrentRootAdapter();
+ // Skip height calculation if recycler view was scrolled to prevent it inaccurately
+ // calculating the height, as the logic below does not account for the scrolled offset.
+ if (gridAdapter == null || recyclerView == null
+ || recyclerView.computeVerticalScrollOffset() != 0) {
+ return;
+ }
+
+ final int availableWidth = right - left - v.getPaddingLeft() - v.getPaddingRight();
+ boolean isLayoutUpdated =
+ gridAdapter.calculateChooserTargetWidth(availableWidth)
+ || recyclerView.getAdapter() == null
+ || availableWidth != mCurrAvailableWidth;
+
+ boolean insetsChanged = !Objects.equals(mLastAppliedInsets, mSystemWindowInsets);
+
+ if (isLayoutUpdated
+ || insetsChanged
+ || mLastNumberOfChildren != recyclerView.getChildCount()) {
+ mCurrAvailableWidth = availableWidth;
+ if (isLayoutUpdated) {
+ // It is very important we call setAdapter from here. Otherwise in some cases
+ // the resolver list doesn't get populated, such as b/150922090, b/150918223
+ // and b/150936654
+ recyclerView.setAdapter(gridAdapter);
+ ((GridLayoutManager) recyclerView.getLayoutManager()).setSpanCount(
+ mMaxTargetsPerRow);
+
+ updateTabPadding();
+ }
+
+ UserHandle currentUserHandle = mChooserMultiProfilePagerAdapter.getCurrentUserHandle();
+ int currentProfile = getProfileForUser(currentUserHandle);
+ int initialProfile = findSelectedProfile();
+ if (currentProfile != initialProfile) {
+ return;
+ }
+
+ if (mLastNumberOfChildren == recyclerView.getChildCount() && !insetsChanged) {
+ return;
+ }
+
+ getMainThreadHandler().post(() -> {
+ if (mResolverDrawerLayout == null || gridAdapter == null) {
+ return;
+ }
+ int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter);
+ mResolverDrawerLayout.setCollapsibleHeightReserved(offset);
+ mEnterTransitionAnimationDelegate.markOffsetCalculated();
+ mLastAppliedInsets = mSystemWindowInsets;
+ });
+ }
+ }
+
+ private int calculateDrawerOffset(
+ int top, int bottom, RecyclerView recyclerView, ChooserGridAdapter gridAdapter) {
+
+ int offset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0;
+ int rowsToShow = gridAdapter.getSystemRowCount()
+ + gridAdapter.getProfileRowCount()
+ + gridAdapter.getServiceTargetRowCount()
+ + gridAdapter.getCallerAndRankedTargetRowCount();
+
+ // then this is most likely not a SEND_* action, so check
+ // the app target count
+ if (rowsToShow == 0) {
+ rowsToShow = gridAdapter.getRowCount();
+ }
+
+ // still zero? then use a default height and leave, which
+ // can happen when there are no targets to show
+ if (rowsToShow == 0 && !shouldShowStickyContentPreview()) {
+ offset += getResources().getDimensionPixelSize(
+ R.dimen.chooser_max_collapsed_height);
+ return offset;
+ }
+
+ View stickyContentPreview = findViewById(com.android.internal.R.id.content_preview_container);
+ if (shouldShowStickyContentPreview() && isStickyContentPreviewShowing()) {
+ offset += stickyContentPreview.getHeight();
+ }
+
+ if (shouldShowTabs()) {
+ offset += findViewById(com.android.internal.R.id.tabs).getHeight();
+ }
+
+ if (recyclerView.getVisibility() == View.VISIBLE) {
+ rowsToShow = Math.min(4, rowsToShow);
+ boolean shouldShowExtraRow = shouldShowExtraRow(rowsToShow);
+ mLastNumberOfChildren = recyclerView.getChildCount();
+ for (int i = 0, childCount = recyclerView.getChildCount();
+ i < childCount && rowsToShow > 0; i++) {
+ View child = recyclerView.getChildAt(i);
+ if (((GridLayoutManager.LayoutParams)
+ child.getLayoutParams()).getSpanIndex() != 0) {
+ continue;
+ }
+ int height = child.getHeight();
+ offset += height;
+ if (shouldShowExtraRow) {
+ offset += height;
+ }
+ rowsToShow--;
+ }
+ } else {
+ ViewGroup currentEmptyStateView =
+ mChooserMultiProfilePagerAdapter.getActiveEmptyStateView();
+ if (currentEmptyStateView.getVisibility() == View.VISIBLE) {
+ offset += currentEmptyStateView.getHeight();
+ }
+ }
+
+ return Math.min(offset, bottom - top);
+ }
+
+ /**
+ * If we have a tabbed view and are showing 1 row in the current profile and an empty
+ * state screen in another profile, to prevent cropping of the empty state screen we show
+ * a second row in the current profile.
+ */
+ private boolean shouldShowExtraRow(int rowsToShow) {
+ return rowsToShow == 1
+ && mChooserMultiProfilePagerAdapter
+ .shouldShowEmptyStateScreenInAnyInactiveAdapter();
+ }
+
+ /**
+ * Returns {@link #PROFILE_WORK}, if the given user handle matches work user handle.
+ * Returns {@link #PROFILE_PERSONAL}, otherwise.
+ **/
+ private int getProfileForUser(UserHandle currentUserHandle) {
+ if (currentUserHandle.equals(requireAnnotatedUserHandles().workProfileUserHandle)) {
+ return PROFILE_WORK;
+ }
+ // We return personal profile, as it is the default when there is no work profile, personal
+ // profile represents rootUser, clonedUser & secondaryUser, covering all use cases.
+ return PROFILE_PERSONAL;
+ }
+
+ @Override
+ protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) {
+ setupScrollListener();
+ maybeSetupGlobalLayoutListener();
+
+ ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter;
+ UserHandle listProfileUserHandle = chooserListAdapter.getUserHandle();
+ if (listProfileUserHandle.equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) {
+ mChooserMultiProfilePagerAdapter.getActiveAdapterView()
+ .setAdapter(mChooserMultiProfilePagerAdapter.getCurrentRootAdapter());
+ mChooserMultiProfilePagerAdapter
+ .setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage());
+ }
+
+ //TODO: move this block inside ChooserListAdapter (should be called when
+ // ResolverListAdapter#mPostListReadyRunnable is executed.
+ if (chooserListAdapter.getDisplayResolveInfoCount() == 0) {
+ chooserListAdapter.notifyDataSetChanged();
+ } else {
+ chooserListAdapter.updateAlphabeticalList();
+ }
+
+ if (rebuildComplete) {
+ long duration = Tracer.INSTANCE.endAppTargetLoadingSection(listProfileUserHandle);
+ if (duration >= 0) {
+ Log.d(TAG, "app target loading time " + duration + " ms");
+ }
+ addCallerChooserTargets();
+ getEventLog().logSharesheetAppLoadComplete();
+ maybeQueryAdditionalPostProcessingTargets(
+ listProfileUserHandle,
+ chooserListAdapter.getDisplayResolveInfos());
+ mLatencyTracker.onActionEnd(ACTION_LOAD_SHARE_SHEET);
+ }
+ }
+
+ private void maybeQueryAdditionalPostProcessingTargets(
+ UserHandle userHandle,
+ DisplayResolveInfo[] displayResolveInfos) {
+ ProfileRecord record = getProfileRecord(userHandle);
+ if (record == null || record.shortcutLoader == null) {
+ return;
+ }
+ record.loadingStartTime = SystemClock.elapsedRealtime();
+ record.shortcutLoader.updateAppTargets(displayResolveInfos);
+ }
+
+ @MainThread
+ private void onShortcutsLoaded(UserHandle userHandle, ShortcutLoader.Result result) {
+ if (DEBUG) {
+ Log.d(TAG, "onShortcutsLoaded for user: " + userHandle);
+ }
+ mDirectShareShortcutInfoCache.putAll(result.getDirectShareShortcutInfoCache());
+ mDirectShareAppTargetCache.putAll(result.getDirectShareAppTargetCache());
+ ChooserListAdapter adapter =
+ mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle);
+ if (adapter != null) {
+ for (ShortcutLoader.ShortcutResultInfo resultInfo : result.getShortcutsByApp()) {
+ adapter.addServiceResults(
+ resultInfo.getAppTarget(),
+ resultInfo.getShortcuts(),
+ result.isFromAppPredictor()
+ ? TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
+ : TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER,
+ mDirectShareShortcutInfoCache,
+ mDirectShareAppTargetCache);
+ }
+ adapter.completeServiceTargetLoading();
+ }
+
+ if (mMultiProfilePagerAdapter.getActiveListAdapter() == adapter) {
+ long duration = Tracer.INSTANCE.endLaunchToShortcutTrace();
+ if (duration >= 0) {
+ Log.d(TAG, "stat to first shortcut time: " + duration + " ms");
+ }
+ }
+ logDirectShareTargetReceived(userHandle);
+ sendVoiceChoicesIfNeeded();
+ getEventLog().logSharesheetDirectLoadComplete();
+ }
+
+ private void setupScrollListener() {
+ if (mResolverDrawerLayout == null) {
+ return;
+ }
+ int elevatedViewResId = shouldShowTabs() ? com.android.internal.R.id.tabs : com.android.internal.R.id.chooser_header;
+ final View elevatedView = mResolverDrawerLayout.findViewById(elevatedViewResId);
+ final float defaultElevation = elevatedView.getElevation();
+ final float chooserHeaderScrollElevation =
+ getResources().getDimensionPixelSize(R.dimen.chooser_header_scroll_elevation);
+ mChooserMultiProfilePagerAdapter.getActiveAdapterView().addOnScrollListener(
+ new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrollStateChanged(RecyclerView view, int scrollState) {
+ if (scrollState == RecyclerView.SCROLL_STATE_IDLE) {
+ if (mScrollStatus == SCROLL_STATUS_SCROLLING_VERTICAL) {
+ mScrollStatus = SCROLL_STATUS_IDLE;
+ setHorizontalScrollingEnabled(true);
+ }
+ } else if (scrollState == RecyclerView.SCROLL_STATE_DRAGGING) {
+ if (mScrollStatus == SCROLL_STATUS_IDLE) {
+ mScrollStatus = SCROLL_STATUS_SCROLLING_VERTICAL;
+ setHorizontalScrollingEnabled(false);
+ }
+ }
+ }
+
+ @Override
+ public void onScrolled(RecyclerView view, int dx, int dy) {
+ if (view.getChildCount() > 0) {
+ View child = view.getLayoutManager().findViewByPosition(0);
+ if (child == null || child.getTop() < 0) {
+ elevatedView.setElevation(chooserHeaderScrollElevation);
+ return;
+ }
+ }
+
+ elevatedView.setElevation(defaultElevation);
+ }
+ });
+ }
+
+ private void maybeSetupGlobalLayoutListener() {
+ if (shouldShowTabs()) {
+ return;
+ }
+ final View recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
+ recyclerView.getViewTreeObserver()
+ .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ // Fixes an issue were the accessibility border disappears on list creation.
+ recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ final TextView titleView = findViewById(com.android.internal.R.id.title);
+ if (titleView != null) {
+ titleView.setFocusable(true);
+ titleView.setFocusableInTouchMode(true);
+ titleView.requestFocus();
+ titleView.requestAccessibilityFocus();
+ }
+ }
+ });
+ }
+
+ /**
+ * The sticky content preview is shown only when we have a tabbed view. It's shown above
+ * the tabs so it is not part of the scrollable list. If we are not in tabbed view,
+ * we instead show the content preview as a regular list item.
+ */
+ private boolean shouldShowStickyContentPreview() {
+ return shouldShowStickyContentPreviewNoOrientationCheck();
+ }
+
+ private boolean shouldShowStickyContentPreviewNoOrientationCheck() {
+ if (!shouldShowContentPreview()) {
+ return false;
+ }
+ boolean isEmpty = mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+ UserHandle.of(UserHandle.myUserId())).getCount() == 0;
+ return (mFeatureFlags.scrollablePreview() || shouldShowTabs())
+ && (!isEmpty || shouldShowContentPreviewWhenEmpty());
+ }
+
+ /**
+ * This method could be used to override the default behavior when we hide the preview area
+ * when the current tab doesn't have any items.
+ *
+ * @return true if we want to show the content preview area even if the tab for the current
+ * user is empty
+ */
+ protected boolean shouldShowContentPreviewWhenEmpty() {
+ return false;
+ }
+
+ /**
+ * @return true if we want to show the content preview area
+ */
+ protected boolean shouldShowContentPreview() {
+ ChooserRequestParameters chooserRequest = getChooserRequest();
+ return (chooserRequest != null) && chooserRequest.isSendActionTarget();
+ }
+
+ private void updateStickyContentPreview() {
+ if (shouldShowStickyContentPreviewNoOrientationCheck()) {
+ // The sticky content preview is only shown when we show the work and personal tabs.
+ // We don't show it in landscape as otherwise there is no room for scrolling.
+ // If the sticky content preview will be shown at some point with orientation change,
+ // then always preload it to avoid subsequent resizing of the share sheet.
+ ViewGroup contentPreviewContainer =
+ findViewById(com.android.internal.R.id.content_preview_container);
+ if (contentPreviewContainer.getChildCount() == 0) {
+ ViewGroup contentPreviewView = createContentPreviewView(contentPreviewContainer);
+ contentPreviewContainer.addView(contentPreviewView);
+ }
+ }
+ if (shouldShowStickyContentPreview()) {
+ showStickyContentPreview();
+ } else {
+ hideStickyContentPreview();
+ }
+ }
+
+ private void showStickyContentPreview() {
+ if (isStickyContentPreviewShowing()) {
+ return;
+ }
+ ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
+ contentPreviewContainer.setVisibility(View.VISIBLE);
+ }
+
+ private boolean isStickyContentPreviewShowing() {
+ ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
+ return contentPreviewContainer.getVisibility() == View.VISIBLE;
+ }
+
+ private void hideStickyContentPreview() {
+ if (!isStickyContentPreviewShowing()) {
+ return;
+ }
+ ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
+ contentPreviewContainer.setVisibility(View.GONE);
+ }
+
+ private View findRootView() {
+ if (mContentView == null) {
+ mContentView = findViewById(android.R.id.content);
+ }
+ return mContentView;
+ }
+
+ /**
+ * Intentionally override the {@link ResolverActivity} implementation as we only need that
+ * implementation for the intent resolver case.
+ */
+ @Override
+ public void onButtonClick(View v) {}
+
+ /**
+ * Intentionally override the {@link ResolverActivity} implementation as we only need that
+ * implementation for the intent resolver case.
+ */
+ @Override
+ protected void resetButtonBar() {}
+
+ @Override
+ protected String getMetricsCategory() {
+ return METRICS_CATEGORY_CHOOSER;
+ }
+
+ @Override
+ protected void onProfileTabSelected() {
+ // This fixes an edge case where after performing a variety of gestures, vertical scrolling
+ // ends up disabled. That's because at some point the old tab's vertical scrolling is
+ // disabled and the new tab's is enabled. For context, see b/159997845
+ setVerticalScrollEnabled(true);
+ if (mResolverDrawerLayout != null) {
+ mResolverDrawerLayout.scrollNestedScrollableChildBackToTop();
+ }
+ }
+
+ @Override
+ protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
+ if (shouldShowTabs()) {
+ mChooserMultiProfilePagerAdapter
+ .setEmptyStateBottomOffset(insets.getSystemWindowInsetBottom());
+ }
+
+ WindowInsets result = super.onApplyWindowInsets(v, insets);
+ if (mResolverDrawerLayout != null) {
+ mResolverDrawerLayout.requestLayout();
+ }
+ return result;
+ }
+
+ private void setHorizontalScrollingEnabled(boolean enabled) {
+ ResolverViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ viewPager.setSwipingEnabled(enabled);
+ }
+
+ private void setVerticalScrollEnabled(boolean enabled) {
+ ChooserGridLayoutManager layoutManager =
+ (ChooserGridLayoutManager) mChooserMultiProfilePagerAdapter.getActiveAdapterView()
+ .getLayoutManager();
+ layoutManager.setVerticalScrollEnabled(enabled);
+ }
+
+ @Override
+ void onHorizontalSwipeStateChanged(int state) {
+ if (state == ViewPager.SCROLL_STATE_DRAGGING) {
+ if (mScrollStatus == SCROLL_STATUS_IDLE) {
+ mScrollStatus = SCROLL_STATUS_SCROLLING_HORIZONTAL;
+ setVerticalScrollEnabled(false);
+ }
+ } else if (state == ViewPager.SCROLL_STATE_IDLE) {
+ if (mScrollStatus == SCROLL_STATUS_SCROLLING_HORIZONTAL) {
+ mScrollStatus = SCROLL_STATUS_IDLE;
+ setVerticalScrollEnabled(true);
+ }
+ }
+ }
+
+ @Override
+ protected void maybeLogProfileChange() {
+ getEventLog().logSharesheetProfileChanged();
+ }
+
+ private static class ProfileRecord {
+ /** The {@link AppPredictor} for this profile, if any. */
+ @Nullable
+ public final AppPredictor appPredictor;
+ /**
+ * null if we should not load shortcuts.
+ */
+ @Nullable
+ public final ShortcutLoader shortcutLoader;
+ public long loadingStartTime;
+
+ private ProfileRecord(
+ @Nullable AppPredictor appPredictor,
+ @Nullable ShortcutLoader shortcutLoader) {
+ this.appPredictor = appPredictor;
+ this.shortcutLoader = shortcutLoader;
+ }
+
+ public void destroy() {
+ if (appPredictor != null) {
+ appPredictor.destroy();
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt b/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt
new file mode 100644
index 00000000..7bc39a24
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ChooserActivityLogic.kt
@@ -0,0 +1,87 @@
+package com.android.intentresolver.v2
+
+import android.app.Activity
+import android.content.Intent
+import android.util.Log
+import androidx.activity.ComponentActivity
+import androidx.annotation.OpenForTesting
+import com.android.intentresolver.ChooserRequestParameters
+import com.android.intentresolver.R
+import com.android.intentresolver.icons.TargetDataLoader
+import com.android.intentresolver.v2.util.mutableLazy
+
+private const val TAG = "ChooserActivityLogic"
+
+/**
+ * Activity logic for [ChooserActivity].
+ *
+ * TODO: Make this class no longer open once [ChooserActivity] no longer needs to cast to access
+ * [chooserRequestParameters]. For now, this class being open is better than using reflection
+ * there.
+ */
+@OpenForTesting
+open class ChooserActivityLogic(
+ tag: String,
+ activityProvider: () -> ComponentActivity,
+ onWorkProfileStatusUpdated: () -> Unit,
+ targetDataLoaderProvider: () -> TargetDataLoader,
+ private val onPreInitialization: () -> Unit,
+) :
+ ActivityLogic,
+ CommonActivityLogic by CommonActivityLogicImpl(
+ tag,
+ activityProvider,
+ onWorkProfileStatusUpdated,
+ ) {
+
+ override val targetIntent: Intent by lazy { chooserRequestParameters?.targetIntent ?: Intent() }
+
+ override val resolvingHome: Boolean = false
+
+ override val title: CharSequence? by lazy { chooserRequestParameters?.title }
+
+ override val defaultTitleResId: Int by lazy {
+ chooserRequestParameters?.defaultTitleResource ?: 0
+ }
+
+ override val initialIntents: List<Intent>? by lazy {
+ chooserRequestParameters?.initialIntents?.toList()
+ }
+
+ override val supportsAlwaysUseOption: Boolean = false
+
+ override val targetDataLoader: TargetDataLoader by lazy { targetDataLoaderProvider() }
+
+ override val themeResId: Int = R.style.Theme_DeviceDefault_Chooser
+
+ private val _profileSwitchMessage = mutableLazy { forwardMessageFor(targetIntent) }
+ override val profileSwitchMessage: String? by _profileSwitchMessage
+
+ override val payloadIntents: List<Intent> by lazy {
+ buildList {
+ add(targetIntent)
+ chooserRequestParameters?.additionalTargets?.let { addAll(it) }
+ }
+ }
+
+ val chooserRequestParameters: ChooserRequestParameters? by lazy {
+ try {
+ ChooserRequestParameters(
+ (activity as Activity).intent,
+ referrerPackageName,
+ (activity as Activity).referrer,
+ )
+ } catch (e: IllegalArgumentException) {
+ Log.e(tag, "Caller provided invalid Chooser request parameters", e)
+ null
+ }
+ }
+
+ override fun preInitialization() {
+ onPreInitialization()
+ }
+
+ override fun clearProfileSwitchMessage() {
+ _profileSwitchMessage.setLazy(null)
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java
new file mode 100644
index 00000000..de0a9426
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ChooserMultiProfilePagerAdapter.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2;
+
+import android.content.Context;
+import android.os.UserHandle;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.viewpager.widget.PagerAdapter;
+
+import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.ChooserRecyclerViewAccessibilityDelegate;
+import com.android.intentresolver.FeatureFlags;
+import com.android.intentresolver.R;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.grid.ChooserGridAdapter;
+import com.android.intentresolver.measurements.Tracer;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+
+/**
+ * A {@link PagerAdapter} which describes the work and personal profile share sheet screens.
+ */
+@VisibleForTesting
+public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter<
+ RecyclerView, ChooserGridAdapter, ChooserListAdapter> {
+ private static final int SINGLE_CELL_SPAN_SIZE = 1;
+
+ private final ChooserProfileAdapterBinder mAdapterBinder;
+ private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier;
+
+ public ChooserMultiProfilePagerAdapter(
+ Context context,
+ ChooserGridAdapter adapter,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle,
+ int maxTargetsPerRow,
+ FeatureFlags featureFlags) {
+ this(
+ context,
+ new ChooserProfileAdapterBinder(maxTargetsPerRow),
+ ImmutableList.of(adapter),
+ emptyStateProvider,
+ workProfileQuietModeChecker,
+ /* defaultProfile= */ 0,
+ workProfileUserHandle,
+ cloneProfileUserHandle,
+ new BottomPaddingOverrideSupplier(context),
+ featureFlags);
+ }
+
+ public ChooserMultiProfilePagerAdapter(
+ Context context,
+ ChooserGridAdapter personalAdapter,
+ ChooserGridAdapter workAdapter,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ @Profile int defaultProfile,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle,
+ int maxTargetsPerRow,
+ FeatureFlags featureFlags) {
+ this(
+ context,
+ new ChooserProfileAdapterBinder(maxTargetsPerRow),
+ ImmutableList.of(personalAdapter, workAdapter),
+ emptyStateProvider,
+ workProfileQuietModeChecker,
+ defaultProfile,
+ workProfileUserHandle,
+ cloneProfileUserHandle,
+ new BottomPaddingOverrideSupplier(context),
+ featureFlags);
+ }
+
+ private ChooserMultiProfilePagerAdapter(
+ Context context,
+ ChooserProfileAdapterBinder adapterBinder,
+ ImmutableList<ChooserGridAdapter> gridAdapters,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ @Profile int defaultProfile,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle,
+ BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier,
+ FeatureFlags featureFlags) {
+ super(
+ gridAdapter -> gridAdapter.getListAdapter(),
+ adapterBinder,
+ gridAdapters,
+ emptyStateProvider,
+ workProfileQuietModeChecker,
+ defaultProfile,
+ workProfileUserHandle,
+ cloneProfileUserHandle,
+ () -> makeProfileView(context, featureFlags),
+ bottomPaddingOverrideSupplier);
+ mAdapterBinder = adapterBinder;
+ mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier;
+ }
+
+ public void setMaxTargetsPerRow(int maxTargetsPerRow) {
+ mAdapterBinder.setMaxTargetsPerRow(maxTargetsPerRow);
+ }
+
+ public void setEmptyStateBottomOffset(int bottomOffset) {
+ mBottomPaddingOverrideSupplier.setEmptyStateBottomOffset(bottomOffset);
+ setupContainerPadding();
+ }
+
+ /**
+ * Notify adapter about the drawer's collapse state. This will affect the app divider's
+ * visibility.
+ */
+ public void setIsCollapsed(boolean isCollapsed) {
+ for (int i = 0, size = getItemCount(); i < size; i++) {
+ getAdapterForIndex(i).setAzLabelVisibility(!isCollapsed);
+ }
+ }
+
+ private static ViewGroup makeProfileView(
+ Context context, FeatureFlags featureFlags) {
+ LayoutInflater inflater = LayoutInflater.from(context);
+ ViewGroup rootView = featureFlags.scrollablePreview()
+ ? (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile_wrap, null, false)
+ : (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile, null, false);
+ RecyclerView recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list);
+ recyclerView.setAccessibilityDelegateCompat(
+ new ChooserRecyclerViewAccessibilityDelegate(recyclerView));
+ return rootView;
+ }
+
+ @Override
+ public boolean onHandlePackagesChanged(
+ ChooserListAdapter listAdapter, boolean waitingToEnableWorkProfile) {
+ // TODO: why do we need to do the extra `notifyDataSetChanged()` in (only) the Chooser case?
+ getActiveListAdapter().notifyDataSetChanged();
+ return super.onHandlePackagesChanged(listAdapter, waitingToEnableWorkProfile);
+ }
+
+ @Override
+ protected final boolean rebuildTab(ChooserListAdapter listAdapter, boolean doPostProcessing) {
+ if (doPostProcessing) {
+ Tracer.INSTANCE.beginAppTargetLoadingSection(listAdapter.getUserHandle());
+ }
+ return super.rebuildTab(listAdapter, doPostProcessing);
+ }
+
+ /** Apply the specified {@code height} as the footer in each tab's adapter. */
+ public void setFooterHeightInEveryAdapter(int height) {
+ for (int i = 0; i < getItemCount(); ++i) {
+ getAdapterForIndex(i).setFooterHeight(height);
+ }
+ }
+
+ private static class BottomPaddingOverrideSupplier implements Supplier<Optional<Integer>> {
+ private final Context mContext;
+ private int mBottomOffset;
+
+ BottomPaddingOverrideSupplier(Context context) {
+ mContext = context;
+ }
+
+ public void setEmptyStateBottomOffset(int bottomOffset) {
+ mBottomOffset = bottomOffset;
+ }
+
+ @Override
+ public Optional<Integer> get() {
+ int initialBottomPadding = mContext.getResources().getDimensionPixelSize(
+ R.dimen.resolver_empty_state_container_padding_bottom);
+ return Optional.of(initialBottomPadding + mBottomOffset);
+ }
+ }
+
+ private static class ChooserProfileAdapterBinder implements
+ AdapterBinder<RecyclerView, ChooserGridAdapter> {
+ private int mMaxTargetsPerRow;
+
+ ChooserProfileAdapterBinder(int maxTargetsPerRow) {
+ mMaxTargetsPerRow = maxTargetsPerRow;
+ }
+
+ public void setMaxTargetsPerRow(int maxTargetsPerRow) {
+ mMaxTargetsPerRow = maxTargetsPerRow;
+ }
+
+ @Override
+ public void bind(
+ RecyclerView recyclerView, ChooserGridAdapter chooserGridAdapter) {
+ GridLayoutManager glm = (GridLayoutManager) recyclerView.getLayoutManager();
+ glm.setSpanCount(mMaxTargetsPerRow);
+ glm.setSpanSizeLookup(
+ new GridLayoutManager.SpanSizeLookup() {
+ @Override
+ public int getSpanSize(int position) {
+ return chooserGridAdapter.shouldCellSpan(position)
+ ? SINGLE_CELL_SPAN_SIZE
+ : glm.getSpanCount();
+ }
+ });
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ChooserSelector.kt b/java/src/com/android/intentresolver/v2/ChooserSelector.kt
new file mode 100644
index 00000000..378bc06c
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ChooserSelector.kt
@@ -0,0 +1,36 @@
+package com.android.intentresolver.v2
+
+import android.content.BroadcastReceiver
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import com.android.intentresolver.FeatureFlags
+import dagger.hilt.android.AndroidEntryPoint
+import javax.inject.Inject
+
+@AndroidEntryPoint(BroadcastReceiver::class)
+class ChooserSelector : Hilt_ChooserSelector() {
+
+ @Inject lateinit var featureFlags: FeatureFlags
+
+ override fun onReceive(context: Context, intent: Intent) {
+ super.onReceive(context, intent)
+ if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
+ context.packageManager.setComponentEnabledSetting(
+ ComponentName(CHOOSER_PACKAGE, CHOOSER_PACKAGE + CHOOSER_CLASS),
+ if (featureFlags.modularFramework()) {
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+ } else {
+ PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
+ },
+ /* flags = */ 0,
+ )
+ }
+ }
+
+ companion object {
+ private const val CHOOSER_PACKAGE = "com.android.intentresolver"
+ private const val CHOOSER_CLASS = ".v2.ChooserActivity"
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java
new file mode 100644
index 00000000..2d9be816
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/MultiProfilePagerAdapter.java
@@ -0,0 +1,666 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.v2;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.viewpager.widget.PagerAdapter;
+import androidx.viewpager.widget.ViewPager;
+
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.v2.emptystate.EmptyStateUiHelper;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+/**
+ * Skeletal {@link PagerAdapter} implementation for a UI with per-profile tabs (as in Sharesheet).
+ * <p>
+ * TODO: attempt to further restrict visibility/improve encapsulation in the methods we expose.
+ * <p>
+ * TODO: deprecate and audit/fix usages of any methods that refer to the "active" or "inactive"
+ * <p>
+ * adapters; these were marked {@link VisibleForTesting} and their usage seems like an accident
+ * waiting to happen since clients seem to make assumptions about which adapter will be "active" in
+ * a particular context, and more explicit APIs would make sure those were valid.
+ * <p>
+ * TODO: consider renaming legacy methods (e.g. why do we know it's a "list", not just a "page"?)
+ * <p>
+ * TODO: this is part of an in-progress refactor to merge with `GenericMultiProfilePagerAdapter`.
+ * As originally noted there, we've reduced explicit references to the `ResolverListAdapter` base
+ * type and may be able to drop the type constraint.
+ *
+ * @param <PageViewT> the type of the widget that represents the contents of a page in this adapter
+ * @param <SinglePageAdapterT> the type of a "root" adapter class to be instantiated and included in
+ * the per-profile records.
+ * @param <ListAdapterT> the concrete type of a {@link ResolverListAdapter} implementation to
+ * control the contents of a given per-profile list. This is provided for convenience, since it must
+ * be possible to get the list adapter from the page adapter via our
+ * <code>mListAdapterExtractor</code>.
+ */
+public class MultiProfilePagerAdapter<
+ PageViewT extends ViewGroup,
+ SinglePageAdapterT,
+ ListAdapterT extends ResolverListAdapter> extends PagerAdapter {
+
+ /**
+ * Delegate to set up a given adapter and page view to be used together.
+ * @param <PageViewT> (as in {@link MultiProfilePagerAdapter}).
+ * @param <SinglePageAdapterT> (as in {@link MultiProfilePagerAdapter}).
+ */
+ public interface AdapterBinder<PageViewT, SinglePageAdapterT> {
+ /**
+ * The given {@code view} will be associated with the given {@code adapter}. Do any work
+ * necessary to configure them compatibly, introduce them to each other, etc.
+ */
+ void bind(PageViewT view, SinglePageAdapterT adapter);
+ }
+
+ public static final int PROFILE_PERSONAL = 0;
+ public static final int PROFILE_WORK = 1;
+
+ @IntDef({PROFILE_PERSONAL, PROFILE_WORK})
+ public @interface Profile {}
+
+ private final Function<SinglePageAdapterT, ListAdapterT> mListAdapterExtractor;
+ private final AdapterBinder<PageViewT, SinglePageAdapterT> mAdapterBinder;
+ private final Supplier<ViewGroup> mPageViewInflater;
+
+ private final ImmutableList<ProfileDescriptor<PageViewT, SinglePageAdapterT>> mItems;
+
+ private final EmptyStateProvider mEmptyStateProvider;
+ private final UserHandle mWorkProfileUserHandle;
+ private final UserHandle mCloneProfileUserHandle;
+ private final Supplier<Boolean> mWorkProfileQuietModeChecker; // True when work is quiet.
+
+ private Set<Integer> mLoadedPages;
+ private int mCurrentPage;
+ private OnProfileSelectedListener mOnProfileSelectedListener;
+
+ protected MultiProfilePagerAdapter(
+ Function<SinglePageAdapterT, ListAdapterT> listAdapterExtractor,
+ AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder,
+ ImmutableList<SinglePageAdapterT> adapters,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ @Profile int defaultProfile,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle,
+ Supplier<ViewGroup> pageViewInflater,
+ Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
+ mCurrentPage = defaultProfile;
+ mLoadedPages = new HashSet<>();
+ mWorkProfileUserHandle = workProfileUserHandle;
+ mCloneProfileUserHandle = cloneProfileUserHandle;
+ mEmptyStateProvider = emptyStateProvider;
+ mWorkProfileQuietModeChecker = workProfileQuietModeChecker;
+
+ mListAdapterExtractor = listAdapterExtractor;
+ mAdapterBinder = adapterBinder;
+ mPageViewInflater = pageViewInflater;
+
+ ImmutableList.Builder<ProfileDescriptor<PageViewT, SinglePageAdapterT>> items =
+ new ImmutableList.Builder<>();
+ for (SinglePageAdapterT adapter : adapters) {
+ items.add(createProfileDescriptor(adapter, containerBottomPaddingOverrideSupplier));
+ }
+ mItems = items.build();
+ }
+
+ private ProfileDescriptor<PageViewT, SinglePageAdapterT> createProfileDescriptor(
+ SinglePageAdapterT adapter,
+ Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
+ return new ProfileDescriptor<>(
+ mPageViewInflater.get(), adapter, containerBottomPaddingOverrideSupplier);
+ }
+
+ public void setOnProfileSelectedListener(OnProfileSelectedListener listener) {
+ mOnProfileSelectedListener = listener;
+ }
+
+ /**
+ * Sets this instance of this class as {@link ViewPager}'s {@link PagerAdapter} and sets
+ * an {@link ViewPager.OnPageChangeListener} where it keeps track of the currently displayed
+ * page and rebuilds the list.
+ */
+ public void setupViewPager(ViewPager viewPager) {
+ viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
+ @Override
+ public void onPageSelected(int position) {
+ mCurrentPage = position;
+ if (!mLoadedPages.contains(position)) {
+ rebuildActiveTab(true);
+ mLoadedPages.add(position);
+ }
+ if (mOnProfileSelectedListener != null) {
+ mOnProfileSelectedListener.onProfileSelected(position);
+ }
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ if (mOnProfileSelectedListener != null) {
+ mOnProfileSelectedListener.onProfilePageStateChanged(state);
+ }
+ }
+ });
+ viewPager.setAdapter(this);
+ viewPager.setCurrentItem(mCurrentPage);
+ mLoadedPages.add(mCurrentPage);
+ }
+
+ public void clearInactiveProfileCache() {
+ if (mLoadedPages.size() == 1) {
+ return;
+ }
+ mLoadedPages.remove(1 - mCurrentPage);
+ }
+
+ @Override
+ public final ViewGroup instantiateItem(ViewGroup container, int position) {
+ setupListAdapter(position);
+ final ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(position);
+ container.addView(descriptor.mRootView);
+ return descriptor.mRootView;
+ }
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object view) {
+ container.removeView((View) view);
+ }
+
+ @Override
+ public int getCount() {
+ return getItemCount();
+ }
+
+ public int getCurrentPage() {
+ return mCurrentPage;
+ }
+
+ public final @Profile int getActiveProfile() {
+ // TODO: here and elsewhere in this class, distinguish between a "profile ID" integer and
+ // its mapped "page index." When we support more than two profiles, this won't be a "stable
+ // mapping" -- some particular profile may not be represented by a "page," but the ones that
+ // are will be assigned contiguous page numbers that skip over the holes.
+ return getCurrentPage();
+ }
+
+ @VisibleForTesting
+ public UserHandle getCurrentUserHandle() {
+ return getActiveListAdapter().getUserHandle();
+ }
+
+ @Override
+ public boolean isViewFromObject(View view, Object object) {
+ return view == object;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ return null;
+ }
+
+ public UserHandle getCloneUserHandle() {
+ return mCloneProfileUserHandle;
+ }
+
+ /**
+ * Returns the {@link ProfileDescriptor} relevant to the given <code>pageIndex</code>.
+ * <ul>
+ * <li>For a device with only one user, <code>pageIndex</code> value of
+ * <code>0</code> would return the personal profile {@link ProfileDescriptor}.</li>
+ * <li>For a device with a work profile, <code>pageIndex</code> value of <code>0</code> would
+ * return the personal profile {@link ProfileDescriptor}, and <code>pageIndex</code> value of
+ * <code>1</code> would return the work profile {@link ProfileDescriptor}.</li>
+ * </ul>
+ */
+ private ProfileDescriptor<PageViewT, SinglePageAdapterT> getItem(int pageIndex) {
+ return mItems.get(pageIndex);
+ }
+
+ private ViewGroup getEmptyStateView(int pageIndex) {
+ return getItem(pageIndex).getEmptyStateView();
+ }
+
+ public ViewGroup getActiveEmptyStateView() {
+ return getEmptyStateView(getCurrentPage());
+ }
+
+ /**
+ * Returns the number of {@link ProfileDescriptor} objects.
+ * <p>For a normal consumer device with only one user returns <code>1</code>.
+ * <p>For a device with a work profile returns <code>2</code>.
+ */
+ public final int getItemCount() {
+ return mItems.size();
+ }
+
+ public final PageViewT getListViewForIndex(int index) {
+ return getItem(index).mView;
+ }
+
+ /**
+ * Returns the adapter of the list view for the relevant page specified by
+ * <code>pageIndex</code>.
+ * <p>This method is meant to be implemented with an implementation-specific return type
+ * depending on the adapter type.
+ */
+ @VisibleForTesting
+ public final SinglePageAdapterT getAdapterForIndex(int index) {
+ return getItem(index).mAdapter;
+ }
+
+ /**
+ * Performs view-related initialization procedures for the adapter specified
+ * by <code>pageIndex</code>.
+ */
+ public final void setupListAdapter(int pageIndex) {
+ mAdapterBinder.bind(getListViewForIndex(pageIndex), getAdapterForIndex(pageIndex));
+ }
+
+ /**
+ * Returns the {@link ListAdapterT} instance of the profile that represents
+ * <code>userHandle</code>. If there is no such adapter for the specified
+ * <code>userHandle</code>, returns {@code null}.
+ * <p>For example, if there is a work profile on the device with user id 10, calling this method
+ * with <code>UserHandle.of(10)</code> returns the work profile {@link ListAdapterT}.
+ */
+ @Nullable
+ public final ListAdapterT getListAdapterForUserHandle(UserHandle userHandle) {
+ if (getPersonalListAdapter().getUserHandle().equals(userHandle)
+ || userHandle.equals(getCloneUserHandle())) {
+ return getPersonalListAdapter();
+ } else if ((getWorkListAdapter() != null)
+ && getWorkListAdapter().getUserHandle().equals(userHandle)) {
+ return getWorkListAdapter();
+ }
+ return null;
+ }
+
+ /**
+ * Returns the {@link ListAdapterT} instance of the profile that is currently visible
+ * to the user.
+ * <p>For example, if the user is viewing the work tab in the share sheet, this method returns
+ * the work profile {@link ListAdapterT}.
+ * @see #getInactiveListAdapter()
+ */
+ @VisibleForTesting
+ public final ListAdapterT getActiveListAdapter() {
+ return mListAdapterExtractor.apply(getAdapterForIndex(getCurrentPage()));
+ }
+
+ /**
+ * If this is a device with a work profile, returns the {@link ListAdapterT} instance
+ * of the profile that is <b><i>not</i></b> currently visible to the user. Otherwise returns
+ * {@code null}.
+ * <p>For example, if the user is viewing the work tab in the share sheet, this method returns
+ * the personal profile {@link ListAdapterT}.
+ * @see #getActiveListAdapter()
+ */
+ @VisibleForTesting
+ @Nullable
+ public final ListAdapterT getInactiveListAdapter() {
+ if (getCount() < 2) {
+ return null;
+ }
+ return mListAdapterExtractor.apply(getAdapterForIndex(1 - getCurrentPage()));
+ }
+
+ public final ListAdapterT getPersonalListAdapter() {
+ return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_PERSONAL));
+ }
+
+ /** @return whether our tab data contains a page for the specified {@code profile} ID. */
+ public final boolean hasPageForProfile(@Profile int profile) {
+ // TODO: here and elsewhere in this class, distinguish between a "profile ID" integer and
+ // its mapped "page index." When we support more than two profiles, this won't be a "stable
+ // mapping" -- some particular profile may not be represented by a "page," but the ones that
+ // are will be assigned contiguous page numbers that skip over the holes.
+ return hasAdapterForIndex(profile);
+ }
+
+ @Nullable
+ public final ListAdapterT getWorkListAdapter() {
+ if (!hasAdapterForIndex(PROFILE_WORK)) {
+ return null;
+ }
+ return mListAdapterExtractor.apply(getAdapterForIndex(PROFILE_WORK));
+ }
+
+ public final SinglePageAdapterT getCurrentRootAdapter() {
+ return getAdapterForIndex(getCurrentPage());
+ }
+
+ public final PageViewT getActiveAdapterView() {
+ return getListViewForIndex(getCurrentPage());
+ }
+
+ @Nullable
+ public final PageViewT getInactiveAdapterView() {
+ if (getCount() < 2) {
+ return null;
+ }
+ return getListViewForIndex(1 - getCurrentPage());
+ }
+
+ private boolean anyAdapterHasItems() {
+ for (int i = 0; i < mItems.size(); ++i) {
+ ListAdapterT listAdapter = mListAdapterExtractor.apply(getAdapterForIndex(i));
+ if (listAdapter.getCount() > 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public void refreshPackagesInAllTabs() {
+ // TODO: handle all inactive profiles; for now we can only have at most one. It's unclear if
+ // this legacy logic really requires the active tab to be rebuilt first, or if we could just
+ // iterate over the tabs in arbitrary order.
+ getActiveListAdapter().handlePackagesChanged();
+ if (getCount() > 1) {
+ getInactiveListAdapter().handlePackagesChanged();
+ }
+ }
+
+ /**
+ * Notify that there has been a package change which could potentially modify the set of targets
+ * that should be shown in the specified {@code listAdapter}. This <em>may</em> result in
+ * "rebuilding" the target list for that adapter.
+ *
+ * @param listAdapter an adapter that may need to be updated after the package-change event.
+ * @param waitingToEnableWorkProfile whether we've turned on the work profile, but haven't yet
+ * seen an {@code ACTION_USER_UNLOCKED} broadcast. In this case we skip the rebuild of any
+ * work-profile adapter because we wouldn't expect meaningful results -- but another rebuild
+ * will be prompted when we eventually get the broadcast.
+ *
+ * @return whether we're able to proceed with a Sharesheet session after processing this
+ * package-change event. If false, we were able to rebuild the targets but determined that there
+ * aren't any we could present in the UI without the app looking broken, so we should just quit.
+ */
+ public boolean onHandlePackagesChanged(
+ ListAdapterT listAdapter, boolean waitingToEnableWorkProfile) {
+ if (listAdapter == getActiveListAdapter()) {
+ if (listAdapter.getUserHandle().equals(mWorkProfileUserHandle)
+ && waitingToEnableWorkProfile) {
+ // We have just turned on the work profile and entered the passcode to start it,
+ // now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no
+ // point in reloading the list now, since the work profile user is still turning on.
+ return true;
+ }
+
+ boolean listRebuilt = rebuildActiveTab(true);
+ if (listRebuilt) {
+ listAdapter.notifyDataSetChanged();
+ }
+
+ // TODO: shouldn't we check that the inactive tabs are built before declaring that we
+ // have to quit for lack of items?
+ return anyAdapterHasItems();
+ } else {
+ clearInactiveProfileCache();
+ return true;
+ }
+ }
+
+ /**
+ * Fully-rebuild the active tab and, if specified, partially-rebuild any other inactive tabs.
+ */
+ public boolean rebuildTabs(boolean includePartialRebuildOfInactiveTabs) {
+ // TODO: we may be able to determine `includePartialRebuildOfInactiveTabs` ourselves as
+ // a function of our own instance state. OTOH the purpose of this "partial rebuild" is to
+ // be able to evaluate the intermediate state of one particular profile tab (i.e. work
+ // profile) that may not generalize well when we have other "inactive tabs." I.e., either we
+ // rebuild *all* the inactive tabs just to evaluate some auto-launch conditions that only
+ // depend on personal and/or work tabs, or we have to explicitly specify the ones we care
+ // about. It's not the pager-adapter's business to know "which ones we care about," so maybe
+ // they should be rebuilt lazily when-and-if it comes up (e.g. during the evaluation of
+ // autolaunch conditions).
+ boolean rebuildCompleted = rebuildActiveTab(true) || getActiveListAdapter().isTabLoaded();
+ if (includePartialRebuildOfInactiveTabs) {
+ boolean rebuildInactiveCompleted =
+ rebuildInactiveTab(false) || getInactiveListAdapter().isTabLoaded();
+ rebuildCompleted = rebuildCompleted && rebuildInactiveCompleted;
+ }
+ return rebuildCompleted;
+ }
+
+ /**
+ * Rebuilds the tab that is currently visible to the user.
+ * <p>Returns {@code true} if rebuild has completed.
+ */
+ public final boolean rebuildActiveTab(boolean doPostProcessing) {
+ Trace.beginSection("MultiProfilePagerAdapter#rebuildActiveTab");
+ boolean result = rebuildTab(getActiveListAdapter(), doPostProcessing);
+ Trace.endSection();
+ return result;
+ }
+
+ /**
+ * Rebuilds the tab that is not currently visible to the user, if such one exists.
+ * <p>Returns {@code true} if rebuild has completed.
+ */
+ private boolean rebuildInactiveTab(boolean doPostProcessing) {
+ Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab");
+ if (getItemCount() == 1) {
+ Trace.endSection();
+ return false;
+ }
+ boolean result = rebuildTab(getInactiveListAdapter(), doPostProcessing);
+ Trace.endSection();
+ return result;
+ }
+
+ private int userHandleToPageIndex(UserHandle userHandle) {
+ if (userHandle.equals(getPersonalListAdapter().getUserHandle())) {
+ return PROFILE_PERSONAL;
+ } else {
+ return PROFILE_WORK;
+ }
+ }
+
+ protected boolean rebuildTab(ListAdapterT activeListAdapter, boolean doPostProcessing) {
+ if (shouldSkipRebuild(activeListAdapter)) {
+ activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true);
+ return false;
+ }
+ return activeListAdapter.rebuildList(doPostProcessing);
+ }
+
+ private boolean shouldSkipRebuild(ListAdapterT activeListAdapter) {
+ EmptyState emptyState = mEmptyStateProvider.getEmptyState(activeListAdapter);
+ return emptyState != null && emptyState.shouldSkipDataRebuild();
+ }
+
+ private boolean hasAdapterForIndex(int pageIndex) {
+ return (pageIndex < getCount());
+ }
+
+ /**
+ * The empty state screens are shown according to their priority:
+ * <ol>
+ * <li>(highest priority) cross-profile disabled by policy (handled in
+ * {@link #rebuildTab(ListAdapterT, boolean)})</li>
+ * <li>no apps available</li>
+ * <li>(least priority) work is off</li>
+ * </ol>
+ *
+ * The intention is to prevent the user from having to turn
+ * the work profile on if there will not be any apps resolved
+ * anyway.
+ *
+ * TODO: move this comment to the place where we configure our composite provider.
+ */
+ public void showEmptyResolverListEmptyState(ListAdapterT listAdapter) {
+ final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter);
+
+ if (emptyState == null) {
+ return;
+ }
+
+ emptyState.onEmptyStateShown();
+
+ View.OnClickListener clickListener = null;
+
+ if (emptyState.getButtonClickListener() != null) {
+ clickListener = v -> emptyState.getButtonClickListener().onClick(() -> {
+ ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
+ userHandleToPageIndex(listAdapter.getUserHandle()));
+ descriptor.mEmptyStateUi.showSpinner();
+ });
+ }
+
+ showEmptyState(listAdapter, emptyState, clickListener);
+ }
+
+ /**
+ * Class to get user id of the current process
+ */
+ public static class MyUserIdProvider {
+ /**
+ * @return user id of the current process
+ */
+ public int getMyUserId() {
+ return UserHandle.myUserId();
+ }
+ }
+
+ private void showEmptyState(
+ ListAdapterT activeListAdapter,
+ EmptyState emptyState,
+ View.OnClickListener buttonOnClick) {
+ ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
+ userHandleToPageIndex(activeListAdapter.getUserHandle()));
+ descriptor.mEmptyStateUi.showEmptyState(emptyState, buttonOnClick);
+ activeListAdapter.markTabLoaded();
+ }
+
+ /**
+ * Sets up the padding of the view containing the empty state screens for the current adapter
+ * view.
+ */
+ protected final void setupContainerPadding() {
+ getItem(getCurrentPage()).setupContainerPadding();
+ }
+
+ public void showListView(ListAdapterT activeListAdapter) {
+ ProfileDescriptor<PageViewT, SinglePageAdapterT> descriptor = getItem(
+ userHandleToPageIndex(activeListAdapter.getUserHandle()));
+ descriptor.mEmptyStateUi.hide();
+ }
+
+ /**
+ * @return whether any "inactive" tab's adapter would show an empty-state screen in our current
+ * application state.
+ */
+ public final boolean shouldShowEmptyStateScreenInAnyInactiveAdapter() {
+ if (getCount() < 2) {
+ return false;
+ }
+ // TODO: check against *any* inactive adapter; for now we only have one.
+ return shouldShowEmptyStateScreen(getInactiveListAdapter());
+ }
+
+ public boolean shouldShowEmptyStateScreen(ListAdapterT listAdapter) {
+ int count = listAdapter.getUnfilteredCount();
+ return (count == 0 && listAdapter.getPlaceholderCount() == 0)
+ || (listAdapter.getUserHandle().equals(mWorkProfileUserHandle)
+ && mWorkProfileQuietModeChecker.get());
+ }
+
+ // TODO: `ChooserActivity` also has a per-profile record type. Maybe the "multi-profile pager"
+ // should be the owner of all per-profile data (especially now that the API is generic)?
+ private static class ProfileDescriptor<PageViewT, SinglePageAdapterT> {
+ final ViewGroup mRootView;
+ final EmptyStateUiHelper mEmptyStateUi;
+
+ // TODO: post-refactoring, we may not need to retain these ivars directly (since they may
+ // be encapsulated within the `EmptyStateUiHelper`?).
+ private final ViewGroup mEmptyStateView;
+
+ private final SinglePageAdapterT mAdapter;
+ private final PageViewT mView;
+
+ ProfileDescriptor(
+ ViewGroup rootView,
+ SinglePageAdapterT adapter,
+ Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
+ mRootView = rootView;
+ mAdapter = adapter;
+ mEmptyStateView = rootView.findViewById(com.android.internal.R.id.resolver_empty_state);
+ mView = (PageViewT) rootView.findViewById(com.android.internal.R.id.resolver_list);
+ mEmptyStateUi = new EmptyStateUiHelper(
+ rootView,
+ com.android.internal.R.id.resolver_list,
+ containerBottomPaddingOverrideSupplier);
+ }
+
+ protected ViewGroup getEmptyStateView() {
+ return mEmptyStateView;
+ }
+
+ private void setupContainerPadding() {
+ mEmptyStateUi.setupContainerPadding();
+ }
+ }
+
+ /** Listener interface for changes between the per-profile UI tabs. */
+ public interface OnProfileSelectedListener {
+ /**
+ * Callback for when the user changes the active tab from personal to work or vice versa.
+ * <p>This callback is only called when the intent resolver or share sheet shows
+ * the work and personal profiles.
+ * @param profileIndex {@link #PROFILE_PERSONAL} if the personal profile was selected or
+ * {@link #PROFILE_WORK} if the work profile was selected.
+ */
+ void onProfileSelected(int profileIndex);
+
+
+ /**
+ * Callback for when the scroll state changes. Useful for discovering when the user begins
+ * dragging, when the pager is automatically settling to the current page, or when it is
+ * fully stopped/idle.
+ * @param state {@link ViewPager#SCROLL_STATE_IDLE}, {@link ViewPager#SCROLL_STATE_DRAGGING}
+ * or {@link ViewPager#SCROLL_STATE_SETTLING}
+ * @see ViewPager.OnPageChangeListener#onPageScrollStateChanged
+ */
+ void onProfilePageStateChanged(int state);
+ }
+
+ /**
+ * Listener for when the user switches on the work profile from the work tab.
+ */
+ public interface OnSwitchOnWorkSelectedListener {
+ /**
+ * Callback for when the user switches on the work profile from the work tab.
+ */
+ void onSwitchOnWorkSelected();
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ResolverActivity.java b/java/src/com/android/intentresolver/v2/ResolverActivity.java
new file mode 100644
index 00000000..2ba50ec3
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ResolverActivity.java
@@ -0,0 +1,2181 @@
+/*
+ * Copyright (C) 2008 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2;
+
+import static android.Manifest.permission.INTERACT_ACROSS_PROFILES;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.PermissionChecker.PID_UNKNOWN;
+import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
+import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK;
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+
+import static com.android.internal.annotations.VisibleForTesting.Visibility.PROTECTED;
+
+import static java.util.Collections.emptyList;
+import static java.util.Objects.requireNonNull;
+import static java.util.Objects.requireNonNullElse;
+
+import android.app.ActivityManager;
+import android.app.ActivityThread;
+import android.app.VoiceInteractor.PickOptionRequest;
+import android.app.VoiceInteractor.PickOptionRequest.Option;
+import android.app.VoiceInteractor.Prompt;
+import android.app.admin.DevicePolicyEventLogger;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.PermissionChecker;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Insets;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.PatternMatcher;
+import android.os.RemoteException;
+import android.os.StrictMode;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.stats.devicepolicy.DevicePolicyEnums;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.Space;
+import android.widget.TabHost;
+import android.widget.TabWidget;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.annotation.UiThread;
+import androidx.fragment.app.FragmentActivity;
+import androidx.viewpager.widget.ViewPager;
+
+import com.android.intentresolver.AnnotatedUserHandles;
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.ResolverListController;
+import com.android.intentresolver.WorkProfileAvailabilityManager;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.emptystate.CompositeEmptyStateProvider;
+import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.intentresolver.icons.TargetDataLoader;
+import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
+import com.android.intentresolver.v2.MultiProfilePagerAdapter.MyUserIdProvider;
+import com.android.intentresolver.v2.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener;
+import com.android.intentresolver.v2.MultiProfilePagerAdapter.Profile;
+import com.android.intentresolver.v2.data.repository.DevicePolicyResources;
+import com.android.intentresolver.v2.emptystate.NoAppsAvailableEmptyStateProvider;
+import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider;
+import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
+import com.android.intentresolver.v2.emptystate.WorkProfilePausedEmptyStateProvider;
+import com.android.intentresolver.v2.ui.ActionTitle;
+import com.android.intentresolver.widget.ResolverDrawerLayout;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.internal.util.LatencyTracker;
+
+import kotlin.Unit;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This is a copy of ResolverActivity to support IntentResolver's ChooserActivity. This code is
+ * *not* the resolver that is actually triggered by the system right now (you want
+ * frameworks/base/core/java/com/android/internal/app/ResolverActivity.java for that), the full
+ * migration is not complete.
+ */
+@UiThread
+public class ResolverActivity extends FragmentActivity implements
+ ResolverListAdapter.ResolverListCommunicator {
+
+ private final List<Runnable> mInit = new ArrayList<>();
+
+ protected ActivityLogic mLogic;
+
+ private DevicePolicyResources mDevicePolicyResources;
+
+ public ResolverActivity() {
+ mIsIntentPicker = getClass().equals(ResolverActivity.class);
+ }
+
+ protected ResolverActivity(boolean isIntentPicker) {
+ mIsIntentPicker = isIntentPicker;
+ }
+
+ private Button mAlwaysButton;
+ private Button mOnceButton;
+ protected View mProfileView;
+ private int mLastSelected = AbsListView.INVALID_POSITION;
+ private int mLayoutId;
+ private PickTargetOptionRequest mPickOptionRequest;
+ // Expected to be true if this object is ResolverActivity or is ResolverWrapperActivity.
+ private final boolean mIsIntentPicker;
+ protected ResolverDrawerLayout mResolverDrawerLayout;
+ protected PackageManager mPm;
+
+ private static final String TAG = "ResolverActivity";
+ private static final boolean DEBUG = false;
+ private static final String LAST_SHOWN_TAB_KEY = "last_shown_tab_key";
+
+ private boolean mRegistered;
+
+ protected Insets mSystemWindowInsets = null;
+ private Space mFooterSpacer = null;
+
+ /** See {@link #setRetainInOnStop}. */
+ private boolean mRetainInOnStop;
+
+ protected static final String METRICS_CATEGORY_RESOLVER = "intent_resolver";
+ protected static final String METRICS_CATEGORY_CHOOSER = "intent_chooser";
+
+ /** Tracks if we should ignore future broadcasts telling us the work profile is enabled */
+ private boolean mWorkProfileHasBeenEnabled = false;
+
+ private static final String TAB_TAG_PERSONAL = "personal";
+ private static final String TAB_TAG_WORK = "work";
+
+ private PackageMonitor mPersonalPackageMonitor;
+ private PackageMonitor mWorkPackageMonitor;
+
+ @VisibleForTesting
+ protected MultiProfilePagerAdapter mMultiProfilePagerAdapter;
+
+
+ // Intent extra for connected audio devices
+ public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device";
+
+ /**
+ * Integer extra to indicate which profile should be automatically selected.
+ * <p>Can only be used if there is a work profile.
+ * <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}.
+ */
+ protected static final String EXTRA_SELECTED_PROFILE =
+ "com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE";
+
+ /**
+ * {@link UserHandle} extra to indicate the user of the user that the starting intent
+ * originated from.
+ * <p>This is not necessarily the same as {@link #getUserId()} or {@link UserHandle#myUserId()},
+ * as there are edge cases when the intent resolver is launched in the other profile.
+ * For example, when we have 0 resolved apps in current profile and multiple resolved
+ * apps in the other profile, opening a link from the current profile launches the intent
+ * resolver in the other one. b/148536209 for more info.
+ */
+ static final String EXTRA_CALLING_USER =
+ "com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER";
+
+ protected static final int PROFILE_PERSONAL = MultiProfilePagerAdapter.PROFILE_PERSONAL;
+ protected static final int PROFILE_WORK = MultiProfilePagerAdapter.PROFILE_WORK;
+
+ private UserHandle mHeaderCreatorUser;
+
+ @Nullable
+ private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
+
+ protected final LatencyTracker mLatencyTracker = getLatencyTracker();
+
+ protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) {
+ return new PackageMonitor() {
+ @Override
+ public void onSomePackagesChanged() {
+ listAdapter.handlePackagesChanged();
+ updateProfileViewButton();
+ }
+
+ @Override
+ public boolean onPackageChanged(String packageName, int uid, String[] components) {
+ // We care about all package changes, not just the whole package itself which is
+ // default behavior.
+ return true;
+ }
+ };
+ }
+ protected interface Initializer {
+ void initialize(ActivityLogic value);
+ }
+
+ protected void setLogic(ActivityLogic logic) {
+ mLogic = logic;
+ }
+
+ protected void addInitializer(Runnable initializer) {
+ mInit.add(initializer);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (isFinishing()) {
+ // Performing a clean exit:
+ // Skip initializing anything.
+ return;
+ }
+ mDevicePolicyResources = new DevicePolicyResources(getApplication().getResources(),
+ requireNonNull(getSystemService(DevicePolicyManager.class)));
+ setLogic(new ResolverActivityLogic(
+ TAG,
+ () -> this,
+ this::onWorkProfileStatusUpdated));
+ addInitializer(this::init);
+ }
+
+ @Override
+ protected final void onPostCreate(@Nullable Bundle savedInstanceState) {
+ super.onPostCreate(savedInstanceState);
+ mInit.forEach(Runnable::run);
+
+ if (savedInstanceState != null) {
+ resetButtonBar();
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ if (viewPager != null) {
+ viewPager.setCurrentItem(savedInstanceState.getInt(LAST_SHOWN_TAB_KEY));
+ }
+ mMultiProfilePagerAdapter.clearInactiveProfileCache();
+ }
+ }
+
+ private void init() {
+ setTheme(mLogic.getThemeResId());
+ mLogic.preInitialization();
+
+ Intent intent = mLogic.getTargetIntent();
+ List<Intent> initialIntents = mLogic.getInitialIntents();
+ TargetDataLoader targetDataLoader = mLogic.getTargetDataLoader();
+
+ // Calling UID did not have valid permissions
+ if (mLogic.getAnnotatedUserHandles() == null) {
+ finish();
+ return;
+ }
+
+ mPm = getPackageManager();
+
+ // The last argument of createResolverListAdapter is whether to do special handling
+ // of the last used choice to highlight it in the list. We need to always
+ // turn this off when running under voice interaction, since it results in
+ // a more complicated UI that the current voice interaction flow is not able
+ // to handle. We also turn it off when multiple tabs are shown to simplify the UX.
+ // We also turn it off when clonedProfile is present on the device, because we might have
+ // different "last chosen" activities in the different profiles, and PackageManager doesn't
+ // provide any more information to help us select between them.
+ boolean filterLastUsed = mLogic.getSupportsAlwaysUseOption() && !isVoiceInteraction()
+ && !shouldShowTabs() && !hasCloneProfile();
+ mMultiProfilePagerAdapter = createMultiProfilePagerAdapter(
+ requireNonNullElse(initialIntents, emptyList()).toArray(new Intent[0]),
+ /* resolutionList = */ null,
+ filterLastUsed,
+ targetDataLoader
+ );
+ if (configureContentView(targetDataLoader)) {
+ return;
+ }
+
+ mPersonalPackageMonitor = createPackageMonitor(
+ mMultiProfilePagerAdapter.getPersonalListAdapter());
+ mPersonalPackageMonitor.register(
+ this,
+ getMainLooper(),
+ requireAnnotatedUserHandles().personalProfileUserHandle,
+ false
+ );
+ if (hasWorkProfile()) {
+ mWorkPackageMonitor = createPackageMonitor(
+ mMultiProfilePagerAdapter.getWorkListAdapter());
+ mWorkPackageMonitor.register(
+ this,
+ getMainLooper(),
+ requireAnnotatedUserHandles().workProfileUserHandle,
+ false
+ );
+ }
+
+ mRegistered = true;
+
+ final ResolverDrawerLayout rdl = findViewById(com.android.internal.R.id.contentPanel);
+ if (rdl != null) {
+ rdl.setOnDismissedListener(new ResolverDrawerLayout.OnDismissedListener() {
+ @Override
+ public void onDismissed() {
+ finish();
+ }
+ });
+
+ boolean hasTouchScreen = getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN);
+
+ if (isVoiceInteraction() || !hasTouchScreen) {
+ rdl.setCollapsed(false);
+ }
+
+ rdl.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ rdl.setOnApplyWindowInsetsListener(this::onApplyWindowInsets);
+
+ mResolverDrawerLayout = rdl;
+ }
+
+ mProfileView = findViewById(com.android.internal.R.id.profile_button);
+ if (mProfileView != null) {
+ mProfileView.setOnClickListener(this::onProfileClick);
+ updateProfileViewButton();
+ }
+
+ final Set<String> categories = intent.getCategories();
+ MetricsLogger.action(this, mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()
+ ? MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_APP_FEATURED
+ : MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_NONE_FEATURED,
+ intent.getAction() + ":" + intent.getType() + ":"
+ + (categories != null ? Arrays.toString(categories.toArray()) : ""));
+ }
+
+ protected MultiProfilePagerAdapter createMultiProfilePagerAdapter(
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ MultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null;
+ if (shouldShowTabs()) {
+ resolverMultiProfilePagerAdapter =
+ createResolverMultiProfilePagerAdapterForTwoProfiles(
+ initialIntents, resolutionList, filterLastUsed, targetDataLoader);
+ } else {
+ resolverMultiProfilePagerAdapter = createResolverMultiProfilePagerAdapterForOneProfile(
+ initialIntents, resolutionList, filterLastUsed, targetDataLoader);
+ }
+ return resolverMultiProfilePagerAdapter;
+ }
+
+ protected EmptyStateProvider createBlockerEmptyStateProvider() {
+ final boolean shouldShowNoCrossProfileIntentsEmptyState = getUser().equals(getIntentUser());
+
+ if (!shouldShowNoCrossProfileIntentsEmptyState) {
+ // Implementation that doesn't show any blockers
+ return new EmptyStateProvider() {};
+ }
+
+ final EmptyState noWorkToPersonalEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
+ /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
+ /* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_PERSONAL,
+ /* defaultSubtitleResource= */
+ R.string.resolver_cant_access_personal_apps_explanation,
+ /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL,
+ /* devicePolicyEventCategory= */
+ ResolverActivity.METRICS_CATEGORY_RESOLVER);
+
+ final EmptyState noPersonalToWorkEmptyState =
+ new DevicePolicyBlockerEmptyState(
+ /* context= */ this,
+ /* devicePolicyStringTitleId= */ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ /* defaultTitleResource= */ R.string.resolver_cross_profile_blocked,
+ /* devicePolicyStringSubtitleId= */ RESOLVER_CANT_ACCESS_WORK,
+ /* defaultSubtitleResource= */
+ R.string.resolver_cant_access_work_apps_explanation,
+ /* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK,
+ /* devicePolicyEventCategory= */
+ ResolverActivity.METRICS_CATEGORY_RESOLVER);
+
+ return new NoCrossProfileEmptyStateProvider(
+ requireAnnotatedUserHandles().personalProfileUserHandle,
+ noWorkToPersonalEmptyState,
+ noPersonalToWorkEmptyState,
+ createCrossProfileIntentsChecker(),
+ requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch);
+ }
+
+ /**
+ * Numerous layouts are supported, each with optional ViewGroups.
+ * Make sure the inset gets added to the correct View, using
+ * a footer for Lists so it can properly scroll under the navbar.
+ */
+ protected boolean shouldAddFooterView() {
+ if (useLayoutWithDefault()) return true;
+
+ View buttonBar = findViewById(com.android.internal.R.id.button_bar);
+ if (buttonBar == null || buttonBar.getVisibility() == View.GONE) return true;
+
+ return false;
+ }
+
+ protected void applyFooterView(int height) {
+ if (mFooterSpacer == null) {
+ mFooterSpacer = new Space(getApplicationContext());
+ } else {
+ ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
+ .getActiveAdapterView().removeFooterView(mFooterSpacer);
+ }
+ mFooterSpacer.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT,
+ mSystemWindowInsets.bottom));
+ ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
+ .getActiveAdapterView().addFooterView(mFooterSpacer);
+ }
+
+ protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
+ mSystemWindowInsets = insets.getSystemWindowInsets();
+
+ mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top,
+ mSystemWindowInsets.right, 0);
+
+ resetButtonBar();
+
+ if (shouldUseMiniResolver()) {
+ View buttonContainer = findViewById(com.android.internal.R.id.button_bar_container);
+ buttonContainer.setPadding(0, 0, 0, mSystemWindowInsets.bottom
+ + getResources().getDimensionPixelOffset(R.dimen.resolver_button_bar_spacing));
+ }
+
+ // Need extra padding so the list can fully scroll up
+ if (shouldAddFooterView()) {
+ applyFooterView(mSystemWindowInsets.bottom);
+ }
+
+ return insets.consumeSystemWindowInsets();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
+ if (mIsIntentPicker && shouldShowTabs() && !useLayoutWithDefault()
+ && !shouldUseMiniResolver()) {
+ updateIntentPickerPaddings();
+ }
+
+ if (mSystemWindowInsets != null) {
+ mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top,
+ mSystemWindowInsets.right, 0);
+ }
+ }
+
+ public int getLayoutResource() {
+ return R.layout.resolver_list;
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ final Window window = this.getWindow();
+ final WindowManager.LayoutParams attrs = window.getAttributes();
+ attrs.privateFlags &= ~SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+ window.setAttributes(attrs);
+
+ if (mRegistered) {
+ mPersonalPackageMonitor.unregister();
+ if (mWorkPackageMonitor != null) {
+ mWorkPackageMonitor.unregister();
+ }
+ mRegistered = false;
+ }
+ final Intent intent = getIntent();
+ if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction()
+ && !mLogic.getResolvingHome() && !mRetainInOnStop) {
+ // This resolver is in the unusual situation where it has been
+ // launched at the top of a new task. We don't let it be added
+ // to the recent tasks shown to the user, and we need to make sure
+ // that each time we are launched we get the correct launching
+ // uid (not re-using the same resolver from an old launching uid),
+ // so we will now finish ourself since being no longer visible,
+ // the user probably can't get back to us.
+ if (!isChangingConfigurations()) {
+ finish();
+ }
+ }
+ // TODO: should we clean up the work-profile manager before we potentially finish() above?
+ mLogic.getWorkProfileAvailabilityManager().unregisterWorkProfileStateReceiver(this);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (!isChangingConfigurations() && mPickOptionRequest != null) {
+ mPickOptionRequest.cancel();
+ }
+ if (mMultiProfilePagerAdapter != null
+ && mMultiProfilePagerAdapter.getActiveListAdapter() != null) {
+ mMultiProfilePagerAdapter.getActiveListAdapter().onDestroy();
+ }
+ }
+
+ public void onButtonClick(View v) {
+ final int id = v.getId();
+ ListView listView = (ListView) mMultiProfilePagerAdapter.getActiveAdapterView();
+ ResolverListAdapter currentListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();
+ int which = currentListAdapter.hasFilteredItem()
+ ? currentListAdapter.getFilteredPosition()
+ : listView.getCheckedItemPosition();
+ boolean hasIndexBeenFiltered = !currentListAdapter.hasFilteredItem();
+ startSelected(which, id == com.android.internal.R.id.button_always, hasIndexBeenFiltered);
+ }
+
+ public void startSelected(int which, boolean always, boolean hasIndexBeenFiltered) {
+ if (isFinishing()) {
+ return;
+ }
+ ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .resolveInfoForPosition(which, hasIndexBeenFiltered);
+ if (mLogic.getResolvingHome() && hasManagedProfile() && !supportsManagedProfiles(ri)) {
+ String launcherName = ri.activityInfo.loadLabel(getPackageManager()).toString();
+ Toast.makeText(this,
+ mDevicePolicyResources.getWorkProfileNotSupportedMessage(launcherName),
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .targetInfoForPosition(which, hasIndexBeenFiltered);
+ if (target == null) {
+ return;
+ }
+ if (onTargetSelected(target, always)) {
+ if (always && mLogic.getSupportsAlwaysUseOption()) {
+ MetricsLogger.action(
+ this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_ALWAYS);
+ } else if (mLogic.getSupportsAlwaysUseOption()) {
+ MetricsLogger.action(
+ this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_JUST_ONCE);
+ } else {
+ MetricsLogger.action(
+ this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_TAP);
+ }
+ MetricsLogger.action(this,
+ mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()
+ ? MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_APP_FEATURED
+ : MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_NONE_FEATURED);
+ finish();
+ }
+ }
+
+ /**
+ * Replace me in subclasses!
+ */
+ @Override // ResolverListCommunicator
+ public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
+ return defIntent;
+ }
+
+ protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildCompleted) {
+ final ItemClickListener listener = new ItemClickListener();
+ setupAdapterListView((ListView) mMultiProfilePagerAdapter.getActiveAdapterView(), listener);
+ if (shouldShowTabs() && mIsIntentPicker) {
+ final ResolverDrawerLayout rdl = findViewById(com.android.internal.R.id.contentPanel);
+ if (rdl != null) {
+ rdl.setMaxCollapsedHeight(getResources()
+ .getDimensionPixelSize(useLayoutWithDefault()
+ ? R.dimen.resolver_max_collapsed_height_with_default_with_tabs
+ : R.dimen.resolver_max_collapsed_height_with_tabs));
+ }
+ }
+ }
+
+ protected boolean onTargetSelected(TargetInfo target, boolean always) {
+ final ResolveInfo ri = target.getResolveInfo();
+ final Intent intent = target != null ? target.getResolvedIntent() : null;
+
+ if (intent != null && (mLogic.getSupportsAlwaysUseOption()
+ || mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem())
+ && mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredResolveList() != null) {
+ // Build a reasonable intent filter, based on what matched.
+ IntentFilter filter = new IntentFilter();
+ Intent filterIntent;
+
+ if (intent.getSelector() != null) {
+ filterIntent = intent.getSelector();
+ } else {
+ filterIntent = intent;
+ }
+
+ String action = filterIntent.getAction();
+ if (action != null) {
+ filter.addAction(action);
+ }
+ Set<String> categories = filterIntent.getCategories();
+ if (categories != null) {
+ for (String cat : categories) {
+ filter.addCategory(cat);
+ }
+ }
+ filter.addCategory(Intent.CATEGORY_DEFAULT);
+
+ int cat = ri.match & IntentFilter.MATCH_CATEGORY_MASK;
+ Uri data = filterIntent.getData();
+ if (cat == IntentFilter.MATCH_CATEGORY_TYPE) {
+ String mimeType = filterIntent.resolveType(this);
+ if (mimeType != null) {
+ try {
+ filter.addDataType(mimeType);
+ } catch (IntentFilter.MalformedMimeTypeException e) {
+ Log.w("ResolverActivity", e);
+ filter = null;
+ }
+ }
+ }
+ if (data != null && data.getScheme() != null) {
+ // We need the data specification if there was no type,
+ // OR if the scheme is not one of our magical "file:"
+ // or "content:" schemes (see IntentFilter for the reason).
+ if (cat != IntentFilter.MATCH_CATEGORY_TYPE
+ || (!"file".equals(data.getScheme())
+ && !"content".equals(data.getScheme()))) {
+ filter.addDataScheme(data.getScheme());
+
+ // Look through the resolved filter to determine which part
+ // of it matched the original Intent.
+ Iterator<PatternMatcher> pIt = ri.filter.schemeSpecificPartsIterator();
+ if (pIt != null) {
+ String ssp = data.getSchemeSpecificPart();
+ while (ssp != null && pIt.hasNext()) {
+ PatternMatcher p = pIt.next();
+ if (p.match(ssp)) {
+ filter.addDataSchemeSpecificPart(p.getPath(), p.getType());
+ break;
+ }
+ }
+ }
+ Iterator<IntentFilter.AuthorityEntry> aIt = ri.filter.authoritiesIterator();
+ if (aIt != null) {
+ while (aIt.hasNext()) {
+ IntentFilter.AuthorityEntry a = aIt.next();
+ if (a.match(data) >= 0) {
+ int port = a.getPort();
+ filter.addDataAuthority(a.getHost(),
+ port >= 0 ? Integer.toString(port) : null);
+ break;
+ }
+ }
+ }
+ pIt = ri.filter.pathsIterator();
+ if (pIt != null) {
+ String path = data.getPath();
+ while (path != null && pIt.hasNext()) {
+ PatternMatcher p = pIt.next();
+ if (p.match(path)) {
+ filter.addDataPath(p.getPath(), p.getType());
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (filter != null) {
+ final int N = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getUnfilteredResolveList().size();
+ ComponentName[] set;
+ // If we don't add back in the component for forwarding the intent to a managed
+ // profile, the preferred activity may not be updated correctly (as the set of
+ // components we tell it we knew about will have changed).
+ final boolean needToAddBackProfileForwardingComponent =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null;
+ if (!needToAddBackProfileForwardingComponent) {
+ set = new ComponentName[N];
+ } else {
+ set = new ComponentName[N + 1];
+ }
+
+ int bestMatch = 0;
+ for (int i=0; i<N; i++) {
+ ResolveInfo r = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getUnfilteredResolveList().get(i).getResolveInfoAt(0);
+ set[i] = new ComponentName(r.activityInfo.packageName,
+ r.activityInfo.name);
+ if (r.match > bestMatch) bestMatch = r.match;
+ }
+
+ if (needToAddBackProfileForwardingComponent) {
+ set[N] = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getOtherProfile().getResolvedComponentName();
+ final int otherProfileMatch = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getOtherProfile().getResolveInfo().match;
+ if (otherProfileMatch > bestMatch) bestMatch = otherProfileMatch;
+ }
+
+ if (always) {
+ final int userId = getUserId();
+ final PackageManager pm = getPackageManager();
+
+ // Set the preferred Activity
+ pm.addUniquePreferredActivity(filter, bestMatch, set, intent.getComponent());
+
+ if (ri.handleAllWebDataURI) {
+ // Set default Browser if needed
+ final String packageName = pm.getDefaultBrowserPackageNameAsUser(userId);
+ if (TextUtils.isEmpty(packageName)) {
+ pm.setDefaultBrowserPackageNameAsUser(ri.activityInfo.packageName, userId);
+ }
+ }
+ } else {
+ try {
+ mMultiProfilePagerAdapter.getActiveListAdapter()
+ .mResolverListController.setLastChosen(intent, filter, bestMatch);
+ } catch (RemoteException re) {
+ Log.d(TAG, "Error calling setLastChosenActivity\n" + re);
+ }
+ }
+ }
+ }
+
+ if (target != null) {
+ safelyStartActivity(target);
+
+ // Rely on the ActivityManager to pop up a dialog regarding app suspension
+ // and return false
+ if (target.isSuspended()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public void onActivityStarted(TargetInfo cti) {
+ // Do nothing
+ }
+
+ @Override // ResolverListCommunicator
+ public boolean shouldGetActivityMetadata() {
+ return false;
+ }
+
+ public boolean shouldAutoLaunchSingleChoice(TargetInfo target) {
+ return !target.isSuspended();
+ }
+
+ // TODO: this method takes an unused `UserHandle` because the override in `ChooserActivity` uses
+ // that data to set up other components as dependencies of the controller. In reality, these
+ // methods don't require polymorphism, because they're only invoked from within their respective
+ // concrete class; `ResolverActivity` will never call this method expecting to get a
+ // `ChooserListController` (subclass) result, because `ResolverActivity` only invokes this
+ // method as part of handling `createMultiProfilePagerAdapter()`, which is itself overridden in
+ // `ChooserActivity`. A future refactoring could better express the coupling between the adapter
+ // and controller types; in the meantime, structuring as an override (with matching signatures)
+ // shows that these methods are *structurally* related, and helps to prevent any regressions in
+ // the future if resolver *were* to make any (non-overridden) calls to a version that used a
+ // different signature (and thus didn't return the subclass type).
+ @VisibleForTesting
+ protected ResolverListController createListController(UserHandle userHandle) {
+ ResolverRankerServiceResolverComparator resolverComparator =
+ new ResolverRankerServiceResolverComparator(
+ this,
+ mLogic.getTargetIntent(),
+ mLogic.getReferrerPackageName(),
+ null,
+ null,
+ getResolverRankerServiceUserHandleList(userHandle),
+ null);
+ return new ResolverListController(
+ this,
+ mPm,
+ mLogic.getTargetIntent(),
+ mLogic.getReferrerPackageName(),
+ requireAnnotatedUserHandles().userIdOfCallingApp,
+ resolverComparator,
+ getQueryIntentsUser(userHandle));
+ }
+
+ /**
+ * Finishing procedures to be performed after the list has been rebuilt.
+ * </p>Subclasses must call postRebuildListInternal at the end of postRebuildList.
+ * @param rebuildCompleted
+ * @return <code>true</code> if the activity is finishing and creation should halt.
+ */
+ protected boolean postRebuildList(boolean rebuildCompleted) {
+ return postRebuildListInternal(rebuildCompleted);
+ }
+
+ void onHorizontalSwipeStateChanged(int state) {}
+
+ /**
+ * Callback called when user changes the profile tab.
+ * <p>This method is intended to be overridden by subclasses.
+ */
+ protected void onProfileTabSelected() { }
+
+ /**
+ * Add a label to signify that the user can pick a different app.
+ * @param adapter The adapter used to provide data to item views.
+ */
+ public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) {
+ final boolean useHeader = adapter.hasFilteredItem();
+ if (useHeader) {
+ FrameLayout stub = findViewById(com.android.internal.R.id.stub);
+ stub.setVisibility(View.VISIBLE);
+ TextView textView = (TextView) LayoutInflater.from(this).inflate(
+ R.layout.resolver_different_item_header, null, false);
+ if (shouldShowTabs()) {
+ textView.setGravity(Gravity.CENTER);
+ }
+ stub.addView(textView);
+ }
+ }
+
+ protected void resetButtonBar() {
+ if (!mLogic.getSupportsAlwaysUseOption()) {
+ return;
+ }
+ final ViewGroup buttonLayout = findViewById(com.android.internal.R.id.button_bar);
+ if (buttonLayout == null) {
+ Log.e(TAG, "Layout unexpectedly does not have a button bar");
+ return;
+ }
+ ResolverListAdapter activeListAdapter =
+ mMultiProfilePagerAdapter.getActiveListAdapter();
+ View buttonBarDivider = findViewById(com.android.internal.R.id.resolver_button_bar_divider);
+ if (!useLayoutWithDefault()) {
+ int inset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0;
+ buttonLayout.setPadding(buttonLayout.getPaddingLeft(), buttonLayout.getPaddingTop(),
+ buttonLayout.getPaddingRight(), getResources().getDimensionPixelSize(
+ R.dimen.resolver_button_bar_spacing) + inset);
+ }
+ if (activeListAdapter.isTabLoaded()
+ && mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)
+ && !useLayoutWithDefault()) {
+ buttonLayout.setVisibility(View.INVISIBLE);
+ if (buttonBarDivider != null) {
+ buttonBarDivider.setVisibility(View.INVISIBLE);
+ }
+ setButtonBarIgnoreOffset(/* ignoreOffset */ false);
+ return;
+ }
+ if (buttonBarDivider != null) {
+ buttonBarDivider.setVisibility(View.VISIBLE);
+ }
+ buttonLayout.setVisibility(View.VISIBLE);
+ setButtonBarIgnoreOffset(/* ignoreOffset */ true);
+
+ mOnceButton = (Button) buttonLayout.findViewById(com.android.internal.R.id.button_once);
+ mAlwaysButton = (Button) buttonLayout.findViewById(com.android.internal.R.id.button_always);
+
+ resetAlwaysOrOnceButtonBar();
+ }
+
+ protected String getMetricsCategory() {
+ return METRICS_CATEGORY_RESOLVER;
+ }
+
+ @Override // ResolverListCommunicator
+ public final void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
+ if (!mMultiProfilePagerAdapter.onHandlePackagesChanged(
+ listAdapter,
+ mLogic.getWorkProfileAvailabilityManager().isWaitingToEnableWorkProfile())) {
+ // We no longer have any items... just finish the activity.
+ finish();
+ }
+ }
+
+ protected void maybeLogProfileChange() {}
+
+ // @NonFinalForTesting
+ @VisibleForTesting
+ protected MyUserIdProvider createMyUserIdProvider() {
+ return new MyUserIdProvider();
+ }
+
+ // @NonFinalForTesting
+ @VisibleForTesting
+ protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() {
+ return new CrossProfileIntentsChecker(getContentResolver());
+ }
+
+ protected Unit onWorkProfileStatusUpdated() {
+ if (mMultiProfilePagerAdapter.getCurrentUserHandle().equals(
+ requireAnnotatedUserHandles().workProfileUserHandle)) {
+ mMultiProfilePagerAdapter.rebuildActiveTab(true);
+ } else {
+ mMultiProfilePagerAdapter.clearInactiveProfileCache();
+ }
+ return Unit.INSTANCE;
+ }
+
+ // @NonFinalForTesting
+ @VisibleForTesting
+ protected ResolverListAdapter createResolverListAdapter(
+ Context context,
+ List<Intent> payloadIntents,
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean filterLastUsed,
+ UserHandle userHandle,
+ TargetDataLoader targetDataLoader) {
+ UserHandle initialIntentsUserSpace = isLaunchedAsCloneProfile()
+ && userHandle.equals(requireAnnotatedUserHandles().personalProfileUserHandle)
+ ? requireAnnotatedUserHandles().cloneProfileUserHandle : userHandle;
+ return new ResolverListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ resolutionList,
+ filterLastUsed,
+ createListController(userHandle),
+ userHandle,
+ mLogic.getTargetIntent(),
+ this,
+ initialIntentsUserSpace,
+ targetDataLoader);
+ }
+
+ private LatencyTracker getLatencyTracker() {
+ return LatencyTracker.getInstance(this);
+ }
+
+ /**
+ * Get the string resource to be used as a label for the link to the resolver activity for an
+ * action.
+ *
+ * @param action The action to resolve
+ *
+ * @return The string resource to be used as a label
+ */
+ public static @StringRes int getLabelRes(String action) {
+ return ActionTitle.forAction(action).labelRes;
+ }
+
+ protected final EmptyStateProvider createEmptyStateProvider(
+ @Nullable UserHandle workProfileUserHandle) {
+ final EmptyStateProvider blockerEmptyStateProvider = createBlockerEmptyStateProvider();
+
+ final EmptyStateProvider workProfileOffEmptyStateProvider =
+ new WorkProfilePausedEmptyStateProvider(this, workProfileUserHandle,
+ mLogic.getWorkProfileAvailabilityManager(),
+ /* onSwitchOnWorkSelectedListener= */
+ () -> {
+ if (mOnSwitchOnWorkSelectedListener != null) {
+ mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected();
+ }
+ },
+ getMetricsCategory());
+
+ final EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider(
+ this,
+ workProfileUserHandle,
+ requireAnnotatedUserHandles().personalProfileUserHandle,
+ getMetricsCategory(),
+ requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch
+ );
+
+ // Return composite provider, the order matters (the higher, the more priority)
+ return new CompositeEmptyStateProvider(
+ blockerEmptyStateProvider,
+ workProfileOffEmptyStateProvider,
+ noAppsEmptyStateProvider
+ );
+ }
+
+ private ResolverMultiProfilePagerAdapter
+ createResolverMultiProfilePagerAdapterForOneProfile(
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ ResolverListAdapter adapter = createResolverListAdapter(
+ /* context */ this,
+ mLogic.getPayloadIntents(),
+ initialIntents,
+ resolutionList,
+ filterLastUsed,
+ /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle,
+ targetDataLoader);
+ return new ResolverMultiProfilePagerAdapter(
+ /* context */ this,
+ adapter,
+ createEmptyStateProvider(/* workProfileUserHandle= */ null),
+ /* workProfileQuietModeChecker= */ () -> false,
+ /* workProfileUserHandle= */ null,
+ requireAnnotatedUserHandles().cloneProfileUserHandle);
+ }
+
+ private UserHandle getIntentUser() {
+ return getIntent().hasExtra(EXTRA_CALLING_USER)
+ ? getIntent().getParcelableExtra(EXTRA_CALLING_USER)
+ : requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch;
+ }
+
+ private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles(
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean filterLastUsed,
+ TargetDataLoader targetDataLoader) {
+ // In the edge case when we have 0 apps in the current profile and >1 apps in the other,
+ // the intent resolver is started in the other profile. Since this is the only case when
+ // this happens, we check for it here and set the current profile's tab.
+ int selectedProfile = getCurrentProfile();
+ UserHandle intentUser = getIntentUser();
+ if (!requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch.equals(intentUser)) {
+ if (requireAnnotatedUserHandles().personalProfileUserHandle.equals(intentUser)) {
+ selectedProfile = PROFILE_PERSONAL;
+ } else if (requireAnnotatedUserHandles().workProfileUserHandle.equals(intentUser)) {
+ selectedProfile = PROFILE_WORK;
+ }
+ } else {
+ int selectedProfileExtra = getSelectedProfileExtra();
+ if (selectedProfileExtra != -1) {
+ selectedProfile = selectedProfileExtra;
+ }
+ }
+ // We only show the default app for the profile of the current user. The filterLastUsed
+ // flag determines whether to show a default app and that app is not shown in the
+ // resolver list. So filterLastUsed should be false for the other profile.
+ ResolverListAdapter personalAdapter = createResolverListAdapter(
+ /* context */ this,
+ mLogic.getPayloadIntents(),
+ selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
+ resolutionList,
+ (filterLastUsed && UserHandle.myUserId()
+ == requireAnnotatedUserHandles().personalProfileUserHandle.getIdentifier()),
+ /* userHandle */ requireAnnotatedUserHandles().personalProfileUserHandle,
+ targetDataLoader);
+ UserHandle workProfileUserHandle = requireAnnotatedUserHandles().workProfileUserHandle;
+ ResolverListAdapter workAdapter = createResolverListAdapter(
+ /* context */ this,
+ mLogic.getPayloadIntents(),
+ selectedProfile == PROFILE_WORK ? initialIntents : null,
+ resolutionList,
+ (filterLastUsed && UserHandle.myUserId()
+ == workProfileUserHandle.getIdentifier()),
+ /* userHandle */ workProfileUserHandle,
+ targetDataLoader);
+ return new ResolverMultiProfilePagerAdapter(
+ /* context */ this,
+ personalAdapter,
+ workAdapter,
+ createEmptyStateProvider(workProfileUserHandle),
+ () -> mLogic.getWorkProfileAvailabilityManager().isQuietModeEnabled(),
+ selectedProfile,
+ workProfileUserHandle,
+ requireAnnotatedUserHandles().cloneProfileUserHandle);
+ }
+
+ /**
+ * Returns {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK} if the {@link
+ * #EXTRA_SELECTED_PROFILE} extra was supplied, or {@code -1} if no extra was supplied.
+ * @throws IllegalArgumentException if the value passed to the {@link #EXTRA_SELECTED_PROFILE}
+ * extra is not {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}
+ */
+ final int getSelectedProfileExtra() {
+ int selectedProfile = -1;
+ if (getIntent().hasExtra(EXTRA_SELECTED_PROFILE)) {
+ selectedProfile = getIntent().getIntExtra(EXTRA_SELECTED_PROFILE, /* defValue = */ -1);
+ if (selectedProfile != PROFILE_PERSONAL && selectedProfile != PROFILE_WORK) {
+ throw new IllegalArgumentException(EXTRA_SELECTED_PROFILE + " has invalid value "
+ + selectedProfile + ". Must be either ResolverActivity.PROFILE_PERSONAL or "
+ + "ResolverActivity.PROFILE_WORK.");
+ }
+ }
+ return selectedProfile;
+ }
+
+ protected final @Profile int getCurrentProfile() {
+ UserHandle launchUser = requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch;
+ UserHandle personalUser = requireAnnotatedUserHandles().personalProfileUserHandle;
+ return launchUser.equals(personalUser) ? PROFILE_PERSONAL : PROFILE_WORK;
+ }
+
+ private AnnotatedUserHandles requireAnnotatedUserHandles() {
+ return requireNonNull(mLogic.getAnnotatedUserHandles());
+ }
+
+ private boolean hasWorkProfile() {
+ return requireAnnotatedUserHandles().workProfileUserHandle != null;
+ }
+
+ private boolean hasCloneProfile() {
+ return requireAnnotatedUserHandles().cloneProfileUserHandle != null;
+ }
+
+ protected final boolean isLaunchedAsCloneProfile() {
+ UserHandle launchUser = requireAnnotatedUserHandles().userHandleSharesheetLaunchedAs;
+ UserHandle cloneUser = requireAnnotatedUserHandles().cloneProfileUserHandle;
+ return hasCloneProfile() && launchUser.equals(cloneUser);
+ }
+
+ protected final boolean shouldShowTabs() {
+ return hasWorkProfile();
+ }
+
+ protected final void onProfileClick(View v) {
+ final DisplayResolveInfo dri =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile();
+ if (dri == null) {
+ return;
+ }
+
+ // Do not show the profile switch message anymore.
+ mLogic.clearProfileSwitchMessage();
+
+ onTargetSelected(dri, false);
+ finish();
+ }
+
+ private void updateIntentPickerPaddings() {
+ View titleCont = findViewById(com.android.internal.R.id.title_container);
+ titleCont.setPadding(
+ titleCont.getPaddingLeft(),
+ titleCont.getPaddingTop(),
+ titleCont.getPaddingRight(),
+ getResources().getDimensionPixelSize(R.dimen.resolver_title_padding_bottom));
+ View buttonBar = findViewById(com.android.internal.R.id.button_bar);
+ buttonBar.setPadding(
+ buttonBar.getPaddingLeft(),
+ getResources().getDimensionPixelSize(R.dimen.resolver_button_bar_spacing),
+ buttonBar.getPaddingRight(),
+ getResources().getDimensionPixelSize(R.dimen.resolver_button_bar_spacing));
+ }
+
+ private void maybeLogCrossProfileTargetLaunch(TargetInfo cti, UserHandle currentUserHandle) {
+ if (!hasWorkProfile() || currentUserHandle.equals(getUser())) {
+ return;
+ }
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_CROSS_PROFILE_TARGET_OPENED)
+ .setBoolean(
+ currentUserHandle.equals(
+ requireAnnotatedUserHandles().personalProfileUserHandle))
+ .setStrings(getMetricsCategory(),
+ cti.isInDirectShareMetricsCategory() ? "direct_share" : "other_target")
+ .write();
+ }
+
+ @Override // ResolverListCommunicator
+ public final void sendVoiceChoicesIfNeeded() {
+ if (!isVoiceInteraction()) {
+ // Clearly not needed.
+ return;
+ }
+
+ int count = mMultiProfilePagerAdapter.getActiveListAdapter().getCount();
+ final Option[] options = new Option[count];
+ for (int i = 0; i < options.length; i++) {
+ TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter().getItem(i);
+ if (target == null) {
+ // If this occurs, a new set of targets is being loaded. Let that complete,
+ // and have the next call to send voice choices proceed instead.
+ return;
+ }
+ options[i] = optionForChooserTarget(target, i);
+ }
+
+ mPickOptionRequest = new PickTargetOptionRequest(
+ new Prompt(getTitle()), options, null);
+ getVoiceInteractor().submitRequest(mPickOptionRequest);
+ }
+
+ final Option optionForChooserTarget(TargetInfo target, int index) {
+ return new Option(getOrLoadDisplayLabel(target), index);
+ }
+
+ @Override // ResolverListCommunicator
+ public final void updateProfileViewButton() {
+ if (mProfileView == null) {
+ return;
+ }
+
+ final DisplayResolveInfo dri =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile();
+ if (dri != null && !shouldShowTabs()) {
+ mProfileView.setVisibility(View.VISIBLE);
+ View text = mProfileView.findViewById(com.android.internal.R.id.profile_button);
+ if (!(text instanceof TextView)) {
+ text = mProfileView.findViewById(com.android.internal.R.id.text1);
+ }
+ ((TextView) text).setText(dri.getDisplayLabel());
+ } else {
+ mProfileView.setVisibility(View.GONE);
+ }
+ }
+
+ protected final CharSequence getTitleForAction(Intent intent, int defaultTitleRes) {
+ final ActionTitle title = mLogic.getResolvingHome()
+ ? ActionTitle.HOME
+ : ActionTitle.forAction(intent.getAction());
+
+ // While there may already be a filtered item, we can only use it in the title if the list
+ // is already sorted and all information relevant to it is already in the list.
+ final boolean named =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getFilteredPosition() >= 0;
+ if (title == ActionTitle.DEFAULT && defaultTitleRes != 0) {
+ return getString(defaultTitleRes);
+ } else {
+ return named
+ ? getString(
+ title.namedTitleRes,
+ getOrLoadDisplayLabel(
+ mMultiProfilePagerAdapter
+ .getActiveListAdapter().getFilteredItem()))
+ : getString(title.titleRes);
+ }
+ }
+
+ final void dismiss() {
+ if (!isFinishing()) {
+ finish();
+ }
+ }
+
+ @Override
+ protected final void onRestart() {
+ super.onRestart();
+ if (!mRegistered) {
+ mPersonalPackageMonitor.register(
+ this,
+ getMainLooper(),
+ requireAnnotatedUserHandles().personalProfileUserHandle,
+ false);
+ if (hasWorkProfile()) {
+ if (mWorkPackageMonitor == null) {
+ mWorkPackageMonitor = createPackageMonitor(
+ mMultiProfilePagerAdapter.getWorkListAdapter());
+ }
+ mWorkPackageMonitor.register(
+ this,
+ getMainLooper(),
+ requireAnnotatedUserHandles().workProfileUserHandle,
+ false);
+ }
+ mRegistered = true;
+ }
+ WorkProfileAvailabilityManager workProfileAvailabilityManager =
+ mLogic.getWorkProfileAvailabilityManager();
+ if (hasWorkProfile() && workProfileAvailabilityManager.isWaitingToEnableWorkProfile()) {
+ if (workProfileAvailabilityManager.isQuietModeEnabled()) {
+ workProfileAvailabilityManager.markWorkProfileEnabledBroadcastReceived();
+ }
+ }
+ mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
+ updateProfileViewButton();
+ }
+
+ @Override
+ protected final void onStart() {
+ super.onStart();
+
+ this.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
+ if (hasWorkProfile()) {
+ mLogic.getWorkProfileAvailabilityManager().registerWorkProfileStateReceiver(this);
+ }
+ }
+
+ @Override
+ protected final void onSaveInstanceState(@NonNull Bundle outState) {
+ super.onSaveInstanceState(outState);
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ if (viewPager != null) {
+ outState.putInt(LAST_SHOWN_TAB_KEY, viewPager.getCurrentItem());
+ }
+ }
+
+ private boolean hasManagedProfile() {
+ UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
+ if (userManager == null) {
+ return false;
+ }
+
+ try {
+ List<UserInfo> profiles = userManager.getProfiles(getUserId());
+ for (UserInfo userInfo : profiles) {
+ if (userInfo != null && userInfo.isManagedProfile()) {
+ return true;
+ }
+ }
+ } catch (SecurityException e) {
+ return false;
+ }
+ return false;
+ }
+
+ private boolean supportsManagedProfiles(ResolveInfo resolveInfo) {
+ try {
+ ApplicationInfo appInfo = getPackageManager().getApplicationInfo(
+ resolveInfo.activityInfo.packageName, 0 /* default flags */);
+ return appInfo.targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP;
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ private void setAlwaysButtonEnabled(boolean hasValidSelection, int checkedPos,
+ boolean filtered) {
+ if (!mMultiProfilePagerAdapter.getCurrentUserHandle().equals(getUser())) {
+ // Never allow the inactive profile to always open an app.
+ mAlwaysButton.setEnabled(false);
+ return;
+ }
+ // In case of clonedProfile being active, we do not allow the 'Always' option in the
+ // disambiguation dialog of Personal Profile as the package manager cannot distinguish
+ // between cross-profile preferred activities.
+ if (hasCloneProfile() && (mMultiProfilePagerAdapter.getCurrentPage() == PROFILE_PERSONAL)) {
+ mAlwaysButton.setEnabled(false);
+ return;
+ }
+ boolean enabled = false;
+ ResolveInfo ri = null;
+ if (hasValidSelection) {
+ ri = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .resolveInfoForPosition(checkedPos, filtered);
+ if (ri == null) {
+ Log.e(TAG, "Invalid position supplied to setAlwaysButtonEnabled");
+ return;
+ } else if (ri.targetUserId != UserHandle.USER_CURRENT) {
+ Log.e(TAG, "Attempted to set selection to resolve info for another user");
+ return;
+ } else {
+ enabled = true;
+ }
+
+ mAlwaysButton.setText(getResources()
+ .getString(R.string.activity_resolver_use_always));
+ }
+
+ if (ri != null) {
+ ActivityInfo activityInfo = ri.activityInfo;
+
+ boolean hasRecordPermission =
+ mPm.checkPermission(android.Manifest.permission.RECORD_AUDIO,
+ activityInfo.packageName)
+ == PackageManager.PERMISSION_GRANTED;
+
+ if (!hasRecordPermission) {
+ // OK, we know the record permission, is this a capture device
+ boolean hasAudioCapture =
+ getIntent().getBooleanExtra(
+ ResolverActivity.EXTRA_IS_AUDIO_CAPTURE_DEVICE, false);
+ enabled = !hasAudioCapture;
+ }
+ }
+ mAlwaysButton.setEnabled(enabled);
+ }
+
+ @Override // ResolverListCommunicator
+ public final void onPostListReady(ResolverListAdapter listAdapter, boolean doPostProcessing,
+ boolean rebuildCompleted) {
+ if (isAutolaunching()) {
+ return;
+ }
+ if (mIsIntentPicker) {
+ ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
+ .setUseLayoutWithDefault(useLayoutWithDefault());
+ }
+ if (mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(listAdapter)) {
+ mMultiProfilePagerAdapter.showEmptyResolverListEmptyState(listAdapter);
+ } else {
+ mMultiProfilePagerAdapter.showListView(listAdapter);
+ }
+ // showEmptyResolverListEmptyState can mark the tab as loaded,
+ // which is a precondition for auto launching
+ if (rebuildCompleted && maybeAutolaunchActivity()) {
+ return;
+ }
+ if (doPostProcessing) {
+ maybeCreateHeader(listAdapter);
+ resetButtonBar();
+ onListRebuilt(listAdapter, rebuildCompleted);
+ }
+ }
+
+ /** Start the activity specified by the {@link TargetInfo}.*/
+ public final void safelyStartActivity(TargetInfo cti) {
+ // In case cloned apps are present, we would want to start those apps in cloned user
+ // space, which will not be same as the adapter's userHandle. resolveInfo.userHandle
+ // identifies the correct user space in such cases.
+ UserHandle activityUserHandle = cti.getResolveInfo().userHandle;
+ safelyStartActivityAsUser(cti, activityUserHandle, null);
+ }
+
+ /**
+ * Start activity as a fixed user handle.
+ * @param cti TargetInfo to be launched.
+ * @param user User to launch this activity as.
+ */
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
+ public final void safelyStartActivityAsUser(TargetInfo cti, UserHandle user) {
+ safelyStartActivityAsUser(cti, user, null);
+ }
+
+ protected final void safelyStartActivityAsUser(
+ TargetInfo cti, UserHandle user, @Nullable Bundle options) {
+ // We're dispatching intents that might be coming from legacy apps, so
+ // don't kill ourselves.
+ StrictMode.disableDeathOnFileUriExposure();
+ try {
+ safelyStartActivityInternal(cti, user, options);
+ } finally {
+ StrictMode.enableDeathOnFileUriExposure();
+ }
+ }
+
+ @VisibleForTesting
+ protected void safelyStartActivityInternal(
+ TargetInfo cti, UserHandle user, @Nullable Bundle options) {
+ // If the target is suspended, the activity will not be successfully launched.
+ // Do not unregister from package manager updates in this case
+ if (!cti.isSuspended() && mRegistered) {
+ if (mPersonalPackageMonitor != null) {
+ mPersonalPackageMonitor.unregister();
+ }
+ if (mWorkPackageMonitor != null) {
+ mWorkPackageMonitor.unregister();
+ }
+ mRegistered = false;
+ }
+ // If needed, show that intent is forwarded
+ // from managed profile to owner or other way around.
+ String profileSwitchMessage = mLogic.getProfileSwitchMessage();
+ if (profileSwitchMessage != null) {
+ Toast.makeText(this, profileSwitchMessage, Toast.LENGTH_LONG).show();
+ }
+ try {
+ if (cti.startAsCaller(this, options, user.getIdentifier())) {
+ onActivityStarted(cti);
+ maybeLogCrossProfileTargetLaunch(cti, user);
+ }
+ } catch (RuntimeException e) {
+ Slog.wtf(TAG,
+ "Unable to launch as uid " + requireAnnotatedUserHandles().userIdOfCallingApp
+ + " package " + getLaunchedFromPackage() + ", while running in "
+ + ActivityThread.currentProcessName(), e);
+ }
+ }
+
+ final void showTargetDetails(ResolveInfo ri) {
+ Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
+ .setData(Uri.fromParts("package", ri.activityInfo.packageName, null))
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+ startActivityAsUser(in, mMultiProfilePagerAdapter.getCurrentUserHandle());
+ }
+
+ /**
+ * Sets up the content view.
+ * @return <code>true</code> if the activity is finishing and creation should halt.
+ */
+ private boolean configureContentView(TargetDataLoader targetDataLoader) {
+ if (mMultiProfilePagerAdapter.getActiveListAdapter() == null) {
+ throw new IllegalStateException("mMultiProfilePagerAdapter.getCurrentListAdapter() "
+ + "cannot be null.");
+ }
+ Trace.beginSection("configureContentView");
+ // We partially rebuild the inactive adapter to determine if we should auto launch
+ // isTabLoaded will be true here if the empty state screen is shown instead of the list.
+ // To date, we really only care about "partially rebuilding" tabs for work and/or personal.
+ boolean rebuildCompleted = mMultiProfilePagerAdapter.rebuildTabs(shouldShowTabs());
+
+ if (shouldUseMiniResolver()) {
+ configureMiniResolverContent(targetDataLoader);
+ Trace.endSection();
+ return false;
+ }
+
+ if (useLayoutWithDefault()) {
+ mLayoutId = R.layout.resolver_list_with_default;
+ } else {
+ mLayoutId = getLayoutResource();
+ }
+ setContentView(mLayoutId);
+ mMultiProfilePagerAdapter.setupViewPager(findViewById(com.android.internal.R.id.profile_pager));
+ boolean result = postRebuildList(rebuildCompleted);
+ Trace.endSection();
+ return result;
+ }
+
+ /**
+ * Mini resolver is shown when the user is choosing between browser[s] in this profile and a
+ * single app in the other profile (see shouldUseMiniResolver()). It shows the single app icon
+ * and asks the user if they'd like to open that cross-profile app or use the in-profile
+ * browser.
+ */
+ private void configureMiniResolverContent(TargetDataLoader targetDataLoader) {
+ mLayoutId = R.layout.miniresolver;
+ setContentView(mLayoutId);
+
+ // TODO: try to dedupe and use the pager's `getActiveProfile()` instead of the activity
+ // `getCurrentProfile()` (or align them if they're not currently equivalent). If they truly
+ // need to be distinct here, then `getCurrentProfile()` should at *least* get a more
+ // specific name -- but note that checking `getCurrentProfile()` here, then following
+ // `getActiveProfile()` to find the "in/active adapter," is exactly the legacy behavior.
+ boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK;
+
+ ResolverListAdapter sameProfileAdapter =
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getPersonalListAdapter()
+ : mMultiProfilePagerAdapter.getWorkListAdapter();
+
+ ResolverListAdapter inactiveAdapter =
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getWorkListAdapter()
+ : mMultiProfilePagerAdapter.getPersonalListAdapter();
+
+ DisplayResolveInfo sameProfileResolveInfo = sameProfileAdapter.getFirstDisplayResolveInfo();
+
+ final DisplayResolveInfo otherProfileResolveInfo =
+ inactiveAdapter.getFirstDisplayResolveInfo();
+
+ // Load the icon asynchronously
+ ImageView icon = findViewById(com.android.internal.R.id.icon);
+ targetDataLoader.loadAppTargetIcon(
+ otherProfileResolveInfo,
+ inactiveAdapter.getUserHandle(),
+ (drawable) -> {
+ if (!isDestroyed()) {
+ otherProfileResolveInfo.getDisplayIconHolder().setDisplayIcon(drawable);
+ new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo);
+ }
+ });
+
+ ((TextView) findViewById(com.android.internal.R.id.open_cross_profile)).setText(
+ getResources().getString(
+ inWorkProfile
+ ? R.string.miniresolver_open_in_personal
+ : R.string.miniresolver_open_in_work,
+ getOrLoadDisplayLabel(otherProfileResolveInfo)));
+ ((Button) findViewById(com.android.internal.R.id.use_same_profile_browser)).setText(
+ inWorkProfile ? R.string.miniresolver_use_work_browser
+ : R.string.miniresolver_use_personal_browser);
+
+ findViewById(com.android.internal.R.id.use_same_profile_browser).setOnClickListener(
+ v -> {
+ safelyStartActivity(sameProfileResolveInfo);
+ finish();
+ });
+
+ findViewById(com.android.internal.R.id.button_open).setOnClickListener(v -> {
+ Intent intent = otherProfileResolveInfo.getResolvedIntent();
+ safelyStartActivityAsUser(otherProfileResolveInfo, inactiveAdapter.getUserHandle());
+ finish();
+ });
+ }
+
+ private boolean isTwoPagePersonalAndWorkConfiguration() {
+ return (mMultiProfilePagerAdapter.getCount() == 2)
+ && mMultiProfilePagerAdapter.hasPageForProfile(PROFILE_PERSONAL)
+ && mMultiProfilePagerAdapter.hasPageForProfile(PROFILE_WORK);
+ }
+
+ /**
+ * Mini resolver should be used when all of the following are true:
+ * 1. This is the intent picker (ResolverActivity).
+ * 2. There are exactly two tabs, for the "personal" and "work" profiles.
+ * 3. This profile only has web browser matches.
+ * 4. The other profile has a single non-browser match.
+ */
+ private boolean shouldUseMiniResolver() {
+ if (!mIsIntentPicker) {
+ return false;
+ }
+ if (!isTwoPagePersonalAndWorkConfiguration()) {
+ return false;
+ }
+
+ ResolverListAdapter sameProfileAdapter =
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getPersonalListAdapter()
+ : mMultiProfilePagerAdapter.getWorkListAdapter();
+
+ ResolverListAdapter otherProfileAdapter =
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getWorkListAdapter()
+ : mMultiProfilePagerAdapter.getPersonalListAdapter();
+
+ if (sameProfileAdapter.getDisplayResolveInfoCount() == 0) {
+ Log.d(TAG, "No targets in the current profile");
+ return false;
+ }
+
+ if (otherProfileAdapter.getDisplayResolveInfoCount() != 1) {
+ Log.d(TAG, "Other-profile count: " + otherProfileAdapter.getDisplayResolveInfoCount());
+ return false;
+ }
+
+ if (otherProfileAdapter.allResolveInfosHandleAllWebDataUri()) {
+ Log.d(TAG, "Other profile is a web browser");
+ return false;
+ }
+
+ if (!sameProfileAdapter.allResolveInfosHandleAllWebDataUri()) {
+ Log.d(TAG, "Non-browser found in this profile");
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Finishing procedures to be performed after the list has been rebuilt.
+ * @param rebuildCompleted
+ * @return <code>true</code> if the activity is finishing and creation should halt.
+ */
+ final boolean postRebuildListInternal(boolean rebuildCompleted) {
+ int count = mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();
+
+ // We only rebuild asynchronously when we have multiple elements to sort. In the case where
+ // we're already done, we can check if we should auto-launch immediately.
+ if (rebuildCompleted && maybeAutolaunchActivity()) {
+ return true;
+ }
+
+ setupViewVisibilities();
+
+ if (shouldShowTabs()) {
+ setupProfileTabs();
+ }
+
+ return false;
+ }
+
+ private int isPermissionGranted(String permission, int uid) {
+ return ActivityManager.checkComponentPermission(permission, uid,
+ /* owningUid= */-1, /* exported= */ true);
+ }
+
+ /**
+ * @return {@code true} if a resolved target is autolaunched, otherwise {@code false}
+ */
+ private boolean maybeAutolaunchActivity() {
+ int numberOfProfiles = mMultiProfilePagerAdapter.getItemCount();
+ if (numberOfProfiles == 1 && maybeAutolaunchIfSingleTarget()) {
+ return true;
+ } else if (maybeAutolaunchIfCrossProfileSupported()) {
+ // TODO(b/280988288): If the ChooserActivity is shown we should consider showing the
+ // correct intent-picker UIs (e.g., mini-resolver) if it was launched without
+ // ACTION_SEND.
+ return true;
+ }
+ return false;
+ }
+
+ private boolean maybeAutolaunchIfSingleTarget() {
+ int count = mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();
+ if (count != 1) {
+ return false;
+ }
+
+ if (mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null) {
+ return false;
+ }
+
+ // Only one target, so we're a candidate to auto-launch!
+ final TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .targetInfoForPosition(0, false);
+ if (shouldAutoLaunchSingleChoice(target)) {
+ safelyStartActivity(target);
+ finish();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * When we have just a personal and a work profile, we auto launch in the following scenario:
+ * - There is 1 resolved target on each profile
+ * - That target is the same app on both profiles
+ * - The target app has permission to communicate cross profiles
+ * - The target app has declared it supports cross-profile communication via manifest metadata
+ */
+ private boolean maybeAutolaunchIfCrossProfileSupported() {
+ if (!isTwoPagePersonalAndWorkConfiguration()) {
+ return false;
+ }
+
+ ResolverListAdapter activeListAdapter =
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getPersonalListAdapter()
+ : mMultiProfilePagerAdapter.getWorkListAdapter();
+
+ ResolverListAdapter inactiveListAdapter =
+ (mMultiProfilePagerAdapter.getActiveProfile() == PROFILE_PERSONAL)
+ ? mMultiProfilePagerAdapter.getWorkListAdapter()
+ : mMultiProfilePagerAdapter.getPersonalListAdapter();
+
+ if (!activeListAdapter.isTabLoaded() || !inactiveListAdapter.isTabLoaded()) {
+ return false;
+ }
+
+ if ((activeListAdapter.getUnfilteredCount() != 1)
+ || (inactiveListAdapter.getUnfilteredCount() != 1)) {
+ return false;
+ }
+
+ TargetInfo activeProfileTarget = activeListAdapter.targetInfoForPosition(0, false);
+ TargetInfo inactiveProfileTarget = inactiveListAdapter.targetInfoForPosition(0, false);
+ if (!Objects.equals(
+ activeProfileTarget.getResolvedComponentName(),
+ inactiveProfileTarget.getResolvedComponentName())) {
+ return false;
+ }
+
+ if (!shouldAutoLaunchSingleChoice(activeProfileTarget)) {
+ return false;
+ }
+
+ String packageName = activeProfileTarget.getResolvedComponentName().getPackageName();
+ if (!canAppInteractCrossProfiles(packageName)) {
+ return false;
+ }
+
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET)
+ .setBoolean(activeListAdapter.getUserHandle()
+ .equals(requireAnnotatedUserHandles().personalProfileUserHandle))
+ .setStrings(getMetricsCategory())
+ .write();
+ safelyStartActivity(activeProfileTarget);
+ finish();
+ return true;
+ }
+
+ /**
+ * Returns whether the package has the necessary permissions to interact across profiles on
+ * behalf of a given user.
+ *
+ * <p>This means meeting the following condition:
+ * <ul>
+ * <li>The app's {@link ApplicationInfo#crossProfile} flag must be true, and at least
+ * one of the following conditions must be fulfilled</li>
+ * <li>{@code Manifest.permission.INTERACT_ACROSS_USERS_FULL} granted.</li>
+ * <li>{@code Manifest.permission.INTERACT_ACROSS_USERS} granted.</li>
+ * <li>{@code Manifest.permission.INTERACT_ACROSS_PROFILES} granted, or the corresponding
+ * AppOps {@code android:interact_across_profiles} is set to "allow".</li>
+ * </ul>
+ *
+ */
+ private boolean canAppInteractCrossProfiles(String packageName) {
+ ApplicationInfo applicationInfo;
+ try {
+ applicationInfo = getPackageManager().getApplicationInfo(packageName, 0);
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Package " + packageName + " does not exist on current user.");
+ return false;
+ }
+ if (!applicationInfo.crossProfile) {
+ return false;
+ }
+
+ int packageUid = applicationInfo.uid;
+
+ if (isPermissionGranted(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+ packageUid) == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+ if (isPermissionGranted(android.Manifest.permission.INTERACT_ACROSS_USERS, packageUid)
+ == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+ if (PermissionChecker.checkPermissionForPreflight(this, INTERACT_ACROSS_PROFILES,
+ PID_UNKNOWN, packageUid, packageName) == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isAutolaunching() {
+ return !mRegistered && isFinishing();
+ }
+
+ private void setupProfileTabs() {
+ maybeHideDivider();
+ TabHost tabHost = findViewById(com.android.internal.R.id.profile_tabhost);
+ tabHost.setup();
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ viewPager.setSaveEnabled(false);
+
+ Button personalButton = (Button) getLayoutInflater().inflate(
+ R.layout.resolver_profile_tab_button, tabHost.getTabWidget(), false);
+ personalButton.setText(mDevicePolicyResources.getPersonalTabLabel());
+ personalButton.setContentDescription(
+ mDevicePolicyResources.getPersonalTabAccessibilityLabel());
+
+ TabHost.TabSpec tabSpec = tabHost.newTabSpec(TAB_TAG_PERSONAL)
+ .setContent(com.android.internal.R.id.profile_pager)
+ .setIndicator(personalButton);
+ tabHost.addTab(tabSpec);
+
+ Button workButton = (Button) getLayoutInflater().inflate(
+ R.layout.resolver_profile_tab_button, tabHost.getTabWidget(), false);
+ workButton.setText(mDevicePolicyResources.getWorkTabLabel());
+ workButton.setContentDescription(mDevicePolicyResources.getWorkTabAccessibilityLabel());
+
+ tabSpec = tabHost.newTabSpec(TAB_TAG_WORK)
+ .setContent(com.android.internal.R.id.profile_pager)
+ .setIndicator(workButton);
+ tabHost.addTab(tabSpec);
+
+ TabWidget tabWidget = tabHost.getTabWidget();
+ tabWidget.setVisibility(View.VISIBLE);
+ updateActiveTabStyle(tabHost);
+
+ tabHost.setOnTabChangedListener(tabId -> {
+ updateActiveTabStyle(tabHost);
+ if (TAB_TAG_PERSONAL.equals(tabId)) {
+ viewPager.setCurrentItem(0);
+ } else {
+ viewPager.setCurrentItem(1);
+ }
+ setupViewVisibilities();
+ maybeLogProfileChange();
+ onProfileTabSelected();
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_SWITCH_TABS)
+ .setInt(viewPager.getCurrentItem())
+ .setStrings(getMetricsCategory())
+ .write();
+ });
+
+ viewPager.setVisibility(View.VISIBLE);
+ tabHost.setCurrentTab(mMultiProfilePagerAdapter.getCurrentPage());
+ mMultiProfilePagerAdapter.setOnProfileSelectedListener(
+ new MultiProfilePagerAdapter.OnProfileSelectedListener() {
+ @Override
+ public void onProfileSelected(int index) {
+ tabHost.setCurrentTab(index);
+ resetButtonBar();
+ resetCheckedItem();
+ }
+
+ @Override
+ public void onProfilePageStateChanged(int state) {
+ onHorizontalSwipeStateChanged(state);
+ }
+ });
+ mOnSwitchOnWorkSelectedListener = () -> {
+ final View workTab = tabHost.getTabWidget().getChildAt(1);
+ workTab.setFocusable(true);
+ workTab.setFocusableInTouchMode(true);
+ workTab.requestFocus();
+ };
+ }
+
+ private void maybeHideDivider() {
+ if (!mIsIntentPicker) {
+ return;
+ }
+ final View divider = findViewById(com.android.internal.R.id.divider);
+ if (divider == null) {
+ return;
+ }
+ divider.setVisibility(View.GONE);
+ }
+
+ private void resetCheckedItem() {
+ if (!mIsIntentPicker) {
+ return;
+ }
+ mLastSelected = ListView.INVALID_POSITION;
+ ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
+ .clearCheckedItemsInInactiveProfiles();
+ }
+
+ private static int getAttrColor(Context context, int attr) {
+ TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
+ int colorAccent = ta.getColor(0, 0);
+ ta.recycle();
+ return colorAccent;
+ }
+
+ private void updateActiveTabStyle(TabHost tabHost) {
+ int currentTab = tabHost.getCurrentTab();
+ TextView selected = (TextView) tabHost.getTabWidget().getChildAt(currentTab);
+ TextView unselected = (TextView) tabHost.getTabWidget().getChildAt(1 - currentTab);
+ selected.setSelected(true);
+ unselected.setSelected(false);
+ }
+
+ private void setupViewVisibilities() {
+ ResolverListAdapter activeListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();
+ if (!mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)) {
+ addUseDifferentAppLabelIfNecessary(activeListAdapter);
+ }
+ }
+
+ /**
+ * Updates the button bar container {@code ignoreOffset} layout param.
+ * <p>Setting this to {@code true} means that the button bar will be glued to the bottom of
+ * the screen.
+ */
+ private void setButtonBarIgnoreOffset(boolean ignoreOffset) {
+ View buttonBarContainer = findViewById(com.android.internal.R.id.button_bar_container);
+ if (buttonBarContainer != null) {
+ ResolverDrawerLayout.LayoutParams layoutParams =
+ (ResolverDrawerLayout.LayoutParams) buttonBarContainer.getLayoutParams();
+ layoutParams.ignoreOffset = ignoreOffset;
+ buttonBarContainer.setLayoutParams(layoutParams);
+ }
+ }
+
+ private void setupAdapterListView(ListView listView, ItemClickListener listener) {
+ listView.setOnItemClickListener(listener);
+ listView.setOnItemLongClickListener(listener);
+
+ if (mLogic.getSupportsAlwaysUseOption()) {
+ listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
+ }
+ }
+
+ /**
+ * Configure the area above the app selection list (title, content preview, etc).
+ */
+ private void maybeCreateHeader(ResolverListAdapter listAdapter) {
+ if (mHeaderCreatorUser != null
+ && !listAdapter.getUserHandle().equals(mHeaderCreatorUser)) {
+ return;
+ }
+ if (!shouldShowTabs()
+ && listAdapter.getCount() == 0 && listAdapter.getPlaceholderCount() == 0) {
+ final TextView titleView = findViewById(com.android.internal.R.id.title);
+ if (titleView != null) {
+ titleView.setVisibility(View.GONE);
+ }
+ }
+
+
+ CharSequence title = mLogic.getTitle() != null
+ ? mLogic.getTitle()
+ : getTitleForAction(mLogic.getTargetIntent(), mLogic.getDefaultTitleResId());
+
+ if (!TextUtils.isEmpty(title)) {
+ final TextView titleView = findViewById(com.android.internal.R.id.title);
+ if (titleView != null) {
+ titleView.setText(title);
+ }
+ setTitle(title);
+ }
+
+ final ImageView iconView = findViewById(com.android.internal.R.id.icon);
+ if (iconView != null) {
+ listAdapter.loadFilteredItemIconTaskAsync(iconView);
+ }
+ mHeaderCreatorUser = listAdapter.getUserHandle();
+ }
+
+ private void resetAlwaysOrOnceButtonBar() {
+ // Disable both buttons initially
+ setAlwaysButtonEnabled(false, ListView.INVALID_POSITION, false);
+ mOnceButton.setEnabled(false);
+
+ int filteredPosition = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getFilteredPosition();
+ if (useLayoutWithDefault() && filteredPosition != ListView.INVALID_POSITION) {
+ setAlwaysButtonEnabled(true, filteredPosition, false);
+ mOnceButton.setEnabled(true);
+ // Focus the button if we already have the default option
+ mOnceButton.requestFocus();
+ return;
+ }
+
+ // When the items load in, if an item was already selected, enable the buttons
+ ListView currentAdapterView = (ListView) mMultiProfilePagerAdapter.getActiveAdapterView();
+ if (currentAdapterView != null
+ && currentAdapterView.getCheckedItemPosition() != ListView.INVALID_POSITION) {
+ setAlwaysButtonEnabled(true, currentAdapterView.getCheckedItemPosition(), true);
+ mOnceButton.setEnabled(true);
+ }
+ }
+
+ @Override // ResolverListCommunicator
+ public final boolean useLayoutWithDefault() {
+ // We only use the default app layout when the profile of the active user has a
+ // filtered item. We always show the same default app even in the inactive user profile.
+ boolean adapterForCurrentUserHasFilteredItem =
+ mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+ requireAnnotatedUserHandles().tabOwnerUserHandleForLaunch
+ ).hasFilteredItem();
+ return mLogic.getSupportsAlwaysUseOption() && adapterForCurrentUserHasFilteredItem;
+ }
+
+ /**
+ * If {@code retainInOnStop} is set to true, we will not finish ourselves when onStop gets
+ * called and we are launched in a new task.
+ */
+ protected final void setRetainInOnStop(boolean retainInOnStop) {
+ mRetainInOnStop = retainInOnStop;
+ }
+
+ final class ItemClickListener implements AdapterView.OnItemClickListener,
+ AdapterView.OnItemLongClickListener {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final ListView listView = parent instanceof ListView ? (ListView) parent : null;
+ if (listView != null) {
+ position -= listView.getHeaderViewsCount();
+ }
+ if (position < 0) {
+ // Header views don't count.
+ return;
+ }
+ // If we're still loading, we can't yet enable the buttons.
+ if (mMultiProfilePagerAdapter.getActiveListAdapter()
+ .resolveInfoForPosition(position, true) == null) {
+ return;
+ }
+ ListView currentAdapterView =
+ (ListView) mMultiProfilePagerAdapter.getActiveAdapterView();
+ final int checkedPos = currentAdapterView.getCheckedItemPosition();
+ final boolean hasValidSelection = checkedPos != ListView.INVALID_POSITION;
+ if (!useLayoutWithDefault()
+ && (!hasValidSelection || mLastSelected != checkedPos)
+ && mAlwaysButton != null) {
+ setAlwaysButtonEnabled(hasValidSelection, checkedPos, true);
+ mOnceButton.setEnabled(hasValidSelection);
+ if (hasValidSelection) {
+ currentAdapterView.smoothScrollToPosition(checkedPos);
+ mOnceButton.requestFocus();
+ }
+ mLastSelected = checkedPos;
+ } else {
+ startSelected(position, false, true);
+ }
+ }
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ final ListView listView = parent instanceof ListView ? (ListView) parent : null;
+ if (listView != null) {
+ position -= listView.getHeaderViewsCount();
+ }
+ if (position < 0) {
+ // Header views don't count.
+ return false;
+ }
+ ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .resolveInfoForPosition(position, true);
+ showTargetDetails(ri);
+ return true;
+ }
+
+ }
+
+ /** Determine whether a given match result is considered "specific" in our application. */
+ public static final boolean isSpecificUriMatch(int match) {
+ match = (match & IntentFilter.MATCH_CATEGORY_MASK);
+ return match >= IntentFilter.MATCH_CATEGORY_HOST
+ && match <= IntentFilter.MATCH_CATEGORY_PATH;
+ }
+
+ static final class PickTargetOptionRequest extends PickOptionRequest {
+ public PickTargetOptionRequest(@Nullable Prompt prompt, Option[] options,
+ @Nullable Bundle extras) {
+ super(prompt, options, extras);
+ }
+
+ @Override
+ public void onCancel() {
+ super.onCancel();
+ final ResolverActivity ra = (ResolverActivity) getActivity();
+ if (ra != null) {
+ ra.mPickOptionRequest = null;
+ ra.finish();
+ }
+ }
+
+ @Override
+ public void onPickOptionResult(boolean finished, Option[] selections, Bundle result) {
+ super.onPickOptionResult(finished, selections, result);
+ if (selections.length != 1) {
+ // TODO In a better world we would filter the UI presented here and let the
+ // user refine. Maybe later.
+ return;
+ }
+
+ final ResolverActivity ra = (ResolverActivity) getActivity();
+ if (ra != null) {
+ final TargetInfo ti = ra.mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getItem(selections[0].getIndex());
+ if (ra.onTargetSelected(ti, false)) {
+ ra.mPickOptionRequest = null;
+ ra.finish();
+ }
+ }
+ }
+ }
+ /**
+ * Returns the {@link UserHandle} to use when querying resolutions for intents in a
+ * {@link ResolverListController} configured for the provided {@code userHandle}.
+ */
+ protected final UserHandle getQueryIntentsUser(UserHandle userHandle) {
+ return requireAnnotatedUserHandles().getQueryIntentsUser(userHandle);
+ }
+
+ /**
+ * Returns the {@link List} of {@link UserHandle} to pass on to the
+ * {@link ResolverRankerServiceResolverComparator} as per the provided {@code userHandle}.
+ */
+ @VisibleForTesting(visibility = PROTECTED)
+ public final List<UserHandle> getResolverRankerServiceUserHandleList(UserHandle userHandle) {
+ return getResolverRankerServiceUserHandleListInternal(userHandle);
+ }
+
+ @VisibleForTesting
+ protected List<UserHandle> getResolverRankerServiceUserHandleListInternal(
+ UserHandle userHandle) {
+ List<UserHandle> userList = new ArrayList<>();
+ userList.add(userHandle);
+ // Add clonedProfileUserHandle to the list only if we are:
+ // a. Building the Personal Tab.
+ // b. CloneProfile exists on the device.
+ if (userHandle.equals(requireAnnotatedUserHandles().personalProfileUserHandle)
+ && hasCloneProfile()) {
+ userList.add(requireAnnotatedUserHandles().cloneProfileUserHandle);
+ }
+ return userList;
+ }
+
+ private CharSequence getOrLoadDisplayLabel(TargetInfo info) {
+ if (info.isDisplayResolveInfo()) {
+ mLogic.getTargetDataLoader().getOrLoadLabel((DisplayResolveInfo) info);
+ }
+ CharSequence displayLabel = info.getDisplayLabel();
+ return displayLabel == null ? "" : displayLabel;
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt b/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt
new file mode 100644
index 00000000..0e2b25ec
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ResolverActivityLogic.kt
@@ -0,0 +1,81 @@
+package com.android.intentresolver.v2
+
+import android.content.Intent
+import androidx.activity.ComponentActivity
+import androidx.annotation.OpenForTesting
+import com.android.intentresolver.R
+import com.android.intentresolver.icons.DefaultTargetDataLoader
+import com.android.intentresolver.icons.TargetDataLoader
+import com.android.intentresolver.v2.util.mutableLazy
+
+/** Activity logic for [ResolverActivity]. */
+@OpenForTesting
+open class ResolverActivityLogic(
+ tag: String,
+ activityProvider: () -> ComponentActivity,
+ onWorkProfileStatusUpdated: () -> Unit,
+) :
+ ActivityLogic,
+ CommonActivityLogic by CommonActivityLogicImpl(
+ tag,
+ activityProvider,
+ onWorkProfileStatusUpdated,
+ ) {
+
+ override val targetIntent: Intent by lazy {
+ val intent = Intent(activity.intent)
+ intent.setComponent(null)
+ // The resolver activity is set to be hidden from recent tasks.
+ // we don't want this attribute to be propagated to the next activity
+ // being launched. Note that if the original Intent also had this
+ // flag set, we are now losing it. That should be a very rare case
+ // and we can live with this.
+ intent.setFlags(intent.flags and Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS.inv())
+
+ // If FLAG_ACTIVITY_LAUNCH_ADJACENT was set, ResolverActivity was opened in the alternate
+ // side, which means we want to open the target app on the same side as ResolverActivity.
+ if (intent.flags and Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT != 0) {
+ intent.setFlags(intent.flags and Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT.inv())
+ }
+ intent
+ }
+
+ override val resolvingHome: Boolean by lazy {
+ targetIntent.action == Intent.ACTION_MAIN &&
+ targetIntent.categories.singleOrNull() == Intent.CATEGORY_HOME
+ }
+
+ override val title: CharSequence? = null
+
+ override val defaultTitleResId: Int = 0
+
+ override val initialIntents: List<Intent>? = null
+
+ override val supportsAlwaysUseOption: Boolean = true
+
+ override val targetDataLoader: TargetDataLoader by lazy {
+ DefaultTargetDataLoader(
+ activity,
+ activity.lifecycle,
+ activity.intent.getBooleanExtra(
+ ResolverActivity.EXTRA_IS_AUDIO_CAPTURE_DEVICE,
+ /* defaultValue = */ false,
+ ),
+ )
+ }
+
+ override val themeResId: Int = R.style.Theme_DeviceDefault_Resolver
+
+ private val _profileSwitchMessage = mutableLazy { forwardMessageFor(targetIntent) }
+ override val profileSwitchMessage: String? by _profileSwitchMessage
+
+ override val payloadIntents: List<Intent> by lazy { listOf(targetIntent) }
+
+ override fun preInitialization() {
+ // Do nothing
+ }
+
+ override fun clearProfileSwitchMessage() {
+ _profileSwitchMessage.setLazy(null)
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java
new file mode 100644
index 00000000..d96fd15a
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ResolverMultiProfilePagerAdapter.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2;
+
+import android.content.Context;
+import android.os.UserHandle;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+import androidx.viewpager.widget.PagerAdapter;
+
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+
+/**
+ * A {@link PagerAdapter} which describes the work and personal profile intent resolver screens.
+ */
+@VisibleForTesting
+public class ResolverMultiProfilePagerAdapter extends
+ MultiProfilePagerAdapter<ListView, ResolverListAdapter, ResolverListAdapter> {
+ private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier;
+
+ public ResolverMultiProfilePagerAdapter(
+ Context context,
+ ResolverListAdapter adapter,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle) {
+ this(
+ context,
+ ImmutableList.of(adapter),
+ emptyStateProvider,
+ workProfileQuietModeChecker,
+ /* defaultProfile= */ 0,
+ workProfileUserHandle,
+ cloneProfileUserHandle,
+ new BottomPaddingOverrideSupplier());
+ }
+
+ public ResolverMultiProfilePagerAdapter(Context context,
+ ResolverListAdapter personalAdapter,
+ ResolverListAdapter workAdapter,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ @Profile int defaultProfile,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle) {
+ this(
+ context,
+ ImmutableList.of(personalAdapter, workAdapter),
+ emptyStateProvider,
+ workProfileQuietModeChecker,
+ defaultProfile,
+ workProfileUserHandle,
+ cloneProfileUserHandle,
+ new BottomPaddingOverrideSupplier());
+ }
+
+ private ResolverMultiProfilePagerAdapter(
+ Context context,
+ ImmutableList<ResolverListAdapter> listAdapters,
+ EmptyStateProvider emptyStateProvider,
+ Supplier<Boolean> workProfileQuietModeChecker,
+ @Profile int defaultProfile,
+ UserHandle workProfileUserHandle,
+ UserHandle cloneProfileUserHandle,
+ BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {
+ super(
+ listAdapter -> listAdapter,
+ (listView, bindAdapter) -> listView.setAdapter(bindAdapter),
+ listAdapters,
+ emptyStateProvider,
+ workProfileQuietModeChecker,
+ defaultProfile,
+ workProfileUserHandle,
+ cloneProfileUserHandle,
+ () -> (ViewGroup) LayoutInflater.from(context).inflate(
+ R.layout.resolver_list_per_profile, null, false),
+ bottomPaddingOverrideSupplier);
+ mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier;
+ }
+
+ public void setUseLayoutWithDefault(boolean useLayoutWithDefault) {
+ mBottomPaddingOverrideSupplier.setUseLayoutWithDefault(useLayoutWithDefault);
+ }
+
+ /** Un-check any item(s) that may be checked in any of our inactive adapter(s). */
+ public void clearCheckedItemsInInactiveProfiles() {
+ // TODO: apply to all inactive adapters; for now we just have the one.
+ ListView inactiveListView = getInactiveAdapterView();
+ if (inactiveListView.getCheckedItemCount() > 0) {
+ inactiveListView.setItemChecked(inactiveListView.getCheckedItemPosition(), false);
+ }
+ }
+
+ private static class BottomPaddingOverrideSupplier implements Supplier<Optional<Integer>> {
+ private boolean mUseLayoutWithDefault;
+
+ public void setUseLayoutWithDefault(boolean useLayoutWithDefault) {
+ mUseLayoutWithDefault = useLayoutWithDefault;
+ }
+
+ @Override
+ public Optional<Integer> get() {
+ return mUseLayoutWithDefault ? Optional.empty() : Optional.of(0);
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/data/BroadcastFlow.kt b/java/src/com/android/intentresolver/v2/data/BroadcastFlow.kt
new file mode 100644
index 00000000..1a58afcb
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/data/BroadcastFlow.kt
@@ -0,0 +1,46 @@
+package com.android.intentresolver.v2.data
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.os.UserHandle
+import android.util.Log
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.channels.onFailure
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+
+private const val TAG = "BroadcastFlow"
+
+/**
+ * Returns a [callbackFlow] that, when collected, registers a broadcast receiver and emits a new
+ * value whenever broadcast matching _filter_ is received. The result value will be computed using
+ * [transform] and emitted if non-null.
+ */
+internal fun <T> broadcastFlow(
+ context: Context,
+ filter: IntentFilter,
+ user: UserHandle,
+ transform: (Intent) -> T?
+): Flow<T> = callbackFlow {
+ val receiver =
+ object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ transform(intent)?.also { result ->
+ trySend(result).onFailure { Log.e(TAG, "Failed to send $result", it) }
+ }
+ ?: Log.w(TAG, "Ignored broadcast $intent")
+ }
+ }
+
+ context.registerReceiverAsUser(
+ receiver,
+ user,
+ IntentFilter(filter),
+ null,
+ null,
+ Context.RECEIVER_NOT_EXPORTED
+ )
+ awaitClose { context.unregisterReceiver(receiver) }
+}
diff --git a/java/src/com/android/intentresolver/v2/data/model/User.kt b/java/src/com/android/intentresolver/v2/data/model/User.kt
new file mode 100644
index 00000000..504b04c8
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/data/model/User.kt
@@ -0,0 +1,50 @@
+package com.android.intentresolver.v2.data.model
+
+import android.annotation.UserIdInt
+import android.os.UserHandle
+import com.android.intentresolver.v2.data.model.User.Type
+import com.android.intentresolver.v2.data.model.User.Type.FULL
+import com.android.intentresolver.v2.data.model.User.Type.PROFILE
+
+/**
+ * A User represents the owner of a distinct set of content.
+ * * maps 1:1 to a UserHandle or UserId (Int) value.
+ * * refers to either [Full][Type.FULL], or a [Profile][Type.PROFILE] user, as indicated by the
+ * [type] property.
+ *
+ * See
+ * [Users for system developers](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/os/Users.md)
+ *
+ * ```
+ * val users = listOf(
+ * User(id = 0, role = PERSONAL),
+ * User(id = 10, role = WORK),
+ * User(id = 11, role = CLONE),
+ * User(id = 12, role = PRIVATE),
+ * )
+ * ```
+ */
+data class User(
+ @UserIdInt val id: Int,
+ val role: Role,
+) {
+ val handle: UserHandle = UserHandle.of(id)
+
+ val type: Type
+ get() = role.type
+
+ enum class Type {
+ FULL,
+ PROFILE
+ }
+
+ enum class Role(
+ /** The type of the role user. */
+ val type: Type
+ ) {
+ PERSONAL(FULL),
+ PRIVATE(PROFILE),
+ WORK(PROFILE),
+ CLONE(PROFILE)
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt b/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt
new file mode 100644
index 00000000..7debdf07
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/data/repository/DevicePolicyResources.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.v2.data.repository
+
+import android.app.admin.DevicePolicyManager
+import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB
+import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB_ACCESSIBILITY
+import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED
+import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB
+import android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY
+import android.content.res.Resources
+import com.android.intentresolver.R
+import com.android.intentresolver.inject.ApplicationOwned
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class DevicePolicyResources @Inject constructor(
+ @ApplicationOwned private val resources: Resources,
+ devicePolicyManager: DevicePolicyManager
+) {
+ private val policyResources = devicePolicyManager.resources
+
+ val personalTabLabel by lazy {
+ requireNotNull(policyResources.getString(RESOLVER_PERSONAL_TAB) {
+ resources.getString(R.string.resolver_personal_tab)
+ })
+ }
+
+ val workTabLabel by lazy {
+ requireNotNull(policyResources.getString(RESOLVER_WORK_TAB) {
+ resources.getString(R.string.resolver_work_tab)
+ })
+ }
+
+ val personalTabAccessibilityLabel by lazy {
+ requireNotNull(policyResources.getString(RESOLVER_PERSONAL_TAB_ACCESSIBILITY) {
+ resources.getString(R.string.resolver_personal_tab_accessibility)
+ })
+ }
+
+ val workTabAccessibilityLabel by lazy {
+ requireNotNull(policyResources.getString(RESOLVER_WORK_TAB_ACCESSIBILITY) {
+ resources.getString(R.string.resolver_work_tab_accessibility)
+ })
+ }
+
+ fun getWorkProfileNotSupportedMessage(launcherName: String): String {
+ return requireNotNull(policyResources.getString(RESOLVER_WORK_PROFILE_NOT_SUPPORTED, {
+ resources.getString(
+ R.string.activity_resolver_work_profiles_support,
+ launcherName)
+ }, launcherName))
+ }
+} \ No newline at end of file
diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt b/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt
new file mode 100644
index 00000000..fc82efee
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/data/repository/UserInfoExt.kt
@@ -0,0 +1,29 @@
+package com.android.intentresolver.v2.data.repository
+
+import android.content.pm.UserInfo
+import com.android.intentresolver.v2.data.model.User
+import com.android.intentresolver.v2.data.model.User.Role
+
+/** Maps the UserInfo to one of the defined [Roles][User.Role], if possible. */
+fun UserInfo.getSupportedUserRole(): Role? =
+ when {
+ isFull -> Role.PERSONAL
+ isManagedProfile -> Role.WORK
+ isCloneProfile -> Role.CLONE
+ isPrivateProfile -> Role.PRIVATE
+ else -> null
+ }
+
+/**
+ * Creates a [User], based on values from a [UserInfo].
+ *
+ * ```
+ * val users: List<User> =
+ * getEnabledProfiles(user).map(::toUser).filterNotNull()
+ * ```
+ *
+ * @return a [User] if the [UserInfo] matched a supported [Role], otherwise null
+ */
+fun UserInfo.toUser(): User? {
+ return getSupportedUserRole()?.let { role -> User(userHandle.identifier, role) }
+}
diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt b/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt
new file mode 100644
index 00000000..dc809b46
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/data/repository/UserRepository.kt
@@ -0,0 +1,261 @@
+package com.android.intentresolver.v2.data.repository
+
+import android.content.Context
+import android.content.Intent
+import android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE
+import android.content.Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE
+import android.content.Intent.ACTION_PROFILE_ADDED
+import android.content.Intent.ACTION_PROFILE_AVAILABLE
+import android.content.Intent.ACTION_PROFILE_REMOVED
+import android.content.Intent.ACTION_PROFILE_UNAVAILABLE
+import android.content.Intent.EXTRA_QUIET_MODE
+import android.content.Intent.EXTRA_USER
+import android.content.IntentFilter
+import android.content.pm.UserInfo
+import android.os.UserHandle
+import android.os.UserManager
+import android.util.Log
+import androidx.annotation.VisibleForTesting
+import com.android.intentresolver.inject.Background
+import com.android.intentresolver.inject.Main
+import com.android.intentresolver.inject.ProfileParent
+import com.android.intentresolver.v2.data.broadcastFlow
+import com.android.intentresolver.v2.data.model.User
+import com.android.intentresolver.v2.data.repository.UserRepositoryImpl.UserEvent
+import dagger.hilt.android.qualifiers.ApplicationContext
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNot
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.runningFold
+import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.withContext
+
+interface UserRepository {
+ /**
+ * A [Flow] user profile groups. Each map contains the context user along with all members of
+ * the profile group. This includes the (Full) parent user, if the context user is a profile.
+ */
+ val users: Flow<Map<UserHandle, User>>
+
+ /**
+ * A [Flow] of availability. Only profile users may become unavailable.
+ *
+ * Availability is currently defined as not being in [quietMode][UserInfo.isQuietModeEnabled].
+ */
+ fun isAvailable(user: User): Flow<Boolean>
+
+ /**
+ * Request that availability be updated to the requested state. This currently includes toggling
+ * quiet mode as needed. This may involve additional background actions, such as starting or
+ * stopping a profile user (along with their many associated processes).
+ *
+ * If successful, the change will be applied after the call returns and can be observed using
+ * [UserRepository.isAvailable] for the given user.
+ *
+ * No actions are taken if the user is already in requested state.
+ *
+ * @throws IllegalArgumentException if called for an unsupported user type
+ */
+ suspend fun requestState(user: User, available: Boolean)
+}
+
+private const val TAG = "UserRepository"
+
+private data class UserWithState(val user: User, val available: Boolean)
+
+private typealias UserStateMap = Map<UserHandle, UserWithState>
+
+/** Tracks and publishes state for the parent user and associated profiles. */
+class UserRepositoryImpl
+@VisibleForTesting
+constructor(
+ private val profileParent: UserHandle,
+ private val userManager: UserManager,
+ /** A flow of events which represent user-state changes from [UserManager]. */
+ private val userEvents: Flow<UserEvent>,
+ scope: CoroutineScope,
+ private val backgroundDispatcher: CoroutineDispatcher
+) : UserRepository {
+ @Inject
+ constructor(
+ @ApplicationContext context: Context,
+ @ProfileParent profileParent: UserHandle,
+ userManager: UserManager,
+ @Main scope: CoroutineScope,
+ @Background background: CoroutineDispatcher
+ ) : this(
+ profileParent,
+ userManager,
+ userEvents = userBroadcastFlow(context, profileParent),
+ scope,
+ background
+ )
+
+ data class UserEvent(val action: String, val user: UserHandle, val quietMode: Boolean = false)
+
+ /**
+ * An exception which indicates that an inconsistency exists between the user state map and the
+ * rest of the system.
+ */
+ internal class UserStateException(
+ override val message: String,
+ val event: UserEvent,
+ override val cause: Throwable? = null
+ ) : RuntimeException("$message: event=$event", cause)
+
+ private val usersWithState: Flow<UserStateMap> =
+ userEvents
+ .onStart { emit(UserEvent(INITIALIZE, profileParent)) }
+ .onEach { Log.i("UserDataSource", "userEvent: $it") }
+ .runningFold<UserEvent, UserStateMap>(emptyMap()) { users, event ->
+ try {
+ // Handle an action by performing some operation, then returning a new map
+ when (event.action) {
+ INITIALIZE -> createNewUserStateMap(profileParent)
+ ACTION_PROFILE_ADDED -> handleProfileAdded(event, users)
+ ACTION_PROFILE_REMOVED -> handleProfileRemoved(event, users)
+ ACTION_MANAGED_PROFILE_UNAVAILABLE,
+ ACTION_MANAGED_PROFILE_AVAILABLE,
+ ACTION_PROFILE_AVAILABLE,
+ ACTION_PROFILE_UNAVAILABLE -> handleAvailability(event, users)
+ else -> {
+ Log.w(TAG, "Unhandled event: $event)")
+ users
+ }
+ }
+ } catch (e: UserStateException) {
+ Log.e(TAG, "An error occurred handling an event: ${e.event}", e)
+ Log.e(TAG, "Attempting to recover...")
+ createNewUserStateMap(profileParent)
+ }
+ }
+ .onEach { Log.i("UserDataSource", "userStateMap: $it") }
+ .stateIn(scope, SharingStarted.Eagerly, emptyMap())
+ .filterNot { it.isEmpty() }
+
+ override val users: Flow<Map<UserHandle, User>> =
+ usersWithState.map { map -> map.mapValues { it.value.user } }.distinctUntilChanged()
+
+ private val availability: Flow<Map<UserHandle, Boolean>> =
+ usersWithState.map { map -> map.mapValues { it.value.available } }.distinctUntilChanged()
+
+ override fun isAvailable(user: User): Flow<Boolean> {
+ return isAvailable(user.handle)
+ }
+
+ @VisibleForTesting
+ fun isAvailable(handle: UserHandle): Flow<Boolean> {
+ return availability.map { it[handle] ?: false }
+ }
+
+ override suspend fun requestState(user: User, available: Boolean) {
+ require(user.type == User.Type.PROFILE) { "Only profile users are supported" }
+ return requestState(user.handle, available)
+ }
+
+ @VisibleForTesting
+ suspend fun requestState(user: UserHandle, available: Boolean) {
+ return withContext(backgroundDispatcher) {
+ Log.i(TAG, "requestQuietModeEnabled: ${!available} for user $user")
+ userManager.requestQuietModeEnabled(/* enableQuietMode = */ !available, user)
+ }
+ }
+
+ private fun handleAvailability(event: UserEvent, current: UserStateMap): UserStateMap {
+ val userEntry =
+ current[event.user]
+ ?: throw UserStateException("User was not present in the map", event)
+ return current + (event.user to userEntry.copy(available = !event.quietMode))
+ }
+
+ private fun handleProfileRemoved(event: UserEvent, current: UserStateMap): UserStateMap {
+ if (!current.containsKey(event.user)) {
+ throw UserStateException("User was not present in the map", event)
+ }
+ return current.filterKeys { it != event.user }
+ }
+
+ private suspend fun handleProfileAdded(event: UserEvent, current: UserStateMap): UserStateMap {
+ val user =
+ try {
+ requireNotNull(readUser(event.user))
+ } catch (e: Exception) {
+ throw UserStateException("Failed to read user from UserManager", event, e)
+ }
+ return current + (event.user to UserWithState(user, !event.quietMode))
+ }
+
+ private suspend fun createNewUserStateMap(user: UserHandle): UserStateMap {
+ val profiles = readProfileGroup(user)
+ return profiles
+ .mapNotNull { userInfo ->
+ userInfo.toUser()?.let { user -> UserWithState(user, userInfo.isAvailable()) }
+ }
+ .associateBy { it.user.handle }
+ }
+
+ private suspend fun readProfileGroup(handle: UserHandle): List<UserInfo> {
+ return withContext(backgroundDispatcher) {
+ @Suppress("DEPRECATION") userManager.getEnabledProfiles(handle.identifier)
+ }
+ .toList()
+ }
+
+ /** Read [UserInfo] from [UserManager], or null if not found or an unsupported type. */
+ private suspend fun readUser(user: UserHandle): User? {
+ val userInfo =
+ withContext(backgroundDispatcher) { userManager.getUserInfo(user.identifier) }
+ return userInfo?.let { info ->
+ info.getSupportedUserRole()?.let { role -> User(info.id, role) }
+ }
+ }
+}
+
+/** Used with [broadcastFlow] to transform a UserManager broadcast action into a [UserEvent]. */
+private fun Intent.toUserEvent(): UserEvent? {
+ val action = action
+ val user = extras?.getParcelable(EXTRA_USER, UserHandle::class.java)
+ val quietMode = extras?.getBoolean(EXTRA_QUIET_MODE, false) ?: false
+ return if (user == null || action == null) {
+ null
+ } else {
+ UserEvent(action, user, quietMode)
+ }
+}
+
+const val INITIALIZE = "INITIALIZE"
+
+private fun createFilter(actions: Iterable<String>): IntentFilter {
+ return IntentFilter().apply { actions.forEach(::addAction) }
+}
+
+private fun UserInfo?.isAvailable(): Boolean {
+ return this?.isQuietModeEnabled != true
+}
+
+private fun userBroadcastFlow(context: Context, profileParent: UserHandle): Flow<UserEvent> {
+ val userActions =
+ setOf(
+ ACTION_PROFILE_ADDED,
+ ACTION_PROFILE_REMOVED,
+
+ // Quiet mode enabled/disabled for managed
+ // From: UserController.broadcastProfileAvailabilityChanges
+ // In response to setQuietModeEnabled
+ ACTION_MANAGED_PROFILE_AVAILABLE, // quiet mode, sent for manage profiles only
+ ACTION_MANAGED_PROFILE_UNAVAILABLE, // quiet mode, sent for manage profiles only
+
+ // Quiet mode toggled for profile type, requires flag 'android.os.allow_private_profile
+ // true'
+ ACTION_PROFILE_AVAILABLE, // quiet mode,
+ ACTION_PROFILE_UNAVAILABLE, // quiet mode, sent for any profile type
+ )
+ return broadcastFlow(context, createFilter(userActions), profileParent, Intent::toUserEvent)
+}
diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt b/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt
new file mode 100644
index 00000000..94f985e7
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/data/repository/UserRepositoryModule.kt
@@ -0,0 +1,34 @@
+package com.android.intentresolver.v2.data.repository
+
+import android.content.Context
+import android.os.UserHandle
+import android.os.UserManager
+import com.android.intentresolver.inject.ApplicationUser
+import com.android.intentresolver.inject.ProfileParent
+import dagger.Binds
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.qualifiers.ApplicationContext
+import dagger.hilt.components.SingletonComponent
+import javax.inject.Singleton
+
+@Module
+@InstallIn(SingletonComponent::class)
+interface UserRepositoryModule {
+ companion object {
+ @Provides
+ @Singleton
+ @ApplicationUser
+ fun applicationUser(@ApplicationContext context: Context): UserHandle = context.user
+
+ @Provides
+ @Singleton
+ @ProfileParent
+ fun profileParent(@ApplicationUser user: UserHandle, userManager: UserManager): UserHandle {
+ return userManager.getProfileParent(user) ?: user
+ }
+ }
+
+ @Binds @Singleton fun userRepository(impl: UserRepositoryImpl): UserRepository
+}
diff --git a/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt b/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt
new file mode 100644
index 00000000..7ee78d91
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/data/repository/UserScopedService.kt
@@ -0,0 +1,46 @@
+package com.android.intentresolver.v2.data.repository
+
+import android.content.Context
+import androidx.core.content.getSystemService
+import com.android.intentresolver.v2.data.model.User
+
+/**
+ * Provides cached instances of a [system service][Context.getSystemService] created with
+ * [the context of a specified user][Context.createContextAsUser].
+ *
+ * System services which have only `@UserHandleAware` APIs operate on the user id available from
+ * [Context.getUser], the context used to retrieve the service. This utility helps adapt a per-user
+ * API model to work in multi-user manner.
+ *
+ * Example usage:
+ * ```
+ * val usageStats = userScopedService<UsageStatsManager>(context)
+ *
+ * fun getStatsForUser(
+ * user: User,
+ * from: Long,
+ * to: Long
+ * ): UsageStats {
+ * return usageStats.forUser(user)
+ * .queryUsageStats(INTERVAL_BEST, from, to)
+ * }
+ * ```
+ */
+interface UserScopedService<T> {
+ fun forUser(user: User): T
+}
+
+inline fun <reified T> userScopedService(context: Context): UserScopedService<T> {
+ return object : UserScopedService<T> {
+ private val map = mutableMapOf<User, T>()
+
+ override fun forUser(user: User): T {
+ return synchronized(this) {
+ map.getOrPut(user) {
+ val userContext = context.createContextAsUser(user.handle, 0)
+ requireNotNull(userContext.getSystemService())
+ }
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java b/java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java
new file mode 100644
index 00000000..2f1e1b59
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/emptystate/EmptyStateUiHelper.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.v2.emptystate;
+
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+
+/**
+ * Helper for building `MultiProfilePagerAdapter` tab UIs for profile tabs that are "blocked" by
+ * some empty-state status.
+ */
+public class EmptyStateUiHelper {
+ private final Supplier<Optional<Integer>> mContainerBottomPaddingOverrideSupplier;
+ private final View mEmptyStateView;
+ private final View mListView;
+ private final View mEmptyStateContainerView;
+ private final TextView mEmptyStateTitleView;
+ private final TextView mEmptyStateSubtitleView;
+ private final Button mEmptyStateButtonView;
+ private final View mEmptyStateProgressView;
+ private final View mEmptyStateEmptyView;
+
+ public EmptyStateUiHelper(
+ ViewGroup rootView,
+ int listViewResourceId,
+ Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
+ mContainerBottomPaddingOverrideSupplier = containerBottomPaddingOverrideSupplier;
+ mEmptyStateView =
+ rootView.requireViewById(com.android.internal.R.id.resolver_empty_state);
+ mListView = rootView.requireViewById(listViewResourceId);
+ mEmptyStateContainerView = mEmptyStateView.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_container);
+ mEmptyStateTitleView = mEmptyStateView.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_title);
+ mEmptyStateSubtitleView = mEmptyStateView.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_subtitle);
+ mEmptyStateButtonView = mEmptyStateView.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_button);
+ mEmptyStateProgressView = mEmptyStateView.requireViewById(
+ com.android.internal.R.id.resolver_empty_state_progress);
+ mEmptyStateEmptyView = mEmptyStateView.requireViewById(com.android.internal.R.id.empty);
+ }
+
+ /**
+ * Display the described empty state.
+ * @param emptyState the data describing the cause of this empty-state condition.
+ * @param buttonOnClick handler for a button that the user might be able to use to circumvent
+ * the empty-state condition. If null, no button will be displayed.
+ */
+ public void showEmptyState(EmptyState emptyState, View.OnClickListener buttonOnClick) {
+ resetViewVisibilities();
+ setupContainerPadding();
+
+ String title = emptyState.getTitle();
+ if (title != null) {
+ mEmptyStateTitleView.setVisibility(View.VISIBLE);
+ mEmptyStateTitleView.setText(title);
+ } else {
+ mEmptyStateTitleView.setVisibility(View.GONE);
+ }
+
+ String subtitle = emptyState.getSubtitle();
+ if (subtitle != null) {
+ mEmptyStateSubtitleView.setVisibility(View.VISIBLE);
+ mEmptyStateSubtitleView.setText(subtitle);
+ } else {
+ mEmptyStateSubtitleView.setVisibility(View.GONE);
+ }
+
+ mEmptyStateEmptyView.setVisibility(
+ emptyState.useDefaultEmptyView() ? View.VISIBLE : View.GONE);
+ // TODO: The EmptyState API says that if `useDefaultEmptyView()` is true, we'll ignore the
+ // state's specified title/subtitle; where (if anywhere) is that implemented?
+
+ mEmptyStateButtonView.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE);
+ mEmptyStateButtonView.setOnClickListener(buttonOnClick);
+
+ // Don't show the main list view when we're showing an empty state.
+ mListView.setVisibility(View.GONE);
+ }
+
+ /** Sets up the padding of the view containing the empty state screens. */
+ public void setupContainerPadding() {
+ Optional<Integer> bottomPaddingOverride = mContainerBottomPaddingOverrideSupplier.get();
+ bottomPaddingOverride.ifPresent(paddingBottom ->
+ mEmptyStateContainerView.setPadding(
+ mEmptyStateContainerView.getPaddingLeft(),
+ mEmptyStateContainerView.getPaddingTop(),
+ mEmptyStateContainerView.getPaddingRight(),
+ paddingBottom));
+ }
+
+ public void showSpinner() {
+ mEmptyStateTitleView.setVisibility(View.INVISIBLE);
+ // TODO: subtitle?
+ mEmptyStateButtonView.setVisibility(View.INVISIBLE);
+ mEmptyStateProgressView.setVisibility(View.VISIBLE);
+ mEmptyStateEmptyView.setVisibility(View.GONE);
+ }
+
+ public void hide() {
+ mEmptyStateView.setVisibility(View.GONE);
+ mListView.setVisibility(View.VISIBLE);
+ }
+
+ // TODO: this is exposed for testing so we can thoroughly prepare initial conditions that let us
+ // observe the resulting change. In reality it's only invoked as part of `showEmptyState()` and
+ // we could consider setting up narrower "realistic" preconditions to make assertions about the
+ // higher-level operation.
+ @VisibleForTesting
+ void resetViewVisibilities() {
+ mEmptyStateTitleView.setVisibility(View.VISIBLE);
+ mEmptyStateSubtitleView.setVisibility(View.VISIBLE);
+ mEmptyStateButtonView.setVisibility(View.INVISIBLE);
+ mEmptyStateProgressView.setVisibility(View.GONE);
+ mEmptyStateEmptyView.setVisibility(View.GONE);
+ mEmptyStateView.setVisibility(View.VISIBLE);
+ }
+}
+
diff --git a/java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java
new file mode 100644
index 00000000..e9d1bb34
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/emptystate/NoAppsAvailableEmptyStateProvider.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2.emptystate;
+
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_PERSONAL_APPS;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS;
+
+import android.app.admin.DevicePolicyEventLogger;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+import android.stats.devicepolicy.nano.DevicePolicyEnums;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.ResolvedComponentInfo;
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+import com.android.internal.R;
+
+import java.util.List;
+
+/**
+ * Chooser/ResolverActivity empty state provider that returns empty state which is shown when
+ * there are no apps available.
+ */
+public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider {
+
+ @NonNull
+ private final Context mContext;
+ @Nullable
+ private final UserHandle mWorkProfileUserHandle;
+ @Nullable
+ private final UserHandle mPersonalProfileUserHandle;
+ @NonNull
+ private final String mMetricsCategory;
+ @NonNull
+ private final UserHandle mTabOwnerUserHandleForLaunch;
+
+ public NoAppsAvailableEmptyStateProvider(@NonNull Context context,
+ @Nullable UserHandle workProfileUserHandle,
+ @Nullable UserHandle personalProfileUserHandle, @NonNull String metricsCategory,
+ @NonNull UserHandle tabOwnerUserHandleForLaunch) {
+ mContext = context;
+ mWorkProfileUserHandle = workProfileUserHandle;
+ mPersonalProfileUserHandle = personalProfileUserHandle;
+ mMetricsCategory = metricsCategory;
+ mTabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch;
+ }
+
+ @Nullable
+ @Override
+ @SuppressWarnings("ReferenceEquality")
+ public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ UserHandle listUserHandle = resolverListAdapter.getUserHandle();
+
+ if (mWorkProfileUserHandle != null
+ && (mTabOwnerUserHandleForLaunch.equals(listUserHandle)
+ || !hasAppsInOtherProfile(resolverListAdapter))) {
+
+ String title;
+ if (listUserHandle == mPersonalProfileUserHandle) {
+ title = mContext.getSystemService(
+ DevicePolicyManager.class).getResources().getString(
+ RESOLVER_NO_PERSONAL_APPS,
+ () -> mContext.getString(R.string.resolver_no_personal_apps_available));
+ } else {
+ title = mContext.getSystemService(
+ DevicePolicyManager.class).getResources().getString(
+ RESOLVER_NO_WORK_APPS,
+ () -> mContext.getString(R.string.resolver_no_work_apps_available));
+ }
+
+ return new NoAppsAvailableEmptyState(
+ title, mMetricsCategory,
+ /* isPersonalProfile= */ listUserHandle == mPersonalProfileUserHandle
+ );
+ } else if (mWorkProfileUserHandle == null) {
+ // Return default empty state without tracking
+ return new DefaultEmptyState();
+ }
+
+ return null;
+ }
+
+ private boolean hasAppsInOtherProfile(ResolverListAdapter adapter) {
+ if (mWorkProfileUserHandle == null) {
+ return false;
+ }
+ List<ResolvedComponentInfo> resolversForIntent =
+ adapter.getResolversForUser(mTabOwnerUserHandleForLaunch);
+ for (ResolvedComponentInfo info : resolversForIntent) {
+ ResolveInfo resolveInfo = info.getResolveInfoAt(0);
+ if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static class DefaultEmptyState implements EmptyState {
+ @Override
+ public boolean useDefaultEmptyView() {
+ return true;
+ }
+ }
+
+ public static class NoAppsAvailableEmptyState implements EmptyState {
+
+ @NonNull
+ private final String mTitle;
+
+ @NonNull
+ private final String mMetricsCategory;
+
+ private final boolean mIsPersonalProfile;
+
+ public NoAppsAvailableEmptyState(@NonNull String title, @NonNull String metricsCategory,
+ boolean isPersonalProfile) {
+ mTitle = title;
+ mMetricsCategory = metricsCategory;
+ mIsPersonalProfile = isPersonalProfile;
+ }
+
+ @NonNull
+ @Override
+ public String getTitle() {
+ return mTitle;
+ }
+
+ @Override
+ public void onEmptyStateShown() {
+ DevicePolicyEventLogger.createEvent(
+ DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_APPS_RESOLVED)
+ .setStrings(mMetricsCategory)
+ .setBoolean(/*isPersonalProfile*/ mIsPersonalProfile)
+ .write();
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/emptystate/NoCrossProfileEmptyStateProvider.java b/java/src/com/android/intentresolver/v2/emptystate/NoCrossProfileEmptyStateProvider.java
new file mode 100644
index 00000000..b744c589
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/emptystate/NoCrossProfileEmptyStateProvider.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2.emptystate;
+
+import android.app.admin.DevicePolicyEventLogger;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.os.UserHandle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.emptystate.CrossProfileIntentsChecker;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+
+/**
+ * Empty state provider that does not allow cross profile sharing, it will return a blocker
+ * in case if the profile of the current tab is not the same as the profile of the calling app.
+ */
+public class NoCrossProfileEmptyStateProvider implements EmptyStateProvider {
+
+ private final UserHandle mPersonalProfileUserHandle;
+ private final EmptyState mNoWorkToPersonalEmptyState;
+ private final EmptyState mNoPersonalToWorkEmptyState;
+ private final CrossProfileIntentsChecker mCrossProfileIntentsChecker;
+ private final UserHandle mTabOwnerUserHandleForLaunch;
+
+ public NoCrossProfileEmptyStateProvider(UserHandle personalUserHandle,
+ EmptyState noWorkToPersonalEmptyState,
+ EmptyState noPersonalToWorkEmptyState,
+ CrossProfileIntentsChecker crossProfileIntentsChecker,
+ UserHandle tabOwnerUserHandleForLaunch) {
+ mPersonalProfileUserHandle = personalUserHandle;
+ mNoWorkToPersonalEmptyState = noWorkToPersonalEmptyState;
+ mNoPersonalToWorkEmptyState = noPersonalToWorkEmptyState;
+ mCrossProfileIntentsChecker = crossProfileIntentsChecker;
+ mTabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch;
+ }
+
+ @Nullable
+ @Override
+ public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ boolean shouldShowBlocker =
+ !mTabOwnerUserHandleForLaunch.equals(resolverListAdapter.getUserHandle())
+ && !mCrossProfileIntentsChecker
+ .hasCrossProfileIntents(resolverListAdapter.getIntents(),
+ mTabOwnerUserHandleForLaunch.getIdentifier(),
+ resolverListAdapter.getUserHandle().getIdentifier());
+
+ if (!shouldShowBlocker) {
+ return null;
+ }
+
+ if (resolverListAdapter.getUserHandle().equals(mPersonalProfileUserHandle)) {
+ return mNoWorkToPersonalEmptyState;
+ } else {
+ return mNoPersonalToWorkEmptyState;
+ }
+ }
+
+
+ /**
+ * Empty state that gets strings from the device policy manager and tracks events into
+ * event logger of the device policy events.
+ */
+ public static class DevicePolicyBlockerEmptyState implements EmptyState {
+
+ @NonNull
+ private final Context mContext;
+ private final String mDevicePolicyStringTitleId;
+ @StringRes
+ private final int mDefaultTitleResource;
+ private final String mDevicePolicyStringSubtitleId;
+ @StringRes
+ private final int mDefaultSubtitleResource;
+ private final int mEventId;
+ @NonNull
+ private final String mEventCategory;
+
+ public DevicePolicyBlockerEmptyState(@NonNull Context context,
+ String devicePolicyStringTitleId, @StringRes int defaultTitleResource,
+ String devicePolicyStringSubtitleId, @StringRes int defaultSubtitleResource,
+ int devicePolicyEventId, @NonNull String devicePolicyEventCategory) {
+ mContext = context;
+ mDevicePolicyStringTitleId = devicePolicyStringTitleId;
+ mDefaultTitleResource = defaultTitleResource;
+ mDevicePolicyStringSubtitleId = devicePolicyStringSubtitleId;
+ mDefaultSubtitleResource = defaultSubtitleResource;
+ mEventId = devicePolicyEventId;
+ mEventCategory = devicePolicyEventCategory;
+ }
+
+ @Nullable
+ @Override
+ public String getTitle() {
+ return mContext.getSystemService(DevicePolicyManager.class).getResources().getString(
+ mDevicePolicyStringTitleId,
+ () -> mContext.getString(mDefaultTitleResource));
+ }
+
+ @Nullable
+ @Override
+ public String getSubtitle() {
+ return mContext.getSystemService(DevicePolicyManager.class).getResources().getString(
+ mDevicePolicyStringSubtitleId,
+ () -> mContext.getString(mDefaultSubtitleResource));
+ }
+
+ @Override
+ public void onEmptyStateShown() {
+ DevicePolicyEventLogger.createEvent(mEventId)
+ .setStrings(mEventCategory)
+ .write();
+ }
+
+ @Override
+ public boolean shouldSkipDataRebuild() {
+ return true;
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/emptystate/WorkProfilePausedEmptyStateProvider.java b/java/src/com/android/intentresolver/v2/emptystate/WorkProfilePausedEmptyStateProvider.java
new file mode 100644
index 00000000..a6fee3ec
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/emptystate/WorkProfilePausedEmptyStateProvider.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2.emptystate;
+
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE;
+
+import android.app.admin.DevicePolicyEventLogger;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.os.UserHandle;
+import android.stats.devicepolicy.nano.DevicePolicyEnums;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.MultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener;
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolverListAdapter;
+import com.android.intentresolver.WorkProfileAvailabilityManager;
+import com.android.intentresolver.emptystate.EmptyState;
+import com.android.intentresolver.emptystate.EmptyStateProvider;
+
+/**
+ * Chooser/ResolverActivity empty state provider that returns empty state which is shown when
+ * work profile is paused and we need to show a button to enable it.
+ */
+public class WorkProfilePausedEmptyStateProvider implements EmptyStateProvider {
+
+ private final UserHandle mWorkProfileUserHandle;
+ private final WorkProfileAvailabilityManager mWorkProfileAvailability;
+ private final String mMetricsCategory;
+ private final OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
+ private final Context mContext;
+
+ public WorkProfilePausedEmptyStateProvider(@NonNull Context context,
+ @Nullable UserHandle workProfileUserHandle,
+ @NonNull WorkProfileAvailabilityManager workProfileAvailability,
+ @Nullable OnSwitchOnWorkSelectedListener onSwitchOnWorkSelectedListener,
+ @NonNull String metricsCategory) {
+ mContext = context;
+ mWorkProfileUserHandle = workProfileUserHandle;
+ mWorkProfileAvailability = workProfileAvailability;
+ mMetricsCategory = metricsCategory;
+ mOnSwitchOnWorkSelectedListener = onSwitchOnWorkSelectedListener;
+ }
+
+ @Nullable
+ @Override
+ public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ if (!resolverListAdapter.getUserHandle().equals(mWorkProfileUserHandle)
+ || !mWorkProfileAvailability.isQuietModeEnabled()
+ || resolverListAdapter.getCount() == 0) {
+ return null;
+ }
+
+ final String title = mContext.getSystemService(DevicePolicyManager.class)
+ .getResources().getString(RESOLVER_WORK_PAUSED_TITLE,
+ () -> mContext.getString(R.string.resolver_turn_on_work_apps));
+
+ return new WorkProfileOffEmptyState(title, (tab) -> {
+ tab.showSpinner();
+ if (mOnSwitchOnWorkSelectedListener != null) {
+ mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected();
+ }
+ mWorkProfileAvailability.requestQuietModeEnabled(false);
+ }, mMetricsCategory);
+ }
+
+ public static class WorkProfileOffEmptyState implements EmptyState {
+
+ private final String mTitle;
+ private final ClickListener mOnClick;
+ private final String mMetricsCategory;
+
+ public WorkProfileOffEmptyState(String title, @NonNull ClickListener onClick,
+ @NonNull String metricsCategory) {
+ mTitle = title;
+ mOnClick = onClick;
+ mMetricsCategory = metricsCategory;
+ }
+
+ @Nullable
+ @Override
+ public String getTitle() {
+ return mTitle;
+ }
+
+ @Nullable
+ @Override
+ public ClickListener getButtonClickListener() {
+ return mOnClick;
+ }
+
+ @Override
+ public void onEmptyStateShown() {
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_EMPTY_STATE_WORK_APPS_DISABLED)
+ .setStrings(mMetricsCategory)
+ .write();
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt b/java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt
new file mode 100644
index 00000000..4e8783f8
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/icons/TargetDataLoaderModule.kt
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2.icons
+
+import android.content.Context
+import androidx.lifecycle.Lifecycle
+import com.android.intentresolver.icons.DefaultTargetDataLoader
+import com.android.intentresolver.icons.TargetDataLoader
+import com.android.intentresolver.inject.ActivityOwned
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.android.components.ActivityComponent
+import dagger.hilt.android.qualifiers.ActivityContext
+import dagger.hilt.android.scopes.ActivityScoped
+
+@Module
+@InstallIn(ActivityComponent::class)
+object TargetDataLoaderModule {
+ @Provides
+ @ActivityScoped
+ fun targetDataLoader(
+ @ActivityContext context: Context,
+ @ActivityOwned lifecycle: Lifecycle,
+ ): TargetDataLoader = DefaultTargetDataLoader(context, lifecycle, isAudioCaptureDevice = false)
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt b/java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt
new file mode 100644
index 00000000..5855e2fc
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/FilterableComponents.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import com.android.intentresolver.ChooserRequestParameters
+
+/** A class that is able to identify components that should be hidden from the user. */
+interface FilterableComponents {
+ /** Whether this component should hidden from the user. */
+ fun isComponentFiltered(name: ComponentName): Boolean
+}
+
+/** A class that never filters components. */
+class NoComponentFiltering : FilterableComponents {
+ override fun isComponentFiltered(name: ComponentName): Boolean = false
+}
+
+/** A class that filters components by chooser request filter. */
+class ChooserRequestFilteredComponents(
+ private val chooserRequestParameters: ChooserRequestParameters,
+) : FilterableComponents {
+ override fun isComponentFiltered(name: ComponentName): Boolean =
+ chooserRequestParameters.filteredComponentNames.contains(name)
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt b/java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt
new file mode 100644
index 00000000..bb9394b4
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/IntentResolver.kt
@@ -0,0 +1,70 @@
+package com.android.intentresolver.v2.listcontroller
+
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.os.UserHandle
+import com.android.intentresolver.ResolvedComponentInfo
+
+/** A class for translating [Intent]s to [ResolvedComponentInfo]s. */
+interface IntentResolver {
+ /**
+ * Get data about all the ways the user with the specified handle can resolve any of the
+ * provided `intents`.
+ */
+ fun getResolversForIntentAsUser(
+ shouldGetResolvedFilter: Boolean,
+ shouldGetActivityMetadata: Boolean,
+ shouldGetOnlyDefaultActivities: Boolean,
+ intents: List<Intent>,
+ userHandle: UserHandle,
+ ): List<ResolvedComponentInfo>
+}
+
+/** Resolves [Intent]s using the [packageManager], deduping using the given [ResolveListDeduper]. */
+class IntentResolverImpl(
+ private val packageManager: PackageManager,
+ resolveListDeduper: ResolveListDeduper,
+) : IntentResolver, ResolveListDeduper by resolveListDeduper {
+ override fun getResolversForIntentAsUser(
+ shouldGetResolvedFilter: Boolean,
+ shouldGetActivityMetadata: Boolean,
+ shouldGetOnlyDefaultActivities: Boolean,
+ intents: List<Intent>,
+ userHandle: UserHandle,
+ ): List<ResolvedComponentInfo> {
+ val baseFlags =
+ ((if (shouldGetOnlyDefaultActivities) PackageManager.MATCH_DEFAULT_ONLY else 0) or
+ PackageManager.MATCH_DIRECT_BOOT_AWARE or
+ PackageManager.MATCH_DIRECT_BOOT_UNAWARE or
+ (if (shouldGetResolvedFilter) PackageManager.GET_RESOLVED_FILTER else 0) or
+ (if (shouldGetActivityMetadata) PackageManager.GET_META_DATA else 0) or
+ PackageManager.MATCH_CLONE_PROFILE)
+ return getResolversForIntentAsUserInternal(
+ intents,
+ userHandle,
+ baseFlags,
+ )
+ }
+
+ private fun getResolversForIntentAsUserInternal(
+ intents: List<Intent>,
+ userHandle: UserHandle,
+ baseFlags: Int,
+ ): List<ResolvedComponentInfo> = buildList {
+ for (intent in intents) {
+ var flags = baseFlags
+ if (intent.isWebIntent || intent.flags and Intent.FLAG_ACTIVITY_MATCH_EXTERNAL != 0) {
+ flags = flags or PackageManager.MATCH_INSTANT
+ }
+ // Because of AIDL bug, queryIntentActivitiesAsUser can't accept subclasses of Intent.
+ val fixedIntent =
+ if (intent.javaClass != Intent::class.java) {
+ Intent(intent)
+ } else {
+ intent
+ }
+ val infos = packageManager.queryIntentActivitiesAsUser(fixedIntent, flags, userHandle)
+ addToResolveListWithDedupe(this, fixedIntent, infos)
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt b/java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt
new file mode 100644
index 00000000..b2856526
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/LastChosenManager.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2.listcontroller
+
+import android.app.AppGlobals
+import android.content.ContentResolver
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.IPackageManager
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
+import android.os.RemoteException
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/** Class that stores and retrieves the most recently chosen resolutions. */
+interface LastChosenManager {
+
+ /** Returns the most recently chosen resolution. */
+ suspend fun getLastChosen(): ResolveInfo
+
+ /** Sets the most recently chosen resolution. */
+ suspend fun setLastChosen(intent: Intent, filter: IntentFilter, match: Int)
+}
+
+/**
+ * Stores and retrieves the most recently chosen resolutions using the [PackageManager] provided by
+ * the [packageManagerProvider].
+ */
+class PackageManagerLastChosenManager(
+ private val contentResolver: ContentResolver,
+ private val bgDispatcher: CoroutineDispatcher,
+ private val targetIntent: Intent,
+ private val packageManagerProvider: () -> IPackageManager = AppGlobals::getPackageManager,
+) : LastChosenManager {
+
+ @Throws(RemoteException::class)
+ override suspend fun getLastChosen(): ResolveInfo {
+ return withContext(bgDispatcher) {
+ packageManagerProvider()
+ .getLastChosenActivity(
+ targetIntent,
+ targetIntent.resolveTypeIfNeeded(contentResolver),
+ PackageManager.MATCH_DEFAULT_ONLY,
+ )
+ }
+ }
+
+ @Throws(RemoteException::class)
+ override suspend fun setLastChosen(intent: Intent, filter: IntentFilter, match: Int) {
+ return withContext(bgDispatcher) {
+ packageManagerProvider()
+ .setLastChosenActivity(
+ intent,
+ intent.resolveType(contentResolver),
+ PackageManager.MATCH_DEFAULT_ONLY,
+ filter,
+ match,
+ intent.component,
+ )
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/ListController.kt b/java/src/com/android/intentresolver/v2/listcontroller/ListController.kt
new file mode 100644
index 00000000..4ddab755
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/ListController.kt
@@ -0,0 +1,21 @@
+/*
+ * 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
new file mode 100644
index 00000000..cae2af95
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/PermissionChecker.kt
@@ -0,0 +1,34 @@
+package com.android.intentresolver.v2.listcontroller
+
+import android.app.ActivityManager
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/** Class for checking if a permission has been granted. */
+interface PermissionChecker {
+ /** Checks if the given [permission] has been granted. */
+ suspend fun checkComponentPermission(
+ permission: String,
+ uid: Int,
+ owningUid: Int,
+ exported: Boolean,
+ ): Int
+}
+
+/**
+ * Class for checking if a permission has been granted using the static
+ * [ActivityManager.checkComponentPermission].
+ */
+class ActivityManagerPermissionChecker(
+ private val bgDispatcher: CoroutineDispatcher,
+) : PermissionChecker {
+ override suspend fun checkComponentPermission(
+ permission: String,
+ uid: Int,
+ owningUid: Int,
+ exported: Boolean,
+ ): Int =
+ withContext(bgDispatcher) {
+ ActivityManager.checkComponentPermission(permission, uid, owningUid, exported)
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt b/java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt
new file mode 100644
index 00000000..8be45ba2
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/PinnableComponents.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.SharedPreferences
+
+/** A class that is able to identify components that should be pinned for the user. */
+interface PinnableComponents {
+ /** Whether this component is pinned by the user. */
+ fun isComponentPinned(name: ComponentName): Boolean
+}
+
+/** A class that never pins components. */
+class NoComponentPinning : PinnableComponents {
+ override fun isComponentPinned(name: ComponentName): Boolean = false
+}
+
+/** A class that determines pinnable components by user preferences. */
+class SharedPreferencesPinnedComponents(
+ private val pinnedSharedPreferences: SharedPreferences,
+) : PinnableComponents {
+ override fun isComponentPinned(name: ComponentName): Boolean =
+ pinnedSharedPreferences.getBoolean(name.flattenToString(), false)
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt b/java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt
new file mode 100644
index 00000000..f0b4bf3f
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/ResolveListDeduper.kt
@@ -0,0 +1,69 @@
+package com.android.intentresolver.v2.listcontroller
+
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ResolveInfo
+import android.util.Log
+import com.android.intentresolver.ResolvedComponentInfo
+
+/** A class for adding [ResolveInfo]s to a list of [ResolvedComponentInfo]s without duplicates. */
+interface ResolveListDeduper {
+ /**
+ * Adds [ResolveInfo]s in [from] to [ResolvedComponentInfo]s in [into], creating new
+ * [ResolvedComponentInfo]s when there is not already a corresponding one.
+ *
+ * This method may be destructive to both the given [into] list and the underlying
+ * [ResolvedComponentInfo]s.
+ */
+ fun addToResolveListWithDedupe(
+ into: MutableList<ResolvedComponentInfo>,
+ intent: Intent,
+ from: List<ResolveInfo>,
+ )
+}
+
+/**
+ * Default implementation for adding [ResolveInfo]s to a list of [ResolvedComponentInfo]s without
+ * duplicates. Uses the given [PinnableComponents] to determine the pinning state of newly created
+ * [ResolvedComponentInfo]s.
+ */
+class ResolveListDeduperImpl(pinnableComponents: PinnableComponents) :
+ ResolveListDeduper, PinnableComponents by pinnableComponents {
+ override fun addToResolveListWithDedupe(
+ into: MutableList<ResolvedComponentInfo>,
+ intent: Intent,
+ from: List<ResolveInfo>,
+ ) {
+ from.forEach { newInfo ->
+ if (newInfo.userHandle == null) {
+ Log.w(TAG, "Skipping ResolveInfo with no userHandle: $newInfo")
+ return@forEach
+ }
+ val oldInfo = into.firstOrNull { isSameResolvedComponent(newInfo, it) }
+ // If existing resolution found, add to existing and filter out
+ if (oldInfo != null) {
+ oldInfo.add(intent, newInfo)
+ } else {
+ with(newInfo.activityInfo) {
+ into.add(
+ ResolvedComponentInfo(
+ ComponentName(packageName, name),
+ intent,
+ newInfo,
+ )
+ .apply { isPinned = isComponentPinned(name) },
+ )
+ }
+ }
+ }
+ }
+
+ private fun isSameResolvedComponent(a: ResolveInfo, b: ResolvedComponentInfo): Boolean {
+ val ai = a.activityInfo
+ return ai.packageName == b.name.packageName && ai.name == b.name.className
+ }
+
+ companion object {
+ const val TAG = "ResolveListDeduper"
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt b/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt
new file mode 100644
index 00000000..e78bff00
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentFiltering.kt
@@ -0,0 +1,121 @@
+package com.android.intentresolver.v2.listcontroller
+
+import android.content.pm.PackageManager
+import android.util.Log
+import com.android.intentresolver.ResolvedComponentInfo
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.coroutineScope
+
+/** Provides filtering methods for lists of [ResolvedComponentInfo]. */
+interface ResolvedComponentFiltering {
+ /**
+ * Returns a list with all the [ResolvedComponentInfo] in [inputList], less the ones that are
+ * not eligible.
+ */
+ suspend fun filterIneligibleActivities(
+ inputList: List<ResolvedComponentInfo>,
+ ): List<ResolvedComponentInfo>
+
+ /** Filter out any low priority items. */
+ fun filterLowPriority(inputList: List<ResolvedComponentInfo>): List<ResolvedComponentInfo>
+}
+
+/**
+ * Default instantiation of the filtering methods for lists of [ResolvedComponentInfo].
+ *
+ * Binder calls are performed on the given [bgDispatcher] and permissions are checked as if launched
+ * from the given [launchedFromUid] UID. Component filtering is handled by the given
+ * [FilterableComponents] and permission checking is handled by the given [PermissionChecker].
+ */
+class ResolvedComponentFilteringImpl(
+ private val launchedFromUid: Int,
+ filterableComponents: FilterableComponents,
+ permissionChecker: PermissionChecker,
+) :
+ ResolvedComponentFiltering,
+ PermissionChecker by permissionChecker,
+ FilterableComponents by filterableComponents {
+ constructor(
+ bgDispatcher: CoroutineDispatcher,
+ launchedFromUid: Int,
+ filterableComponents: FilterableComponents,
+ ) : this(
+ launchedFromUid = launchedFromUid,
+ filterableComponents = filterableComponents,
+ permissionChecker = ActivityManagerPermissionChecker(bgDispatcher),
+ )
+
+ /**
+ * Filter out items that are filtered by [FilterableComponents] or do not have the necessary
+ * permissions.
+ */
+ override suspend fun filterIneligibleActivities(
+ inputList: List<ResolvedComponentInfo>,
+ ): List<ResolvedComponentInfo> = coroutineScope {
+ inputList
+ .map {
+ val activityInfo = it.getResolveInfoAt(0).activityInfo
+ if (isComponentFiltered(activityInfo.componentName)) {
+ CompletableDeferred(value = null)
+ } else {
+ // Do all permission checks in parallel
+ async {
+ val granted =
+ checkComponentPermission(
+ activityInfo.permission,
+ launchedFromUid,
+ activityInfo.applicationInfo.uid,
+ activityInfo.exported,
+ ) == PackageManager.PERMISSION_GRANTED
+ if (granted) it else null
+ }
+ }
+ }
+ .awaitAll()
+ .filterNotNull()
+ }
+
+ /**
+ * Filters out all elements starting with the first elements with a different priority or
+ * default status than the first element.
+ */
+ override fun filterLowPriority(
+ inputList: List<ResolvedComponentInfo>,
+ ): List<ResolvedComponentInfo> {
+ val firstResolveInfo = inputList[0].getResolveInfoAt(0)
+ // Only display the first matches that are either of equal
+ // priority or have asked to be default options.
+ val firstDiffIndex =
+ inputList.indexOfFirst { resolvedComponentInfo ->
+ val resolveInfo = resolvedComponentInfo.getResolveInfoAt(0)
+ if (firstResolveInfo == resolveInfo) {
+ false
+ } else {
+ if (DEBUG) {
+ Log.v(
+ TAG,
+ "${firstResolveInfo?.activityInfo?.name}=" +
+ "${firstResolveInfo?.priority}/${firstResolveInfo?.isDefault}" +
+ " vs ${resolveInfo?.activityInfo?.name}=" +
+ "${resolveInfo?.priority}/${resolveInfo?.isDefault}"
+ )
+ }
+ firstResolveInfo!!.priority != resolveInfo!!.priority ||
+ firstResolveInfo.isDefault != resolveInfo.isDefault
+ }
+ }
+ return if (firstDiffIndex == -1) {
+ inputList
+ } else {
+ inputList.subList(0, firstDiffIndex)
+ }
+ }
+
+ companion object {
+ private const val TAG = "ResolvedComponentFilter"
+ private const val DEBUG = false
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt b/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt
new file mode 100644
index 00000000..8ab41ef0
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/listcontroller/ResolvedComponentSorting.kt
@@ -0,0 +1,108 @@
+package com.android.intentresolver.v2.listcontroller
+
+import android.os.UserHandle
+import android.util.Log
+import com.android.intentresolver.ResolvedComponentInfo
+import com.android.intentresolver.chooser.DisplayResolveInfo
+import com.android.intentresolver.chooser.TargetInfo
+import com.android.intentresolver.model.AbstractResolverComparator
+import java.util.concurrent.atomic.AtomicReference
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+/** Provides sorting methods for lists of [ResolvedComponentInfo]. */
+interface ResolvedComponentSorting {
+ /** Returns the a copy of the [inputList] sorted by app share score. */
+ suspend fun sorted(inputList: List<ResolvedComponentInfo>?): List<ResolvedComponentInfo>?
+
+ /** Returns the app share score of the [target]. */
+ fun getScore(target: DisplayResolveInfo): Float
+
+ /** Returns the app share score of the [targetInfo]. */
+ fun getScore(targetInfo: TargetInfo): Float
+
+ /** Updates the model about [targetInfo]. */
+ suspend fun updateModel(targetInfo: TargetInfo)
+
+ /** Updates the model about Activity selection. */
+ suspend fun updateChooserCounts(packageName: String, user: UserHandle, action: String)
+
+ /** Cleans up resources. Nothing should be called after calling this. */
+ fun destroy()
+}
+
+/**
+ * Provides sorting methods using the given [resolverComparator].
+ *
+ * Long calculations and binder calls are performed on the given [bgDispatcher].
+ */
+class ResolvedComponentSortingImpl(
+ private val bgDispatcher: CoroutineDispatcher,
+ private val resolverComparator: AbstractResolverComparator,
+) : ResolvedComponentSorting {
+
+ private val computeComplete = AtomicReference<CompletableDeferred<Unit>?>(null)
+
+ @Throws(InterruptedException::class)
+ private suspend fun computeIfNeeded(inputList: List<ResolvedComponentInfo>) {
+ if (computeComplete.compareAndSet(null, CompletableDeferred())) {
+ resolverComparator.setCallBack { computeComplete.get()!!.complete(Unit) }
+ resolverComparator.compute(inputList)
+ }
+ with(computeComplete.get()!!) { if (isCompleted) return else return await() }
+ }
+
+ override suspend fun sorted(
+ inputList: List<ResolvedComponentInfo>?,
+ ): List<ResolvedComponentInfo>? {
+ if (inputList.isNullOrEmpty()) return inputList
+
+ return withContext(bgDispatcher) {
+ try {
+ val beforeRank = System.currentTimeMillis()
+ computeIfNeeded(inputList)
+ val sorted = inputList.sortedWith(resolverComparator)
+ val afterRank = System.currentTimeMillis()
+ if (DEBUG) {
+ Log.d(TAG, "Time Cost: ${afterRank - beforeRank}")
+ }
+ sorted
+ } catch (e: InterruptedException) {
+ Log.e(TAG, "Compute & Sort was interrupted: $e")
+ null
+ }
+ }
+ }
+
+ override fun getScore(target: DisplayResolveInfo): Float {
+ return resolverComparator.getScore(target)
+ }
+
+ override fun getScore(targetInfo: TargetInfo): Float {
+ return resolverComparator.getScore(targetInfo)
+ }
+
+ override suspend fun updateModel(targetInfo: TargetInfo) {
+ withContext(bgDispatcher) { resolverComparator.updateModel(targetInfo) }
+ }
+
+ override suspend fun updateChooserCounts(
+ packageName: String,
+ user: UserHandle,
+ action: String,
+ ) {
+ withContext(bgDispatcher) {
+ resolverComparator.updateChooserCounts(packageName, user, action)
+ }
+ }
+
+ override fun destroy() {
+ resolverComparator.destroy()
+ }
+
+ companion object {
+ private const val TAG = "ResolvedComponentSort"
+ private const val DEBUG = false
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt b/java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt
new file mode 100644
index 00000000..efbf053e
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/platform/ImageEditorModule.kt
@@ -0,0 +1,35 @@
+package com.android.intentresolver.v2.platform
+
+import android.content.ComponentName
+import android.content.res.Resources
+import androidx.annotation.StringRes
+import com.android.intentresolver.R
+import com.android.intentresolver.inject.ApplicationOwned
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import java.util.Optional
+import javax.inject.Qualifier
+import javax.inject.Singleton
+
+internal fun Resources.componentName(@StringRes resId: Int): ComponentName? {
+ check(getResourceTypeName(resId) == "string") { "resId must be a string" }
+ return ComponentName.unflattenFromString(getString(resId))
+}
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ImageEditor
+
+@Module
+@InstallIn(SingletonComponent::class)
+object ImageEditorModule {
+ /**
+ * The name of the preferred Activity to launch for editing images. This is added to Intents to
+ * edit images using Intent.ACTION_EDIT.
+ */
+ @Provides
+ @Singleton
+ @ImageEditor
+ fun imageEditorComponent(@ApplicationOwned resources: Resources) =
+ Optional.ofNullable(resources.componentName(R.string.config_systemImageEditor))
+}
diff --git a/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt b/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt
new file mode 100644
index 00000000..25ee9198
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/platform/NearbyShareModule.kt
@@ -0,0 +1,32 @@
+package com.android.intentresolver.v2.platform
+
+import android.content.ComponentName
+import android.content.res.Resources
+import android.provider.Settings.Secure.NEARBY_SHARING_COMPONENT
+import com.android.intentresolver.R
+import com.android.intentresolver.inject.ApplicationOwned
+import dagger.Module
+import dagger.Provides
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+import java.util.Optional
+import javax.inject.Qualifier
+import javax.inject.Singleton
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class NearbyShare
+
+@Module
+@InstallIn(SingletonComponent::class)
+object NearbyShareModule {
+
+ @Provides
+ @Singleton
+ @NearbyShare
+ fun nearbyShareComponent(@ApplicationOwned resources: Resources, settings: SecureSettings) =
+ Optional.ofNullable(
+ ComponentName.unflattenFromString(
+ settings.getString(NEARBY_SHARING_COMPONENT)?.ifEmpty { null }
+ ?: resources.getString(R.string.config_defaultNearbySharingComponent),
+ )
+ )
+}
diff --git a/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt b/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt
new file mode 100644
index 00000000..531152ba
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/platform/PlatformSecureSettings.kt
@@ -0,0 +1,30 @@
+package com.android.intentresolver.v2.platform
+
+import android.content.ContentResolver
+import android.provider.Settings
+import javax.inject.Inject
+
+/**
+ * Implements [SecureSettings] backed by Settings.Secure and a ContentResolver.
+ *
+ * These methods make Binder calls and may block, so use on the Main thread should be avoided.
+ */
+class PlatformSecureSettings @Inject constructor(private val resolver: ContentResolver) :
+ SecureSettings {
+
+ override fun getString(name: String): String? {
+ return Settings.Secure.getString(resolver, name)
+ }
+
+ override fun getInt(name: String): Int? {
+ return runCatching { Settings.Secure.getInt(resolver, name) }.getOrNull()
+ }
+
+ override fun getLong(name: String): Long? {
+ return runCatching { Settings.Secure.getLong(resolver, name) }.getOrNull()
+ }
+
+ override fun getFloat(name: String): Float? {
+ return runCatching { Settings.Secure.getFloat(resolver, name) }.getOrNull()
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt b/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt
new file mode 100644
index 00000000..62ee8ae9
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/platform/SecureSettings.kt
@@ -0,0 +1,25 @@
+package com.android.intentresolver.v2.platform
+
+import android.provider.Settings.SettingNotFoundException
+
+/**
+ * A component which provides access to values from [android.provider.Settings.Secure].
+ *
+ * All methods return nullable types instead of throwing [SettingNotFoundException] which yields
+ * cleaner, more idiomatic Kotlin code:
+ *
+ * // apply a default: val foo = settings.getInt(FOO) ?: DEFAULT_FOO
+ *
+ * // assert if missing: val required = settings.getInt(REQUIRED_VALUE) ?: error("required value
+ * missing")
+ */
+interface SecureSettings {
+
+ fun getString(name: String): String?
+
+ fun getInt(name: String): Int?
+
+ fun getLong(name: String): Long?
+
+ fun getFloat(name: String): Float?
+}
diff --git a/java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt b/java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt
new file mode 100644
index 00000000..18f47023
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/platform/SecureSettingsModule.kt
@@ -0,0 +1,14 @@
+package com.android.intentresolver.v2.platform
+
+import dagger.Binds
+import dagger.Module
+import dagger.Reusable
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@Module
+@InstallIn(SingletonComponent::class)
+interface SecureSettingsModule {
+
+ @Binds @Reusable fun secureSettings(settings: PlatformSecureSettings): SecureSettings
+}
diff --git a/java/src/com/android/intentresolver/v2/ui/ActionTitle.java b/java/src/com/android/intentresolver/v2/ui/ActionTitle.java
new file mode 100644
index 00000000..271c6f38
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/ui/ActionTitle.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.v2.ui;
+
+import android.content.Intent;
+import android.provider.MediaStore;
+
+import androidx.annotation.StringRes;
+
+import com.android.intentresolver.R;
+import com.android.intentresolver.v2.ResolverActivity;
+
+/**
+ * Provides a set of related resources for different use cases.
+ */
+public enum ActionTitle {
+ VIEW(Intent.ACTION_VIEW,
+ R.string.whichViewApplication,
+ R.string.whichViewApplicationNamed,
+ R.string.whichViewApplicationLabel),
+ EDIT(Intent.ACTION_EDIT,
+ R.string.whichEditApplication,
+ R.string.whichEditApplicationNamed,
+ R.string.whichEditApplicationLabel),
+ SEND(Intent.ACTION_SEND,
+ R.string.whichSendApplication,
+ R.string.whichSendApplicationNamed,
+ R.string.whichSendApplicationLabel),
+ SENDTO(Intent.ACTION_SENDTO,
+ R.string.whichSendToApplication,
+ R.string.whichSendToApplicationNamed,
+ R.string.whichSendToApplicationLabel),
+ SEND_MULTIPLE(Intent.ACTION_SEND_MULTIPLE,
+ R.string.whichSendApplication,
+ R.string.whichSendApplicationNamed,
+ R.string.whichSendApplicationLabel),
+ CAPTURE_IMAGE(MediaStore.ACTION_IMAGE_CAPTURE,
+ R.string.whichImageCaptureApplication,
+ R.string.whichImageCaptureApplicationNamed,
+ R.string.whichImageCaptureApplicationLabel),
+ DEFAULT(null,
+ R.string.whichApplication,
+ R.string.whichApplicationNamed,
+ R.string.whichApplicationLabel),
+ HOME(Intent.ACTION_MAIN,
+ R.string.whichHomeApplication,
+ R.string.whichHomeApplicationNamed,
+ R.string.whichHomeApplicationLabel);
+
+ // titles for layout that deals with http(s) intents
+ public static final int BROWSABLE_TITLE_RES = R.string.whichOpenLinksWith;
+ public static final int BROWSABLE_HOST_TITLE_RES = R.string.whichOpenHostLinksWith;
+ public static final int BROWSABLE_HOST_APP_TITLE_RES = R.string.whichOpenHostLinksWithApp;
+ public static final int BROWSABLE_APP_TITLE_RES = R.string.whichOpenLinksWithApp;
+
+ public final String action;
+ public final int titleRes;
+ public final int namedTitleRes;
+ public final @StringRes int labelRes;
+
+ ActionTitle(String action, int titleRes, int namedTitleRes, @StringRes int labelRes) {
+ this.action = action;
+ this.titleRes = titleRes;
+ this.namedTitleRes = namedTitleRes;
+ this.labelRes = labelRes;
+ }
+
+ public static ActionTitle forAction(String action) {
+ for (ActionTitle title : values()) {
+ if (title != HOME && action != null && action.equals(title.action)) {
+ return title;
+ }
+ }
+ return DEFAULT;
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/util/MutableLazy.kt b/java/src/com/android/intentresolver/v2/util/MutableLazy.kt
new file mode 100644
index 00000000..4ce9b7fd
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/util/MutableLazy.kt
@@ -0,0 +1,36 @@
+package com.android.intentresolver.v2.util
+
+import java.util.concurrent.atomic.AtomicReference
+import kotlin.reflect.KProperty
+
+/** A lazy delegate that can be changed to a new lazy or null at any time. */
+class MutableLazy<T>(initializer: () -> T?) : Lazy<T?> {
+
+ override val value: T?
+ get() = lazy.get()?.value
+
+ private var lazy: AtomicReference<Lazy<T?>?> = AtomicReference(lazy(initializer))
+
+ override fun isInitialized(): Boolean = lazy.get()?.isInitialized() != false
+
+ operator fun getValue(thisRef: Any?, property: KProperty<*>): T? =
+ lazy.get()?.getValue(thisRef, property)
+
+ /** Replace the existing lazy logic with the [newLazy] */
+ fun setLazy(newLazy: Lazy<T?>?) {
+ lazy.set(newLazy)
+ }
+
+ /** Replace the existing lazy logic with a [Lazy] created from the [newInitializer]. */
+ fun setLazy(newInitializer: () -> T?) {
+ lazy.set(lazy(newInitializer))
+ }
+
+ /** Set the lazy logic to null. */
+ fun clear() {
+ lazy.set(null)
+ }
+}
+
+/** Constructs a [MutableLazy] using the given [initializer] */
+fun <T> mutableLazy(initializer: () -> T?) = MutableLazy(initializer)
diff --git a/java/src/com/android/intentresolver/v2/validation/Findings.kt b/java/src/com/android/intentresolver/v2/validation/Findings.kt
new file mode 100644
index 00000000..9a3cc9c7
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/Findings.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.v2.validation
+
+import android.util.Log
+import com.android.intentresolver.v2.validation.Importance.CRITICAL
+import com.android.intentresolver.v2.validation.Importance.WARNING
+import kotlin.reflect.KClass
+
+sealed interface Finding {
+ val importance: Importance
+ val message: String
+}
+
+enum class Importance {
+ CRITICAL,
+ WARNING,
+}
+
+val Finding.logcatPriority
+ get() =
+ when (importance) {
+ CRITICAL -> Log.ERROR
+ else -> Log.WARN
+ }
+
+private fun formatMessage(key: String? = null, msg: String) = buildString {
+ key?.also { append("['$key']: ") }
+ append(msg)
+}
+
+data class IgnoredValue(
+ val key: String,
+ val reason: String,
+) : Finding {
+ override val importance = WARNING
+
+ override val message: String
+ get() = formatMessage(key, "Ignored. $reason")
+}
+
+data class RequiredValueMissing(
+ val key: String,
+ val allowedType: KClass<*>,
+) : Finding {
+
+ override val importance = CRITICAL
+
+ override val message: String
+ get() =
+ formatMessage(
+ key,
+ "expected value of ${allowedType.simpleName}, " + "but no value was present"
+ )
+}
+
+data class WrongElementType(
+ val key: String,
+ override val importance: Importance,
+ val container: KClass<*>,
+ val actualType: KClass<*>,
+ val expectedType: KClass<*>
+) : Finding {
+ override val message: String
+ get() =
+ formatMessage(
+ key,
+ "${container.simpleName} expected with elements of " +
+ "${expectedType.simpleName} " +
+ "but found ${actualType.simpleName} values instead"
+ )
+}
+
+data class ValueIsWrongType(
+ val key: String,
+ override val importance: Importance,
+ val actualType: KClass<*>,
+ val allowedTypes: List<KClass<*>>,
+) : Finding {
+
+ override val message: String
+ get() =
+ formatMessage(
+ key,
+ "expected value of ${allowedTypes.map(KClass<*>::simpleName)} " +
+ "but was ${actualType.simpleName}"
+ )
+}
+
+data class UncaughtException(val thrown: Throwable, val key: String? = null) : Finding {
+ override val importance: Importance
+ get() = CRITICAL
+ override val message: String
+ get() =
+ formatMessage(
+ key,
+ "An unhandled exception was caught during validation: " +
+ thrown.stackTraceToString()
+ )
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/Validation.kt b/java/src/com/android/intentresolver/v2/validation/Validation.kt
new file mode 100644
index 00000000..46939602
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/Validation.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.v2.validation
+
+import com.android.intentresolver.v2.validation.Importance.CRITICAL
+import com.android.intentresolver.v2.validation.Importance.WARNING
+
+/**
+ * Provides a mechanism for validating a result from a set of properties.
+ *
+ * The results of validation are provided as [findings].
+ */
+interface Validation {
+ val findings: List<Finding>
+
+ /**
+ * Require a valid property.
+ *
+ * If [property] is not valid, this [Validation] will be immediately completed as [Invalid].
+ *
+ * @param property the required property
+ * @return a valid **T**
+ */
+ @Throws(InvalidResultError::class) fun <T> required(property: Validator<T>): T
+
+ /**
+ * Request an optional value for a property.
+ *
+ * If [property] is not valid, this [Validation] will be immediately completed as [Invalid].
+ *
+ * @param property the required property
+ * @return a valid **T**
+ */
+ fun <T> optional(property: Validator<T>): T?
+
+ /**
+ * Report a property as __ignored__.
+ *
+ * The presence of any value will report a warning citing [reason].
+ */
+ fun <T> ignored(property: Validator<T>, reason: String)
+}
+
+/** Performs validation for a specific key -> value pair. */
+interface Validator<T> {
+ val key: String
+
+ /**
+ * Performs validation on a specific value from [source].
+ *
+ * @param source a source for reading the property value. Values are intentionally untyped
+ * (Any?) to avoid upstream code from making type assertions through type inference. Types are
+ * asserted later using a [Validator].
+ * @param importance the importance of any findings
+ */
+ fun validate(source: (String) -> Any?, importance: Importance): ValidationResult<T>
+}
+
+internal class InvalidResultError internal constructor() : Error()
+
+/**
+ * Perform a number of validations on the source, assembling and returning a Result.
+ *
+ * When an exception is thrown by [validate], it is caught here. In response, a failed
+ * [ValidationResult] is returned containing a [CRITICAL] [Finding] for the exception.
+ *
+ * @param validate perform validations and return a [ValidationResult]
+ */
+fun <T> validateFrom(source: (String) -> Any?, validate: Validation.() -> T): ValidationResult<T> {
+ val validation = ValidationImpl(source)
+ return runCatching { validate(validation) }
+ .fold(
+ onSuccess = { result -> Valid(result, validation.findings) },
+ onFailure = {
+ when (it) {
+ // A validator has interrupted validation. Return the findings.
+ is InvalidResultError -> Invalid(validation.findings)
+
+ // Some other exception was thrown from [validate],
+ else -> Invalid(findings = listOf(UncaughtException(it)))
+ }
+ }
+ )
+}
+
+private class ValidationImpl(val source: (String) -> Any?) : Validation {
+ override val findings = mutableListOf<Finding>()
+
+ override fun <T> optional(property: Validator<T>): T? = validate(property, WARNING)
+
+ override fun <T> required(property: Validator<T>): T {
+ return validate(property, CRITICAL) ?: throw InvalidResultError()
+ }
+
+ override fun <T> ignored(property: Validator<T>, reason: String) {
+ val result = property.validate(source, WARNING)
+ if (result.value != null) {
+ // Note: Any findings about the value (result.findings) are ignored.
+ findings += IgnoredValue(property.key, reason)
+ }
+ }
+
+ private fun <T> validate(property: Validator<T>, importance: Importance): T? {
+ return runCatching { property.validate(source, importance) }
+ .fold(
+ onSuccess = { result ->
+ findings += result.findings
+ result.value
+ },
+ onFailure = {
+ findings += UncaughtException(it, property.key)
+ null
+ }
+ )
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt
new file mode 100644
index 00000000..092cabe8
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/ValidationResult.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.v2.validation
+
+import android.util.Log
+
+sealed interface ValidationResult<T> {
+ val value: T?
+ val findings: List<Finding>
+
+ fun isSuccess() = value != null
+
+ fun getOrThrow(): T =
+ checkNotNull(value) { "The result was invalid: " + findings.joinToString(separator = "\n") }
+
+ fun <T> reportToLogcat(tag: String) {
+ findings.forEach { Log.println(it.logcatPriority, tag, it.toString()) }
+ }
+}
+
+data class Valid<T>(override val value: T?, override val findings: List<Finding> = emptyList()) :
+ ValidationResult<T>
+
+data class Invalid<T>(override val findings: List<Finding>) : ValidationResult<T> {
+ override val value: T? = null
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt
new file mode 100644
index 00000000..3cefeb15
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/types/IntentOrUri.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.v2.validation.types
+
+import android.content.Intent
+import android.net.Uri
+import com.android.intentresolver.v2.validation.Importance
+import com.android.intentresolver.v2.validation.RequiredValueMissing
+import com.android.intentresolver.v2.validation.Valid
+import com.android.intentresolver.v2.validation.ValidationResult
+import com.android.intentresolver.v2.validation.Validator
+import com.android.intentresolver.v2.validation.ValueIsWrongType
+
+class IntentOrUri(override val key: String) : Validator<Intent> {
+
+ override fun validate(
+ source: (String) -> Any?,
+ importance: Importance
+ ): ValidationResult<Intent> {
+
+ return when (val value = source(key)) {
+ // An intent, return it.
+ is Intent -> Valid(value)
+
+ // A Uri was supplied.
+ // Unfortunately, converting Uri -> Intent requires a toString().
+ is Uri -> Valid(Intent.parseUri(value.toString(), Intent.URI_INTENT_SCHEME))
+
+ // No value present.
+ null -> createResult(importance, RequiredValueMissing(key, Intent::class))
+
+ // Some other type.
+ else -> {
+ return createResult(
+ importance,
+ ValueIsWrongType(
+ key,
+ importance,
+ actualType = value::class,
+ allowedTypes = listOf(Intent::class, Uri::class)
+ )
+ )
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt
new file mode 100644
index 00000000..c6c4abba
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/types/ParceledArray.kt
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.v2.validation.types
+
+import com.android.intentresolver.v2.validation.Importance
+import com.android.intentresolver.v2.validation.RequiredValueMissing
+import com.android.intentresolver.v2.validation.Valid
+import com.android.intentresolver.v2.validation.ValidationResult
+import com.android.intentresolver.v2.validation.Validator
+import com.android.intentresolver.v2.validation.ValueIsWrongType
+import com.android.intentresolver.v2.validation.WrongElementType
+import kotlin.reflect.KClass
+import kotlin.reflect.cast
+
+class ParceledArray<T : Any>(
+ override val key: String,
+ private val elementType: KClass<T>,
+) : Validator<List<T>> {
+
+ override fun validate(
+ source: (String) -> Any?,
+ importance: Importance
+ ): ValidationResult<List<T>> {
+
+ return when (val value: Any? = source(key)) {
+ // No value present.
+ null -> createResult(importance, RequiredValueMissing(key, elementType))
+
+ // A parcel does not transfer the element type information for parcelable
+ // arrays. This leads to a restored type of Array<Parcelable>, which is
+ // incompatible with Array<T : Parcelable>.
+
+ // To handle this safely, treat as Array<*>, assert contents of the expected
+ // parcelable type, and return as a list.
+
+ is Array<*> -> {
+ val invalid = value.filterNotNull().firstOrNull { !elementType.isInstance(it) }
+ when (invalid) {
+ // No invalid elements, result is ok.
+ null -> Valid(value.map { elementType.cast(it) })
+
+ // At least one incorrect element type found.
+ else ->
+ createResult(
+ importance,
+ WrongElementType(
+ key,
+ importance,
+ actualType = invalid::class,
+ container = Array::class,
+ expectedType = elementType
+ )
+ )
+ }
+ }
+
+ // The value is not an Array at all.
+ else ->
+ createResult(
+ importance,
+ ValueIsWrongType(
+ key,
+ importance,
+ actualType = value::class,
+ allowedTypes = listOf(elementType)
+ )
+ )
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt
new file mode 100644
index 00000000..3287b84b
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/types/SimpleValue.kt
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.v2.validation.types
+
+import com.android.intentresolver.v2.validation.Importance
+import com.android.intentresolver.v2.validation.RequiredValueMissing
+import com.android.intentresolver.v2.validation.Valid
+import com.android.intentresolver.v2.validation.ValidationResult
+import com.android.intentresolver.v2.validation.Validator
+import com.android.intentresolver.v2.validation.ValueIsWrongType
+import kotlin.reflect.KClass
+import kotlin.reflect.cast
+
+class SimpleValue<T : Any>(
+ override val key: String,
+ private val expected: KClass<T>,
+) : Validator<T> {
+
+ override fun validate(source: (String) -> Any?, importance: Importance): ValidationResult<T> {
+ val value: Any? = source(key)
+ return when {
+ // The value is present and of the expected type.
+ expected.isInstance(value) -> return Valid(expected.cast(value))
+
+ // No value is present.
+ value == null -> createResult(importance, RequiredValueMissing(key, expected))
+
+ // The value is some other type.
+ else ->
+ createResult(
+ importance,
+ ValueIsWrongType(
+ key,
+ importance,
+ actualType = value::class,
+ allowedTypes = listOf(expected)
+ )
+ )
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/v2/validation/types/Validators.kt b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt
new file mode 100644
index 00000000..4e6e5dff
--- /dev/null
+++ b/java/src/com/android/intentresolver/v2/validation/types/Validators.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.v2.validation.types
+
+import com.android.intentresolver.v2.validation.Finding
+import com.android.intentresolver.v2.validation.Importance
+import com.android.intentresolver.v2.validation.Importance.CRITICAL
+import com.android.intentresolver.v2.validation.Importance.WARNING
+import com.android.intentresolver.v2.validation.Invalid
+import com.android.intentresolver.v2.validation.Valid
+import com.android.intentresolver.v2.validation.ValidationResult
+import com.android.intentresolver.v2.validation.Validator
+
+inline fun <reified T : Any> value(key: String): Validator<T> {
+ return SimpleValue(key, T::class)
+}
+
+inline fun <reified T : Any> array(key: String): Validator<List<T>> {
+ return ParceledArray(key, T::class)
+}
+
+/**
+ * Convenience function to wrap a finding in an appropriate result type.
+ *
+ * An error [finding] is suppressed when [importance] == [WARNING]
+ */
+internal fun <T> createResult(importance: Importance, finding: Finding): ValidationResult<T> {
+ return when (importance) {
+ WARNING -> Valid(null, listOf(finding).filter { it.importance == WARNING })
+ CRITICAL -> Invalid(listOf(finding))
+ }
+}
diff --git a/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt b/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt
new file mode 100644
index 00000000..26464ca1
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ChooserNestedScrollView.kt
@@ -0,0 +1,90 @@
+package com.android.intentresolver.widget
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.View
+import android.widget.LinearLayout
+import androidx.core.view.ScrollingView
+import androidx.core.view.marginBottom
+import androidx.core.view.marginLeft
+import androidx.core.view.marginRight
+import androidx.core.view.marginTop
+import androidx.core.widget.NestedScrollView
+
+/**
+ * A narrowly tailored [NestedScrollView] to be used inside [ResolverDrawerLayout] and help to
+ * orchestrate content preview scrolling. It expects one [LinearLayout] child with
+ * [LinearLayout.VERTICAL] orientation. If the child has more than one child, the first its child
+ * will be made scrollable (it is expected to be a content preview view).
+ */
+class ChooserNestedScrollView : NestedScrollView {
+ constructor(context: Context) : super(context)
+ constructor(context: Context, attrs: AttributeSet?) : super(context, attrs)
+ constructor(
+ context: Context,
+ attrs: AttributeSet?,
+ defStyleAttr: Int
+ ) : super(context, attrs, defStyleAttr)
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val content =
+ getChildAt(0) as? LinearLayout ?: error("Exactly one child, LinerLayout, is expected")
+ require(content.orientation == LinearLayout.VERTICAL) { "VERTICAL orientation is expected" }
+ require(MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
+ "Expected to have an exact width"
+ }
+
+ val lp = content.layoutParams ?: error("LayoutParams is missing")
+ val contentWidthSpec =
+ getChildMeasureSpec(
+ widthMeasureSpec,
+ paddingLeft + content.marginLeft + content.marginRight + paddingRight,
+ lp.width
+ )
+ val contentHeightSpec =
+ getChildMeasureSpec(
+ heightMeasureSpec,
+ paddingTop + content.marginTop + content.marginBottom + paddingBottom,
+ lp.height
+ )
+ content.measure(contentWidthSpec, contentHeightSpec)
+
+ if (content.childCount > 1) {
+ // We expect that the first child should be scrollable up
+ val child = content.getChildAt(0)
+ val height =
+ MeasureSpec.getSize(heightMeasureSpec) +
+ child.measuredHeight +
+ child.marginTop +
+ child.marginBottom
+
+ content.measure(
+ contentWidthSpec,
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.getMode(heightMeasureSpec))
+ )
+ }
+ setMeasuredDimension(
+ MeasureSpec.getSize(widthMeasureSpec),
+ minOf(
+ MeasureSpec.getSize(heightMeasureSpec),
+ paddingTop +
+ content.marginTop +
+ content.measuredHeight +
+ content.marginBottom +
+ paddingBottom
+ )
+ )
+ }
+
+ override fun onNestedPreScroll(target: View, dx: Int, dy: Int, consumed: IntArray, type: Int) {
+ // let the parent scroll
+ super.onNestedPreScroll(target, dx, dy, consumed, type)
+ // scroll ourselves, if recycler has not scrolled
+ val delta = dy - consumed[1]
+ if (delta > 0 && target is ScrollingView && !target.canScrollVertically(-1)) {
+ val preScrollY = scrollY
+ scrollBy(0, delta)
+ consumed[1] += scrollY - preScrollY
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
index de76a1d2..2c8140d9 100644
--- a/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
+++ b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
@@ -19,7 +19,6 @@ package com.android.intentresolver.widget;
import static android.content.res.Resources.ID_NULL;
-import android.annotation.IdRes;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Canvas;
@@ -45,6 +44,10 @@ import android.view.animation.AnimationUtils;
import android.widget.AbsListView;
import android.widget.OverScroller;
+import androidx.annotation.IdRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.core.view.ScrollingView;
import androidx.recyclerview.widget.RecyclerView;
import com.android.intentresolver.R;
@@ -131,6 +134,9 @@ public class ResolverDrawerLayout extends ViewGroup {
private AbsListView mNestedListChild;
private RecyclerView mNestedRecyclerChild;
+ @Nullable
+ private final ScrollablePreviewFlingLogicDelegate mFlingLogicDelegate;
+
private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener =
new ViewTreeObserver.OnTouchModeChangeListener() {
@Override
@@ -167,6 +173,12 @@ public class ResolverDrawerLayout extends ViewGroup {
mIgnoreOffsetTopLimitViewId = a.getResourceId(
R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit, ID_NULL);
}
+ mFlingLogicDelegate =
+ a.getBoolean(
+ R.styleable.ResolverDrawerLayout_useScrollablePreviewNestedFlingLogic,
+ false)
+ ? new ScrollablePreviewFlingLogicDelegate() {}
+ : null;
a.recycle();
mScrollIndicatorDrawable = mContext.getDrawable(
@@ -832,6 +844,9 @@ public class ResolverDrawerLayout extends ViewGroup {
@Override
public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+ if (mFlingLogicDelegate != null) {
+ return mFlingLogicDelegate.onNestedPreFling(this, target, velocityX, velocityY);
+ }
if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) {
smoothScrollTo(0, velocityY);
return true;
@@ -841,9 +856,12 @@ public class ResolverDrawerLayout extends ViewGroup {
@Override
public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+ if (mFlingLogicDelegate != null) {
+ return mFlingLogicDelegate.onNestedFling(this, target, velocityX, velocityY, consumed);
+ }
// TODO: find a more suitable way to fix it.
// RecyclerView started reporting `consumed` as true whenever a scrolling is enabled,
- // previously the value was based whether the fling can be performed in given direction
+ // previously the value was based on whether the fling can be performed in given direction
// i.e. whether it is at the top or at the bottom. isRecyclerViewAtTheTop method is a
// workaround that restores the legacy functionality.
boolean shouldConsume = (Math.abs(velocityY) > mMinFlingVelocity)
@@ -885,6 +903,13 @@ public class ResolverDrawerLayout extends ViewGroup {
&& firstChild.getTop() >= recyclerView.getPaddingTop();
}
+ private static boolean isFlingTargetAtTop(View target) {
+ if (target instanceof ScrollingView) {
+ return !target.canScrollVertically(-1);
+ }
+ return false;
+ }
+
private boolean performAccessibilityActionCommon(int action) {
switch (action) {
case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
@@ -974,7 +999,7 @@ public class ResolverDrawerLayout extends ViewGroup {
}
@Override
- public void onDrawForeground(Canvas canvas) {
+ public void onDrawForeground(@NonNull Canvas canvas) {
if (mScrollIndicatorDrawable != null) {
mScrollIndicatorDrawable.draw(canvas);
}
@@ -1299,4 +1324,74 @@ public class ResolverDrawerLayout extends ViewGroup {
}
return mMetricsLogger;
}
+
+ /**
+ * Controlled by
+ * {@link com.android.intentresolver.Flags#FLAG_SCROLLABLE_PREVIEW}
+ */
+ private interface ScrollablePreviewFlingLogicDelegate {
+ default boolean onNestedPreFling(
+ ResolverDrawerLayout drawer, View target, float velocityX, float velocityY) {
+ boolean shouldScroll = !drawer.getShowAtTop() && velocityY > drawer.mMinFlingVelocity
+ && drawer.mCollapseOffset != 0;
+ if (shouldScroll) {
+ drawer.smoothScrollTo(0, velocityY);
+ return true;
+ }
+ boolean shouldDismiss = (Math.abs(velocityY) > drawer.mMinFlingVelocity)
+ && velocityY < 0
+ && isFlingTargetAtTop(target);
+ if (shouldDismiss) {
+ if (drawer.getShowAtTop()) {
+ drawer.smoothScrollTo(drawer.mCollapsibleHeight, velocityY);
+ } else {
+ if (drawer.isDismissable()
+ && drawer.mCollapseOffset > drawer.mCollapsibleHeight) {
+ drawer.smoothScrollTo(drawer.mHeightUsed, velocityY);
+ drawer.mDismissOnScrollerFinished = true;
+ } else {
+ drawer.smoothScrollTo(drawer.mCollapsibleHeight, velocityY);
+ }
+ }
+ return true;
+ }
+ return false;
+ }
+
+ default boolean onNestedFling(
+ ResolverDrawerLayout drawer,
+ View target,
+ float velocityX,
+ float velocityY,
+ boolean consumed) {
+ // TODO: find a more suitable way to fix it.
+ // RecyclerView started reporting `consumed` as true whenever a scrolling is enabled,
+ // previously the value was based on whether the fling can be performed in given
+ // direction i.e. whether it is at the top or at the bottom. isRecyclerViewAtTheTop
+ // method is a workaround that restores the legacy functionality.
+ boolean shouldConsume = (Math.abs(velocityY) > drawer.mMinFlingVelocity) && !consumed;
+ if (shouldConsume) {
+ if (drawer.getShowAtTop()) {
+ if (drawer.isDismissable() && velocityY > 0) {
+ drawer.abortAnimation();
+ drawer.dismiss();
+ } else {
+ drawer.smoothScrollTo(
+ velocityY < 0 ? drawer.mCollapsibleHeight : 0, velocityY);
+ }
+ } else {
+ if (drawer.isDismissable()
+ && velocityY < 0
+ && drawer.mCollapseOffset > drawer.mCollapsibleHeight) {
+ drawer.smoothScrollTo(drawer.mHeightUsed, velocityY);
+ drawer.mDismissOnScrollerFinished = true;
+ } else {
+ drawer.smoothScrollTo(
+ velocityY > 0 ? 0 : drawer.mCollapsibleHeight, velocityY);
+ }
+ }
+ }
+ return shouldConsume;
+ }
+ }
}
diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
index 3bbafc40..7fe16091 100644
--- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
+++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
@@ -26,11 +26,16 @@ import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
+import android.view.animation.AlphaAnimation
+import android.view.animation.Animation
+import android.view.animation.Animation.AnimationListener
+import android.view.animation.DecelerateInterpolator
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.view.ViewCompat
+import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.android.intentresolver.R
@@ -45,6 +50,7 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
private const val TRANSITION_NAME = "screenshot_preview_image"
private const val PLURALS_COUNT = "count"
@@ -65,7 +71,6 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
defStyleAttr: Int
) : super(context, attrs, defStyleAttr) {
layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
- adapter = Adapter(context)
context
.obtainStyledAttributes(attrs, R.styleable.ScrollableImagePreviewView, defStyleAttr, 0)
@@ -98,11 +103,14 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
)
.toInt()
}
- addItemDecoration(SpacingDecoration(innerSpacing, outerSpacing))
+ super.addItemDecoration(SpacingDecoration(innerSpacing, outerSpacing))
maxWidthHint =
a.getDimensionPixelSize(R.styleable.ScrollableImagePreviewView_maxWidthHint, -1)
}
+ val itemAnimator = ItemAnimator()
+ super.setItemAnimator(itemAnimator)
+ super.setAdapter(Adapter(context, itemAnimator.getAddDuration()))
}
private var batchLoader: BatchPreviewLoader? = null
@@ -167,6 +175,14 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
return null
}
+ override fun setAdapter(adapter: RecyclerView.Adapter<*>?) {
+ error("This method is not supported")
+ }
+
+ override fun setItemAnimator(animator: RecyclerView.ItemAnimator?) {
+ error("This method is not supported")
+ }
+
fun setImageLoader(imageLoader: CachingImageLoader) {
previewAdapter.imageLoader = imageLoader
}
@@ -269,7 +285,10 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
File
}
- private class Adapter(private val context: Context) : RecyclerView.Adapter<ViewHolder>() {
+ private class Adapter(
+ private val context: Context,
+ private val fadeInDurationMs: Long,
+ ) : RecyclerView.Adapter<ViewHolder>() {
private val previews = ArrayList<Preview>()
private val imagePreviewDescription =
context.resources.getString(R.string.image_preview_a11y_description)
@@ -311,15 +330,17 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
if (newPreviews.isEmpty()) return
val insertPos = previews.size
val hadOtherItem = hasOtherItem
- val wasEmpty = previews.isEmpty()
+ val oldItemCount = getItemCount()
previews.addAll(newPreviews)
if (firstImagePos < 0) {
val pos = newPreviews.indexOfFirst { it.type == PreviewType.Image }
if (pos >= 0) firstImagePos = insertPos + pos
}
- if (wasEmpty) {
- // we don't want any item animation in that case
- notifyDataSetChanged()
+ if (insertPos == 0) {
+ if (oldItemCount > 0) {
+ notifyItemRangeRemoved(0, oldItemCount)
+ }
+ notifyItemRangeInserted(insertPos, getItemCount())
} else {
notifyItemRangeInserted(insertPos, newPreviews.size)
when {
@@ -366,6 +387,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
vh.bind(
previews[position],
imageLoader ?: error("ImageLoader is missing"),
+ fadeInDurationMs,
isSharedTransitionElement = position == firstImagePos,
previewReadyCallback =
if (
@@ -416,10 +438,13 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
fun bind(
preview: Preview,
imageLoader: CachingImageLoader,
+ fadeInDurationMs: Long,
isSharedTransitionElement: Boolean,
previewReadyCallback: ((String) -> Unit)?
) {
image.setImageDrawable(null)
+ image.alpha = 1f
+ image.clearAnimation()
(image.layoutParams as? ConstraintLayout.LayoutParams)?.let { params ->
params.dimensionRatio = preview.aspectRatioString
}
@@ -453,11 +478,11 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
}
resetScope().launch {
loadImage(preview, imageLoader)
- if (preview.type == PreviewType.Image) {
- previewReadyCallback?.let { callback ->
- image.waitForPreDraw()
- callback(TRANSITION_NAME)
- }
+ if (preview.type == PreviewType.Image && previewReadyCallback != null) {
+ image.waitForPreDraw()
+ previewReadyCallback(TRANSITION_NAME)
+ } else if (image.isAttachedToWindow()) {
+ fadeInPreview(fadeInDurationMs)
}
}
}
@@ -473,6 +498,30 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
image.setImageBitmap(bitmap)
}
+ private suspend fun fadeInPreview(durationMs: Long) =
+ suspendCancellableCoroutine { continuation ->
+ val animation =
+ AlphaAnimation(0f, 1f).apply {
+ duration = durationMs
+ interpolator = DecelerateInterpolator()
+ setAnimationListener(
+ object : AnimationListener {
+ override fun onAnimationStart(animation: Animation?) = Unit
+ override fun onAnimationRepeat(animation: Animation?) = Unit
+
+ override fun onAnimationEnd(animation: Animation?) {
+ continuation.resumeWith(Result.success(Unit))
+ }
+ }
+ )
+ }
+ image.startAnimation(animation)
+ continuation.invokeOnCancellation {
+ image.clearAnimation()
+ image.alpha = 1f
+ }
+ }
+
private fun resetScope(): CoroutineScope =
CoroutineScope(Dispatchers.Main.immediate).also {
scope?.cancel()
@@ -521,6 +570,70 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
}
}
+ /**
+ * ItemAnimator to handle a special case of addng first image items into the view. The view is
+ * used with wrap_content width spec thus after adding the first views it, generally, changes
+ * its size and position breaking the animation. This class handles that by preserving loading
+ * idicator position in this special case.
+ */
+ private inner class ItemAnimator() : DefaultItemAnimator() {
+ private var animatedVH: ViewHolder? = null
+ private var originalTranslation = 0f
+
+ override fun recordPreLayoutInformation(
+ state: State,
+ viewHolder: RecyclerView.ViewHolder,
+ changeFlags: Int,
+ payloads: MutableList<Any>
+ ): ItemHolderInfo {
+ return super.recordPreLayoutInformation(state, viewHolder, changeFlags, payloads).let {
+ holderInfo ->
+ if (viewHolder is LoadingItemViewHolder && getChildCount() == 1) {
+ LoadingItemHolderInfo(holderInfo, parentLeft = left)
+ } else {
+ holderInfo
+ }
+ }
+ }
+
+ override fun animateDisappearance(
+ viewHolder: RecyclerView.ViewHolder,
+ preLayoutInfo: ItemHolderInfo,
+ postLayoutInfo: ItemHolderInfo?
+ ): Boolean {
+ if (viewHolder is LoadingItemViewHolder && preLayoutInfo is LoadingItemHolderInfo) {
+ val view = viewHolder.itemView
+ animatedVH = viewHolder
+ originalTranslation = view.getTranslationX()
+ view.setTranslationX(
+ (preLayoutInfo.parentLeft - left + preLayoutInfo.left).toFloat() - view.left
+ )
+ }
+ return super.animateDisappearance(viewHolder, preLayoutInfo, postLayoutInfo)
+ }
+
+ override fun onRemoveFinished(viewHolder: RecyclerView.ViewHolder) {
+ if (animatedVH === viewHolder) {
+ viewHolder.itemView.setTranslationX(originalTranslation)
+ animatedVH = null
+ }
+ super.onRemoveFinished(viewHolder)
+ }
+
+ private inner class LoadingItemHolderInfo(
+ holderInfo: ItemHolderInfo,
+ val parentLeft: Int,
+ ) : ItemHolderInfo() {
+ init {
+ left = holderInfo.left
+ top = holderInfo.top
+ right = holderInfo.right
+ bottom = holderInfo.bottom
+ changeFlags = holderInfo.changeFlags
+ }
+ }
+ }
+
@VisibleForTesting
class BatchPreviewLoader(
private val imageLoader: CachingImageLoader,