Merge tag 'android-13.0.0_r32' into int/13/fp3

Android 13.0.0 release 32

* tag 'android-13.0.0_r32': (67 commits)
  Move EnterTransitionAnimationDelegate out of ChooserActivity
  ResolverDrawerLayout: unify mCollapsibleHeight calculation.
  Introduce an image preview view
  Add a mechanism to switch between action row view varians
  [Chooser/ResolverActivity] Fix flakiness in work profile tests
  A base scrollable action row implementation
  Split ActionRow view into internface and implementation
  Extract TargetPresentationGetter hierarchy
  Create an action row view
  Introduce GenericMultiProfilePagerAdapter
  Extract remainining ChooserActivity logging
  Extract ChooserGridAdapter.
  SelectableTargetInfo "immutability"/other cleanup
  Load app icons defensively
  Fix ChooserActivity crash when launched with a caller-provided target
  Fix flakey unit test
  Fix NPE from dependency initialization order
  Tighten visibility in the -ListAdapter classes.
  Pre-work to decouple ChooserGridAdapter
  Fail startup via ResolverActivity.super_onCreate()
  ...

Change-Id: I5b5e9a6e7ae5343cf60c2947953cc3c01d13c209
diff --git a/Android.bp b/Android.bp
index 2407fc7..31d7d6d 100644
--- a/Android.bp
+++ b/Android.bp
@@ -36,6 +36,7 @@
     min_sdk_version: "current",
     srcs: [
         "java/src/**/*.java",
+        "java/src/**/*.kt",
     ],
     resource_dirs: [
         "java/res",
@@ -45,7 +46,18 @@
 
     static_libs: [
         "androidx.annotation_annotation",
-        "unsupportedappusage",
+        "androidx.concurrent_concurrent-futures",
+        "androidx.recyclerview_recyclerview",
+        "androidx.viewpager_viewpager",
+        "androidx.lifecycle_lifecycle-common-java8",
+        "androidx.lifecycle_lifecycle-extensions",
+        "androidx.lifecycle_lifecycle-runtime-ktx",
+        "androidx.lifecycle_lifecycle-viewmodel-ktx",
+        "kotlin-stdlib",
+        "kotlinx_coroutines",
+        "kotlinx-coroutines-android",
+        "//external/kotlinc:kotlin-annotations",
+        "guava",
     ],
 
     plugins: ["java_api_finder"],
@@ -68,10 +80,7 @@
         "IntentResolver-core",
     ],
     optimize: {
-        // TODO: consider re-enabling after setting up Proguard rules to
-        // preserve the name of the ChooserGridLayoutManager class, which is
-        // referenced by name in the chooser_list_per_profile layout XML.
-        enabled: false,
+        enabled: true,
     },
     apex_available: [
         "//apex_available:platform",
diff --git a/java/res/layout/chooser_action_row.xml b/java/res/layout/chooser_action_row.xml
index ea75611..620ff70 100644
--- a/java/res/layout/chooser_action_row.xml
+++ b/java/res/layout/chooser_action_row.xml
@@ -13,14 +13,20 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License
   -->
-
-<LinearLayout
+<FrameLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
     android:layout_width="match_parent"
-    android:layout_height="wrap_content"
-    android:paddingLeft="@dimen/chooser_edge_margin_normal"
-    android:paddingRight="@dimen/chooser_edge_margin_normal"
-    android:gravity="center"
-    >
+    android:layout_height="wrap_content">
 
-</LinearLayout>
+    <com.android.intentresolver.widget.ChooserActionRow
+        android:id="@androidprv:id/chooser_action_row"
+        android:layout_width="@dimen/chooser_preview_width"
+        android:layout_height="wrap_content"
+        android:paddingLeft="@dimen/chooser_edge_margin_normal"
+        android:paddingRight="@dimen/chooser_edge_margin_normal"
+        android:layout_marginBottom="@dimen/chooser_view_spacing"
+        android:layout_gravity="center_horizontal"
+        android:gravity="center" />
+
+</FrameLayout>
\ No newline at end of file
diff --git a/java/res/layout/chooser_action_view.xml b/java/res/layout/chooser_action_view.xml
new file mode 100644
index 0000000..d74798e
--- /dev/null
+++ b/java/res/layout/chooser_action_view.xml
@@ -0,0 +1,28 @@
+<!--
+  ~ 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
+  -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+    android:gravity="center"
+    android:drawablePadding="8dp"
+    android:textColor="?android:attr/textColorPrimary"
+    android:textSize="12sp"
+    android:maxWidth="192dp"
+    android:singleLine="true"
+    android:clickable="true"
+    android:drawableTint="?android:attr/textColorPrimary"
+    android:drawableTintMode="src_in"
+    style="?android:attr/borderlessButtonStyle"
+    />
diff --git a/java/res/layout/chooser_dialog.xml b/java/res/layout/chooser_dialog.xml
index ff66bbb..e31712c 100644
--- a/java/res/layout/chooser_dialog.xml
+++ b/java/res/layout/chooser_dialog.xml
@@ -50,9 +50,9 @@
 
     </LinearLayout>
 
-    <com.android.internal.widget.RecyclerView
+    <androidx.recyclerview.widget.RecyclerView
         xmlns:app="http://schemas.android.com/apk/res-auto"
-        androidprv:layoutManager="com.android.internal.widget.LinearLayoutManager"
+        app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
         android:id="@androidprv:id/listContainer"
         android:overScrollMode="never"
         android:layout_width="match_parent"
diff --git a/java/res/layout/chooser_grid.xml b/java/res/layout/chooser_grid.xml
index a95b0eb..d863495 100644
--- a/java/res/layout/chooser_grid.xml
+++ b/java/res/layout/chooser_grid.xml
@@ -16,14 +16,15 @@
 * limitations under the License.
 */
 -->
-<com.android.internal.widget.ResolverDrawerLayout
+<com.android.intentresolver.widget.ResolverDrawerLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:layout_gravity="center"
-    androidprv:maxCollapsedHeight="0dp"
-    androidprv:maxCollapsedHeightSmall="56dp"
+    app:maxCollapsedHeight="0dp"
+    app:maxCollapsedHeightSmall="56dp"
     android:maxWidth="@dimen/chooser_width"
     android:id="@androidprv:id/contentPanel">
 
@@ -31,7 +32,7 @@
         android:id="@androidprv:id/chooser_header"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        androidprv:layout_alwaysShow="true"
+        app:layout_alwaysShow="true"
         android:elevation="0dp"
         android:background="@drawable/bottomsheet_background">
 
@@ -94,4 +95,4 @@
         </LinearLayout>
     </TabHost>
 
-</com.android.internal.widget.ResolverDrawerLayout>
+</com.android.intentresolver.widget.ResolverDrawerLayout>
diff --git a/java/res/layout/chooser_grid_preview_file.xml b/java/res/layout/chooser_grid_preview_file.xml
index 6d2e76a..e98c327 100644
--- a/java/res/layout/chooser_grid_preview_file.xml
+++ b/java/res/layout/chooser_grid_preview_file.xml
@@ -37,7 +37,7 @@
       android:layout_marginBottom="@dimen/chooser_view_spacing"
       android:id="@androidprv:id/content_preview_file_layout">
 
-    <view class="com.android.intentresolver.ChooserActivity$RoundedRectImageView"
+    <com.android.intentresolver.widget.RoundedRectImageView
           android:id="@androidprv:id/content_preview_file_thumbnail"
           android:layout_width="75dp"
           android:layout_height="75dp"
@@ -68,13 +68,10 @@
         android:singleLine="true"/>
   </LinearLayout>
 
-  <include
-      android:id="@androidprv:id/chooser_action_row"
-      layout="@layout/chooser_action_row"
-      android:layout_width="@dimen/chooser_preview_width"
-      android:layout_height="wrap_content"
-      android:layout_marginBottom="@dimen/chooser_view_spacing"
-      android:layout_gravity="center"
-  />
+  <ViewStub
+      android:id="@+id/action_row_stub"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content" />
+
 </LinearLayout>
 
diff --git a/java/res/layout/chooser_grid_preview_image.xml b/java/res/layout/chooser_grid_preview_image.xml
index 96054eb..5c32414 100644
--- a/java/res/layout/chooser_grid_preview_image.xml
+++ b/java/res/layout/chooser_grid_preview_image.xml
@@ -24,70 +24,19 @@
     android:layout_height="wrap_content"
     android:orientation="vertical"
     android:background="?android:attr/colorBackground">
-  <RelativeLayout
+
+  <com.android.intentresolver.widget.ImagePreviewView
       android:id="@androidprv:id/content_preview_image_area"
       android:layout_width="wrap_content"
       android:layout_height="wrap_content"
       android:layout_gravity="center_horizontal"
       android:paddingBottom="@dimen/chooser_view_spacing"
-      android:background="?android:attr/colorBackground">
+      android:background="?android:attr/colorBackground" />
 
-    <view class="com.android.intentresolver.ChooserActivity$RoundedRectImageView"
-          android:id="@androidprv:id/content_preview_image_1_large"
-          android:layout_width="120dp"
-          android:layout_height="104dp"
-          android:layout_alignParentTop="true"
-          android:adjustViewBounds="true"
-          android:gravity="center"
-          android:scaleType="centerCrop"/>
-
-    <view class="com.android.intentresolver.ChooserActivity$RoundedRectImageView"
-          android:id="@androidprv:id/content_preview_image_2_large"
-          android:visibility="gone"
-          android:layout_width="120dp"
-          android:layout_height="104dp"
-          android:layout_alignParentTop="true"
-          android:layout_toRightOf="@androidprv:id/content_preview_image_1_large"
-          android:layout_marginLeft="10dp"
-          android:adjustViewBounds="true"
-          android:gravity="center"
-          android:scaleType="centerCrop"/>
-
-    <view class="com.android.intentresolver.ChooserActivity$RoundedRectImageView"
-          android:id="@androidprv:id/content_preview_image_2_small"
-          android:visibility="gone"
-          android:layout_width="120dp"
-          android:layout_height="65dp"
-          android:layout_alignParentTop="true"
-          android:layout_toRightOf="@androidprv:id/content_preview_image_1_large"
-          android:layout_marginLeft="10dp"
-          android:adjustViewBounds="true"
-          android:gravity="center"
-          android:scaleType="centerCrop"/>
-
-    <view class="com.android.intentresolver.ChooserActivity$RoundedRectImageView"
-          android:id="@androidprv:id/content_preview_image_3_small"
-          android:visibility="gone"
-          android:layout_width="120dp"
-          android:layout_height="65dp"
-          android:layout_below="@androidprv:id/content_preview_image_2_small"
-          android:layout_toRightOf="@androidprv:id/content_preview_image_1_large"
-          android:layout_marginLeft="10dp"
-          android:layout_marginTop="10dp"
-          android:adjustViewBounds="true"
-          android:gravity="center"
-          android:scaleType="centerCrop"/>
-
-  </RelativeLayout>
-
-  <include
-      android:id="@androidprv:id/chooser_action_row"
-      layout="@layout/chooser_action_row"
-      android:layout_width="@dimen/chooser_preview_width"
-      android:layout_height="wrap_content"
-      android:layout_marginBottom="@dimen/chooser_view_spacing"
-      android:layout_gravity="center"
-  />
+  <ViewStub
+      android:id="@+id/action_row_stub"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content" />
 
 </LinearLayout>
 
diff --git a/java/res/layout/chooser_grid_preview_text.xml b/java/res/layout/chooser_grid_preview_text.xml
index a9ed71b..db7282e 100644
--- a/java/res/layout/chooser_grid_preview_text.xml
+++ b/java/res/layout/chooser_grid_preview_text.xml
@@ -52,14 +52,10 @@
 
   </RelativeLayout>
 
-  <include
-      android:id="@androidprv:id/chooser_action_row"
-      layout="@layout/chooser_action_row"
-      android:layout_width="@dimen/chooser_preview_width"
-      android:layout_height="wrap_content"
-      android:layout_marginBottom="@dimen/chooser_view_spacing"
-      android:layout_gravity="center"
-      />
+  <ViewStub
+      android:id="@+id/action_row_stub"
+      android:layout_width="match_parent"
+      android:layout_height="wrap_content" />
 
   <!-- Required sub-layout so we can get the nice rounded corners-->
   <!-- around this section -->
@@ -75,7 +71,7 @@
       android:background="@androidprv:drawable/chooser_content_preview_rounded"
       android:id="@androidprv:id/content_preview_title_layout">
 
-    <view class="com.android.intentresolver.ChooserActivity$RoundedRectImageView"
+    <com.android.intentresolver.widget.RoundedRectImageView
           android:id="@androidprv:id/content_preview_thumbnail"
           android:layout_width="75dp"
           android:layout_height="75dp"
diff --git a/java/res/layout/chooser_list_per_profile.xml b/java/res/layout/chooser_list_per_profile.xml
index 8d876cd..1753e2f 100644
--- a/java/res/layout/chooser_list_per_profile.xml
+++ b/java/res/layout/chooser_list_per_profile.xml
@@ -16,12 +16,13 @@
 <RelativeLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent">
-    <com.android.internal.widget.RecyclerView
+    <androidx.recyclerview.widget.RecyclerView
         android:layout_width="match_parent"
         android:layout_height="match_parent"
-        androidprv:layoutManager="com.android.intentresolver.ChooserGridLayoutManager"
+        app:layoutManager="com.android.intentresolver.ChooserGridLayoutManager"
         android:id="@androidprv:id/resolver_list"
         android:clipToPadding="false"
         android:background="?android:attr/colorBackground"
diff --git a/java/res/layout/image_preview_view.xml b/java/res/layout/image_preview_view.xml
new file mode 100644
index 0000000..d2f9469
--- /dev/null
+++ b/java/res/layout/image_preview_view.xml
@@ -0,0 +1,72 @@
+<!--
+  ~ 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.
+  -->
+
+<merge
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:layout_width="wrap_content"
+    android:layout_height="wrap_content"
+    android:layout_gravity="center_horizontal"
+    android:paddingBottom="@dimen/chooser_view_spacing"
+    android:background="?android:attr/colorBackground">
+
+    <com.android.intentresolver.widget.RoundedRectImageView
+        android:id="@androidprv:id/content_preview_image_1_large"
+        android:layout_width="120dp"
+        android:layout_height="104dp"
+        android:layout_alignParentTop="true"
+        android:adjustViewBounds="true"
+        android:gravity="center"
+        android:scaleType="centerCrop"/>
+
+    <com.android.intentresolver.widget.RoundedRectImageView
+        android:id="@androidprv:id/content_preview_image_2_large"
+        android:visibility="gone"
+        android:layout_width="120dp"
+        android:layout_height="104dp"
+        android:layout_alignParentTop="true"
+        android:layout_toRightOf="@androidprv:id/content_preview_image_1_large"
+        android:layout_marginLeft="10dp"
+        android:adjustViewBounds="true"
+        android:gravity="center"
+        android:scaleType="centerCrop"/>
+
+    <com.android.intentresolver.widget.RoundedRectImageView
+        android:id="@androidprv:id/content_preview_image_2_small"
+        android:visibility="gone"
+        android:layout_width="120dp"
+        android:layout_height="65dp"
+        android:layout_alignParentTop="true"
+        android:layout_toRightOf="@androidprv:id/content_preview_image_1_large"
+        android:layout_marginLeft="10dp"
+        android:adjustViewBounds="true"
+        android:gravity="center"
+        android:scaleType="centerCrop"/>
+
+    <com.android.intentresolver.widget.RoundedRectImageView
+        android:id="@androidprv:id/content_preview_image_3_small"
+        android:visibility="gone"
+        android:layout_width="120dp"
+        android:layout_height="65dp"
+        android:layout_below="@androidprv:id/content_preview_image_2_small"
+        android:layout_toRightOf="@androidprv:id/content_preview_image_1_large"
+        android:layout_marginLeft="10dp"
+        android:layout_marginTop="10dp"
+        android:adjustViewBounds="true"
+        android:gravity="center"
+        android:scaleType="centerCrop"/>
+
+</merge>
diff --git a/java/res/layout/miniresolver.xml b/java/res/layout/miniresolver.xml
index ab65aa9..7e31de5 100644
--- a/java/res/layout/miniresolver.xml
+++ b/java/res/layout/miniresolver.xml
@@ -14,20 +14,21 @@
   ~ See the License for the specific language governing permissions and
   ~ limitations under the License.
   -->
-<com.android.internal.widget.ResolverDrawerLayout
+<com.android.intentresolver.widget.ResolverDrawerLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:maxWidth="@dimen/resolver_max_width"
-    androidprv:maxCollapsedHeight="@dimen/resolver_max_collapsed_height"
-    androidprv:maxCollapsedHeightSmall="56dp"
+    app:maxCollapsedHeight="@dimen/resolver_max_collapsed_height"
+    app:maxCollapsedHeightSmall="56dp"
     android:id="@androidprv:id/contentPanel">
 
     <RelativeLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        androidprv:layout_alwaysShow="true"
+        app:layout_alwaysShow="true"
         android:elevation="@dimen/resolver_elevation"
         android:paddingTop="24dp"
         android:paddingStart="@dimen/resolver_edge_margin"
@@ -62,18 +63,18 @@
         android:id="@androidprv:id/button_bar_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        androidprv:layout_alwaysShow="true"
+        app:layout_alwaysShow="true"
         android:paddingTop="32dp"
         android:paddingBottom="@dimen/resolver_button_bar_spacing"
         android:orientation="vertical"
         android:background="?android:attr/colorBackground"
-        androidprv:layout_ignoreOffset="true">
+        app:layout_ignoreOffset="true">
         <RelativeLayout
             style="?android:attr/buttonBarStyle"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            androidprv:layout_ignoreOffset="true"
-            androidprv:layout_hasNestedScrollIndicator="true"
+            app:layout_ignoreOffset="true"
+            app:layout_hasNestedScrollIndicator="true"
             android:gravity="end|center_vertical"
             android:orientation="horizontal"
             android:layoutDirection="locale"
@@ -112,4 +113,4 @@
             />
         </RelativeLayout>
     </LinearLayout>
-</com.android.internal.widget.ResolverDrawerLayout>
+</com.android.intentresolver.widget.ResolverDrawerLayout>
diff --git a/java/res/layout/resolver_different_item_header.xml b/java/res/layout/resolver_different_item_header.xml
index 4f80159..79ce682 100644
--- a/java/res/layout/resolver_different_item_header.xml
+++ b/java/res/layout/resolver_different_item_header.xml
@@ -19,9 +19,10 @@
 <TextView
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
-    androidprv:layout_alwaysShow="true"
+    app:layout_alwaysShow="true"
     android:text="@string/use_a_different_app"
     android:textColor="?android:attr/textColorPrimary"
     android:fontFamily="@androidprv:string/config_headlineFontFamilyMedium"
diff --git a/java/res/layout/resolver_list.xml b/java/res/layout/resolver_list.xml
index 179c407..44b14ba 100644
--- a/java/res/layout/resolver_list.xml
+++ b/java/res/layout/resolver_list.xml
@@ -16,21 +16,22 @@
 * limitations under the License.
 */
 -->
-<com.android.internal.widget.ResolverDrawerLayout
+<com.android.intentresolver.widget.ResolverDrawerLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:maxWidth="@dimen/resolver_max_width"
-    androidprv:maxCollapsedHeight="@dimen/resolver_max_collapsed_height"
-    androidprv:maxCollapsedHeightSmall="56dp"
+    app:maxCollapsedHeight="@dimen/resolver_max_collapsed_height"
+    app:maxCollapsedHeightSmall="56dp"
     android:id="@androidprv:id/contentPanel">
 
     <RelativeLayout
         android:id="@androidprv:id/title_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        androidprv:layout_alwaysShow="true"
+        app:layout_alwaysShow="true"
         android:elevation="@dimen/resolver_elevation"
         android:paddingTop="@dimen/resolver_small_margin"
         android:paddingStart="@dimen/resolver_edge_margin"
@@ -66,7 +67,7 @@
 
     <View
         android:id="@androidprv:id/divider"
-        androidprv:layout_alwaysShow="true"
+        app:layout_alwaysShow="true"
         android:layout_width="match_parent"
         android:layout_height="1dp"
         android:background="?android:attr/colorBackground"
@@ -114,10 +115,10 @@
         android:id="@androidprv:id/button_bar_container"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        androidprv:layout_alwaysShow="true"
+        app:layout_alwaysShow="true"
         android:orientation="vertical"
         android:background="?android:attr/colorBackground"
-        androidprv:layout_ignoreOffset="true">
+        app:layout_ignoreOffset="true">
         <View
             android:id="@androidprv:id/resolver_button_bar_divider"
             android:layout_width="match_parent"
@@ -130,8 +131,8 @@
             style="?android:attr/buttonBarStyle"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            androidprv:layout_ignoreOffset="true"
-            androidprv:layout_hasNestedScrollIndicator="true"
+            app:layout_ignoreOffset="true"
+            app:layout_hasNestedScrollIndicator="true"
             android:gravity="end|center_vertical"
             android:orientation="horizontal"
             android:layoutDirection="locale"
@@ -169,4 +170,4 @@
                 android:onClick="onButtonClick" />
         </LinearLayout>
     </LinearLayout>
-</com.android.internal.widget.ResolverDrawerLayout>
+</com.android.intentresolver.widget.ResolverDrawerLayout>
diff --git a/java/res/layout/resolver_list_with_default.xml b/java/res/layout/resolver_list_with_default.xml
index 341c58e..192a598 100644
--- a/java/res/layout/resolver_list_with_default.xml
+++ b/java/res/layout/resolver_list_with_default.xml
@@ -16,19 +16,20 @@
 * limitations under the License.
 */
 -->
-<com.android.internal.widget.ResolverDrawerLayout
+<com.android.intentresolver.widget.ResolverDrawerLayout
     xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:maxWidth="@dimen/resolver_max_width"
-    androidprv:maxCollapsedHeight="@dimen/resolver_max_collapsed_height_with_default"
+    app:maxCollapsedHeight="@dimen/resolver_max_collapsed_height_with_default"
     android:id="@androidprv:id/contentPanel">
 
     <LinearLayout
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
-        androidprv:layout_alwaysShow="true"
+        app:layout_alwaysShow="true"
         android:orientation="vertical"
         android:background="@drawable/bottomsheet_background"
         android:paddingTop="@dimen/resolver_small_margin"
@@ -105,7 +106,7 @@
             style="?android:attr/buttonBarStyle"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            androidprv:layout_alwaysShow="true"
+            app:layout_alwaysShow="true"
             android:gravity="end|center_vertical"
             android:orientation="horizontal"
             android:layoutDirection="locale"
@@ -146,7 +147,7 @@
 
     <View
         android:id="@androidprv:id/divider"
-        androidprv:layout_alwaysShow="true"
+        app:layout_alwaysShow="true"
         android:layout_width="match_parent"
         android:layout_height="1dp"
         android:background="?android:attr/colorBackground"
@@ -154,14 +155,14 @@
 
     <FrameLayout
         android:id="@androidprv:id/stub"
-        androidprv:layout_alwaysShow="true"
+        app:layout_alwaysShow="true"
         android:visibility="gone"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
         android:background="?android:attr/colorBackground"/>
 
     <TabHost
-        androidprv:layout_alwaysShow="true"
+        app:layout_alwaysShow="true"
         android:id="@androidprv:id/profile_tabhost"
         android:layout_width="match_parent"
         android:layout_height="wrap_content"
@@ -198,9 +199,9 @@
     </TabHost>
 
     <View
-        androidprv:layout_alwaysShow="true"
+        app:layout_alwaysShow="true"
         android:layout_width="match_parent"
         android:layout_height="1dp"
         android:background="?android:attr/colorBackground"
         android:foreground="?android:attr/dividerVertical" />
-</com.android.internal.widget.ResolverDrawerLayout>
+</com.android.intentresolver.widget.ResolverDrawerLayout>
diff --git a/java/res/layout/scrollable_chooser_action_row.xml b/java/res/layout/scrollable_chooser_action_row.xml
new file mode 100644
index 0000000..cb5dabf
--- /dev/null
+++ b/java/res/layout/scrollable_chooser_action_row.xml
@@ -0,0 +1,30 @@
+<!--
+  ~ 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
+  -->
+
+<FrameLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:gravity="center_horizontal">
+
+    <com.android.intentresolver.widget.ScrollableActionRow
+        android:id="@androidprv:id/chooser_action_row"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="center_horizontal"
+        android:gravity="center" />
+</FrameLayout>
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index 3ec7c2f..2f2bbda 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -26,6 +26,12 @@
         <attr name="maxCollapsedHeightSmall" format="dimension" />
         <!-- Whether the Drawer should be positioned at the top rather than at the bottom. -->
         <attr name="showAtTop" format="boolean" />
+        <!-- By default `ResolverDrawerLayout`’s children views with `layout_ignoreOffset` property
+             set to true have a fixed position in the layout that won’t be affected by the drawer’s
+             movements. This property alternates that behavior. It specifies a child view’s id that
+             will push all ignoreOffset siblings below it when the drawer is moved i.e. setting the
+             top limit the ignoreOffset elements. -->
+        <attr name="ignoreOffsetTopLimit" format="reference" />
     </declare-styleable>
 
     <declare-styleable name="ResolverDrawerLayout_LayoutParams">
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index 2d6fe81..93cb463 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -50,4 +50,6 @@
     <dimen name="resolver_profile_tab_margin">4dp</dimen>
 
     <dimen name="chooser_action_button_icon_size">18dp</dimen>
+    <dimen name="chooser_action_view_icon_size">22dp</dimen>
+    <dimen name="chooser_action_margin">0dp</dimen>
 </resources>
diff --git a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
index 4f6c0bf..17dbb8f 100644
--- a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
@@ -16,27 +16,25 @@
 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.app.admin.DevicePolicyEventLogger;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.IPackageManager;
-import android.content.pm.ResolveInfo;
-import android.os.AsyncTask;
 import android.os.Trace;
 import android.os.UserHandle;
-import android.os.UserManager;
-import android.stats.devicepolicy.DevicePolicyEnums;
 import android.view.View;
 import android.view.ViewGroup;
 import android.widget.Button;
 import android.widget.TextView;
 
+import androidx.viewpager.widget.PagerAdapter;
+import androidx.viewpager.widget.ViewPager;
+
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.widget.PagerAdapter;
-import com.android.internal.widget.ViewPager;
 
 import java.util.HashSet;
 import java.util.List;
@@ -59,73 +57,32 @@
     private final Context mContext;
     private int mCurrentPage;
     private OnProfileSelectedListener mOnProfileSelectedListener;
-    private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
+
     private Set<Integer> mLoadedPages;
-    private final UserHandle mPersonalProfileUserHandle;
+    private final EmptyStateProvider mEmptyStateProvider;
     private final UserHandle mWorkProfileUserHandle;
-    private Injector mInjector;
-    private boolean mIsWaitingToEnableWorkProfile;
+    private final QuietModeManager mQuietModeManager;
 
     AbstractMultiProfilePagerAdapter(Context context, int currentPage,
-            UserHandle personalProfileUserHandle,
+            EmptyStateProvider emptyStateProvider,
+            QuietModeManager quietModeManager,
             UserHandle workProfileUserHandle) {
         mContext = Objects.requireNonNull(context);
         mCurrentPage = currentPage;
         mLoadedPages = new HashSet<>();
-        mPersonalProfileUserHandle = personalProfileUserHandle;
         mWorkProfileUserHandle = workProfileUserHandle;
-        UserManager userManager = context.getSystemService(UserManager.class);
-        mInjector = new Injector() {
-            @Override
-            public boolean hasCrossProfileIntents(List<Intent> intents, int sourceUserId,
-                    int targetUserId) {
-                return AbstractMultiProfilePagerAdapter.this
-                        .hasCrossProfileIntents(intents, sourceUserId, targetUserId);
-            }
-
-            @Override
-            public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) {
-                return userManager.isQuietModeEnabled(workProfileUserHandle);
-            }
-
-            @Override
-            public void requestQuietModeEnabled(boolean enabled, UserHandle workProfileUserHandle) {
-                AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
-                    userManager.requestQuietModeEnabled(enabled, workProfileUserHandle);
-                });
-                mIsWaitingToEnableWorkProfile = true;
-            }
-        };
+        mEmptyStateProvider = emptyStateProvider;
+        mQuietModeManager = quietModeManager;
     }
 
-    protected void markWorkProfileEnabledBroadcastReceived() {
-        mIsWaitingToEnableWorkProfile = false;
-    }
-
-    protected boolean isWaitingToEnableWorkProfile() {
-        return mIsWaitingToEnableWorkProfile;
-    }
-
-    /**
-     * Overrides the default {@link Injector} for testing purposes.
-     */
-    @VisibleForTesting
-    public void setInjector(Injector injector) {
-        mInjector = injector;
-    }
-
-    protected boolean isQuietModeEnabled(UserHandle workProfileUserHandle) {
-        return mInjector.isQuietModeEnabled(workProfileUserHandle);
+    private boolean isQuietModeEnabled(UserHandle workProfileUserHandle) {
+        return mQuietModeManager.isQuietModeEnabled(workProfileUserHandle);
     }
 
     void setOnProfileSelectedListener(OnProfileSelectedListener listener) {
         mOnProfileSelectedListener = listener;
     }
 
-    void setOnSwitchOnWorkSelectedListener(OnSwitchOnWorkSelectedListener listener) {
-        mOnSwitchOnWorkSelectedListener = listener;
-    }
-
     Context getContext() {
         return mContext;
     }
@@ -191,7 +148,7 @@
 
     @VisibleForTesting
     public UserHandle getCurrentUserHandle() {
-        return getActiveListAdapter().mResolverListController.getUserHandle();
+        return getActiveListAdapter().getUserHandle();
     }
 
     @Override
@@ -216,6 +173,10 @@
      */
     abstract ProfileDescriptor getItem(int pageIndex);
 
+    protected ViewGroup getEmptyStateView(int pageIndex) {
+        return getItem(pageIndex).getEmptyStateView();
+    }
+
     /**
      * Returns the number of {@link ProfileDescriptor} objects.
      * <p>For a normal consumer device with only one user returns <code>1</code>.
@@ -279,8 +240,6 @@
 
     abstract @Nullable ViewGroup getInactiveAdapterView();
 
-    abstract String getMetricsCategory();
-
     /**
      * Rebuilds the tab that is currently visible to the user.
      * <p>Returns {@code true} if rebuild has completed.
@@ -308,7 +267,7 @@
     }
 
     private int userHandleToPageIndex(UserHandle userHandle) {
-        if (userHandle.equals(getPersonalListAdapter().mResolverListController.getUserHandle())) {
+        if (userHandle.equals(getPersonalListAdapter().getUserHandle())) {
             return PROFILE_PERSONAL;
         } else {
             return PROFILE_WORK;
@@ -316,41 +275,18 @@
     }
 
     private boolean rebuildTab(ResolverListAdapter activeListAdapter, boolean doPostProcessing) {
-        if (shouldShowNoCrossProfileIntentsEmptyState(activeListAdapter)) {
+        if (shouldSkipRebuild(activeListAdapter)) {
             activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true);
             return false;
         }
         return activeListAdapter.rebuildList(doPostProcessing);
     }
 
-    private boolean shouldShowNoCrossProfileIntentsEmptyState(
-            ResolverListAdapter activeListAdapter) {
-        UserHandle listUserHandle = activeListAdapter.getUserHandle();
-        return UserHandle.myUserId() != listUserHandle.getIdentifier()
-                && allowShowNoCrossProfileIntentsEmptyState()
-                && !mInjector.hasCrossProfileIntents(activeListAdapter.getIntents(),
-                        UserHandle.myUserId(), listUserHandle.getIdentifier());
+    private boolean shouldSkipRebuild(ResolverListAdapter activeListAdapter) {
+        EmptyState emptyState = mEmptyStateProvider.getEmptyState(activeListAdapter);
+        return emptyState != null && emptyState.shouldSkipDataRebuild();
     }
 
-    boolean allowShowNoCrossProfileIntentsEmptyState() {
-        return true;
-    }
-
-    protected abstract void showWorkProfileOffEmptyState(
-            ResolverListAdapter activeListAdapter, View.OnClickListener listener);
-
-    protected abstract void showNoPersonalToWorkIntentsEmptyState(
-            ResolverListAdapter activeListAdapter);
-
-    protected abstract void showNoPersonalAppsAvailableEmptyState(
-            ResolverListAdapter activeListAdapter);
-
-    protected abstract void showNoWorkAppsAvailableEmptyState(
-            ResolverListAdapter activeListAdapter);
-
-    protected abstract void showNoWorkToPersonalIntentsEmptyState(
-            ResolverListAdapter activeListAdapter);
-
     /**
      * The empty state screens are shown according to their priority:
      * <ol>
@@ -365,103 +301,88 @@
      * anyway.
      */
     void showEmptyResolverListEmptyState(ResolverListAdapter listAdapter) {
-        if (maybeShowNoCrossProfileIntentsEmptyState(listAdapter)) {
-            return;
-        }
-        if (maybeShowWorkProfileOffEmptyState(listAdapter)) {
-            return;
-        }
-        maybeShowNoAppsAvailableEmptyState(listAdapter);
-    }
+        final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter);
 
-    private boolean maybeShowNoCrossProfileIntentsEmptyState(ResolverListAdapter listAdapter) {
-        if (!shouldShowNoCrossProfileIntentsEmptyState(listAdapter)) {
-            return false;
+        if (emptyState == null) {
+            return;
         }
-        if (listAdapter.getUserHandle().equals(mPersonalProfileUserHandle)) {
-            DevicePolicyEventLogger.createEvent(
-                    DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL)
-                    .setStrings(getMetricsCategory())
-                    .write();
-            showNoWorkToPersonalIntentsEmptyState(listAdapter);
-        } else {
-            DevicePolicyEventLogger.createEvent(
-                    DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK)
-                    .setStrings(getMetricsCategory())
-                    .write();
-            showNoPersonalToWorkIntentsEmptyState(listAdapter);
+
+        emptyState.onEmptyStateShown();
+
+        View.OnClickListener clickListener = null;
+
+        if (emptyState.getButtonClickListener() != null) {
+            clickListener = v -> emptyState.getButtonClickListener().onClick(() -> {
+                ProfileDescriptor descriptor = getItem(
+                        userHandleToPageIndex(listAdapter.getUserHandle()));
+                AbstractMultiProfilePagerAdapter.this.showSpinner(descriptor.getEmptyStateView());
+            });
         }
-        return true;
+
+        showEmptyState(listAdapter, emptyState, clickListener);
     }
 
     /**
-     * Returns {@code true} if the work profile off empty state screen is shown.
+     * Class to get user id of the current process
      */
-    private boolean maybeShowWorkProfileOffEmptyState(ResolverListAdapter listAdapter) {
-        UserHandle listUserHandle = listAdapter.getUserHandle();
-        if (!listUserHandle.equals(mWorkProfileUserHandle)
-                || !mInjector.isQuietModeEnabled(mWorkProfileUserHandle)
-                || listAdapter.getCount() == 0) {
-            return false;
-        }
-        DevicePolicyEventLogger
-                .createEvent(DevicePolicyEnums.RESOLVER_EMPTY_STATE_WORK_APPS_DISABLED)
-                .setStrings(getMetricsCategory())
-                .write();
-        showWorkProfileOffEmptyState(listAdapter,
-                v -> {
-                    ProfileDescriptor descriptor = getItem(
-                            userHandleToPageIndex(listAdapter.getUserHandle()));
-                    showSpinner(descriptor.getEmptyStateView());
-                    if (mOnSwitchOnWorkSelectedListener != null) {
-                        mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected();
-                    }
-                    mInjector.requestQuietModeEnabled(false, mWorkProfileUserHandle);
-                });
-        return true;
-    }
-
-    private void maybeShowNoAppsAvailableEmptyState(ResolverListAdapter listAdapter) {
-        UserHandle listUserHandle = listAdapter.getUserHandle();
-        if (mWorkProfileUserHandle != null
-                && (UserHandle.myUserId() == listUserHandle.getIdentifier()
-                        || !hasAppsInOtherProfile(listAdapter))) {
-            DevicePolicyEventLogger.createEvent(
-                    DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_APPS_RESOLVED)
-                    .setStrings(getMetricsCategory())
-                    .setBoolean(/*isPersonalProfile*/ listUserHandle == mPersonalProfileUserHandle)
-                    .write();
-            if (listUserHandle == mPersonalProfileUserHandle) {
-                showNoPersonalAppsAvailableEmptyState(listAdapter);
-            } else {
-                showNoWorkAppsAvailableEmptyState(listAdapter);
-            }
-        } else if (mWorkProfileUserHandle == null) {
-            showConsumerUserNoAppsAvailableEmptyState(listAdapter);
+    public static class MyUserIdProvider {
+        /**
+         * @return user id of the current process
+         */
+        public int getMyUserId() {
+            return UserHandle.myUserId();
         }
     }
 
-    protected void showEmptyState(ResolverListAdapter activeListAdapter, String title,
-            String subtitle) {
-        showEmptyState(activeListAdapter, title, subtitle, /* buttonOnClick */ null);
+    /**
+     * 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,
-            String title, String subtitle, View.OnClickListener buttonOnClick) {
+    protected void showEmptyState(ResolverListAdapter activeListAdapter, EmptyState emptyState,
+            View.OnClickListener buttonOnClick) {
         ProfileDescriptor descriptor = getItem(
                 userHandleToPageIndex(activeListAdapter.getUserHandle()));
         descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.GONE);
         ViewGroup emptyStateView = descriptor.getEmptyStateView();
-        resetViewVisibilitiesForWorkProfileEmptyState(emptyStateView);
+        resetViewVisibilitiesForEmptyState(emptyStateView);
         emptyStateView.setVisibility(View.VISIBLE);
 
         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);
-        titleView.setText(title);
+        String title = emptyState.getTitle();
+        if (title != null) {
+            titleView.setVisibility(View.VISIBLE);
+            titleView.setText(title);
+        } else {
+            titleView.setVisibility(View.GONE);
+        }
 
         TextView subtitleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle);
+        String subtitle = emptyState.getSubtitle();
         if (subtitle != null) {
             subtitleView.setVisibility(View.VISIBLE);
             subtitleView.setText(subtitle);
@@ -469,6 +390,9 @@
             subtitleView.setVisibility(View.GONE);
         }
 
+        View defaultEmptyText = emptyStateView.findViewById(com.android.internal.R.id.empty);
+        defaultEmptyText.setVisibility(emptyState.useDefaultEmptyView() ? View.VISIBLE : View.GONE);
+
         Button button = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button);
         button.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE);
         button.setOnClickListener(buttonOnClick);
@@ -482,22 +406,6 @@
      */
     protected void setupContainerPadding(View container) {}
 
-    private void showConsumerUserNoAppsAvailableEmptyState(ResolverListAdapter activeListAdapter) {
-        ProfileDescriptor descriptor = getItem(
-                userHandleToPageIndex(activeListAdapter.getUserHandle()));
-        descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.GONE);
-        View emptyStateView = descriptor.getEmptyStateView();
-        resetViewVisibilitiesForConsumerUserEmptyState(emptyStateView);
-        emptyStateView.setVisibility(View.VISIBLE);
-
-        activeListAdapter.markTabLoaded();
-    }
-
-    private boolean isSpinnerShowing(View emptyStateView) {
-        return emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).getVisibility()
-                == View.VISIBLE;
-    }
-
     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);
@@ -505,7 +413,7 @@
         emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE);
     }
 
-    private void resetViewVisibilitiesForWorkProfileEmptyState(View emptyStateView) {
+    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);
@@ -513,14 +421,6 @@
         emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE);
     }
 
-    private void resetViewVisibilitiesForConsumerUserEmptyState(View emptyStateView) {
-        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.GONE);
-        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle).setVisibility(View.GONE);
-        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.GONE);
-        emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).setVisibility(View.GONE);
-        emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.VISIBLE);
-    }
-
     protected void showListView(ResolverListAdapter activeListAdapter) {
         ProfileDescriptor descriptor = getItem(
                 userHandleToPageIndex(activeListAdapter.getUserHandle()));
@@ -529,33 +429,6 @@
         emptyStateView.setVisibility(View.GONE);
     }
 
-    private boolean hasCrossProfileIntents(List<Intent> intents, int source, int target) {
-        IPackageManager packageManager = AppGlobals.getPackageManager();
-        ContentResolver contentResolver = mContext.getContentResolver();
-        for (Intent intent : intents) {
-            if (IntentForwarderActivity.canForward(intent, source, target, packageManager,
-                    contentResolver) != null) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private boolean hasAppsInOtherProfile(ResolverListAdapter adapter) {
-        if (mWorkProfileUserHandle == null) {
-            return false;
-        }
-        List<ResolverActivity.ResolvedComponentInfo> resolversForIntent =
-                adapter.getResolversForUser(UserHandle.of(UserHandle.myUserId()));
-        for (ResolverActivity.ResolvedComponentInfo info : resolversForIntent) {
-            ResolveInfo resolveInfo = info.getResolveInfoAt(0);
-            if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) {
-                return true;
-            }
-        }
-        return false;
-    }
-
     boolean shouldShowEmptyStateScreen(ResolverListAdapter listAdapter) {
         int count = listAdapter.getUnfilteredCount();
         return (count == 0 && listAdapter.getPlaceholderCount() == 0)
@@ -563,7 +436,7 @@
                     && isQuietModeEnabled(mWorkProfileUserHandle));
     }
 
-    protected class ProfileDescriptor {
+    protected static class ProfileDescriptor {
         final ViewGroup rootView;
         private final ViewGroup mEmptyStateView;
         ProfileDescriptor(ViewGroup rootView) {
@@ -599,6 +472,99 @@
     }
 
     /**
+     * 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 {
@@ -611,14 +577,7 @@
     /**
      * Describes an injector to be used for cross profile functionality. Overridable for testing.
      */
-    @VisibleForTesting
-    public interface Injector {
-        /**
-         * Returns {@code true} if at least one of the provided {@code intents} can be forwarded
-         * from {@code sourceUserId} to {@code targetUserId}.
-         */
-        boolean hasCrossProfileIntents(List<Intent> intents, int sourceUserId, int targetUserId);
-
+    public interface QuietModeManager {
         /**
          * Returns whether the given profile is in quiet mode or not.
          */
@@ -628,5 +587,15 @@
          * Enables or disables quiet mode for a managed profile.
          */
         void requestQuietModeEnabled(boolean enabled, UserHandle workProfileUserHandle);
+
+        /**
+         * Should be called when the work profile enabled broadcast received
+         */
+        void markWorkProfileEnabledBroadcastReceived();
+
+        /**
+         * Returns true if enabling of work profile is in progress
+         */
+        boolean isWaitingToEnableWorkProfile();
     }
 }
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 14d7742..ceab62b 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -16,29 +16,26 @@
 
 package com.android.intentresolver;
 
+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 com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
 
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.AnimatorSet;
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
 import android.annotation.IntDef;
 import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.app.Activity;
 import android.app.ActivityManager;
 import android.app.ActivityOptions;
-import android.app.SharedElementCallback;
-import android.app.prediction.AppPredictionContext;
-import android.app.prediction.AppPredictionManager;
 import android.app.prediction.AppPredictor;
 import android.app.prediction.AppTarget;
 import android.app.prediction.AppTargetEvent;
 import android.app.prediction.AppTargetId;
-import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ClipData;
 import android.content.ClipboardManager;
 import android.content.ComponentName;
@@ -50,96 +47,75 @@
 import android.content.IntentSender.SendIntentException;
 import android.content.SharedPreferences;
 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.ShortcutInfo;
-import android.content.pm.ShortcutManager;
 import android.content.res.Configuration;
 import android.content.res.Resources;
 import android.database.Cursor;
-import android.database.DataSetObserver;
 import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Color;
 import android.graphics.Insets;
-import android.graphics.Paint;
-import android.graphics.Path;
-import android.graphics.drawable.AnimatedVectorDrawable;
 import android.graphics.drawable.Drawable;
-import android.metrics.LogMaker;
 import android.net.Uri;
-import android.os.AsyncTask;
 import android.os.Bundle;
 import android.os.Environment;
 import android.os.Handler;
-import android.os.Message;
 import android.os.Parcelable;
 import android.os.PatternMatcher;
 import android.os.ResultReceiver;
+import android.os.SystemClock;
 import android.os.UserHandle;
 import android.os.UserManager;
 import android.os.storage.StorageManager;
 import android.provider.DeviceConfig;
-import android.provider.DocumentsContract;
-import android.provider.Downloads;
-import android.provider.OpenableColumns;
 import android.provider.Settings;
 import android.service.chooser.ChooserTarget;
 import android.text.TextUtils;
-import android.util.AttributeSet;
-import android.util.HashedStringCache;
 import android.util.Log;
-import android.util.PluralsMessageFormatter;
 import android.util.Size;
 import android.util.Slog;
-import android.view.LayoutInflater;
+import android.util.SparseArray;
 import android.view.View;
-import android.view.View.MeasureSpec;
-import android.view.View.OnClickListener;
 import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
 import android.view.ViewTreeObserver;
 import android.view.WindowInsets;
-import android.view.animation.AccelerateInterpolator;
 import android.view.animation.AlphaAnimation;
 import android.view.animation.Animation;
-import android.view.animation.DecelerateInterpolator;
 import android.view.animation.LinearInterpolator;
-import android.widget.Button;
-import android.widget.ImageView;
-import android.widget.Space;
 import android.widget.TextView;
 
-import com.android.intentresolver.ResolverListAdapter.ActivityInfoPresentationGetter;
-import com.android.intentresolver.ResolverListAdapter.ViewHolder;
-import com.android.intentresolver.chooser.ChooserTargetInfo;
+import androidx.annotation.MainThread;
+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.NotSelectableTargetInfo;
-import com.android.intentresolver.chooser.SelectableTargetInfo;
-import com.android.intentresolver.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator;
 import com.android.intentresolver.chooser.TargetInfo;
-
+import com.android.intentresolver.grid.ChooserGridAdapter;
+import com.android.intentresolver.grid.DirectShareViewHolder;
+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.widget.ActionRow;
+import com.android.intentresolver.widget.ResolverDrawerLayout;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 import com.android.internal.content.PackageMonitor;
-import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 import com.android.internal.util.FrameworkStatsLog;
-import com.android.internal.widget.GridLayoutManager;
-import com.android.internal.widget.RecyclerView;
-import com.android.internal.widget.ResolverDrawerLayout;
-import com.android.internal.widget.ViewPager;
-
-import com.google.android.collect.Lists;
 
 import java.io.File;
 import java.io.IOException;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
-import java.net.URISyntaxException;
 import java.text.Collator;
 import java.util.ArrayList;
 import java.util.Collections;
@@ -148,25 +124,19 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.function.Supplier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.function.Consumer;
 
 /**
  * The Chooser Activity handles intent resolution specifically for sharing intents -
- * for example, those generated by @see android.content.Intent#createChooser(Intent, CharSequence).
+ * for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}.
  *
  */
 public class ChooserActivity extends ResolverActivity implements
-        ChooserListAdapter.ChooserListCommunicator,
-        SelectableTargetInfoCommunicator {
+        ResolverListAdapter.ResolverListCommunicator {
     private static final String TAG = "ChooserActivity";
 
-    private AppPredictor mPersonalAppPredictor;
-    private AppPredictor mWorkAppPredictor;
-    private boolean mShouldDisplayLandscape;
-
-    @UnsupportedAppUsage
-    public 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
@@ -175,7 +145,6 @@
     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.
@@ -190,44 +159,31 @@
 
     private static final boolean DEBUG = true;
 
-    private static final boolean USE_PREDICTION_MANAGER_FOR_SHARE_ACTIVITIES = true;
-    // TODO(b/123088566) Share these in a better way.
-    private static final String APP_PREDICTION_SHARE_UI_SURFACE = "share";
     public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share";
-    public static final String CHOOSER_TARGET = "chooser_target";
     private static final String SHORTCUT_TARGET = "shortcut_target";
-    private static final int APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT = 20;
-    public static final String APP_PREDICTION_INTENT_FILTER_KEY = "intent_filter";
-    private static final String SHARED_TEXT_KEY = "shared_text";
 
     private static final String PLURALS_COUNT = "count";
     private static final String PLURALS_FILE_NAME = "file_name";
 
     private static final String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image";
 
-    private boolean mIsAppPredictorComponentAvailable;
-    private Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache;
-    private Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache;
+    // 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<>();
 
     public static final int TARGET_TYPE_DEFAULT = 0;
     public static final int TARGET_TYPE_CHOOSER_TARGET = 1;
     public static final int TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER = 2;
     public static final int TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE = 3;
 
-    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;
-
     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;
 
-    // statsd logger wrapper
-    protected ChooserActivityLogger mChooserActivityLogger;
-
     @IntDef(flag = false, prefix = { "TARGET_TYPE_" }, value = {
             TARGET_TYPE_DEFAULT,
             TARGET_TYPE_CHOOSER_TARGET,
@@ -237,294 +193,68 @@
     @Retention(RetentionPolicy.SOURCE)
     public @interface ShareTargetType {}
 
-    /**
-     * The transition time between placeholders for direct share to a message
-     * indicating that non are available.
-     */
-    private static final int NO_DIRECT_SHARE_ANIM_IN_MILLIS = 200;
-
-    private static final float DIRECT_SHARE_EXPANSION_RATE = 0.78f;
+    public static final float DIRECT_SHARE_EXPANSION_RATE = 0.78f;
 
     private static final int DEFAULT_SALT_EXPIRATION_DAYS = 7;
-    private int mMaxHashSaltDays = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
+    private final int mMaxHashSaltDays = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
             SystemUiDeviceConfigFlags.HASH_SALT_MAX_DAYS,
             DEFAULT_SALT_EXPIRATION_DAYS);
 
-    private static final boolean DEFAULT_IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP = false;
-    private boolean mIsNearbyShareFirstTargetInRankedApp =
-            DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
-                    SystemUiDeviceConfigFlags.IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP,
-                    DEFAULT_IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP);
-
-    private static final int DEFAULT_LIST_VIEW_UPDATE_DELAY_MS = 0;
-
     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;
 
-    @VisibleForTesting
-    int mListViewUpdateDelayMs = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
-            SystemUiDeviceConfigFlags.SHARESHEET_LIST_VIEW_UPDATE_DELAY,
-            DEFAULT_LIST_VIEW_UPDATE_DELAY_MS);
+    /* TODO: this is `nullable` because we have to defer the assignment til onCreate(). We make the
+     * only assignment there, and expect it to be ready by the time we ever use it --
+     * someday if we move all the usage to a component with a narrower lifecycle (something that
+     * matches our Activity's create/destroy lifecycle, not its Java object lifecycle) then we
+     * should be able to make this assignment as "final."
+     */
+    @Nullable
+    private ChooserRequestParameters mChooserRequest;
 
-    private Bundle mReplacementExtras;
-    private IntentSender mChosenComponentSender;
-    private IntentSender mRefinementIntentSender;
+    private boolean mShouldDisplayLandscape;
+    // statsd logger wrapper
+    protected ChooserActivityLogger mChooserActivityLogger;
+
+    @Nullable
     private RefinementResultReceiver mRefinementResultReceiver;
-    private ChooserTarget[] mCallerChooserTargets;
-    private ComponentName[] mFilteredComponentNames;
-
-    private Intent mReferrerFillInIntent;
 
     private long mChooserShownTime;
     protected boolean mIsSuccessfullySelected;
 
-    private long mQueriedSharingShortcutsTimeMs;
-
     private int mCurrAvailableWidth = 0;
     private Insets mLastAppliedInsets = null;
     private int mLastNumberOfChildren = -1;
     private int mMaxTargetsPerRow = 1;
 
-    private static final String TARGET_DETAILS_FRAGMENT_TAG = "targetDetailsFragment";
-
     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";
 
-    @Retention(SOURCE)
-    @IntDef({CONTENT_PREVIEW_FILE, CONTENT_PREVIEW_IMAGE, CONTENT_PREVIEW_TEXT})
-    private @interface ContentPreviewType {
-    }
+    private final ExecutorService mBackgroundThreadPoolExecutor = Executors.newFixedThreadPool(5);
 
-    // Starting at 1 since 0 is considered "undefined" for some of the database transformations
-    // of tron logs.
-    protected static final int CONTENT_PREVIEW_IMAGE = 1;
-    protected static final int CONTENT_PREVIEW_FILE = 2;
-    protected static final int CONTENT_PREVIEW_TEXT = 3;
-    protected MetricsLogger mMetricsLogger;
+    @Nullable
+    private ChooserContentPreviewCoordinator mPreviewCoordinator;
 
-    private ContentPreviewCoordinator mPreviewCoord;
     private int mScrollStatus = SCROLL_STATUS_IDLE;
 
     @VisibleForTesting
     protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter;
     private final EnterTransitionAnimationDelegate mEnterTransitionAnimationDelegate =
-            new EnterTransitionAnimationDelegate();
-
-    private boolean mRemoveSharedElements = false;
+            new EnterTransitionAnimationDelegate(this, () -> mResolverDrawerLayout);
 
     private View mContentView = null;
 
-    private class ContentPreviewCoordinator {
-        private static final int IMAGE_FADE_IN_MILLIS = 150;
-        private static final int IMAGE_LOAD_TIMEOUT = 1;
-        private static final int IMAGE_LOAD_INTO_VIEW = 2;
+    private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>();
 
-        private final int mImageLoadTimeoutMillis =
-                getResources().getInteger(R.integer.config_shortAnimTime);
-
-        private final View mParentView;
-        private boolean mHideParentOnFail;
-        private boolean mAtLeastOneLoaded = false;
-
-        class LoadUriTask {
-            public final Uri mUri;
-            public final int mImageResourceId;
-            public final int mExtraCount;
-            public final Bitmap mBmp;
-
-            LoadUriTask(int imageResourceId, Uri uri, int extraCount, Bitmap bmp) {
-                this.mImageResourceId = imageResourceId;
-                this.mUri = uri;
-                this.mExtraCount = extraCount;
-                this.mBmp = bmp;
-            }
-        }
-
-        // If at least one image loads within the timeout period, allow other
-        // loads to continue. Otherwise terminate and optionally hide
-        // the parent area
-        private final Handler mHandler = new Handler() {
-            @Override
-            public void handleMessage(Message msg) {
-                switch (msg.what) {
-                    case IMAGE_LOAD_TIMEOUT:
-                        maybeHideContentPreview();
-                        break;
-
-                    case IMAGE_LOAD_INTO_VIEW:
-                        if (isFinishing()) break;
-
-                        LoadUriTask task = (LoadUriTask) msg.obj;
-                        RoundedRectImageView imageView = mParentView.findViewById(
-                                task.mImageResourceId);
-                        if (task.mBmp == null) {
-                            imageView.setVisibility(View.GONE);
-                            maybeHideContentPreview();
-                            return;
-                        }
-
-                        mAtLeastOneLoaded = true;
-                        imageView.setVisibility(View.VISIBLE);
-                        imageView.setAlpha(0.0f);
-                        imageView.setImageBitmap(task.mBmp);
-
-                        ValueAnimator fadeAnim = ObjectAnimator.ofFloat(imageView, "alpha", 0.0f,
-                                1.0f);
-                        fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
-                        fadeAnim.setDuration(IMAGE_FADE_IN_MILLIS);
-                        fadeAnim.start();
-
-                        if (task.mExtraCount > 0) {
-                            imageView.setExtraImageCount(task.mExtraCount);
-                        }
-
-                        setupPreDrawForSharedElementTransition(imageView);
-                }
-            }
-        };
-
-        private void setupPreDrawForSharedElementTransition(View v) {
-            v.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
-                @Override
-                public boolean onPreDraw() {
-                    v.getViewTreeObserver().removeOnPreDrawListener(this);
-
-                    if (!mRemoveSharedElements && isActivityTransitionRunning()) {
-                        // Disable the window animations as it interferes with the
-                        // transition animation.
-                        getWindow().setWindowAnimations(0);
-                    }
-                    mEnterTransitionAnimationDelegate.markImagePreviewReady();
-                    return true;
-                }
-            });
-        }
-
-        ContentPreviewCoordinator(View parentView, boolean hideParentOnFail) {
-            super();
-
-            this.mParentView = parentView;
-            this.mHideParentOnFail = hideParentOnFail;
-        }
-
-        private void loadUriIntoView(final int imageResourceId, final Uri uri,
-                final int extraImages) {
-            mHandler.sendEmptyMessageDelayed(IMAGE_LOAD_TIMEOUT, mImageLoadTimeoutMillis);
-
-            AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
-                int size = getResources().getDimensionPixelSize(
-                        R.dimen.chooser_preview_image_max_dimen);
-                final Bitmap bmp = loadThumbnail(uri, new Size(size, size));
-                final Message msg = Message.obtain();
-                msg.what = IMAGE_LOAD_INTO_VIEW;
-                msg.obj = new LoadUriTask(imageResourceId, uri, extraImages, bmp);
-                mHandler.sendMessage(msg);
-            });
-        }
-
-        private void cancelLoads() {
-            mHandler.removeMessages(IMAGE_LOAD_INTO_VIEW);
-            mHandler.removeMessages(IMAGE_LOAD_TIMEOUT);
-        }
-
-        private void maybeHideContentPreview() {
-            if (!mAtLeastOneLoaded) {
-                if (mHideParentOnFail) {
-                    Log.i(TAG, "Hiding image preview area. Timed out waiting for preview to load"
-                            + " within " + mImageLoadTimeoutMillis + "ms.");
-                    collapseParentView();
-                    if (shouldShowTabs()) {
-                        hideStickyContentPreview();
-                    } else if (mChooserMultiProfilePagerAdapter.getCurrentRootAdapter() != null) {
-                        mChooserMultiProfilePagerAdapter.getCurrentRootAdapter()
-                                .hideContentPreview();
-                    }
-                    mHideParentOnFail = false;
-                }
-                mRemoveSharedElements = true;
-                mEnterTransitionAnimationDelegate.markImagePreviewReady();
-            }
-        }
-
-        private void collapseParentView() {
-            // This will effectively hide the content preview row by forcing the height
-            // to zero. It is faster than forcing a relayout of the listview
-            final View v = mParentView;
-            int widthSpec = MeasureSpec.makeMeasureSpec(v.getWidth(), MeasureSpec.EXACTLY);
-            int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY);
-            v.measure(widthSpec, heightSpec);
-            v.getLayoutParams().height = 0;
-            v.layout(v.getLeft(), v.getTop(), v.getRight(), v.getTop());
-            v.invalidate();
-        }
-    }
-
-    private final ChooserHandler mChooserHandler = new ChooserHandler();
-
-    private class ChooserHandler extends Handler {
-        private static final int LIST_VIEW_UPDATE_MESSAGE = 6;
-        private static final int SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS = 7;
-
-        private void removeAllMessages() {
-            removeMessages(LIST_VIEW_UPDATE_MESSAGE);
-            removeMessages(SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS);
-        }
-
-        @Override
-        public void handleMessage(Message msg) {
-            if (mChooserMultiProfilePagerAdapter.getActiveListAdapter() == null || isDestroyed()) {
-                return;
-            }
-
-            switch (msg.what) {
-                case LIST_VIEW_UPDATE_MESSAGE:
-                    if (DEBUG) {
-                        Log.d(TAG, "LIST_VIEW_UPDATE_MESSAGE; ");
-                    }
-
-                    UserHandle userHandle = (UserHandle) msg.obj;
-                    mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle)
-                            .refreshListView();
-                    break;
-
-                case SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS:
-                    if (DEBUG) Log.d(TAG, "SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS");
-                    final ServiceResultInfo[] resultInfos = (ServiceResultInfo[]) msg.obj;
-                    for (ServiceResultInfo resultInfo : resultInfos) {
-                        if (resultInfo.resultTargets != null) {
-                            ChooserListAdapter adapterForUserHandle =
-                                    mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(
-                                            resultInfo.userHandle);
-                            if (adapterForUserHandle != null) {
-                                adapterForUserHandle.addServiceResults(
-                                        resultInfo.originalTarget,
-                                        resultInfo.resultTargets, msg.arg1,
-                                        mDirectShareShortcutInfoCache);
-                            }
-                        }
-                    }
-
-                    logDirectShareTargetReceived(
-                            MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER);
-                    sendVoiceChoicesIfNeeded();
-                    getChooserActivityLogger().logSharesheetDirectLoadComplete();
-
-                    mChooserMultiProfilePagerAdapter.getActiveListAdapter()
-                            .completeServiceTargetLoading();
-                    break;
-
-                default:
-                    super.handleMessage(msg);
-            }
-        }
-    };
+    public ChooserActivity() {}
 
     @Override
     protected void onCreate(Bundle savedInstanceState) {
@@ -532,168 +262,59 @@
         mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
 
         getChooserActivityLogger().logSharesheetTriggered();
-        // This is the only place this value is being set. Effectively final.
-        mIsAppPredictorComponentAvailable = isAppPredictionServiceAvailable();
 
-        mIsSuccessfullySelected = false;
-        Intent intent = getIntent();
-        Parcelable targetParcelable = intent.getParcelableExtra(Intent.EXTRA_INTENT);
-        if (targetParcelable instanceof Uri) {
-            try {
-                targetParcelable = Intent.parseUri(targetParcelable.toString(),
-                        Intent.URI_INTENT_SCHEME);
-            } catch (URISyntaxException ex) {
-                // doesn't parse as an intent; let the next test fail and error out
-            }
-        }
-
-        if (!(targetParcelable instanceof Intent)) {
-            Log.w("ChooserActivity", "Target is not an intent: " + targetParcelable);
+        try {
+            mChooserRequest = new ChooserRequestParameters(
+                    getIntent(), getReferrer(), getNearbySharingComponent());
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "Caller provided invalid Chooser request parameters", e);
             finish();
-            super.onCreate(null);
+            super_onCreate(null);
             return;
         }
-        Intent target = (Intent) targetParcelable;
-        if (target != null) {
-            modifyTargetIntent(target);
-        }
-        Parcelable[] targetsParcelable
-                = intent.getParcelableArrayExtra(Intent.EXTRA_ALTERNATE_INTENTS);
-        if (targetsParcelable != null) {
-            final boolean offset = target == null;
-            Intent[] additionalTargets =
-                    new Intent[offset ? targetsParcelable.length - 1 : targetsParcelable.length];
-            for (int i = 0; i < targetsParcelable.length; i++) {
-                if (!(targetsParcelable[i] instanceof Intent)) {
-                    Log.w(TAG, "EXTRA_ALTERNATE_INTENTS array entry #" + i + " is not an Intent: "
-                            + targetsParcelable[i]);
-                    finish();
-                    super.onCreate(null);
-                    return;
-                }
-                final Intent additionalTarget = (Intent) targetsParcelable[i];
-                if (i == 0 && target == null) {
-                    target = additionalTarget;
-                    modifyTargetIntent(target);
-                } else {
-                    additionalTargets[offset ? i - 1 : i] = additionalTarget;
-                    modifyTargetIntent(additionalTarget);
-                }
-            }
-            setAdditionalTargets(additionalTargets);
-        }
 
-        mReplacementExtras = intent.getBundleExtra(Intent.EXTRA_REPLACEMENT_EXTRAS);
+        setAdditionalTargets(mChooserRequest.getAdditionalTargets());
 
-        // Do not allow the title to be changed when sharing content
-        CharSequence title = null;
-        if (target != null) {
-            if (!isSendAction(target)) {
-                title = intent.getCharSequenceExtra(Intent.EXTRA_TITLE);
-            } else {
-                Log.w(TAG, "Ignoring intent's EXTRA_TITLE, deprecated in P. You may wish to set a"
-                        + " preview title by using EXTRA_TITLE property of the wrapped"
-                        + " EXTRA_INTENT.");
-            }
-        }
-
-        int defaultTitleRes = 0;
-        if (title == null) {
-            defaultTitleRes = com.android.internal.R.string.chooseActivity;
-        }
-
-        Parcelable[] pa = intent.getParcelableArrayExtra(Intent.EXTRA_INITIAL_INTENTS);
-        Intent[] initialIntents = null;
-        if (pa != null) {
-            int count = Math.min(pa.length, MAX_EXTRA_INITIAL_INTENTS);
-            initialIntents = new Intent[count];
-            for (int i = 0; i < count; i++) {
-                if (!(pa[i] instanceof Intent)) {
-                    Log.w(TAG, "Initial intent #" + i + " not an Intent: " + pa[i]);
-                    finish();
-                    super.onCreate(null);
-                    return;
-                }
-                final Intent in = (Intent) pa[i];
-                modifyTargetIntent(in);
-                initialIntents[i] = in;
-            }
-        }
-
-        mReferrerFillInIntent = new Intent().putExtra(Intent.EXTRA_REFERRER, getReferrer());
-
-        mChosenComponentSender = intent.getParcelableExtra(
-                Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER);
-        mRefinementIntentSender = intent.getParcelableExtra(
-                Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER);
         setSafeForwardingMode(true);
 
         mPinnedSharedPrefs = getPinnedSharedPrefs(this);
 
-        pa = intent.getParcelableArrayExtra(Intent.EXTRA_EXCLUDE_COMPONENTS);
-
-
-        // Exclude out Nearby from main list if chip is present, to avoid duplication
-        ComponentName nearbySharingComponent = getNearbySharingComponent();
-        boolean shouldFilterNearby = !shouldNearbyShareBeFirstInRankedRow()
-                && nearbySharingComponent != null;
-
-        if (pa != null) {
-            ComponentName[] names = new ComponentName[pa.length + (shouldFilterNearby ? 1 : 0)];
-            for (int i = 0; i < pa.length; i++) {
-                if (!(pa[i] instanceof ComponentName)) {
-                    Log.w(TAG, "Filtered component #" + i + " not a ComponentName: " + pa[i]);
-                    names = null;
-                    break;
-                }
-                names[i] = (ComponentName) pa[i];
-            }
-            if (shouldFilterNearby) {
-                names[names.length - 1] = nearbySharingComponent;
-            }
-
-            mFilteredComponentNames = names;
-        } else if (shouldFilterNearby) {
-            mFilteredComponentNames = new ComponentName[1];
-            mFilteredComponentNames[0] = nearbySharingComponent;
-        }
-
-        pa = intent.getParcelableArrayExtra(Intent.EXTRA_CHOOSER_TARGETS);
-        if (pa != null) {
-            int count = Math.min(pa.length, MAX_EXTRA_CHOOSER_TARGETS);
-            ChooserTarget[] targets = new ChooserTarget[count];
-            for (int i = 0; i < count; i++) {
-                if (!(pa[i] instanceof ChooserTarget)) {
-                    Log.w(TAG, "Chooser target #" + i + " not a ChooserTarget: " + pa[i]);
-                    targets = null;
-                    break;
-                }
-                targets[i] = (ChooserTarget) pa[i];
-            }
-            mCallerChooserTargets = targets;
-        }
-
         mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
         mShouldDisplayLandscape =
                 shouldDisplayLandscape(getResources().getConfiguration().orientation);
-        setRetainInOnStop(intent.getBooleanExtra(EXTRA_PRIVATE_RETAIN_IN_ON_STOP, false));
-        super.onCreate(savedInstanceState, target, title, defaultTitleRes, initialIntents,
-                null, false);
+        setRetainInOnStop(mChooserRequest.shouldRetainInOnStop());
+
+        createProfileRecords(
+                new AppPredictorFactory(
+                        getApplicationContext(),
+                        mChooserRequest.getSharedText(),
+                        mChooserRequest.getTargetIntentFilter()),
+                mChooserRequest.getTargetIntentFilter());
+
+        mPreviewCoordinator = new ChooserContentPreviewCoordinator(
+                mBackgroundThreadPoolExecutor,
+                this,
+                () -> mEnterTransitionAnimationDelegate.markImagePreviewReady(false));
+
+        super.onCreate(
+                savedInstanceState,
+                mChooserRequest.getTargetIntent(),
+                mChooserRequest.getTitle(),
+                mChooserRequest.getDefaultTitleResource(),
+                mChooserRequest.getInitialIntents(),
+                /* rList: List<ResolveInfo> = */ null,
+                /* supportsAlwaysUseOption = */ false);
 
         mChooserShownTime = System.currentTimeMillis();
         final long systemCost = mChooserShownTime - intentReceivedTime;
-
-        getMetricsLogger().write(new LogMaker(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN)
-                .setSubtype(isWorkProfile() ? MetricsEvent.MANAGED_PROFILE :
-                        MetricsEvent.PARENT_PROFILE)
-                .addTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE, target.getType())
-                .addTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS, systemCost));
+        getChooserActivityLogger().logChooserActivityShown(
+                isWorkProfile(), mChooserRequest.getTargetType(), systemCost);
 
         if (mResolverDrawerLayout != null) {
             mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange);
 
             // expand/shrink direct share 4 -> 8 viewgroup
-            if (isSendAction(target)) {
+            if (mChooserRequest.isSendActionTarget()) {
                 mResolverDrawerLayout.setOnScrollChangeListener(this::handleScroll);
             }
 
@@ -722,26 +343,16 @@
         getChooserActivityLogger().logShareStarted(
                 FrameworkStatsLog.SHARESHEET_STARTED,
                 getReferrerPackageName(),
-                target.getType(),
-                mCallerChooserTargets == null ? 0 : mCallerChooserTargets.length,
-                initialIntents == null ? 0 : initialIntents.length,
+                mChooserRequest.getTargetType(),
+                mChooserRequest.getCallerChooserTargets().size(),
+                (mChooserRequest.getInitialIntents() == null)
+                        ? 0 : mChooserRequest.getInitialIntents().length,
                 isWorkProfile(),
-                findPreferredContentPreview(getTargetIntent(), getContentResolver()),
-                target.getAction()
+                ChooserContentPreviewUi.findPreferredContentPreview(
+                        getTargetIntent(), getContentResolver(), this::isImageType),
+                mChooserRequest.getTargetAction()
         );
-        mDirectShareShortcutInfoCache = new HashMap<>();
 
-        setEnterSharedElementCallback(new SharedElementCallback() {
-            @Override
-            public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
-                if (mRemoveSharedElements) {
-                    names.remove(FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
-                    sharedElements.remove(FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
-                }
-                super.onMapSharedElements(names, sharedElements);
-                mRemoveSharedElements = false;
-            }
-        });
         mEnterTransitionAnimationDelegate.postponeTransition();
     }
 
@@ -750,52 +361,51 @@
         return R.style.Theme_DeviceDefault_Chooser;
     }
 
-    private AppPredictor setupAppPredictorForUser(UserHandle userHandle,
-            AppPredictor.Callback appPredictorCallback) {
-        AppPredictor appPredictor = getAppPredictorForDirectShareIfEnabled(userHandle);
-        if (appPredictor == null) {
-            return null;
+    private void createProfileRecords(
+            AppPredictorFactory factory, IntentFilter targetIntentFilter) {
+        UserHandle mainUserHandle = getPersonalProfileUserHandle();
+        createProfileRecord(mainUserHandle, targetIntentFilter, factory);
+
+        UserHandle workUserHandle = getWorkProfileUserHandle();
+        if (workUserHandle != null) {
+            createProfileRecord(workUserHandle, targetIntentFilter, factory);
         }
-        mDirectShareAppTargetCache = new HashMap<>();
-        appPredictor.registerPredictionUpdates(this.getMainExecutor(), appPredictorCallback);
-        return appPredictor;
     }
 
-    private AppPredictor.Callback createAppPredictorCallback(
-            ChooserListAdapter chooserListAdapter) {
-        return resultList -> {
-            if (isFinishing() || isDestroyed()) {
-                return;
-            }
-            if (chooserListAdapter.getCount() == 0) {
-                return;
-            }
-            if (resultList.isEmpty()
-                    && shouldQueryShortcutManager(chooserListAdapter.getUserHandle())) {
-                // APS may be disabled, so try querying targets ourselves.
-                queryDirectShareTargets(chooserListAdapter, true);
-                return;
-            }
-            final List<ShortcutManager.ShareShortcutInfo> shareShortcutInfos =
-                    new ArrayList<>();
+    private void createProfileRecord(
+            UserHandle userHandle, IntentFilter targetIntentFilter, AppPredictorFactory factory) {
+        AppPredictor appPredictor = factory.create(userHandle);
+        ShortcutLoader shortcutLoader = ActivityManager.isLowRamDeviceStatic()
+                    ? null
+                    : createShortcutLoader(
+                            getApplicationContext(),
+                            appPredictor,
+                            userHandle,
+                            targetIntentFilter,
+                            shortcutsResult -> onShortcutsLoaded(userHandle, shortcutsResult));
+        mProfileRecords.put(
+                userHandle.getIdentifier(),
+                new ProfileRecord(appPredictor, shortcutLoader));
+    }
 
-            List<AppTarget> shortcutResults = new ArrayList<>();
-            for (AppTarget appTarget : resultList) {
-                if (appTarget.getShortcutInfo() == null) {
-                    continue;
-                }
-                shortcutResults.add(appTarget);
-            }
-            resultList = shortcutResults;
-            for (AppTarget appTarget : resultList) {
-                shareShortcutInfos.add(new ShortcutManager.ShareShortcutInfo(
-                        appTarget.getShortcutInfo(),
-                        new ComponentName(
-                                appTarget.getPackageName(), appTarget.getClassName())));
-            }
-            sendShareShortcutInfoList(shareShortcutInfos, chooserListAdapter, resultList,
-                    chooserListAdapter.getUserHandle());
-        };
+    @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,
+                appPredictor,
+                userHandle,
+                targetIntentFilter,
+                callback);
     }
 
     static SharedPreferences getPinnedSharedPrefs(Context context) {
@@ -829,6 +439,41 @@
         return mChooserMultiProfilePagerAdapter;
     }
 
+    @Override
+    protected EmptyStateProvider createBlockerEmptyStateProvider() {
+        final boolean isSendAction = mChooserRequest.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(getPersonalProfileUserHandle(),
+                noWorkToPersonalEmptyState, noPersonalToWorkEmptyState,
+                createCrossProfileIntentsChecker(), createMyUserIdProvider());
+    }
+
     private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile(
             Intent[] initialIntents,
             List<ResolveInfo> rList,
@@ -843,9 +488,10 @@
         return new ChooserMultiProfilePagerAdapter(
                 /* context */ this,
                 adapter,
-                getPersonalProfileUserHandle(),
+                createEmptyStateProvider(/* workProfileUserHandle= */ null),
+                mQuietModeManager,
                 /* workProfileUserHandle= */ null,
-                isSendAction(getTargetIntent()), mMaxTargetsPerRow);
+                mMaxTargetsPerRow);
     }
 
     private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles(
@@ -871,10 +517,11 @@
                 /* context */ this,
                 personalAdapter,
                 workAdapter,
+                createEmptyStateProvider(/* workProfileUserHandle= */ getWorkProfileUserHandle()),
+                mQuietModeManager,
                 selectedProfile,
-                getPersonalProfileUserHandle(),
                 getWorkProfileUserHandle(),
-                isSendAction(getTargetIntent()), mMaxTargetsPerRow);
+                mMaxTargetsPerRow);
     }
 
     private int findSelectedProfile() {
@@ -891,19 +538,14 @@
         if (shouldShowStickyContentPreview()
                 || mChooserMultiProfilePagerAdapter
                         .getCurrentRootAdapter().getSystemRowCount() != 0) {
-            logActionShareWithPreview();
+            getChooserActivityLogger().logActionShareWithPreview(
+                    ChooserContentPreviewUi.findPreferredContentPreview(
+                            getTargetIntent(), getContentResolver(), this::isImageType));
         }
         return postRebuildListInternal(rebuildCompleted);
     }
 
     /**
-     * Returns true if app prediction service is defined and the component exists on device.
-     */
-    private boolean isAppPredictionServiceAvailable() {
-        return getPackageManager().getAppPredictionServicePackageName() != null;
-    }
-
-    /**
      * 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)
@@ -949,7 +591,7 @@
         updateProfileViewButton();
     }
 
-    private void onCopyButtonClicked(View v) {
+    private void onCopyButtonClicked() {
         Intent targetIntent = getTargetIntent();
         if (targetIntent == null) {
             finish();
@@ -987,15 +629,7 @@
                     Context.CLIPBOARD_SERVICE);
             clipboardManager.setPrimaryClipAsPackage(clipData, getReferrerPackageName());
 
-            // Log share completion via copy
-            LogMaker targetLogMaker = new LogMaker(
-                    MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SYSTEM_TARGET).setSubtype(1);
-            getMetricsLogger().write(targetLogMaker);
-            getChooserActivityLogger().logShareTargetSelected(
-                    SELECTION_TYPE_COPY,
-                    "",
-                    -1,
-                    false);
+            getChooserActivityLogger().logActionSelected(ChooserActivityLogger.SELECTION_TYPE_COPY);
 
             setResult(RESULT_OK);
             finish();
@@ -1068,10 +702,59 @@
         }
     }
 
-    private ViewGroup createContentPreviewView(ViewGroup parent) {
+    /**
+     * 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,
+            ChooserContentPreviewUi.ContentPreviewCoordinator previewCoordinator) {
         Intent targetIntent = getTargetIntent();
-        int previewType = findPreferredContentPreview(targetIntent, getContentResolver());
-        return displayContentPreview(previewType, targetIntent, getLayoutInflater(), parent);
+        int previewType = ChooserContentPreviewUi.findPreferredContentPreview(
+                targetIntent, getContentResolver(), this::isImageType);
+
+        ChooserContentPreviewUi.ActionFactory actionFactory =
+                new ChooserContentPreviewUi.ActionFactory() {
+                    @Override
+                    public ActionRow.Action createCopyButton() {
+                        return ChooserActivity.this.createCopyAction();
+                    }
+
+                    @Nullable
+                    @Override
+                    public ActionRow.Action createEditButton() {
+                        return ChooserActivity.this.createEditAction(targetIntent);
+                    }
+
+                    @Nullable
+                    @Override
+                    public ActionRow.Action createNearbyButton() {
+                        return ChooserActivity.this.createNearbyAction(targetIntent);
+                    }
+                };
+
+        ViewGroup layout = ChooserContentPreviewUi.displayContentPreview(
+                previewType,
+                targetIntent,
+                getResources(),
+                getLayoutInflater(),
+                actionFactory,
+                R.layout.chooser_action_row,
+                parent,
+                previewCoordinator,
+                mEnterTransitionAnimationDelegate::markImagePreviewReady,
+                getContentResolver(),
+                this::isImageType);
+
+        if (layout != null) {
+            adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
+        }
+        if (previewType != ChooserContentPreviewUi.CONTENT_PREVIEW_IMAGE) {
+            mEnterTransitionAnimationDelegate.markImagePreviewReady(false);
+        }
+
+        return layout;
     }
 
     @VisibleForTesting
@@ -1108,6 +791,19 @@
         resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS);
         resolveIntent.setComponent(cn);
         resolveIntent.setAction(Intent.ACTION_EDIT);
+        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 = getContentResolver().getType(uri);
+                    resolveIntent.setDataAndType(uri, mimeType);
+                }
+            }
+        } else {
+            Log.e(TAG, originalAction + " is not supported.");
+            return null;
+        }
         final ResolveInfo ri = getPackageManager().resolveActivity(
                 resolveIntent, PackageManager.GET_META_DATA);
         if (ri == null || ri.activityInfo == null) {
@@ -1116,9 +812,15 @@
             return null;
         }
 
-        final DisplayResolveInfo dri = new DisplayResolveInfo(
-                originalIntent, ri, getString(com.android.internal.R.string.screenshot_edit), "", resolveIntent, null);
-        dri.setDisplayIcon(getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));
+        final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
+                originalIntent,
+                ri,
+                getString(com.android.internal.R.string.screenshot_edit),
+                "",
+                resolveIntent,
+                null);
+        dri.getDisplayIconHolder().setDisplayIcon(
+                getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));
         return dri;
     }
 
@@ -1160,70 +862,55 @@
             icon = ri.loadIcon(getPackageManager());
         }
 
-        final DisplayResolveInfo dri = new DisplayResolveInfo(
+        final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
                 originalIntent, ri, name, "", resolveIntent, null);
-        dri.setDisplayIcon(icon);
+        dri.getDisplayIconHolder().setDisplayIcon(icon);
         return dri;
     }
 
-    private Button createActionButton(Drawable icon, CharSequence title, View.OnClickListener r) {
-        Button b = (Button) LayoutInflater.from(this).inflate(R.layout.chooser_action_button, null);
-        if (icon != null) {
-            final int size = getResources()
-                    .getDimensionPixelSize(R.dimen.chooser_action_button_icon_size);
-            icon.setBounds(0, 0, size, size);
-            b.setCompoundDrawablesRelative(icon, null, null, null);
-        }
-        b.setText(title);
-        b.setOnClickListener(r);
-        return b;
-    }
-
-    private Button createCopyButton() {
-        final Button b = createActionButton(
+    private ActionRow.Action createCopyAction() {
+        return new ActionRow.Action(
+                com.android.internal.R.id.chooser_copy_button,
+                getString(com.android.internal.R.string.copy),
                 getDrawable(com.android.internal.R.drawable.ic_menu_copy_material),
-                getString(com.android.internal.R.string.copy), this::onCopyButtonClicked);
-        b.setId(com.android.internal.R.id.chooser_copy_button);
-        return b;
+                this::onCopyButtonClicked);
     }
 
-    private @Nullable Button createNearbyButton(Intent originalIntent) {
+    @Nullable
+    private ActionRow.Action createNearbyAction(Intent originalIntent) {
         final TargetInfo ti = getNearbySharingTarget(originalIntent);
-        if (ti == null) return null;
+        if (ti == null) {
+            return null;
+        }
 
-        final Button b = createActionButton(
-                ti.getDisplayIcon(this),
+        return new ActionRow.Action(
+                com.android.internal.R.id.chooser_nearby_button,
                 ti.getDisplayLabel(),
-                (View unused) -> {
-                    // Log share completion via nearby
-                    getChooserActivityLogger().logShareTargetSelected(
-                            SELECTION_TYPE_NEARBY,
-                            "",
-                            -1,
-                            false);
+                ti.getDisplayIconHolder().getDisplayIcon(),
+                () -> {
+                    getChooserActivityLogger().logActionSelected(
+                            ChooserActivityLogger.SELECTION_TYPE_NEARBY);
                     // Action bar is user-independent, always start as primary
                     safelyStartActivityAsUser(ti, getPersonalProfileUserHandle());
                     finish();
-                }
-        );
-        b.setId(com.android.internal.R.id.chooser_nearby_button);
-        return b;
+                });
     }
 
-    private @Nullable Button createEditButton(Intent originalIntent) {
+    @Nullable
+    private ActionRow.Action createEditAction(Intent originalIntent) {
         final TargetInfo ti = getEditSharingTarget(originalIntent);
-        if (ti == null) return null;
+        if (ti == null) {
+            return null;
+        }
 
-        final Button b = createActionButton(
-                ti.getDisplayIcon(this),
+        return new ActionRow.Action(
+                com.android.internal.R.id.chooser_edit_button,
                 ti.getDisplayLabel(),
-                (View unused) -> {
+                ti.getDisplayIconHolder().getDisplayIcon(),
+                () -> {
                     // Log share completion via edit
-                    getChooserActivityLogger().logShareTargetSelected(
-                            SELECTION_TYPE_EDIT,
-                            "",
-                            -1,
-                            false);
+                    getChooserActivityLogger().logActionSelected(
+                            ChooserActivityLogger.SELECTION_TYPE_EDIT);
                     View firstImgView = getFirstVisibleImgPreviewView();
                     // Action bar is user-independent, always start as primary
                     if (firstImgView == null) {
@@ -1238,8 +925,6 @@
                     }
                 }
         );
-        b.setId(com.android.internal.R.id.chooser_edit_button);
-        return b;
     }
 
     @Nullable
@@ -1248,165 +933,6 @@
         return firstImage != null && firstImage.isVisibleToUser() ? firstImage : null;
     }
 
-    private void addActionButton(ViewGroup parent, Button b) {
-        if (b == null) return;
-        final ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(
-                        LayoutParams.WRAP_CONTENT,
-                        LayoutParams.WRAP_CONTENT
-                );
-        final int gap = getResources().getDimensionPixelSize(R.dimen.resolver_icon_margin) / 2;
-        lp.setMarginsRelative(gap, 0, gap, 0);
-        parent.addView(b, lp);
-    }
-
-    private ViewGroup displayContentPreview(@ContentPreviewType int previewType,
-            Intent targetIntent, LayoutInflater layoutInflater, ViewGroup parent) {
-        ViewGroup layout = null;
-
-        switch (previewType) {
-            case CONTENT_PREVIEW_TEXT:
-                layout = displayTextContentPreview(targetIntent, layoutInflater, parent);
-                break;
-            case CONTENT_PREVIEW_IMAGE:
-                layout = displayImageContentPreview(targetIntent, layoutInflater, parent);
-                break;
-            case CONTENT_PREVIEW_FILE:
-                layout = displayFileContentPreview(targetIntent, layoutInflater, parent);
-                break;
-            default:
-                Log.e(TAG, "Unexpected content preview type: " + previewType);
-        }
-
-        if (layout != null) {
-            adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
-        }
-        if (previewType != CONTENT_PREVIEW_IMAGE) {
-            mEnterTransitionAnimationDelegate.markImagePreviewReady();
-        }
-
-        return layout;
-    }
-
-    private ViewGroup displayTextContentPreview(Intent targetIntent, LayoutInflater layoutInflater,
-            ViewGroup parent) {
-        ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
-                R.layout.chooser_grid_preview_text, parent, false);
-
-        final ViewGroup actionRow =
-                (ViewGroup) contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
-        addActionButton(actionRow, createCopyButton());
-        if (shouldNearbyShareBeIncludedAsActionButton()) {
-            addActionButton(actionRow, createNearbyButton(targetIntent));
-        }
-
-        CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
-        if (sharingText == null) {
-            contentPreviewLayout.findViewById(com.android.internal.R.id.content_preview_text_layout).setVisibility(
-                    View.GONE);
-        } else {
-            TextView textView = contentPreviewLayout.findViewById(com.android.internal.R.id.content_preview_text);
-            textView.setText(sharingText);
-        }
-
-        String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE);
-        if (TextUtils.isEmpty(previewTitle)) {
-            contentPreviewLayout.findViewById(com.android.internal.R.id.content_preview_title_layout).setVisibility(
-                    View.GONE);
-        } else {
-            TextView previewTitleView = contentPreviewLayout.findViewById(
-                    com.android.internal.R.id.content_preview_title);
-            previewTitleView.setText(previewTitle);
-
-            ClipData previewData = targetIntent.getClipData();
-            Uri previewThumbnail = null;
-            if (previewData != null) {
-                if (previewData.getItemCount() > 0) {
-                    ClipData.Item previewDataItem = previewData.getItemAt(0);
-                    previewThumbnail = previewDataItem.getUri();
-                }
-            }
-
-            ImageView previewThumbnailView = contentPreviewLayout.findViewById(
-                    com.android.internal.R.id.content_preview_thumbnail);
-            if (previewThumbnail == null) {
-                previewThumbnailView.setVisibility(View.GONE);
-            } else {
-                mPreviewCoord = new ContentPreviewCoordinator(contentPreviewLayout, false);
-                mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_thumbnail, previewThumbnail, 0);
-            }
-        }
-
-        return contentPreviewLayout;
-    }
-
-    private ViewGroup displayImageContentPreview(Intent targetIntent, LayoutInflater layoutInflater,
-            ViewGroup parent) {
-        ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
-                R.layout.chooser_grid_preview_image, parent, false);
-        ViewGroup imagePreview = contentPreviewLayout.findViewById(com.android.internal.R.id.content_preview_image_area);
-
-        final ViewGroup actionRow =
-                (ViewGroup) contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
-        //TODO: addActionButton(actionRow, createCopyButton());
-        if (shouldNearbyShareBeIncludedAsActionButton()) {
-            addActionButton(actionRow, createNearbyButton(targetIntent));
-        }
-        addActionButton(actionRow, createEditButton(targetIntent));
-
-        mPreviewCoord = new ContentPreviewCoordinator(contentPreviewLayout, false);
-
-        String action = targetIntent.getAction();
-        if (Intent.ACTION_SEND.equals(action)) {
-            Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
-            imagePreview.findViewById(com.android.internal.R.id.content_preview_image_1_large)
-                    .setTransitionName(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
-            mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_image_1_large, uri, 0);
-        } else {
-            ContentResolver resolver = getContentResolver();
-
-            List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
-            List<Uri> imageUris = new ArrayList<>();
-            for (Uri uri : uris) {
-                if (isImageType(resolver.getType(uri))) {
-                    imageUris.add(uri);
-                }
-            }
-
-            if (imageUris.size() == 0) {
-                Log.i(TAG, "Attempted to display image preview area with zero"
-                        + " available images detected in EXTRA_STREAM list");
-                imagePreview.setVisibility(View.GONE);
-                return contentPreviewLayout;
-            }
-
-            imagePreview.findViewById(com.android.internal.R.id.content_preview_image_1_large)
-                    .setTransitionName(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
-            mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_image_1_large, imageUris.get(0), 0);
-
-            if (imageUris.size() == 2) {
-                mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_image_2_large,
-                        imageUris.get(1), 0);
-            } else if (imageUris.size() > 2) {
-                mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_image_2_small,
-                        imageUris.get(1), 0);
-                mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_image_3_small,
-                        imageUris.get(2), imageUris.size() - 3);
-            }
-        }
-
-        return contentPreviewLayout;
-    }
-
-    private static class FileInfo {
-        public final String name;
-        public final boolean hasThumbnail;
-
-        FileInfo(String name, boolean hasThumbnail) {
-            this.name = name;
-            this.hasThumbnail = hasThumbnail;
-        }
-    }
-
     /**
      * Wrapping the ContentResolver call to expose for easier mocking,
      * and to avoid mocking Android core classes.
@@ -1416,175 +942,11 @@
         return resolver.query(uri, null, null, null, null);
     }
 
-    private FileInfo extractFileInfo(Uri uri, ContentResolver resolver) {
-        String fileName = null;
-        boolean hasThumbnail = false;
-
-        try (Cursor cursor = queryResolver(resolver, uri)) {
-            if (cursor != null && cursor.getCount() > 0) {
-                int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
-                int titleIndex = cursor.getColumnIndex(Downloads.Impl.COLUMN_TITLE);
-                int flagsIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS);
-
-                cursor.moveToFirst();
-                if (nameIndex != -1) {
-                    fileName = cursor.getString(nameIndex);
-                } else if (titleIndex != -1) {
-                    fileName = cursor.getString(titleIndex);
-                }
-
-                if (flagsIndex != -1) {
-                    hasThumbnail = (cursor.getInt(flagsIndex)
-                            & DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
-                }
-            }
-        } catch (SecurityException | NullPointerException e) {
-            logContentPreviewWarning(uri);
-        }
-
-        if (TextUtils.isEmpty(fileName)) {
-            fileName = uri.getPath();
-            int index = fileName.lastIndexOf('/');
-            if (index != -1) {
-                fileName = fileName.substring(index + 1);
-            }
-        }
-
-        return new FileInfo(fileName, hasThumbnail);
-    }
-
-    private 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 "
-                + "desired, consider using Intent#createChooser to launch the ChooserActivity, "
-                + "and set your Intent's clipData and flags in accordance with that method's "
-                + "documentation");
-    }
-
-    private ViewGroup displayFileContentPreview(Intent targetIntent, LayoutInflater layoutInflater,
-            ViewGroup parent) {
-
-        ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
-                R.layout.chooser_grid_preview_file, parent, false);
-
-        final ViewGroup actionRow =
-                (ViewGroup) contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
-        //TODO(b/120417119): addActionButton(actionRow, createCopyButton());
-        if (shouldNearbyShareBeIncludedAsActionButton()) {
-            addActionButton(actionRow, createNearbyButton(targetIntent));
-        }
-
-        String action = targetIntent.getAction();
-        if (Intent.ACTION_SEND.equals(action)) {
-            Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
-            loadFileUriIntoView(uri, contentPreviewLayout);
-        } else {
-            List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
-            int uriCount = uris.size();
-
-            if (uriCount == 0) {
-                contentPreviewLayout.setVisibility(View.GONE);
-                Log.i(TAG,
-                        "Appears to be no uris available in EXTRA_STREAM, removing "
-                                + "preview area");
-                return contentPreviewLayout;
-            } else if (uriCount == 1) {
-                loadFileUriIntoView(uris.get(0), contentPreviewLayout);
-            } else {
-                FileInfo fileInfo = extractFileInfo(uris.get(0), getContentResolver());
-                int remUriCount = uriCount - 1;
-                Map<String, Object> arguments = new HashMap<>();
-                arguments.put(PLURALS_COUNT, remUriCount);
-                arguments.put(PLURALS_FILE_NAME, fileInfo.name);
-                String fileName = PluralsMessageFormatter.format(
-                        getResources(),
-                        arguments,
-                        R.string.file_count);
-
-                TextView fileNameView = contentPreviewLayout.findViewById(
-                        com.android.internal.R.id.content_preview_filename);
-                fileNameView.setText(fileName);
-
-                View thumbnailView = contentPreviewLayout.findViewById(
-                        com.android.internal.R.id.content_preview_file_thumbnail);
-                thumbnailView.setVisibility(View.GONE);
-
-                ImageView fileIconView = contentPreviewLayout.findViewById(
-                        com.android.internal.R.id.content_preview_file_icon);
-                fileIconView.setVisibility(View.VISIBLE);
-                fileIconView.setImageResource(R.drawable.ic_file_copy);
-            }
-        }
-
-        return contentPreviewLayout;
-    }
-
-    private void loadFileUriIntoView(final Uri uri, final View parent) {
-        FileInfo fileInfo = extractFileInfo(uri, getContentResolver());
-
-        TextView fileNameView = parent.findViewById(com.android.internal.R.id.content_preview_filename);
-        fileNameView.setText(fileInfo.name);
-
-        if (fileInfo.hasThumbnail) {
-            mPreviewCoord = new ContentPreviewCoordinator(parent, false);
-            mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_file_thumbnail, uri, 0);
-        } else {
-            View thumbnailView = parent.findViewById(com.android.internal.R.id.content_preview_file_thumbnail);
-            thumbnailView.setVisibility(View.GONE);
-
-            ImageView fileIconView = parent.findViewById(com.android.internal.R.id.content_preview_file_icon);
-            fileIconView.setVisibility(View.VISIBLE);
-            fileIconView.setImageResource(R.drawable.chooser_file_generic);
-        }
-    }
-
     @VisibleForTesting
     protected boolean isImageType(String mimeType) {
         return mimeType != null && mimeType.startsWith("image/");
     }
 
-    @ContentPreviewType
-    private int findPreferredContentPreview(Uri uri, ContentResolver resolver) {
-        if (uri == null) {
-            return CONTENT_PREVIEW_TEXT;
-        }
-
-        String mimeType = resolver.getType(uri);
-        return isImageType(mimeType) ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE;
-    }
-
-    /**
-     * In {@link android.content.Intent#getType}, the app may specify a very general
-     * mime-type that broadly covers all data being shared, such as {@literal *}/*
-     * when sending an image and text. We therefore should inspect each item for the
-     * the preferred type, in order of IMAGE, FILE, TEXT.
-     */
-    @ContentPreviewType
-    private int findPreferredContentPreview(Intent targetIntent, ContentResolver resolver) {
-        String action = targetIntent.getAction();
-        if (Intent.ACTION_SEND.equals(action)) {
-            Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
-            return findPreferredContentPreview(uri, resolver);
-        } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
-            List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
-            if (uris == null || uris.isEmpty()) {
-                return CONTENT_PREVIEW_TEXT;
-            }
-
-            for (Uri uri : uris) {
-                // Defaulting to file preview when there are mixed image/file types is
-                // preferable, as it shows the user the correct number of items being shared
-                if (findPreferredContentPreview(uri, resolver) == CONTENT_PREVIEW_FILE) {
-                    return CONTENT_PREVIEW_FILE;
-                }
-            }
-
-            return CONTENT_PREVIEW_IMAGE;
-        }
-
-        return CONTENT_PREVIEW_TEXT;
-    }
-
     private int getNumSheetExpansions() {
         return getPreferences(Context.MODE_PRIVATE).getInt(PREF_NUM_SHEET_EXPANSIONS, 0);
     }
@@ -1614,23 +976,29 @@
             mRefinementResultReceiver.destroy();
             mRefinementResultReceiver = null;
         }
-        mChooserHandler.removeAllMessages();
 
-        if (mPreviewCoord != null) mPreviewCoord.cancelLoads();
+        mBackgroundThreadPoolExecutor.shutdownNow();
 
-        mChooserMultiProfilePagerAdapter.getActiveListAdapter().destroyAppPredictor();
-        if (mChooserMultiProfilePagerAdapter.getInactiveListAdapter() != null) {
-            mChooserMultiProfilePagerAdapter.getInactiveListAdapter().destroyAppPredictor();
+        destroyProfileRecords();
+    }
+
+    private void destroyProfileRecords() {
+        for (int i = 0; i < mProfileRecords.size(); ++i) {
+            mProfileRecords.valueAt(i).destroy();
         }
-        mPersonalAppPredictor = null;
-        mWorkAppPredictor = null;
+        mProfileRecords.clear();
     }
 
     @Override // ResolverListCommunicator
     public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
+        if (mChooserRequest == null) {
+            return defIntent;
+        }
+
         Intent result = defIntent;
-        if (mReplacementExtras != null) {
-            final Bundle replExtras = mReplacementExtras.getBundle(aInfo.packageName);
+        if (mChooserRequest.getReplacementExtras() != null) {
+            final Bundle replExtras =
+                    mChooserRequest.getReplacementExtras().getBundle(aInfo.packageName);
             if (replExtras != null) {
                 result = new Intent(defIntent);
                 result.putExtras(replExtras);
@@ -1651,12 +1019,13 @@
 
     @Override
     public void onActivityStarted(TargetInfo cti) {
-        if (mChosenComponentSender != null) {
+        if (mChooserRequest.getChosenComponentSender() != null) {
             final ComponentName target = cti.getResolvedComponentName();
             if (target != null) {
                 final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target);
                 try {
-                    mChosenComponentSender.sendIntent(this, Activity.RESULT_OK, fillIn, null, null);
+                    mChooserRequest.getChosenComponentSender().sendIntent(
+                            this, Activity.RESULT_OK, fillIn, null, null);
                 } catch (IntentSender.SendIntentException e) {
                     Slog.e(TAG, "Unable to launch supplied IntentSender to report "
                             + "the chosen component: " + e);
@@ -1667,12 +1036,13 @@
 
     @Override
     public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) {
-        if (mCallerChooserTargets != null && mCallerChooserTargets.length > 0) {
+        if (mChooserRequest.getCallerChooserTargets().size() > 0) {
             mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults(
                     /* origTarget */ null,
-                    Lists.newArrayList(mCallerChooserTargets),
+                    new ArrayList<>(mChooserRequest.getCallerChooserTargets()),
                     TARGET_TYPE_DEFAULT,
-                    /* directShareShortcutInfoCache */ null);
+                    /* directShareShortcutInfoCache */ Collections.emptyMap(),
+                    /* directShareAppTargetCache */ Collections.emptyMap());
         }
     }
 
@@ -1701,57 +1071,34 @@
     private void showTargetDetails(TargetInfo targetInfo) {
         if (targetInfo == null) return;
 
-        ArrayList<DisplayResolveInfo> targetList;
-        ChooserTargetActionsDialogFragment fragment = new ChooserTargetActionsDialogFragment();
-        Bundle bundle = new Bundle();
-
-        if (targetInfo instanceof SelectableTargetInfo) {
-            SelectableTargetInfo selectableTargetInfo = (SelectableTargetInfo) targetInfo;
-            if (selectableTargetInfo.getDisplayResolveInfo() == null
-                    || selectableTargetInfo.getChooserTarget() == null) {
-                Log.e(TAG, "displayResolveInfo or chooserTarget in selectableTargetInfo are null");
-                return;
-            }
-            targetList = new ArrayList<>();
-            targetList.add(selectableTargetInfo.getDisplayResolveInfo());
-            bundle.putString(ChooserTargetActionsDialogFragment.SHORTCUT_ID_KEY,
-                    selectableTargetInfo.getChooserTarget().getIntentExtras().getString(
-                            Intent.EXTRA_SHORTCUT_ID));
-            bundle.putBoolean(ChooserTargetActionsDialogFragment.IS_SHORTCUT_PINNED_KEY,
-                    selectableTargetInfo.isPinned());
-            bundle.putParcelable(ChooserTargetActionsDialogFragment.INTENT_FILTER_KEY,
-                    getTargetIntentFilter());
-            if (selectableTargetInfo.getDisplayLabel() != null) {
-                bundle.putString(ChooserTargetActionsDialogFragment.SHORTCUT_TITLE_KEY,
-                        selectableTargetInfo.getDisplayLabel().toString());
-            }
-        } else if (targetInfo instanceof MultiDisplayResolveInfo) {
-            // For multiple targets, include info on all targets
-            MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo;
-            targetList = mti.getTargets();
-        } else {
-            targetList = new ArrayList<DisplayResolveInfo>();
-            targetList.add((DisplayResolveInfo) targetInfo);
+        List<DisplayResolveInfo> targetList = targetInfo.getAllDisplayTargets();
+        if (targetList.isEmpty()) {
+            Log.e(TAG, "No displayable data to show target details");
+            return;
         }
-        bundle.putParcelable(ChooserTargetActionsDialogFragment.USER_HANDLE_KEY,
-                mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
-        bundle.putParcelableArrayList(ChooserTargetActionsDialogFragment.TARGET_INFOS_KEY,
-                targetList);
-        fragment.setArguments(bundle);
 
-        fragment.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG);
-    }
+        // TODO: implement these type-conditioned behaviors polymorphically, and consider moving
+        // the logic into `ChooserTargetActionsDialogFragment.show()`.
+        boolean isShortcutPinned = targetInfo.isSelectableTargetInfo() && targetInfo.isPinned();
+        IntentFilter intentFilter = targetInfo.isSelectableTargetInfo()
+                ? mChooserRequest.getTargetIntentFilter() : null;
+        String shortcutTitle = targetInfo.isSelectableTargetInfo()
+                ? targetInfo.getDisplayLabel().toString() : null;
+        String shortcutIdKey = targetInfo.getDirectShareShortcutId();
 
-    private void modifyTargetIntent(Intent in) {
-        if (isSendAction(in)) {
-            in.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT |
-                    Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
-        }
+        ChooserTargetActionsDialogFragment.show(
+                getSupportFragmentManager(),
+                targetList,
+                mChooserMultiProfilePagerAdapter.getCurrentUserHandle(),
+                shortcutIdKey,
+                shortcutTitle,
+                isShortcutPinned,
+                intentFilter);
     }
 
     @Override
     protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) {
-        if (mRefinementIntentSender != null) {
+        if (mChooserRequest.getRefinementIntentSender() != null) {
             final Intent fillIn = new Intent();
             final List<Intent> sourceIntents = target.getAllSourceIntents();
             if (!sourceIntents.isEmpty()) {
@@ -1770,7 +1117,8 @@
                 fillIn.putExtra(Intent.EXTRA_RESULT_RECEIVER,
                         mRefinementResultReceiver);
                 try {
-                    mRefinementIntentSender.sendIntent(this, 0, fillIn, null, null);
+                    mChooserRequest.getRefinementIntentSender().sendIntent(
+                            this, 0, fillIn, null, null);
                     return false;
                 } catch (SendIntentException e) {
                     Log.e(TAG, "Refinement IntentSender failed to send", e);
@@ -1787,25 +1135,20 @@
                 mChooserMultiProfilePagerAdapter.getActiveListAdapter();
         TargetInfo targetInfo = currentListAdapter
                 .targetInfoForPosition(which, filtered);
-        if (targetInfo != null && targetInfo instanceof NotSelectableTargetInfo) {
+        if (targetInfo != null && targetInfo.isNotSelectableTargetInfo()) {
             return;
         }
 
         final long selectionCost = System.currentTimeMillis() - mChooserShownTime;
 
-        if (targetInfo instanceof MultiDisplayResolveInfo) {
+        if (targetInfo.isMultiDisplayResolveInfo()) {
             MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo;
             if (!mti.hasSelected()) {
-                ChooserStackedAppDialogFragment f = new ChooserStackedAppDialogFragment();
-                Bundle b = new Bundle();
-                b.putParcelable(ChooserTargetActionsDialogFragment.USER_HANDLE_KEY,
+                ChooserStackedAppDialogFragment.show(
+                        getSupportFragmentManager(),
+                        mti,
+                        which,
                         mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
-                b.putObject(ChooserStackedAppDialogFragment.MULTI_DRI_KEY,
-                        mti);
-                b.putInt(ChooserStackedAppDialogFragment.WHICH_KEY, which);
-                f.setArguments(b);
-
-                f.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG);
                 return;
             }
         }
@@ -1813,103 +1156,65 @@
         super.startSelected(which, always, filtered);
 
         if (currentListAdapter.getCount() > 0) {
-            // Log the index of which type of target the user picked.
-            // Lower values mean the ranking was better.
-            int cat = 0;
-            int value = which;
-            int directTargetAlsoRanked = -1;
-            int numCallerProvided = 0;
-            HashedStringCache.HashResult directTargetHashed = null;
             switch (currentListAdapter.getPositionTargetType(which)) {
                 case ChooserListAdapter.TARGET_SERVICE:
-                    cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET;
-                    // Log the package name + target name to answer the question if most users
-                    // share to mostly the same person or to a bunch of different people.
-                    ChooserTarget target = currentListAdapter.getChooserTargetForValue(value);
-                    directTargetHashed = HashedStringCache.getInstance().hashString(
-                            this,
-                            TAG,
-                            target.getComponentName().getPackageName()
-                                    + target.getTitle().toString(),
-                            mMaxHashSaltDays);
-                    SelectableTargetInfo selectableTargetInfo = (SelectableTargetInfo) targetInfo;
-                    directTargetAlsoRanked = getRankedPosition(selectableTargetInfo);
-
-                    if (mCallerChooserTargets != null) {
-                        numCallerProvided = mCallerChooserTargets.length;
-                    }
                     getChooserActivityLogger().logShareTargetSelected(
-                            SELECTION_TYPE_SERVICE,
+                            ChooserActivityLogger.SELECTION_TYPE_SERVICE,
                             targetInfo.getResolveInfo().activityInfo.processName,
-                            value,
-                            selectableTargetInfo.isPinned()
+                            which,
+                            /* directTargetAlsoRanked= */ getRankedPosition(targetInfo),
+                            mChooserRequest.getCallerChooserTargets().size(),
+                            targetInfo.getHashedTargetIdForMetrics(this),
+                            targetInfo.isPinned(),
+                            mIsSuccessfullySelected,
+                            selectionCost
                     );
-                    break;
+                    return;
                 case ChooserListAdapter.TARGET_CALLER:
                 case ChooserListAdapter.TARGET_STANDARD:
-                    cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET;
-                    value -= currentListAdapter.getSurfacedTargetInfo().size();
-                    numCallerProvided = currentListAdapter.getCallerTargetCount();
                     getChooserActivityLogger().logShareTargetSelected(
-                            SELECTION_TYPE_APP,
+                            ChooserActivityLogger.SELECTION_TYPE_APP,
                             targetInfo.getResolveInfo().activityInfo.processName,
-                            value,
-                            targetInfo.isPinned()
+                            (which - currentListAdapter.getSurfacedTargetInfo().size()),
+                            /* directTargetAlsoRanked= */ -1,
+                            currentListAdapter.getCallerTargetCount(),
+                            /* directTargetHashed= */ null,
+                            targetInfo.isPinned(),
+                            mIsSuccessfullySelected,
+                            selectionCost
                     );
-                    break;
+                    return;
                 case ChooserListAdapter.TARGET_STANDARD_AZ:
-                    // A-Z targets are unranked standard targets; we use -1 to mark that they
-                    // are from the alphabetical pool.
-                    value = -1;
-                    cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET;
+                    // 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?
                     getChooserActivityLogger().logShareTargetSelected(
-                            SELECTION_TYPE_STANDARD,
+                            ChooserActivityLogger.SELECTION_TYPE_STANDARD,
                             targetInfo.getResolveInfo().activityInfo.processName,
-                            value,
-                            false
+                            /* value= */ -1,
+                            /* directTargetAlsoRanked= */ -1,
+                            /* numCallerProvided= */ 0,
+                            /* directTargetHashed= */ null,
+                            /* isPinned= */ false,
+                            mIsSuccessfullySelected,
+                            selectionCost
                     );
-                    break;
-            }
-
-            if (cat != 0) {
-                LogMaker targetLogMaker = new LogMaker(cat).setSubtype(value);
-                if (directTargetHashed != null) {
-                    targetLogMaker.addTaggedData(
-                            MetricsEvent.FIELD_HASHED_TARGET_NAME, directTargetHashed.hashedString);
-                    targetLogMaker.addTaggedData(
-                                    MetricsEvent.FIELD_HASHED_TARGET_SALT_GEN,
-                                    directTargetHashed.saltGeneration);
-                    targetLogMaker.addTaggedData(MetricsEvent.FIELD_RANKED_POSITION,
-                                    directTargetAlsoRanked);
-                }
-                targetLogMaker.addTaggedData(MetricsEvent.FIELD_IS_CATEGORY_USED,
-                        numCallerProvided);
-                getMetricsLogger().write(targetLogMaker);
-            }
-
-            if (mIsSuccessfullySelected) {
-                if (DEBUG) {
-                    Log.d(TAG, "User Selection Time Cost is " + selectionCost);
-                    Log.d(TAG, "position of selected app/service/caller is " +
-                            Integer.toString(value));
-                }
-                MetricsLogger.histogram(null, "user_selection_cost_for_smart_sharing",
-                        (int) selectionCost);
-                MetricsLogger.histogram(null, "app_position_for_smart_sharing", value);
+                    return;
             }
         }
     }
 
-    private int getRankedPosition(SelectableTargetInfo targetInfo) {
+    private int getRankedPosition(TargetInfo targetInfo) {
         String targetPackageName =
-                targetInfo.getChooserTarget().getComponentName().getPackageName();
+                targetInfo.getChooserTargetComponentName().getPackageName();
         ChooserListAdapter currentListAdapter =
                 mChooserMultiProfilePagerAdapter.getActiveListAdapter();
-        int maxRankedResults = Math.min(currentListAdapter.mDisplayList.size(),
-                MAX_LOG_RANK_POSITION);
+        int maxRankedResults = Math.min(
+                currentListAdapter.getDisplayResolveInfoCount(), MAX_LOG_RANK_POSITION);
 
         for (int i = 0; i < maxRankedResults; i++) {
-            if (currentListAdapter.mDisplayList.get(i)
+            if (currentListAdapter.getDisplayResolveInfo(i)
                     .getResolveInfo().activityInfo.packageName.equals(targetPackageName)) {
                 return i;
             }
@@ -1933,8 +1238,11 @@
     }
 
     private IntentFilter getTargetIntentFilter() {
+        return getTargetIntentFilter(getTargetIntent());
+    }
+
+    private IntentFilter getTargetIntentFilter(final Intent intent) {
         try {
-            final Intent intent = getTargetIntent();
             String dataString = intent.getDataString();
             if (intent.getType() == null) {
                 if (!TextUtils.isEmpty(dataString)) {
@@ -1968,218 +1276,18 @@
         }
     }
 
-    @VisibleForTesting
-    protected void queryDirectShareTargets(
-                ChooserListAdapter adapter, boolean skipAppPredictionService) {
-        mQueriedSharingShortcutsTimeMs = System.currentTimeMillis();
-        UserHandle userHandle = adapter.getUserHandle();
-        if (!skipAppPredictionService) {
-            AppPredictor appPredictor = getAppPredictorForDirectShareIfEnabled(userHandle);
-            if (appPredictor != null) {
-                appPredictor.requestPredictionUpdate();
-                return;
-            }
-        }
-        // Default to just querying ShortcutManager if AppPredictor not present.
-        final IntentFilter filter = getTargetIntentFilter();
-        if (filter == null) {
+    private void logDirectShareTargetReceived(UserHandle forUser) {
+        ProfileRecord profileRecord = getProfileRecord(forUser);
+        if (profileRecord == null) {
             return;
         }
-
-        AsyncTask.execute(() -> {
-            Context selectedProfileContext = createContextAsUser(userHandle, 0 /* flags */);
-            ShortcutManager sm = (ShortcutManager) selectedProfileContext
-                    .getSystemService(Context.SHORTCUT_SERVICE);
-            List<ShortcutManager.ShareShortcutInfo> resultList = sm.getShareTargets(filter);
-            sendShareShortcutInfoList(resultList, adapter, null, userHandle);
-        });
-    }
-
-    /**
-     * Returns {@code false} if {@code userHandle} is the work profile and it's either
-     * in quiet mode or not running.
-     */
-    private boolean shouldQueryShortcutManager(UserHandle userHandle) {
-        if (!shouldShowTabs()) {
-            return true;
-        }
-        if (!getWorkProfileUserHandle().equals(userHandle)) {
-            return true;
-        }
-        if (!isUserRunning(userHandle)) {
-            return false;
-        }
-        if (!isUserUnlocked(userHandle)) {
-            return false;
-        }
-        if (isQuietModeEnabled(userHandle)) {
-            return false;
-        }
-        return true;
-    }
-
-    private void sendShareShortcutInfoList(
-                List<ShortcutManager.ShareShortcutInfo> resultList,
-                ChooserListAdapter chooserListAdapter,
-                @Nullable List<AppTarget> appTargets, UserHandle userHandle) {
-        if (appTargets != null && appTargets.size() != resultList.size()) {
-            throw new RuntimeException("resultList and appTargets must have the same size."
-                    + " resultList.size()=" + resultList.size()
-                    + " appTargets.size()=" + appTargets.size());
-        }
-        Context selectedProfileContext = createContextAsUser(userHandle, 0 /* flags */);
-        for (int i = resultList.size() - 1; i >= 0; i--) {
-            final String packageName = resultList.get(i).getTargetComponent().getPackageName();
-            if (!isPackageEnabled(selectedProfileContext, packageName)) {
-                resultList.remove(i);
-                if (appTargets != null) {
-                    appTargets.remove(i);
-                }
-            }
-        }
-
-        // If |appTargets| is not null, results are from AppPredictionService and already sorted.
-        final int shortcutType = (appTargets == null ? TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER :
-                TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
-
-        // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path
-        // for direct share targets. After ShareSheet is refactored we should use the
-        // ShareShortcutInfos directly.
-        List<ServiceResultInfo> resultRecords = new ArrayList<>();
-        for (int i = 0; i < chooserListAdapter.getDisplayResolveInfoCount(); i++) {
-            DisplayResolveInfo displayResolveInfo = chooserListAdapter.getDisplayResolveInfo(i);
-            List<ShortcutManager.ShareShortcutInfo> matchingShortcuts =
-                    filterShortcutsByTargetComponentName(
-                            resultList, displayResolveInfo.getResolvedComponentName());
-            if (matchingShortcuts.isEmpty()) {
-                continue;
-            }
-            List<ChooserTarget> chooserTargets = convertToChooserTarget(
-                    matchingShortcuts, resultList, appTargets, shortcutType);
-
-            ServiceResultInfo resultRecord = new ServiceResultInfo(
-                    displayResolveInfo, chooserTargets, userHandle);
-            resultRecords.add(resultRecord);
-        }
-
-        sendShortcutManagerShareTargetResults(
-                shortcutType, resultRecords.toArray(new ServiceResultInfo[0]));
-    }
-
-    private List<ShortcutManager.ShareShortcutInfo> filterShortcutsByTargetComponentName(
-            List<ShortcutManager.ShareShortcutInfo> allShortcuts, ComponentName requiredTarget) {
-        List<ShortcutManager.ShareShortcutInfo> matchingShortcuts = new ArrayList<>();
-        for (ShortcutManager.ShareShortcutInfo shortcut : allShortcuts) {
-            if (requiredTarget.equals(shortcut.getTargetComponent())) {
-                matchingShortcuts.add(shortcut);
-            }
-        }
-        return matchingShortcuts;
-    }
-
-    private void sendShortcutManagerShareTargetResults(
-            int shortcutType, ServiceResultInfo[] results) {
-        final Message msg = Message.obtain();
-        msg.what = ChooserHandler.SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS;
-        msg.obj = results;
-        msg.arg1 = shortcutType;
-        mChooserHandler.sendMessage(msg);
-    }
-
-    private boolean isPackageEnabled(Context context, String packageName) {
-        if (TextUtils.isEmpty(packageName)) {
-            return false;
-        }
-        ApplicationInfo appInfo;
-        try {
-            appInfo = context.getPackageManager().getApplicationInfo(packageName, 0);
-        } catch (NameNotFoundException e) {
-            return false;
-        }
-
-        if (appInfo != null && appInfo.enabled
-                && (appInfo.flags & ApplicationInfo.FLAG_SUSPENDED) == 0) {
-            return true;
-        }
-        return false;
-    }
-
-    /**
-     * Converts a list of ShareShortcutInfos to ChooserTargets.
-     * @param matchingShortcuts List of shortcuts, all from the same package, that match the current
-     *                         share intent filter.
-     * @param allShortcuts List of all the shortcuts from all the packages on the device that are
-     *                    returned for the current sharing action.
-     * @param allAppTargets List of AppTargets. Null if the results are not from prediction service.
-     * @param shortcutType One of the values TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER or
-     *                    TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
-     * @return A list of ChooserTargets sorted by score in descending order.
-     */
-    @VisibleForTesting
-    @NonNull
-    public List<ChooserTarget> convertToChooserTarget(
-            @NonNull List<ShortcutManager.ShareShortcutInfo> matchingShortcuts,
-            @NonNull List<ShortcutManager.ShareShortcutInfo> allShortcuts,
-            @Nullable List<AppTarget> allAppTargets, @ShareTargetType int shortcutType) {
-        // A set of distinct scores for the matched shortcuts. We use index of a rank in the sorted
-        // list instead of the actual rank value when converting a rank to a score.
-        List<Integer> scoreList = new ArrayList<>();
-        if (shortcutType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER) {
-            for (int i = 0; i < matchingShortcuts.size(); i++) {
-                int shortcutRank = matchingShortcuts.get(i).getShortcutInfo().getRank();
-                if (!scoreList.contains(shortcutRank)) {
-                    scoreList.add(shortcutRank);
-                }
-            }
-            Collections.sort(scoreList);
-        }
-
-        List<ChooserTarget> chooserTargetList = new ArrayList<>(matchingShortcuts.size());
-        for (int i = 0; i < matchingShortcuts.size(); i++) {
-            ShortcutInfo shortcutInfo = matchingShortcuts.get(i).getShortcutInfo();
-            int indexInAllShortcuts = allShortcuts.indexOf(matchingShortcuts.get(i));
-
-            float score;
-            if (shortcutType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) {
-                // Incoming results are ordered. Create a score based on index in the original list.
-                score = Math.max(1.0f - (0.01f * indexInAllShortcuts), 0.0f);
-            } else {
-                // Create a score based on the rank of the shortcut.
-                int rankIndex = scoreList.indexOf(shortcutInfo.getRank());
-                score = Math.max(1.0f - (0.01f * rankIndex), 0.0f);
-            }
-
-            Bundle extras = new Bundle();
-            extras.putString(Intent.EXTRA_SHORTCUT_ID, shortcutInfo.getId());
-
-            ChooserTarget chooserTarget = new ChooserTarget(
-                    shortcutInfo.getLabel(),
-                    null, // Icon will be loaded later if this target is selected to be shown.
-                    score, matchingShortcuts.get(i).getTargetComponent().clone(), extras);
-
-            chooserTargetList.add(chooserTarget);
-            if (mDirectShareAppTargetCache != null && allAppTargets != null) {
-                mDirectShareAppTargetCache.put(chooserTarget,
-                        allAppTargets.get(indexInAllShortcuts));
-            }
-            if (mDirectShareShortcutInfoCache != null) {
-                mDirectShareShortcutInfoCache.put(chooserTarget, shortcutInfo);
-            }
-        }
-        // Sort ChooserTargets by score in descending order
-        Comparator<ChooserTarget> byScore =
-                (ChooserTarget a, ChooserTarget b) -> -Float.compare(a.getScore(), b.getScore());
-        Collections.sort(chooserTargetList, byScore);
-        return chooserTargetList;
-    }
-
-    private void logDirectShareTargetReceived(int logCategory) {
-        final int apiLatency = (int) (System.currentTimeMillis() - mQueriedSharingShortcutsTimeMs);
-        getMetricsLogger().write(new LogMaker(logCategory).setSubtype(apiLatency));
+        getChooserActivityLogger().logDirectShareTargetReceived(
+                MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER,
+                (int) (SystemClock.elapsedRealtime() - profileRecord.loadingStartTime));
     }
 
     void updateModelAndChooserCounts(TargetInfo info) {
-        if (info != null && info instanceof MultiDisplayResolveInfo) {
+        if (info != null && info.isMultiDisplayResolveInfo()) {
             info = ((MultiDisplayResolveInfo) info).getSelectedTarget();
         }
         if (info != null) {
@@ -2200,31 +1308,35 @@
                     Log.d(TAG, "Action to be updated is " + targetIntent.getAction());
                 }
             } else if (DEBUG) {
-                Log.d(TAG, "Can not log Chooser Counts of null ResovleInfo");
+                Log.d(TAG, "Can not log Chooser Counts of null ResolveInfo");
             }
         }
         mIsSuccessfullySelected = true;
     }
 
     private void sendImpressionToAppPredictor(TargetInfo targetInfo, ChooserListAdapter adapter) {
-        AppPredictor directShareAppPredictor = getAppPredictorForDirectShareIfEnabled(
+        // 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;
         }
-        // Send DS target impression info to AppPredictor, only when user chooses app share.
-        if (targetInfo instanceof ChooserTargetInfo) {
-            return;
-        }
-        List<ChooserTargetInfo> surfacedTargetInfo = adapter.getSurfacedTargetInfo();
+        List<TargetInfo> surfacedTargetInfo = adapter.getSurfacedTargetInfo();
         List<AppTargetId> targetIds = new ArrayList<>();
-        for (ChooserTargetInfo chooserTargetInfo : surfacedTargetInfo) {
-            ChooserTarget chooserTarget = chooserTargetInfo.getChooserTarget();
-            ComponentName componentName = chooserTarget.getComponentName();
-            if (mDirectShareShortcutInfoCache.containsKey(chooserTarget)) {
-                String shortcutId = mDirectShareShortcutInfoCache.get(chooserTarget).getId();
+        for (TargetInfo chooserTargetInfo : surfacedTargetInfo) {
+            ShortcutInfo shortcutInfo = chooserTargetInfo.getDirectShareShortcutInfo();
+            if (shortcutInfo != null) {
+                ComponentName componentName =
+                        chooserTargetInfo.getChooserTargetComponentName();
                 targetIds.add(new AppTargetId(
-                        String.format("%s/%s/%s", shortcutId, componentName.flattenToString(),
+                        String.format(
+                                "%s/%s/%s",
+                                shortcutInfo.getId(),
+                                componentName.flattenToString(),
                                 SHORTCUT_TARGET)));
             }
         }
@@ -2232,21 +1344,18 @@
     }
 
     private void sendClickToAppPredictor(TargetInfo targetInfo) {
-        AppPredictor directShareAppPredictor = getAppPredictorForDirectShareIfEnabled(
+        if (!targetInfo.isChooserTargetInfo()) {
+            return;
+        }
+
+        AppPredictor directShareAppPredictor = getAppPredictor(
                 mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
         if (directShareAppPredictor == null) {
             return;
         }
-        if (!(targetInfo instanceof ChooserTargetInfo)) {
-            return;
-        }
-        ChooserTarget chooserTarget = ((ChooserTargetInfo) targetInfo).getChooserTarget();
-        AppTarget appTarget = null;
-        if (mDirectShareAppTargetCache != null) {
-            appTarget = mDirectShareAppTargetCache.get(chooserTarget);
-        }
-        // This is a direct share click that was provided by the APS
+        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)
@@ -2255,70 +1364,9 @@
     }
 
     @Nullable
-    private AppPredictor createAppPredictor(UserHandle userHandle) {
-        if (!mIsAppPredictorComponentAvailable) {
-            return null;
-        }
-
-        if (getPersonalProfileUserHandle().equals(userHandle)) {
-            if (mPersonalAppPredictor != null) {
-                return mPersonalAppPredictor;
-            }
-        } else {
-            if (mWorkAppPredictor != null) {
-                return mWorkAppPredictor;
-            }
-        }
-
-        // TODO(b/148230574): Currently AppPredictor fetches only the same-profile app targets.
-        // Make AppPredictor work cross-profile.
-        Context contextAsUser = createContextAsUser(userHandle, 0 /* flags */);
-        final IntentFilter filter = getTargetIntentFilter();
-        Bundle extras = new Bundle();
-        extras.putParcelable(APP_PREDICTION_INTENT_FILTER_KEY, filter);
-        populateTextContent(extras);
-        AppPredictionContext appPredictionContext = new AppPredictionContext.Builder(contextAsUser)
-            .setUiSurface(APP_PREDICTION_SHARE_UI_SURFACE)
-            .setPredictedTargetCount(APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT)
-            .setExtras(extras)
-            .build();
-        AppPredictionManager appPredictionManager =
-                contextAsUser
-                        .getSystemService(AppPredictionManager.class);
-        AppPredictor appPredictionSession = appPredictionManager.createAppPredictionSession(
-                appPredictionContext);
-        if (getPersonalProfileUserHandle().equals(userHandle)) {
-            mPersonalAppPredictor = appPredictionSession;
-        } else {
-            mWorkAppPredictor = appPredictionSession;
-        }
-        return appPredictionSession;
-    }
-
-    private void populateTextContent(Bundle extras) {
-        final Intent intent = getTargetIntent();
-        String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
-        extras.putString(SHARED_TEXT_KEY, sharedText);
-    }
-
-    /**
-     * This will return an app predictor if it is enabled for direct share sorting
-     * and if one exists. Otherwise, it returns null.
-     * @param userHandle
-     */
-    @Nullable
-    private AppPredictor getAppPredictorForDirectShareIfEnabled(UserHandle userHandle) {
-        return ChooserFlags.USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS
-                && !ActivityManager.isLowRamDeviceStatic() ? createAppPredictor(userHandle) : null;
-    }
-
-    /**
-     * This will return an app predictor if it is enabled for share activity sorting
-     * and if one exists. Otherwise, it returns null.
-     */
-    @Nullable
-    private AppPredictor getAppPredictorForShareActivitiesIfEnabled(UserHandle userHandle) {
-        return USE_PREDICTION_MANAGER_FOR_SHARE_ACTIVITIES ? createAppPredictor(userHandle) : null;
+    private AppPredictor getAppPredictor(UserHandle userHandle) {
+        ProfileRecord record = getProfileRecord(userHandle);
+        return (record == null) ? null : record.appPredictor;
     }
 
     void onRefinementResult(TargetInfo selectedTarget, Intent matchingIntent) {
@@ -2377,16 +1425,9 @@
         }
     }
 
-    protected MetricsLogger getMetricsLogger() {
-        if (mMetricsLogger == null) {
-            mMetricsLogger = new MetricsLogger();
-        }
-        return mMetricsLogger;
-    }
-
     protected ChooserActivityLogger getChooserActivityLogger() {
         if (mChooserActivityLogger == null) {
-            mChooserActivityLogger = new ChooserActivityLoggerImpl();
+            mChooserActivityLogger = new ChooserActivityLogger();
         }
         return mChooserActivityLogger;
     }
@@ -2405,56 +1446,139 @@
 
         @Override
         boolean isComponentFiltered(ComponentName name) {
-            if (mFilteredComponentNames == null) {
-                return false;
-            }
-            for (ComponentName filteredComponentName : mFilteredComponentNames) {
-                if (name.equals(filteredComponentName)) {
-                    return true;
-                }
-            }
-            return false;
+            return mChooserRequest.getFilteredComponentNames().contains(name);
         }
 
         @Override
         public boolean isComponentPinned(ComponentName name) {
             return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
         }
-
-        @Override
-        public boolean isFixedAtTop(ComponentName name) {
-            return name != null && name.equals(getNearbySharingComponent())
-                    && shouldNearbyShareBeFirstInRankedRow();
-        }
     }
 
     @VisibleForTesting
-    public ChooserGridAdapter createChooserGridAdapter(Context context,
-            List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList,
-            boolean filterLastUsed, UserHandle userHandle) {
-        ChooserListAdapter chooserListAdapter = createChooserListAdapter(context, payloadIntents,
-                initialIntents, rList, filterLastUsed,
-                createListController(userHandle));
-        AppPredictor.Callback appPredictorCallback = createAppPredictorCallback(chooserListAdapter);
-        AppPredictor appPredictor = setupAppPredictorForUser(userHandle, appPredictorCallback);
-        chooserListAdapter.setAppPredictor(appPredictor);
-        chooserListAdapter.setAppPredictorCallback(appPredictorCallback);
-        return new ChooserGridAdapter(chooserListAdapter);
+    public ChooserGridAdapter createChooserGridAdapter(
+            Context context,
+            List<Intent> payloadIntents,
+            Intent[] initialIntents,
+            List<ResolveInfo> rList,
+            boolean filterLastUsed,
+            UserHandle userHandle) {
+        ChooserListAdapter chooserListAdapter = createChooserListAdapter(
+                context,
+                payloadIntents,
+                initialIntents,
+                rList,
+                filterLastUsed,
+                createListController(userHandle),
+                userHandle,
+                getTargetIntent(),
+                mChooserRequest,
+                mMaxTargetsPerRow);
+
+        return new ChooserGridAdapter(
+                context,
+                new ChooserGridAdapter.ChooserActivityDelegate() {
+                    @Override
+                    public boolean shouldShowTabs() {
+                        return ChooserActivity.this.shouldShowTabs();
+                    }
+
+                    @Override
+                    public View buildContentPreview(ViewGroup parent) {
+                        return createContentPreviewView(parent, mPreviewCoordinator);
+                    }
+
+                    @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);
+                        // ItemViewHolder contents should always be "display resolve info"
+                        // targets, but check just to make sure.
+                        if (longPressedTargetInfo.isDisplayResolveInfo()) {
+                            showTargetDetails(longPressedTargetInfo);
+                        }
+                    }
+
+                    @Override
+                    public void updateProfileViewButton(View newButtonFromProfileRow) {
+                        mProfileView = newButtonFromProfileRow;
+                        mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick);
+                        ChooserActivity.this.updateProfileViewButton();
+                    }
+
+                    @Override
+                    public int getValidTargetCount() {
+                        return mChooserMultiProfilePagerAdapter
+                                .getActiveListAdapter()
+                                .getSelectableServiceTargetCount();
+                    }
+
+                    @Override
+                    public void updateDirectShareExpansion(DirectShareViewHolder directShareGroup) {
+                        RecyclerView activeAdapterView =
+                                mChooserMultiProfilePagerAdapter.getActiveAdapterView();
+                        if (mResolverDrawerLayout.isCollapsed()) {
+                            directShareGroup.collapse(activeAdapterView);
+                        } else {
+                            directShareGroup.expand(activeAdapterView);
+                        }
+                    }
+
+                    @Override
+                    public void handleScrollToExpandDirectShare(
+                            DirectShareViewHolder directShareGroup, int y, int oldy) {
+                        directShareGroup.handleScroll(
+                                mChooserMultiProfilePagerAdapter.getActiveAdapterView(),
+                                y,
+                                oldy,
+                                mMaxTargetsPerRow);
+                    }
+                },
+                chooserListAdapter,
+                shouldShowContentPreview(),
+                mMaxTargetsPerRow,
+                getNumSheetExpansions());
     }
 
     @VisibleForTesting
-    public ChooserListAdapter createChooserListAdapter(Context context,
-            List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList,
-            boolean filterLastUsed, ResolverListController resolverListController) {
-        return new ChooserListAdapter(context, payloadIntents, initialIntents, rList,
-                filterLastUsed, resolverListController, this,
-                this, context.getPackageManager(),
-                getChooserActivityLogger());
+    public ChooserListAdapter createChooserListAdapter(
+            Context context,
+            List<Intent> payloadIntents,
+            Intent[] initialIntents,
+            List<ResolveInfo> rList,
+            boolean filterLastUsed,
+            ResolverListController resolverListController,
+            UserHandle userHandle,
+            Intent targetIntent,
+            ChooserRequestParameters chooserRequest,
+            int maxTargetsPerRow) {
+        return new ChooserListAdapter(
+                context,
+                payloadIntents,
+                initialIntents,
+                rList,
+                filterLastUsed,
+                resolverListController,
+                userHandle,
+                targetIntent,
+                this,
+                context.getPackageManager(),
+                getChooserActivityLogger(),
+                chooserRequest,
+                maxTargetsPerRow);
     }
 
     @VisibleForTesting
     protected ResolverListController createListController(UserHandle userHandle) {
-        AppPredictor appPredictor = getAppPredictorForShareActivitiesIfEnabled(userHandle);
+        AppPredictor appPredictor = getAppPredictor(userHandle);
         AbstractResolverComparator resolverComparator;
         if (appPredictor != null) {
             resolverComparator = new AppPredictionServiceResolverComparator(this, getTargetIntent(),
@@ -2484,28 +1608,11 @@
         try {
             return getContentResolver().loadThumbnail(uri, size, null);
         } catch (IOException | NullPointerException | SecurityException ex) {
-            logContentPreviewWarning(uri);
+            getChooserActivityLogger().logContentPreviewWarning(uri);
         }
         return null;
     }
 
-    static final class PlaceHolderTargetInfo extends NotSelectableTargetInfo {
-        public Drawable getDisplayIcon(Context context) {
-            AnimatedVectorDrawable avd = (AnimatedVectorDrawable)
-                    context.getDrawable(R.drawable.chooser_direct_share_icon_placeholder);
-            avd.start(); // Start animation after generation
-            return avd;
-        }
-    }
-
-    protected static final class EmptyTargetInfo extends NotSelectableTargetInfo {
-        public EmptyTargetInfo() {}
-
-        public Drawable getDisplayIcon(Context context) {
-            return null;
-        }
-    }
-
     private void handleScroll(View view, int x, int y, int oldx, int oldy) {
         if (mChooserMultiProfilePagerAdapter.getCurrentRootAdapter() != null) {
             mChooserMultiProfilePagerAdapter.getCurrentRootAdapter().handleScroll(view, y, oldy);
@@ -2532,8 +1639,8 @@
         }
 
         final int availableWidth = right - left - v.getPaddingLeft() - v.getPaddingRight();
-        boolean isLayoutUpdated = gridAdapter.consumeLayoutRequest()
-                || gridAdapter.calculateChooserTargetWidth(availableWidth)
+        boolean isLayoutUpdated =
+                gridAdapter.calculateChooserTargetWidth(availableWidth)
                 || recyclerView.getAdapter() == null
                 || availableWidth != mCurrAvailableWidth;
 
@@ -2639,7 +1746,7 @@
 
             boolean isExpandable = getResources().getConfiguration().orientation
                     == Configuration.ORIENTATION_PORTRAIT && !isInMultiWindowMode();
-            if (directShareHeight != 0 && isSendAction(getTargetIntent())
+            if (directShareHeight != 0 && shouldShowContentPreview()
                     && isExpandable) {
                 // make sure to leave room for direct share 4->8 expansion
                 int requiredExpansionHeight =
@@ -2688,15 +1795,7 @@
 
     private ViewGroup getActiveEmptyStateView() {
         int currentPage = mChooserMultiProfilePagerAdapter.getCurrentPage();
-        return mChooserMultiProfilePagerAdapter.getItem(currentPage).getEmptyStateView();
-    }
-
-    static class BaseChooserTargetComparator implements Comparator<ChooserTarget> {
-        @Override
-        public int compare(ChooserTarget lhs, ChooserTarget rhs) {
-            // Descending order
-            return (int) Math.signum(rhs.getScore() - lhs.getScore());
-        }
+        return mChooserMultiProfilePagerAdapter.getEmptyStateView(currentPage);
     }
 
     @Override // ResolverListCommunicator
@@ -2705,29 +1804,6 @@
         super.onHandlePackagesChanged(listAdapter);
     }
 
-    @Override // SelectableTargetInfoCommunicator
-    public ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo info) {
-        return mChooserMultiProfilePagerAdapter.getActiveListAdapter().makePresentationGetter(info);
-    }
-
-    @Override // SelectableTargetInfoCommunicator
-    public Intent getReferrerFillInIntent() {
-        return mReferrerFillInIntent;
-    }
-
-    @Override // ChooserListCommunicator
-    public int getMaxRankedTargets() {
-        return mMaxTargetsPerRow;
-    }
-
-    @Override // ChooserListCommunicator
-    public void sendListViewUpdateMessage(UserHandle userHandle) {
-        Message msg = Message.obtain();
-        msg.what = ChooserHandler.LIST_VIEW_UPDATE_MESSAGE;
-        msg.obj = userHandle;
-        mChooserHandler.sendMessageDelayed(msg, mListViewUpdateDelayMs);
-    }
-
     @Override
     public void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) {
         setupScrollListener();
@@ -2742,8 +1818,7 @@
                     .setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage());
         }
 
-        if (chooserListAdapter.mDisplayList == null
-                || chooserListAdapter.mDisplayList.isEmpty()) {
+        if (chooserListAdapter.getDisplayResolveInfoCount() == 0) {
             chooserListAdapter.notifyDataSetChanged();
         } else {
             chooserListAdapter.updateAlphabeticalList();
@@ -2757,41 +1832,45 @@
     }
 
     private void maybeQueryAdditionalPostProcessingTargets(ChooserListAdapter chooserListAdapter) {
-        // don't support direct share on low ram devices
-        if (ActivityManager.isLowRamDeviceStatic()) {
+        UserHandle userHandle = chooserListAdapter.getUserHandle();
+        ProfileRecord record = getProfileRecord(userHandle);
+        if (record == null) {
             return;
         }
-
-        // no need to query direct share for work profile when its locked or disabled
-        if (!shouldQueryShortcutManager(chooserListAdapter.getUserHandle())) {
+        if (record.shortcutLoader == null) {
             return;
         }
+        record.loadingStartTime = SystemClock.elapsedRealtime();
+        record.shortcutLoader.queryShortcuts(chooserListAdapter.getDisplayResolveInfos());
+    }
 
-        if (ChooserFlags.USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS) {
-            if (DEBUG) {
-                Log.d(TAG, "querying direct share targets from ShortcutManager");
+    @MainThread
+    private void onShortcutsLoaded(
+            UserHandle userHandle, ShortcutLoader.Result shortcutsResult) {
+        if (DEBUG) {
+            Log.d(TAG, "onShortcutsLoaded for user: " + userHandle);
+        }
+        mDirectShareShortcutInfoCache.putAll(shortcutsResult.directShareShortcutInfoCache);
+        mDirectShareAppTargetCache.putAll(shortcutsResult.directShareAppTargetCache);
+        ChooserListAdapter adapter =
+                mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle);
+        if (adapter != null) {
+            for (ShortcutLoader.ShortcutResultInfo resultInfo : shortcutsResult.shortcutsByApp) {
+                adapter.addServiceResults(
+                        resultInfo.appTarget,
+                        resultInfo.shortcuts,
+                        shortcutsResult.isFromAppPredictor
+                                ? TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
+                                : TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER,
+                        mDirectShareShortcutInfoCache,
+                        mDirectShareAppTargetCache);
             }
-
-            queryDirectShareTargets(chooserListAdapter, false);
+            adapter.completeServiceTargetLoading();
         }
-    }
 
-    @VisibleForTesting
-    protected boolean isUserRunning(UserHandle userHandle) {
-        UserManager userManager = getSystemService(UserManager.class);
-        return userManager.isUserRunning(userHandle);
-    }
-
-    @VisibleForTesting
-    protected boolean isUserUnlocked(UserHandle userHandle) {
-        UserManager userManager = getSystemService(UserManager.class);
-        return userManager.isUserUnlocked(userHandle);
-    }
-
-    @VisibleForTesting
-    protected boolean isQuietModeEnabled(UserHandle userHandle) {
-        UserManager userManager = getSystemService(UserManager.class);
-        return userManager.isQuietModeEnabled(userHandle);
+        logDirectShareTargetReceived(userHandle);
+        sendVoiceChoicesIfNeeded();
+        getChooserActivityLogger().logSharesheetDirectLoadComplete();
     }
 
     private void setupScrollListener() {
@@ -2855,24 +1934,6 @@
                 });
     }
 
-    @Override // ChooserListCommunicator
-    public boolean isSendAction(Intent targetIntent) {
-        if (targetIntent == null) {
-            return false;
-        }
-
-        String action = targetIntent.getAction();
-        if (action == null) {
-            return false;
-        }
-
-        if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
-            return true;
-        }
-
-        return false;
-    }
-
     /**
      * 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,
@@ -2887,7 +1948,14 @@
         return shouldShowTabs()
                 && mMultiProfilePagerAdapter.getListAdapterForUserHandle(
                 UserHandle.of(UserHandle.myUserId())).getCount() > 0
-                && isSendAction(getTargetIntent());
+                && shouldShowContentPreview();
+    }
+
+    /**
+     * @return true if we want to show the content preview area
+     */
+    protected boolean shouldShowContentPreview() {
+        return (mChooserRequest != null) && mChooserRequest.isSendActionTarget();
     }
 
     private void updateStickyContentPreview() {
@@ -2898,7 +1966,8 @@
             // 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);
+                ViewGroup contentPreviewView =
+                        createContentPreviewView(contentPreviewContainer, mPreviewCoordinator);
                 contentPreviewContainer.addView(contentPreviewView);
             }
         }
@@ -2930,21 +1999,16 @@
         contentPreviewContainer.setVisibility(View.GONE);
     }
 
-    private void logActionShareWithPreview() {
-        Intent targetIntent = getTargetIntent();
-        int previewType = findPreferredContentPreview(targetIntent, getContentResolver());
-        getMetricsLogger().write(new LogMaker(MetricsEvent.ACTION_SHARE_WITH_PREVIEW)
-                .setSubtype(previewType));
-    }
-
     private void startFinishAnimation() {
         View rootView = findRootView();
-        rootView.startAnimation(new FinishAnimation(this, rootView));
+        if (rootView != null) {
+            rootView.startAnimation(new FinishAnimation(this, rootView));
+        }
     }
 
     private boolean maybeCancelFinishAnimation() {
         View rootView = findRootView();
-        Animation animation = rootView.getAnimation();
+        Animation animation = (rootView == null) ? null : rootView.getAnimation();
         if (animation instanceof FinishAnimation) {
             boolean hasEnded = animation.hasEnded();
             animation.cancel();
@@ -2961,69 +2025,6 @@
         return mContentView;
     }
 
-    abstract static class ViewHolderBase extends RecyclerView.ViewHolder {
-        private int mViewType;
-
-        ViewHolderBase(View itemView, int viewType) {
-            super(itemView);
-            this.mViewType = viewType;
-        }
-
-        int getViewType() {
-            return mViewType;
-        }
-    }
-
-    /**
-     * Used to bind types of individual item including
-     * {@link ChooserGridAdapter#VIEW_TYPE_NORMAL},
-     * {@link ChooserGridAdapter#VIEW_TYPE_CONTENT_PREVIEW},
-     * {@link ChooserGridAdapter#VIEW_TYPE_PROFILE},
-     * and {@link ChooserGridAdapter#VIEW_TYPE_AZ_LABEL}.
-     */
-    final class ItemViewHolder extends ViewHolderBase {
-        ResolverListAdapter.ViewHolder mWrappedViewHolder;
-        int mListPosition = ChooserListAdapter.NO_POSITION;
-
-        ItemViewHolder(View itemView, boolean isClickable, int viewType) {
-            super(itemView, viewType);
-            mWrappedViewHolder = new ResolverListAdapter.ViewHolder(itemView);
-            if (isClickable) {
-                itemView.setOnClickListener(v -> startSelected(mListPosition,
-                        false/* always */, true/* filterd */));
-
-                itemView.setOnLongClickListener(v -> {
-                    final TargetInfo ti = mChooserMultiProfilePagerAdapter.getActiveListAdapter()
-                            .targetInfoForPosition(mListPosition, /* filtered */ true);
-
-                    // This should always be the case for ItemViewHolder, check for validity
-                    if (ti instanceof DisplayResolveInfo && shouldShowTargetDetails(ti)) {
-                        showTargetDetails((DisplayResolveInfo) ti);
-                    }
-                    return true;
-                });
-            }
-        }
-    }
-
-    private boolean shouldShowTargetDetails(TargetInfo ti) {
-        ComponentName nearbyShare = getNearbySharingComponent();
-        //  Suppress target details for nearby share to hide pin/unpin action
-        boolean isNearbyShare = nearbyShare != null && nearbyShare.equals(
-                ti.getResolvedComponentName()) && shouldNearbyShareBeFirstInRankedRow();
-        return ti instanceof SelectableTargetInfo
-                || (ti instanceof DisplayResolveInfo && !isNearbyShare);
-    }
-
-    /**
-     * Add a footer to the list, to support scrolling behavior below the navbar.
-     */
-    static final class FooterViewHolder extends ViewHolderBase {
-        FooterViewHolder(View itemView, int viewType) {
-            super(itemView, viewType);
-        }
-    }
-
     /**
      * Intentionally override the {@link ResolverActivity} implementation as we only need that
      * implementation for the intent resolver case.
@@ -3100,763 +2101,6 @@
         }
     }
 
-    /**
-     * Adapter for all types of items and targets in ShareSheet.
-     * Note that ranked sections like Direct Share - while appearing grid-like - are handled on the
-     * row level by this adapter but not on the item level. Individual targets within the row are
-     * handled by {@link ChooserListAdapter}
-     */
-    @VisibleForTesting
-    public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
-        private ChooserListAdapter mChooserListAdapter;
-        private final LayoutInflater mLayoutInflater;
-
-        private DirectShareViewHolder mDirectShareViewHolder;
-        private int mChooserTargetWidth = 0;
-        private boolean mShowAzLabelIfPoss;
-        private boolean mLayoutRequested = false;
-
-        private int mFooterHeight = 0;
-
-        private static final int VIEW_TYPE_DIRECT_SHARE = 0;
-        private static final int VIEW_TYPE_NORMAL = 1;
-        private static final int VIEW_TYPE_CONTENT_PREVIEW = 2;
-        private static final int VIEW_TYPE_PROFILE = 3;
-        private static final int VIEW_TYPE_AZ_LABEL = 4;
-        private static final int VIEW_TYPE_CALLER_AND_RANK = 5;
-        private static final int VIEW_TYPE_FOOTER = 6;
-
-        private static final int NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20;
-
-        ChooserGridAdapter(ChooserListAdapter wrappedAdapter) {
-            super();
-            mChooserListAdapter = wrappedAdapter;
-            mLayoutInflater = LayoutInflater.from(ChooserActivity.this);
-
-            mShowAzLabelIfPoss = getNumSheetExpansions() < NUM_EXPANSIONS_TO_HIDE_AZ_LABEL;
-
-            wrappedAdapter.registerDataSetObserver(new DataSetObserver() {
-                @Override
-                public void onChanged() {
-                    super.onChanged();
-                    notifyDataSetChanged();
-                }
-
-                @Override
-                public void onInvalidated() {
-                    super.onInvalidated();
-                    notifyDataSetChanged();
-                }
-            });
-        }
-
-        public void setFooterHeight(int height) {
-            mFooterHeight = height;
-        }
-
-        /**
-         * Calculate the chooser target width to maximize space per item
-         *
-         * @param width The new row width to use for recalculation
-         * @return true if the view width has changed
-         */
-        public boolean calculateChooserTargetWidth(int width) {
-            if (width == 0) {
-                return false;
-            }
-
-            // Limit width to the maximum width of the chooser activity
-            int maxWidth = getResources().getDimensionPixelSize(R.dimen.chooser_width);
-            width = Math.min(maxWidth, width);
-
-            int newWidth = width / mMaxTargetsPerRow;
-            if (newWidth != mChooserTargetWidth) {
-                mChooserTargetWidth = newWidth;
-                return true;
-            }
-
-            return false;
-        }
-
-        /**
-         * Hides the list item content preview.
-         * <p>Not to be confused with the sticky content preview which is above the
-         * personal and work tabs.
-         */
-        public void hideContentPreview() {
-            mLayoutRequested = true;
-            notifyDataSetChanged();
-        }
-
-        public boolean consumeLayoutRequest() {
-            boolean oldValue = mLayoutRequested;
-            mLayoutRequested = false;
-            return oldValue;
-        }
-
-        public int getRowCount() {
-            return (int) (
-                    getSystemRowCount()
-                            + getProfileRowCount()
-                            + getServiceTargetRowCount()
-                            + getCallerAndRankedTargetRowCount()
-                            + getAzLabelRowCount()
-                            + Math.ceil(
-                            (float) mChooserListAdapter.getAlphaTargetCount()
-                                    / mMaxTargetsPerRow)
-            );
-        }
-
-        /**
-         * Whether the "system" row of targets is displayed.
-         * This area includes the content preview (if present) and action row.
-         */
-        public int getSystemRowCount() {
-            // For the tabbed case we show the sticky content preview above the tabs,
-            // please refer to shouldShowStickyContentPreview
-            if (shouldShowTabs()) {
-                return 0;
-            }
-
-            if (!isSendAction(getTargetIntent())) {
-                return 0;
-            }
-
-            if (mChooserListAdapter == null || mChooserListAdapter.getCount() == 0) {
-                return 0;
-            }
-
-            return 1;
-        }
-
-        public int getProfileRowCount() {
-            if (shouldShowTabs()) {
-                return 0;
-            }
-            return mChooserListAdapter.getOtherProfile() == null ? 0 : 1;
-        }
-
-        public int getFooterRowCount() {
-            return 1;
-        }
-
-        public int getCallerAndRankedTargetRowCount() {
-            return (int) Math.ceil(
-                    ((float) mChooserListAdapter.getCallerTargetCount()
-                            + mChooserListAdapter.getRankedTargetCount()) / mMaxTargetsPerRow);
-        }
-
-        // There can be at most one row in the listview, that is internally
-        // a ViewGroup with 2 rows
-        public int getServiceTargetRowCount() {
-            if (isSendAction(getTargetIntent())
-                    && !ActivityManager.isLowRamDeviceStatic()) {
-                return 1;
-            }
-            return 0;
-        }
-
-        public int getAzLabelRowCount() {
-            // Only show a label if the a-z list is showing
-            return (mShowAzLabelIfPoss && mChooserListAdapter.getAlphaTargetCount() > 0) ? 1 : 0;
-        }
-
-        @Override
-        public int getItemCount() {
-            return (int) (
-                    getSystemRowCount()
-                            + getProfileRowCount()
-                            + getServiceTargetRowCount()
-                            + getCallerAndRankedTargetRowCount()
-                            + getAzLabelRowCount()
-                            + mChooserListAdapter.getAlphaTargetCount()
-                            + getFooterRowCount()
-            );
-        }
-
-        @Override
-        public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
-            switch (viewType) {
-                case VIEW_TYPE_CONTENT_PREVIEW:
-                    return new ItemViewHolder(createContentPreviewView(parent), false, viewType);
-                case VIEW_TYPE_PROFILE:
-                    return new ItemViewHolder(createProfileView(parent), false, viewType);
-                case VIEW_TYPE_AZ_LABEL:
-                    return new ItemViewHolder(createAzLabelView(parent), false, viewType);
-                case VIEW_TYPE_NORMAL:
-                    return new ItemViewHolder(
-                            mChooserListAdapter.createView(parent), true, viewType);
-                case VIEW_TYPE_DIRECT_SHARE:
-                case VIEW_TYPE_CALLER_AND_RANK:
-                    return createItemGroupViewHolder(viewType, parent);
-                case VIEW_TYPE_FOOTER:
-                    Space sp = new Space(parent.getContext());
-                    sp.setLayoutParams(new RecyclerView.LayoutParams(
-                            LayoutParams.MATCH_PARENT, mFooterHeight));
-                    return new FooterViewHolder(sp, viewType);
-                default:
-                    // Since we catch all possible viewTypes above, no chance this is being called.
-                    return null;
-            }
-        }
-
-        @Override
-        public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
-            int viewType = ((ViewHolderBase) holder).getViewType();
-            switch (viewType) {
-                case VIEW_TYPE_DIRECT_SHARE:
-                case VIEW_TYPE_CALLER_AND_RANK:
-                    bindItemGroupViewHolder(position, (ItemGroupViewHolder) holder);
-                    break;
-                case VIEW_TYPE_NORMAL:
-                    bindItemViewHolder(position, (ItemViewHolder) holder);
-                    break;
-                default:
-            }
-        }
-
-        @Override
-        public int getItemViewType(int position) {
-            int count;
-
-            int countSum = (count = getSystemRowCount());
-            if (count > 0 && position < countSum) return VIEW_TYPE_CONTENT_PREVIEW;
-
-            countSum += (count = getProfileRowCount());
-            if (count > 0 && position < countSum) return VIEW_TYPE_PROFILE;
-
-            countSum += (count = getServiceTargetRowCount());
-            if (count > 0 && position < countSum) return VIEW_TYPE_DIRECT_SHARE;
-
-            countSum += (count = getCallerAndRankedTargetRowCount());
-            if (count > 0 && position < countSum) return VIEW_TYPE_CALLER_AND_RANK;
-
-            countSum += (count = getAzLabelRowCount());
-            if (count > 0 && position < countSum) return VIEW_TYPE_AZ_LABEL;
-
-            if (position == getItemCount() - 1) return VIEW_TYPE_FOOTER;
-
-            return VIEW_TYPE_NORMAL;
-        }
-
-        public int getTargetType(int position) {
-            return mChooserListAdapter.getPositionTargetType(getListPosition(position));
-        }
-
-        private View createProfileView(ViewGroup parent) {
-            View profileRow = mLayoutInflater.inflate(R.layout.chooser_profile_row, parent, false);
-            mProfileView = profileRow.findViewById(com.android.internal.R.id.profile_button);
-            mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick);
-            updateProfileViewButton();
-            return profileRow;
-        }
-
-        private View createAzLabelView(ViewGroup parent) {
-            return mLayoutInflater.inflate(R.layout.chooser_az_label_row, parent, false);
-        }
-
-        private ItemGroupViewHolder loadViewsIntoGroup(ItemGroupViewHolder holder) {
-            final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
-            final int exactSpec = MeasureSpec.makeMeasureSpec(mChooserTargetWidth,
-                    MeasureSpec.EXACTLY);
-            int columnCount = holder.getColumnCount();
-
-            final boolean isDirectShare = holder instanceof DirectShareViewHolder;
-
-            for (int i = 0; i < columnCount; i++) {
-                final View v = mChooserListAdapter.createView(holder.getRowByIndex(i));
-                final int column = i;
-                v.setOnClickListener(new OnClickListener() {
-                    @Override
-                    public void onClick(View v) {
-                        startSelected(holder.getItemIndex(column), false, true);
-                    }
-                });
-
-                // Show menu for both direct share and app share targets after long click.
-                v.setOnLongClickListener(v1 -> {
-                    TargetInfo ti = mChooserListAdapter.targetInfoForPosition(
-                            holder.getItemIndex(column), true);
-                    if (shouldShowTargetDetails(ti)) {
-                        showTargetDetails(ti);
-                    }
-                    return true;
-                });
-
-                holder.addView(i, v);
-
-                // Force Direct Share to be 2 lines and auto-wrap to second line via hoz scroll =
-                // false. TextView#setHorizontallyScrolling must be reset after #setLines. Must be
-                // done before measuring.
-                if (isDirectShare) {
-                    final ViewHolder vh = (ViewHolder) v.getTag();
-                    vh.text.setLines(2);
-                    vh.text.setHorizontallyScrolling(false);
-                    vh.text2.setVisibility(View.GONE);
-                }
-
-                // Force height to be a given so we don't have visual disruption during scaling.
-                v.measure(exactSpec, spec);
-                setViewBounds(v, v.getMeasuredWidth(), v.getMeasuredHeight());
-            }
-
-            final ViewGroup viewGroup = holder.getViewGroup();
-
-            // Pre-measure and fix height so we can scale later.
-            holder.measure();
-            setViewBounds(viewGroup, LayoutParams.MATCH_PARENT, holder.getMeasuredRowHeight());
-
-            if (isDirectShare) {
-                DirectShareViewHolder dsvh = (DirectShareViewHolder) holder;
-                setViewBounds(dsvh.getRow(0), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight());
-                setViewBounds(dsvh.getRow(1), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight());
-            }
-
-            viewGroup.setTag(holder);
-            return holder;
-        }
-
-        private void setViewBounds(View view, int widthPx, int heightPx) {
-            LayoutParams lp = view.getLayoutParams();
-            if (lp == null) {
-                lp = new LayoutParams(widthPx, heightPx);
-                view.setLayoutParams(lp);
-            } else {
-                lp.height = heightPx;
-                lp.width = widthPx;
-            }
-        }
-
-        ItemGroupViewHolder createItemGroupViewHolder(int viewType, ViewGroup parent) {
-            if (viewType == VIEW_TYPE_DIRECT_SHARE) {
-                ViewGroup parentGroup = (ViewGroup) mLayoutInflater.inflate(
-                        R.layout.chooser_row_direct_share, parent, false);
-                ViewGroup row1 = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row,
-                        parentGroup, false);
-                ViewGroup row2 = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row,
-                        parentGroup, false);
-                parentGroup.addView(row1);
-                parentGroup.addView(row2);
-
-                mDirectShareViewHolder = new DirectShareViewHolder(parentGroup,
-                        Lists.newArrayList(row1, row2), mMaxTargetsPerRow, viewType,
-                        mChooserMultiProfilePagerAdapter::getActiveListAdapter);
-                loadViewsIntoGroup(mDirectShareViewHolder);
-
-                return mDirectShareViewHolder;
-            } else {
-                ViewGroup row = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row, parent,
-                        false);
-                ItemGroupViewHolder holder =
-                        new SingleRowViewHolder(row, mMaxTargetsPerRow, viewType);
-                loadViewsIntoGroup(holder);
-
-                return holder;
-            }
-        }
-
-        /**
-         * Need to merge CALLER + ranked STANDARD into a single row and prevent a separator from
-         * showing on top of the AZ list if the AZ label is visible. All other types are placed into
-         * their own row as determined by their target type, and dividers are added in the list to
-         * separate each type.
-         */
-        int getRowType(int rowPosition) {
-            // Merge caller and ranked standard into a single row
-            int positionType = mChooserListAdapter.getPositionTargetType(rowPosition);
-            if (positionType == ChooserListAdapter.TARGET_CALLER) {
-                return ChooserListAdapter.TARGET_STANDARD;
-            }
-
-            // If an the A-Z label is shown, prevent a separator from appearing by making the A-Z
-            // row type the same as the suggestion row type
-            if (getAzLabelRowCount() > 0 && positionType == ChooserListAdapter.TARGET_STANDARD_AZ) {
-                return ChooserListAdapter.TARGET_STANDARD;
-            }
-
-            return positionType;
-        }
-
-        void bindItemViewHolder(int position, ItemViewHolder holder) {
-            View v = holder.itemView;
-            int listPosition = getListPosition(position);
-            holder.mListPosition = listPosition;
-            mChooserListAdapter.bindView(listPosition, v);
-        }
-
-        void bindItemGroupViewHolder(int position, ItemGroupViewHolder holder) {
-            final ViewGroup viewGroup = (ViewGroup) holder.itemView;
-            int start = getListPosition(position);
-            int startType = getRowType(start);
-
-            int columnCount = holder.getColumnCount();
-            int end = start + columnCount - 1;
-            while (getRowType(end) != startType && end >= start) {
-                end--;
-            }
-
-            if (end == start && mChooserListAdapter.getItem(start) instanceof EmptyTargetInfo) {
-                final TextView textView = viewGroup.findViewById(com.android.internal.R.id.chooser_row_text_option);
-
-                if (textView.getVisibility() != View.VISIBLE) {
-                    textView.setAlpha(0.0f);
-                    textView.setVisibility(View.VISIBLE);
-                    textView.setText(R.string.chooser_no_direct_share_targets);
-
-                    ValueAnimator fadeAnim = ObjectAnimator.ofFloat(textView, "alpha", 0.0f, 1.0f);
-                    fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
-
-                    float translationInPx = getResources().getDimensionPixelSize(
-                            R.dimen.chooser_row_text_option_translate);
-                    textView.setTranslationY(translationInPx);
-                    ValueAnimator translateAnim = ObjectAnimator.ofFloat(textView, "translationY",
-                            0.0f);
-                    translateAnim.setInterpolator(new DecelerateInterpolator(1.0f));
-
-                    AnimatorSet animSet = new AnimatorSet();
-                    animSet.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
-                    animSet.setStartDelay(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
-                    animSet.playTogether(fadeAnim, translateAnim);
-                    animSet.start();
-                }
-            }
-
-            for (int i = 0; i < columnCount; i++) {
-                final View v = holder.getView(i);
-
-                if (start + i <= end) {
-                    holder.setViewVisibility(i, View.VISIBLE);
-                    holder.setItemIndex(i, start + i);
-                    mChooserListAdapter.bindView(holder.getItemIndex(i), v);
-                } else {
-                    holder.setViewVisibility(i, View.INVISIBLE);
-                }
-            }
-        }
-
-        int getListPosition(int position) {
-            position -= getSystemRowCount() + getProfileRowCount();
-
-            final int serviceCount = mChooserListAdapter.getServiceTargetCount();
-            final int serviceRows = (int) Math.ceil((float) serviceCount / getMaxRankedTargets());
-            if (position < serviceRows) {
-                return position * mMaxTargetsPerRow;
-            }
-
-            position -= serviceRows;
-
-            final int callerAndRankedCount = mChooserListAdapter.getCallerTargetCount()
-                                                 + mChooserListAdapter.getRankedTargetCount();
-            final int callerAndRankedRows = getCallerAndRankedTargetRowCount();
-            if (position < callerAndRankedRows) {
-                return serviceCount + position * mMaxTargetsPerRow;
-            }
-
-            position -= getAzLabelRowCount() + callerAndRankedRows;
-
-            return callerAndRankedCount + serviceCount + position;
-        }
-
-        public void handleScroll(View v, int y, int oldy) {
-            boolean canExpandDirectShare = canExpandDirectShare();
-            if (mDirectShareViewHolder != null && canExpandDirectShare) {
-                mDirectShareViewHolder.handleScroll(
-                        mChooserMultiProfilePagerAdapter.getActiveAdapterView(), y, oldy,
-                        mMaxTargetsPerRow);
-            }
-        }
-
-        /**
-         * Only expand direct share area if there is a minimum number of targets.
-         */
-        private boolean canExpandDirectShare() {
-            // Do not enable until we have confirmed more apps are using sharing shortcuts
-            // Check git history for enablement logic
-            return false;
-        }
-
-        public ChooserListAdapter getListAdapter() {
-            return mChooserListAdapter;
-        }
-
-        boolean shouldCellSpan(int position) {
-            return getItemViewType(position) == VIEW_TYPE_NORMAL;
-        }
-
-        void updateDirectShareExpansion() {
-            if (mDirectShareViewHolder == null || !canExpandDirectShare()) {
-                return;
-            }
-            RecyclerView activeAdapterView =
-                    mChooserMultiProfilePagerAdapter.getActiveAdapterView();
-            if (mResolverDrawerLayout.isCollapsed()) {
-                mDirectShareViewHolder.collapse(activeAdapterView);
-            } else {
-                mDirectShareViewHolder.expand(activeAdapterView);
-            }
-        }
-    }
-
-    /**
-     * Used to bind types for group of items including:
-     * {@link ChooserGridAdapter#VIEW_TYPE_DIRECT_SHARE},
-     * and {@link ChooserGridAdapter#VIEW_TYPE_CALLER_AND_RANK}.
-     */
-    abstract static class ItemGroupViewHolder extends ViewHolderBase {
-        protected int mMeasuredRowHeight;
-        private int[] mItemIndices;
-        protected final View[] mCells;
-        private final int mColumnCount;
-
-        ItemGroupViewHolder(int cellCount, View itemView, int viewType) {
-            super(itemView, viewType);
-            this.mCells = new View[cellCount];
-            this.mItemIndices = new int[cellCount];
-            this.mColumnCount = cellCount;
-        }
-
-        abstract ViewGroup addView(int index, View v);
-
-        abstract ViewGroup getViewGroup();
-
-        abstract ViewGroup getRowByIndex(int index);
-
-        abstract ViewGroup getRow(int rowNumber);
-
-        abstract void setViewVisibility(int i, int visibility);
-
-        public int getColumnCount() {
-            return mColumnCount;
-        }
-
-        public void measure() {
-            final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
-            getViewGroup().measure(spec, spec);
-            mMeasuredRowHeight = getViewGroup().getMeasuredHeight();
-        }
-
-        public int getMeasuredRowHeight() {
-            return mMeasuredRowHeight;
-        }
-
-        public void setItemIndex(int itemIndex, int listIndex) {
-            mItemIndices[itemIndex] = listIndex;
-        }
-
-        public int getItemIndex(int itemIndex) {
-            return mItemIndices[itemIndex];
-        }
-
-        public View getView(int index) {
-            return mCells[index];
-        }
-    }
-
-    static class SingleRowViewHolder extends ItemGroupViewHolder {
-        private final ViewGroup mRow;
-
-        SingleRowViewHolder(ViewGroup row, int cellCount, int viewType) {
-            super(cellCount, row, viewType);
-
-            this.mRow = row;
-        }
-
-        public ViewGroup getViewGroup() {
-            return mRow;
-        }
-
-        public ViewGroup getRowByIndex(int index) {
-            return mRow;
-        }
-
-        public ViewGroup getRow(int rowNumber) {
-            if (rowNumber == 0) return mRow;
-            return null;
-        }
-
-        public ViewGroup addView(int index, View v) {
-            mRow.addView(v);
-            mCells[index] = v;
-
-            return mRow;
-        }
-
-        public void setViewVisibility(int i, int visibility) {
-            getView(i).setVisibility(visibility);
-        }
-    }
-
-    static class DirectShareViewHolder extends ItemGroupViewHolder {
-        private final ViewGroup mParent;
-        private final List<ViewGroup> mRows;
-        private int mCellCountPerRow;
-
-        private boolean mHideDirectShareExpansion = false;
-        private int mDirectShareMinHeight = 0;
-        private int mDirectShareCurrHeight = 0;
-        private int mDirectShareMaxHeight = 0;
-
-        private final boolean[] mCellVisibility;
-
-        private final Supplier<ChooserListAdapter> mListAdapterSupplier;
-
-        DirectShareViewHolder(ViewGroup parent, List<ViewGroup> rows, int cellCountPerRow,
-                int viewType, Supplier<ChooserListAdapter> listAdapterSupplier) {
-            super(rows.size() * cellCountPerRow, parent, viewType);
-
-            this.mParent = parent;
-            this.mRows = rows;
-            this.mCellCountPerRow = cellCountPerRow;
-            this.mCellVisibility = new boolean[rows.size() * cellCountPerRow];
-            this.mListAdapterSupplier = listAdapterSupplier;
-        }
-
-        public ViewGroup addView(int index, View v) {
-            ViewGroup row = getRowByIndex(index);
-            row.addView(v);
-            mCells[index] = v;
-
-            return row;
-        }
-
-        public ViewGroup getViewGroup() {
-            return mParent;
-        }
-
-        public ViewGroup getRowByIndex(int index) {
-            return mRows.get(index / mCellCountPerRow);
-        }
-
-        public ViewGroup getRow(int rowNumber) {
-            return mRows.get(rowNumber);
-        }
-
-        public void measure() {
-            final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
-            getRow(0).measure(spec, spec);
-            getRow(1).measure(spec, spec);
-
-            mDirectShareMinHeight = getRow(0).getMeasuredHeight();
-            mDirectShareCurrHeight = mDirectShareCurrHeight > 0
-                    ? mDirectShareCurrHeight : mDirectShareMinHeight;
-            mDirectShareMaxHeight = 2 * mDirectShareMinHeight;
-        }
-
-        public int getMeasuredRowHeight() {
-            return mDirectShareCurrHeight;
-        }
-
-        public int getMinRowHeight() {
-            return mDirectShareMinHeight;
-        }
-
-        public void setViewVisibility(int i, int visibility) {
-            final View v = getView(i);
-            if (visibility == View.VISIBLE) {
-                mCellVisibility[i] = true;
-                v.setVisibility(visibility);
-                v.setAlpha(1.0f);
-            } else if (visibility == View.INVISIBLE && mCellVisibility[i]) {
-                mCellVisibility[i] = false;
-
-                ValueAnimator fadeAnim = ObjectAnimator.ofFloat(v, "alpha", 1.0f, 0f);
-                fadeAnim.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
-                fadeAnim.setInterpolator(new AccelerateInterpolator(1.0f));
-                fadeAnim.addListener(new AnimatorListenerAdapter() {
-                    public void onAnimationEnd(Animator animation) {
-                        v.setVisibility(View.INVISIBLE);
-                    }
-                });
-                fadeAnim.start();
-            }
-        }
-
-        public void handleScroll(RecyclerView view, int y, int oldy, int maxTargetsPerRow) {
-            // only exit early if fully collapsed, otherwise onListRebuilt() with shifting
-            // targets can lock us into an expanded mode
-            boolean notExpanded = mDirectShareCurrHeight == mDirectShareMinHeight;
-            if (notExpanded) {
-                if (mHideDirectShareExpansion) {
-                    return;
-                }
-
-                // only expand if we have more than maxTargetsPerRow, and delay that decision
-                // until they start to scroll
-                ChooserListAdapter adapter = mListAdapterSupplier.get();
-                int validTargets = adapter.getSelectableServiceTargetCount();
-                if (validTargets <= maxTargetsPerRow) {
-                    mHideDirectShareExpansion = true;
-                    return;
-                }
-            }
-
-            int yDiff = (int) ((oldy - y) * DIRECT_SHARE_EXPANSION_RATE);
-
-            int prevHeight = mDirectShareCurrHeight;
-            int newHeight = Math.min(prevHeight + yDiff, mDirectShareMaxHeight);
-            newHeight = Math.max(newHeight, mDirectShareMinHeight);
-            yDiff = newHeight - prevHeight;
-
-            updateDirectShareRowHeight(view, yDiff, newHeight);
-        }
-
-        void expand(RecyclerView view) {
-            updateDirectShareRowHeight(view, mDirectShareMaxHeight - mDirectShareCurrHeight,
-                    mDirectShareMaxHeight);
-        }
-
-        void collapse(RecyclerView view) {
-            updateDirectShareRowHeight(view, mDirectShareMinHeight - mDirectShareCurrHeight,
-                    mDirectShareMinHeight);
-        }
-
-        private void updateDirectShareRowHeight(RecyclerView view, int yDiff, int newHeight) {
-            if (view == null || view.getChildCount() == 0 || yDiff == 0) {
-                return;
-            }
-
-            // locate the item to expand, and offset the rows below that one
-            boolean foundExpansion = false;
-            for (int i = 0; i < view.getChildCount(); i++) {
-                View child = view.getChildAt(i);
-
-                if (foundExpansion) {
-                    child.offsetTopAndBottom(yDiff);
-                } else {
-                    if (child.getTag() != null && child.getTag() instanceof DirectShareViewHolder) {
-                        int widthSpec = MeasureSpec.makeMeasureSpec(child.getWidth(),
-                                MeasureSpec.EXACTLY);
-                        int heightSpec = MeasureSpec.makeMeasureSpec(newHeight,
-                                MeasureSpec.EXACTLY);
-                        child.measure(widthSpec, heightSpec);
-                        child.getLayoutParams().height = child.getMeasuredHeight();
-                        child.layout(child.getLeft(), child.getTop(), child.getRight(),
-                                child.getTop() + child.getMeasuredHeight());
-
-                        foundExpansion = true;
-                    }
-                }
-            }
-
-            if (foundExpansion) {
-                mDirectShareCurrHeight = newHeight;
-            }
-        }
-    }
-
-    static class ServiceResultInfo {
-        public final DisplayResolveInfo originalTarget;
-        public final List<ChooserTarget> resultTargets;
-        public final UserHandle userHandle;
-
-        public ServiceResultInfo(DisplayResolveInfo ot, List<ChooserTarget> rt,
-                UserHandle userHandle) {
-            originalTarget = ot;
-            resultTargets = rt;
-            this.userHandle = userHandle;
-        }
-    }
-
     static class ChooserTargetRankingInfo {
         public final List<AppTarget> scores;
         public final UserHandle userHandle;
@@ -3918,164 +2162,17 @@
     }
 
     /**
-     * Used internally to round image corners while obeying view padding.
-     */
-    public static class RoundedRectImageView extends ImageView {
-        private int mRadius = 0;
-        private Path mPath = new Path();
-        private Paint mOverlayPaint = new Paint(0);
-        private Paint mRoundRectPaint = new Paint(0);
-        private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
-        private String mExtraImageCount = null;
-
-        public RoundedRectImageView(Context context) {
-            super(context);
-        }
-
-        public RoundedRectImageView(Context context, AttributeSet attrs) {
-            this(context, attrs, 0);
-        }
-
-        public RoundedRectImageView(Context context, AttributeSet attrs, int defStyleAttr) {
-            this(context, attrs, defStyleAttr, 0);
-        }
-
-        public RoundedRectImageView(Context context, AttributeSet attrs, int defStyleAttr,
-                int defStyleRes) {
-            super(context, attrs, defStyleAttr, defStyleRes);
-            mRadius = context.getResources().getDimensionPixelSize(R.dimen.chooser_corner_radius);
-
-            mOverlayPaint.setColor(0x99000000);
-            mOverlayPaint.setStyle(Paint.Style.FILL);
-
-            mRoundRectPaint.setColor(context.getResources().getColor(R.color.chooser_row_divider));
-            mRoundRectPaint.setStyle(Paint.Style.STROKE);
-            mRoundRectPaint.setStrokeWidth(context.getResources()
-                    .getDimensionPixelSize(R.dimen.chooser_preview_image_border));
-
-            mTextPaint.setColor(Color.WHITE);
-            mTextPaint.setTextSize(context.getResources()
-                    .getDimensionPixelSize(R.dimen.chooser_preview_image_font_size));
-            mTextPaint.setTextAlign(Paint.Align.CENTER);
-        }
-
-        private void updatePath(int width, int height) {
-            mPath.reset();
-
-            int imageWidth = width - getPaddingRight() - getPaddingLeft();
-            int imageHeight = height - getPaddingBottom() - getPaddingTop();
-            mPath.addRoundRect(getPaddingLeft(), getPaddingTop(), imageWidth, imageHeight, mRadius,
-                    mRadius, Path.Direction.CW);
-        }
-
-        /**
-          * Sets the corner radius on all corners
-          *
-          * param radius 0 for no radius, &gt; 0 for a visible corner radius
-          */
-        public void setRadius(int radius) {
-            mRadius = radius;
-            updatePath(getWidth(), getHeight());
-        }
-
-        /**
-          * Display an overlay with extra image count on 3rd image
-          */
-        public void setExtraImageCount(int count) {
-            if (count > 0) {
-                this.mExtraImageCount = "+" + count;
-            } else {
-                this.mExtraImageCount = null;
-            }
-        }
-
-        @Override
-        protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
-            super.onSizeChanged(width, height, oldWidth, oldHeight);
-            updatePath(width, height);
-        }
-
-        @Override
-        protected void onDraw(Canvas canvas) {
-            if (mRadius != 0) {
-                canvas.clipPath(mPath);
-            }
-
-            super.onDraw(canvas);
-
-            int x = getPaddingLeft();
-            int y = getPaddingRight();
-            int width = getWidth() - getPaddingRight() - getPaddingLeft();
-            int height = getHeight() - getPaddingBottom() - getPaddingTop();
-            if (mExtraImageCount != null) {
-                canvas.drawRect(x, y, width, height, mOverlayPaint);
-
-                int xPos = canvas.getWidth() / 2;
-                int yPos = (int) ((canvas.getHeight() / 2.0f)
-                        - ((mTextPaint.descent() + mTextPaint.ascent()) / 2.0f));
-
-                canvas.drawText(mExtraImageCount, xPos, yPos, mTextPaint);
-            }
-
-            canvas.drawRoundRect(x, y, width, height, mRadius, mRadius, mRoundRectPaint);
-        }
-    }
-
-    /**
-     * A helper class to track app's readiness for the scene transition animation.
-     * The app is ready when both the image is laid out and the drawer offset is calculated.
-     */
-    private class EnterTransitionAnimationDelegate implements View.OnLayoutChangeListener {
-        private boolean mPreviewReady = false;
-        private boolean mOffsetCalculated = false;
-
-        void postponeTransition() {
-            postponeEnterTransition();
-        }
-
-        void markImagePreviewReady() {
-            if (!mPreviewReady) {
-                mPreviewReady = true;
-                maybeStartListenForLayout();
-            }
-        }
-
-        void markOffsetCalculated() {
-            if (!mOffsetCalculated) {
-                mOffsetCalculated = true;
-                maybeStartListenForLayout();
-            }
-        }
-
-        private void maybeStartListenForLayout() {
-            if (mPreviewReady && mOffsetCalculated && mResolverDrawerLayout != null) {
-                if (mResolverDrawerLayout.isInLayout()) {
-                    startPostponedEnterTransition();
-                } else {
-                    mResolverDrawerLayout.addOnLayoutChangeListener(this);
-                    mResolverDrawerLayout.requestLayout();
-                }
-            }
-        }
-
-        @Override
-        public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
-                int oldTop, int oldRight, int oldBottom) {
-            v.removeOnLayoutChangeListener(this);
-            startPostponedEnterTransition();
-        }
-    }
-
-    /**
      * Used in combination with the scene transition when launching the image editor
      */
     private static class FinishAnimation extends AlphaAnimation implements
             Animation.AnimationListener {
+        @Nullable
         private Activity mActivity;
+        @Nullable
         private View mRootView;
         private final float mFromAlpha;
 
-        FinishAnimation(Activity activity, View rootView) {
+        FinishAnimation(@NonNull Activity activity, @NonNull View rootView) {
             super(rootView.getAlpha(), 0.0f);
             mActivity = activity;
             mRootView = rootView;
@@ -4099,7 +2196,9 @@
 
         @Override
         public void cancel() {
-            mRootView.setAlpha(mFromAlpha);
+            if (mRootView != null) {
+                mRootView.setAlpha(mFromAlpha);
+            }
             cleanup();
             super.cancel();
         }
@@ -4110,9 +2209,10 @@
 
         @Override
         public void onAnimationEnd(Animation animation) {
-            if (mActivity != null) {
-                mActivity.finish();
-                cleanup();
+            Activity activity = mActivity;
+            cleanup();
+            if (activity != null) {
+                activity.finish();
             }
         }
 
@@ -4128,14 +2228,34 @@
 
     @Override
     protected void maybeLogProfileChange() {
-        getChooserActivityLogger().logShareheetProfileChanged();
+        getChooserActivityLogger().logSharesheetProfileChanged();
     }
 
-    private boolean shouldNearbyShareBeFirstInRankedRow() {
-        return ActivityManager.isLowRamDeviceStatic() && mIsNearbyShareFirstTargetInRankedApp;
-    }
+    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 boolean shouldNearbyShareBeIncludedAsActionButton() {
-        return !shouldNearbyShareBeFirstInRankedRow();
+        private ProfileRecord(
+                @Nullable AppPredictor appPredictor,
+                @Nullable ShortcutLoader shortcutLoader) {
+            this.appPredictor = appPredictor;
+            this.shortcutLoader = shortcutLoader;
+        }
+
+        public void destroy() {
+            if (shortcutLoader != null) {
+                shortcutLoader.destroy();
+            }
+            if (appPredictor != null) {
+                appPredictor.destroy();
+            }
+        }
     }
 }
diff --git a/java/src/com/android/intentresolver/ChooserActivityLogger.java b/java/src/com/android/intentresolver/ChooserActivityLogger.java
index 1daae01..9109bf9 100644
--- a/java/src/com/android/intentresolver/ChooserActivityLogger.java
+++ b/java/src/com/android/intentresolver/ChooserActivityLogger.java
@@ -16,48 +16,228 @@
 
 package com.android.intentresolver;
 
+import android.annotation.Nullable;
 import android.content.Intent;
+import android.metrics.LogMaker;
+import android.net.Uri;
 import android.provider.MediaStore;
+import android.util.HashedStringCache;
+import android.util.Log;
 
+import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.logging.InstanceId;
+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;
 
 /**
- * Interface for writing Sharesheet atoms to statsd log.
+ * Helper for writing Sharesheet atoms to statsd log.
  * @hide
  */
-public interface ChooserActivityLogger {
-    /** Logs a UiEventReported event for the system sharesheet completing initial start-up. */
-    void logShareStarted(int eventId, String packageName, String mimeType, int appProvidedDirect,
-            int appProvidedApp, boolean isWorkprofile, int previewType, String intent);
+public class ChooserActivityLogger {
+    private static final String TAG = "ChooserActivity";
+    private static final boolean DEBUG = true;
 
-    /** Logs a UiEventReported event for the system sharesheet when the user selects a target. */
-    void logShareTargetSelected(int targetType, String packageName, int positionPicked,
-            boolean isPinned);
+    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;
+
+    /**
+     * 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);
+
+        /** 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 UiEventLogger mUiEventLogger;
+    private final FrameworkStatsLogger mFrameworkStatsLogger;
+    private final MetricsLogger mMetricsLogger;
+
+    public ChooserActivityLogger() {
+        this(new UiEventLoggerImpl(), new DefaultFrameworkStatsLogger(), new MetricsLogger());
+    }
+
+    @VisibleForTesting
+    ChooserActivityLogger(
+            UiEventLogger uiEventLogger,
+            FrameworkStatsLogger frameworkLogger,
+            MetricsLogger metricsLogger) {
+        mUiEventLogger = uiEventLogger;
+        mFrameworkStatsLogger = frameworkLogger;
+        mMetricsLogger = metricsLogger;
+    }
+
+    /** Records metrics for the start time of the {@link ChooserActivity}. */
+    public void logChooserActivityShown(
+            boolean isWorkProfile, String targetMimeType, long systemCost) {
+        mMetricsLogger.write(new LogMaker(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN)
+                .setSubtype(
+                        isWorkProfile ? MetricsEvent.MANAGED_PROFILE : MetricsEvent.PARENT_PROFILE)
+                .addTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE, targetMimeType)
+                .addTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS, systemCost));
+    }
+
+    /** Logs a UiEventReported event for the system sharesheet completing initial start-up. */
+    public void logShareStarted(int eventId, String packageName, String mimeType,
+            int appProvidedDirect, int appProvidedApp, boolean isWorkprofile, int previewType,
+            String intent) {
+        mFrameworkStatsLogger.write(FrameworkStatsLog.SHARESHEET_STARTED,
+                /* event_id = 1 */ SharesheetStartedEvent.SHARE_STARTED.getId(),
+                /* package_name = 2 */ packageName,
+                /* instance_id = 3 */ getInstanceId().getId(),
+                /* mime_type = 4 */ mimeType,
+                /* num_app_provided_direct_targets = 5 */ appProvidedDirect,
+                /* num_app_provided_app_targets = 6 */ appProvidedApp,
+                /* is_workprofile = 7 */ isWorkprofile,
+                /* previewType = 8 */ typeFromPreviewInt(previewType),
+                /* intentType = 9 */ typeFromIntentString(intent));
+    }
+
+    /**
+     * Logs a UiEventReported event for the system sharesheet when the user selects a target.
+     * TODO: document parameters and/or consider breaking up by targetType so we don't have to
+     * support an overly-generic signature.
+     */
+    public void logShareTargetSelected(
+            int targetType,
+            String packageName,
+            int positionPicked,
+            int directTargetAlsoRanked,
+            int numCallerProvided,
+            @Nullable HashedStringCache.HashResult directTargetHashed,
+            boolean isPinned,
+            boolean successfullySelected,
+            long selectionCost) {
+        mFrameworkStatsLogger.write(FrameworkStatsLog.RANKING_SELECTED,
+                /* event_id = 1 */ SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(),
+                /* package_name = 2 */ packageName,
+                /* instance_id = 3 */ getInstanceId().getId(),
+                /* position_picked = 4 */ positionPicked,
+                /* is_pinned = 5 */ isPinned);
+
+        int category = getTargetSelectionCategory(targetType);
+        if (category != 0) {
+            LogMaker targetLogMaker = new LogMaker(category).setSubtype(positionPicked);
+            if (directTargetHashed != null) {
+                targetLogMaker.addTaggedData(
+                        MetricsEvent.FIELD_HASHED_TARGET_NAME, directTargetHashed.hashedString);
+                targetLogMaker.addTaggedData(
+                                MetricsEvent.FIELD_HASHED_TARGET_SALT_GEN,
+                                directTargetHashed.saltGeneration);
+                targetLogMaker.addTaggedData(MetricsEvent.FIELD_RANKED_POSITION,
+                                directTargetAlsoRanked);
+            }
+            targetLogMaker.addTaggedData(MetricsEvent.FIELD_IS_CATEGORY_USED, numCallerProvided);
+            mMetricsLogger.write(targetLogMaker);
+        }
+
+        if (successfullySelected) {
+            if (DEBUG) {
+                Log.d(TAG, "User Selection Time Cost is " + selectionCost);
+                Log.d(TAG, "position of selected app/service/caller is " + positionPicked);
+            }
+            MetricsLogger.histogram(
+                    null, "user_selection_cost_for_smart_sharing", (int) selectionCost);
+            MetricsLogger.histogram(null, "app_position_for_smart_sharing", positionPicked);
+        }
+    }
+
+    /** Log when direct share targets were received. */
+    public void logDirectShareTargetReceived(int category, int latency) {
+        mMetricsLogger.write(new LogMaker(category).setSubtype(latency));
+    }
+
+    /**
+     * Log when we display a preview UI of the specified {@code previewType} as part of our
+     * Sharesheet session.
+     */
+    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}. */
+    public void logActionSelected(int targetType) {
+        if (targetType == SELECTION_TYPE_COPY) {
+            LogMaker targetLogMaker = new LogMaker(
+                    MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SYSTEM_TARGET).setSubtype(1);
+            mMetricsLogger.write(targetLogMaker);
+        }
+        mFrameworkStatsLogger.write(FrameworkStatsLog.RANKING_SELECTED,
+                /* event_id = 1 */ SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(),
+                /* package_name = 2 */ "",
+                /* instance_id = 3 */ getInstanceId().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}. */
+    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 "
+                + "desired, consider using Intent#createChooser to launch the ChooserActivity, "
+                + "and set your Intent's clipData and flags in accordance with that method's "
+                + "documentation");
+
+    }
 
     /** Logs a UiEventReported event for the system sharesheet being triggered by the user. */
-    default void logSharesheetTriggered() {
+    public void logSharesheetTriggered() {
         log(SharesheetStandardEvent.SHARESHEET_TRIGGERED, getInstanceId());
     }
 
     /** Logs a UiEventReported event for the system sharesheet completing loading app targets. */
-    default void logSharesheetAppLoadComplete() {
+    public void logSharesheetAppLoadComplete() {
         log(SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE, getInstanceId());
     }
 
     /**
      * Logs a UiEventReported event for the system sharesheet completing loading service targets.
      */
-    default void logSharesheetDirectLoadComplete() {
+    public void logSharesheetDirectLoadComplete() {
         log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_COMPLETE, getInstanceId());
     }
 
     /**
      * Logs a UiEventReported event for the system sharesheet timing out loading service targets.
      */
-    default void logSharesheetDirectLoadTimeout() {
+    public void logSharesheetDirectLoadTimeout() {
         log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_TIMEOUT, getInstanceId());
     }
 
@@ -65,12 +245,12 @@
      * Logs a UiEventReported event for the system sharesheet switching
      * between work and main profile.
      */
-    default void logShareheetProfileChanged() {
+    public void logSharesheetProfileChanged() {
         log(SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED, getInstanceId());
     }
 
     /** Logs a UiEventReported event for the system sharesheet getting expanded or collapsed. */
-    default void logSharesheetExpansionChanged(boolean isCollapsed) {
+    public void logSharesheetExpansionChanged(boolean isCollapsed) {
         log(isCollapsed ? SharesheetStandardEvent.SHARESHEET_COLLAPSED :
                 SharesheetStandardEvent.SHARESHEET_EXPANDED, getInstanceId());
     }
@@ -78,14 +258,14 @@
     /**
      * Logs a UiEventReported event for the system sharesheet app share ranking timing out.
      */
-    default void logSharesheetAppShareRankingTimeout() {
+    public void logSharesheetAppShareRankingTimeout() {
         log(SharesheetStandardEvent.SHARESHEET_APP_SHARE_RANKING_TIMEOUT, getInstanceId());
     }
 
     /**
      * Logs a UiEventReported event for the system sharesheet when direct share row is empty.
      */
-    default void logSharesheetEmptyDirectShareRow() {
+    public void logSharesheetEmptyDirectShareRow() {
         log(SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW, getInstanceId());
     }
 
@@ -94,13 +274,26 @@
      * @param event
      * @param instanceId
      */
-    void log(UiEventLogger.UiEventEnum event, InstanceId instanceId);
+    private void log(UiEventLogger.UiEventEnum event, InstanceId instanceId) {
+        mUiEventLogger.logWithInstanceId(
+                event,
+                0,
+                null,
+                instanceId);
+    }
 
     /**
-     *
-     * @return
+     * @return A unique {@link InstanceId} to join across events recorded by this logger instance.
      */
-    InstanceId getInstanceId();
+    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.
@@ -147,17 +340,17 @@
 
         public static SharesheetTargetSelectedEvent fromTargetType(int targetType) {
             switch(targetType) {
-                case ChooserActivity.SELECTION_TYPE_SERVICE:
+                case SELECTION_TYPE_SERVICE:
                     return SHARESHEET_SERVICE_TARGET_SELECTED;
-                case ChooserActivity.SELECTION_TYPE_APP:
+                case SELECTION_TYPE_APP:
                     return SHARESHEET_APP_TARGET_SELECTED;
-                case ChooserActivity.SELECTION_TYPE_STANDARD:
+                case SELECTION_TYPE_STANDARD:
                     return SHARESHEET_STANDARD_TARGET_SELECTED;
-                case ChooserActivity.SELECTION_TYPE_COPY:
+                case SELECTION_TYPE_COPY:
                     return SHARESHEET_COPY_TARGET_SELECTED;
-                case ChooserActivity.SELECTION_TYPE_NEARBY:
+                case SELECTION_TYPE_NEARBY:
                     return SHARESHEET_NEARBY_TARGET_SELECTED;
-                case ChooserActivity.SELECTION_TYPE_EDIT:
+                case SELECTION_TYPE_EDIT:
                     return SHARESHEET_EDIT_TARGET_SELECTED;
                 default:
                     return INVALID;
@@ -201,13 +394,13 @@
     /**
      * Returns the enum used in sharesheet started atom to indicate what preview type was used.
      */
-    default int typeFromPreviewInt(int previewType) {
+    private static int typeFromPreviewInt(int previewType) {
         switch(previewType) {
-            case ChooserActivity.CONTENT_PREVIEW_IMAGE:
+            case ChooserContentPreviewUi.CONTENT_PREVIEW_IMAGE:
                 return FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_IMAGE;
-            case ChooserActivity.CONTENT_PREVIEW_FILE:
+            case ChooserContentPreviewUi.CONTENT_PREVIEW_FILE:
                 return FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_FILE;
-            case ChooserActivity.CONTENT_PREVIEW_TEXT:
+            case ChooserContentPreviewUi.CONTENT_PREVIEW_TEXT:
             default:
                 return FrameworkStatsLog
                         .SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_TYPE_UNKNOWN;
@@ -218,7 +411,7 @@
      * Returns the enum used in sharesheet started atom to indicate what intent triggers the
      * ChooserActivity.
      */
-    default int typeFromIntentString(String intent) {
+    private static int typeFromIntentString(String intent) {
         if (intent == null) {
             return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_DEFAULT;
         }
@@ -243,4 +436,62 @@
                 return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_DEFAULT;
         }
     }
+
+    @VisibleForTesting
+    static int getTargetSelectionCategory(int targetType) {
+        switch (targetType) {
+            case SELECTION_TYPE_SERVICE:
+                return MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET;
+            case SELECTION_TYPE_APP:
+                return MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET;
+            case SELECTION_TYPE_STANDARD:
+                return MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET;
+            default:
+                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) {
+            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);
+        }
+
+        @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/ChooserActivityLoggerImpl.java b/java/src/com/android/intentresolver/ChooserActivityLoggerImpl.java
deleted file mode 100644
index 08a345b..0000000
--- a/java/src/com/android/intentresolver/ChooserActivityLoggerImpl.java
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver;
-
-import com.android.internal.logging.InstanceId;
-import com.android.internal.logging.InstanceIdSequence;
-import com.android.internal.logging.UiEventLogger;
-import com.android.internal.logging.UiEventLoggerImpl;
-import com.android.internal.util.FrameworkStatsLog;
-
-/**
- * Standard implementation of ChooserActivityLogger interface.
- * @hide
- */
-public class ChooserActivityLoggerImpl implements ChooserActivityLogger {
-    private static final int SHARESHEET_INSTANCE_ID_MAX = (1 << 13);
-
-    private UiEventLogger mUiEventLogger = new UiEventLoggerImpl();
-    // A small per-notification ID, used for statsd logging.
-    private InstanceId mInstanceId;
-    private static InstanceIdSequence sInstanceIdSequence;
-
-    @Override
-    public void logShareStarted(int eventId, String packageName, String mimeType,
-            int appProvidedDirect, int appProvidedApp, boolean isWorkprofile, int previewType,
-            String intent) {
-        FrameworkStatsLog.write(FrameworkStatsLog.SHARESHEET_STARTED,
-                /* event_id = 1 */ SharesheetStartedEvent.SHARE_STARTED.getId(),
-                /* package_name = 2 */ packageName,
-                /* instance_id = 3 */ getInstanceId().getId(),
-                /* mime_type = 4 */ mimeType,
-                /* num_app_provided_direct_targets = 5 */ appProvidedDirect,
-                /* num_app_provided_app_targets = 6 */ appProvidedApp,
-                /* is_workprofile = 7 */ isWorkprofile,
-                /* previewType = 8 */ typeFromPreviewInt(previewType),
-                /* intentType = 9 */ typeFromIntentString(intent));
-    }
-
-    @Override
-    public void logShareTargetSelected(int targetType, String packageName, int positionPicked,
-            boolean isPinned) {
-        FrameworkStatsLog.write(FrameworkStatsLog.RANKING_SELECTED,
-                /* event_id = 1 */ SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(),
-                /* package_name = 2 */ packageName,
-                /* instance_id = 3 */ getInstanceId().getId(),
-                /* position_picked = 4 */ positionPicked,
-                /* is_pinned = 5 */ isPinned);
-    }
-
-    @Override
-    public void log(UiEventLogger.UiEventEnum event, InstanceId instanceId) {
-        mUiEventLogger.logWithInstanceId(
-                event,
-                0,
-                null,
-                instanceId);
-    }
-
-    @Override
-    public InstanceId getInstanceId() {
-        if (mInstanceId == null) {
-            if (sInstanceIdSequence == null) {
-                sInstanceIdSequence = new InstanceIdSequence(SHARESHEET_INSTANCE_ID_MAX);
-            }
-            mInstanceId = sInstanceIdSequence.newInstanceId();
-        }
-        return mInstanceId;
-    }
-
-}
diff --git a/java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java b/java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java
new file mode 100644
index 0000000..0b8dbe3
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java
@@ -0,0 +1,132 @@
+/*
+ * 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;
+
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Handler;
+import android.util.Size;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+
+import com.google.common.util.concurrent.FutureCallback;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+
+import java.util.concurrent.ExecutorService;
+import java.util.function.Consumer;
+
+/**
+ * Delegate to manage deferred resource loads for content preview assets, while
+ * implementing Chooser's application logic for determining timeout/success/failure conditions.
+ */
+public class ChooserContentPreviewCoordinator implements
+        ChooserContentPreviewUi.ContentPreviewCoordinator {
+    public ChooserContentPreviewCoordinator(
+            ExecutorService backgroundExecutor,
+            ChooserActivity chooserActivity,
+            Runnable onFailCallback) {
+        this.mBackgroundExecutor = MoreExecutors.listeningDecorator(backgroundExecutor);
+        this.mChooserActivity = chooserActivity;
+        this.mOnFailCallback = onFailCallback;
+
+        this.mImageLoadTimeoutMillis =
+                chooserActivity.getResources().getInteger(R.integer.config_shortAnimTime);
+    }
+
+    @Override
+    public void loadImage(final Uri imageUri, final Consumer<Bitmap> callback) {
+        final int size = mChooserActivity.getResources().getDimensionPixelSize(
+                R.dimen.chooser_preview_image_max_dimen);
+
+        // TODO: apparently this timeout is only used for not holding shared element transition
+        //  animation for too long. If so, we already have a better place for it
+        //  EnterTransitionAnimationDelegate.
+        mHandler.postDelayed(this::onWatchdogTimeout, mImageLoadTimeoutMillis);
+
+        ListenableFuture<Bitmap> bitmapFuture = mBackgroundExecutor.submit(
+                () -> mChooserActivity.loadThumbnail(imageUri, new Size(size, size)));
+
+        Futures.addCallback(
+                bitmapFuture,
+                new FutureCallback<Bitmap>() {
+                    @Override
+                    public void onSuccess(Bitmap loadedBitmap) {
+                        try {
+                            callback.accept(loadedBitmap);
+                            onLoadCompleted(loadedBitmap);
+                        } catch (Exception e) { /* unimportant */ }
+                    }
+
+                    @Override
+                    public void onFailure(Throwable t) {
+                        callback.accept(null);
+                    }
+                },
+                mHandler::post);
+    }
+
+    private final ChooserActivity mChooserActivity;
+    private final ListeningExecutorService mBackgroundExecutor;
+    private final Runnable mOnFailCallback;
+    private final int mImageLoadTimeoutMillis;
+
+    // TODO: this uses a `Handler` because there doesn't seem to be a straightforward way to get a
+    // `ScheduledExecutorService` that posts to the UI thread unless we use Dagger. Eventually we'll
+    // use Dagger and can inject this as a `@UiThread ScheduledExecutorService`.
+    private final Handler mHandler = new Handler();
+
+    private boolean mAtLeastOneLoaded = false;
+
+    @MainThread
+    private void onWatchdogTimeout() {
+        if (mChooserActivity.isFinishing()) {
+            return;
+        }
+
+        // If at least one image loads within the timeout period, allow other loads to continue.
+        if (!mAtLeastOneLoaded) {
+            mOnFailCallback.run();
+        }
+    }
+
+    @MainThread
+    private void onLoadCompleted(@Nullable Bitmap loadedBitmap) {
+        if (mChooserActivity.isFinishing()) {
+            return;
+        }
+
+        // TODO: the following logic can be described as "invoke the fail callback when the first
+        //  image loading has failed". Historically, before we had switched from a single-threaded
+        //  pool to a multi-threaded pool, we first loaded the transition element's image (the image
+        //  preview is the only case when those callbacks matter) and aborting the animation on it's
+        //  failure was reasonable. With the multi-thread pool, the first result may belong to any
+        //  image and thus we can falsely abort the animation.
+        //  Now, when we track the transition view state directly and after the timeout logic will
+        //  be moved into ChooserActivity$EnterTransitionAnimationDelegate, we can just get rid of
+        //  the fail callback and the following logic altogether.
+        mAtLeastOneLoaded |= loadedBitmap != null;
+        boolean wholeBatchFailed = !mAtLeastOneLoaded;
+
+        if (wholeBatchFailed) {
+            mOnFailCallback.run();
+        }
+    }
+}
diff --git a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
new file mode 100644
index 0000000..ff88e5e
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
@@ -0,0 +1,566 @@
+/*
+ * 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 static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.annotation.IntDef;
+import android.content.ClipData;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.provider.DocumentsContract;
+import android.provider.Downloads;
+import android.provider.OpenableColumns;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.PluralsMessageFormatter;
+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.LayoutRes;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.widget.ActionRow;
+import com.android.intentresolver.widget.ImagePreviewView;
+import com.android.intentresolver.widget.RoundedRectImageView;
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.function.Consumer;
+
+/**
+ * Collection of helpers for building the content preview UI displayed in {@link ChooserActivity}.
+ *
+ * TODO: this "namespace" was pulled out of {@link ChooserActivity} as a bucket of static methods
+ * to show that they're one-shot procedures with no dependencies back to {@link ChooserActivity}
+ * state other than the delegates that are explicitly provided. There may be more appropriate
+ * abstractions (e.g., maybe this can be a "widget" added directly to the view hierarchy to show the
+ * appropriate preview), or it may at least be safe (and more convenient) to adopt a more "object
+ * oriented" design where the static specifiers are removed and some of the dependencies are cached
+ * as ivars when this "class" is initialized.
+ */
+public final class ChooserContentPreviewUi {
+    private static final int IMAGE_FADE_IN_MILLIS = 150;
+
+    /**
+     * Delegate to handle background resource loads that are dependencies of content previews.
+     */
+    public interface ContentPreviewCoordinator {
+        /**
+         * Request that an image be loaded in the background and set into a view.
+         *
+         * @param imageUri The {@link Uri} of the image to load.
+         *
+         * TODO: it looks like clients are probably capable of passing the view directly, but the
+         * deferred computation here is a closer match to the legacy model for now.
+         */
+        void loadImage(Uri imageUri, Consumer<Bitmap> callback);
+    }
+
+    /**
+     * Delegate to build the default system action buttons to display in the preview layout, if/when
+     * they're determined to be appropriate for the particular preview we display.
+     * TODO: clarify why action buttons are part of preview logic.
+     */
+    public interface ActionFactory {
+        /** Create an action that copies the share content to the clipboard. */
+        ActionRow.Action createCopyButton();
+
+        /** Create an action that opens the share content in a system-default editor. */
+        @Nullable
+        ActionRow.Action createEditButton();
+
+        /** Create an "Share to Nearby" action. */
+        @Nullable
+        ActionRow.Action createNearbyButton();
+    }
+
+    /**
+     * Testing shim to specify whether a given mime type is considered to be an "image."
+     *
+     * TODO: move away from {@link ChooserActivityOverrideData} as a model to configure our tests,
+     * then migrate {@link ChooserActivity#isImageType(String)} into this class.
+     */
+    public interface ImageMimeTypeClassifier {
+        /** @return whether the specified {@code mimeType} is classified as an "image" type. */
+        boolean isImageType(String mimeType);
+    }
+
+    @Retention(SOURCE)
+    @IntDef({CONTENT_PREVIEW_FILE, CONTENT_PREVIEW_IMAGE, CONTENT_PREVIEW_TEXT})
+    private @interface ContentPreviewType {
+    }
+
+    // Starting at 1 since 0 is considered "undefined" for some of the database transformations
+    // of tron logs.
+    @VisibleForTesting
+    public static final int CONTENT_PREVIEW_IMAGE = 1;
+    @VisibleForTesting
+    public static final int CONTENT_PREVIEW_FILE = 2;
+    @VisibleForTesting
+    public static final int CONTENT_PREVIEW_TEXT = 3;
+
+    private static final String TAG = "ChooserPreview";
+
+    private static final String PLURALS_COUNT  = "count";
+    private static final String PLURALS_FILE_NAME = "file_name";
+
+    /** Determine the most appropriate type of preview to show for the provided {@link Intent}. */
+    @ContentPreviewType
+    public static int findPreferredContentPreview(
+            Intent targetIntent,
+            ContentResolver resolver,
+            ImageMimeTypeClassifier imageClassifier) {
+        /* In {@link android.content.Intent#getType}, the app may specify a very general mime type
+         * that broadly covers all data being shared, such as {@literal *}/* when sending an image
+         * and text. We therefore should inspect each item for the preferred type, in order: IMAGE,
+         * FILE, TEXT.  */
+        String action = targetIntent.getAction();
+        if (Intent.ACTION_SEND.equals(action)) {
+            Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+            return findPreferredContentPreview(uri, resolver, imageClassifier);
+        } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+            List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+            if (uris == null || uris.isEmpty()) {
+                return CONTENT_PREVIEW_TEXT;
+            }
+
+            for (Uri uri : uris) {
+                // Defaulting to file preview when there are mixed image/file types is
+                // preferable, as it shows the user the correct number of items being shared
+                int uriPreviewType = findPreferredContentPreview(uri, resolver, imageClassifier);
+                if (uriPreviewType == CONTENT_PREVIEW_FILE) {
+                    return CONTENT_PREVIEW_FILE;
+                }
+            }
+
+            return CONTENT_PREVIEW_IMAGE;
+        }
+
+        return CONTENT_PREVIEW_TEXT;
+    }
+
+    /**
+     * Display a content preview of the specified {@code previewType} to preview the content of the
+     * specified {@code intent}.
+     */
+    public static ViewGroup displayContentPreview(
+            @ContentPreviewType int previewType,
+            Intent targetIntent,
+            Resources resources,
+            LayoutInflater layoutInflater,
+            ActionFactory actionFactory,
+            @LayoutRes int actionRowLayout,
+            ViewGroup parent,
+            ContentPreviewCoordinator previewCoord,
+            Consumer<Boolean> onTransitionTargetReady,
+            ContentResolver contentResolver,
+            ImageMimeTypeClassifier imageClassifier) {
+        ViewGroup layout = null;
+
+        switch (previewType) {
+            case CONTENT_PREVIEW_TEXT:
+                layout = displayTextContentPreview(
+                        targetIntent,
+                        layoutInflater,
+                        createTextPreviewActions(actionFactory),
+                        parent,
+                        previewCoord,
+                        actionRowLayout);
+                break;
+            case CONTENT_PREVIEW_IMAGE:
+                layout = displayImageContentPreview(
+                        targetIntent,
+                        layoutInflater,
+                        createImagePreviewActions(actionFactory),
+                        parent,
+                        previewCoord,
+                        onTransitionTargetReady,
+                        contentResolver,
+                        imageClassifier,
+                        actionRowLayout);
+                break;
+            case CONTENT_PREVIEW_FILE:
+                layout = displayFileContentPreview(
+                        targetIntent,
+                        resources,
+                        layoutInflater,
+                        createFilePreviewActions(actionFactory),
+                        parent,
+                        previewCoord,
+                        contentResolver,
+                        actionRowLayout);
+                break;
+            default:
+                Log.e(TAG, "Unexpected content preview type: " + previewType);
+        }
+
+        return layout;
+    }
+
+    private static Cursor queryResolver(ContentResolver resolver, Uri uri) {
+        return resolver.query(uri, null, null, null, null);
+    }
+
+    @ContentPreviewType
+    private static int findPreferredContentPreview(
+            Uri uri, ContentResolver resolver, ImageMimeTypeClassifier imageClassifier) {
+        if (uri == null) {
+            return CONTENT_PREVIEW_TEXT;
+        }
+
+        String mimeType = resolver.getType(uri);
+        return imageClassifier.isImageType(mimeType) ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE;
+    }
+
+    private static ViewGroup displayTextContentPreview(
+            Intent targetIntent,
+            LayoutInflater layoutInflater,
+            List<ActionRow.Action> actions,
+            ViewGroup parent,
+            ContentPreviewCoordinator previewCoord,
+            @LayoutRes int actionRowLayout) {
+        ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+                R.layout.chooser_grid_preview_text, parent, false);
+
+        final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
+        if (actionRow != null) {
+            actionRow.setActions(actions);
+        }
+
+        CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
+        if (sharingText == null) {
+            contentPreviewLayout
+                    .findViewById(com.android.internal.R.id.content_preview_text_layout)
+                    .setVisibility(View.GONE);
+        } else {
+            TextView textView = contentPreviewLayout.findViewById(
+                    com.android.internal.R.id.content_preview_text);
+            textView.setText(sharingText);
+        }
+
+        String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE);
+        if (TextUtils.isEmpty(previewTitle)) {
+            contentPreviewLayout
+                    .findViewById(com.android.internal.R.id.content_preview_title_layout)
+                    .setVisibility(View.GONE);
+        } else {
+            TextView previewTitleView = contentPreviewLayout.findViewById(
+                    com.android.internal.R.id.content_preview_title);
+            previewTitleView.setText(previewTitle);
+
+            ClipData previewData = targetIntent.getClipData();
+            Uri previewThumbnail = null;
+            if (previewData != null) {
+                if (previewData.getItemCount() > 0) {
+                    ClipData.Item previewDataItem = previewData.getItemAt(0);
+                    previewThumbnail = previewDataItem.getUri();
+                }
+            }
+
+            ImageView previewThumbnailView = contentPreviewLayout.findViewById(
+                    com.android.internal.R.id.content_preview_thumbnail);
+            if (previewThumbnail == null) {
+                previewThumbnailView.setVisibility(View.GONE);
+            } else {
+                previewCoord.loadImage(
+                        previewThumbnail,
+                        (bitmap) -> updateViewWithImage(
+                                contentPreviewLayout.findViewById(
+                                    com.android.internal.R.id.content_preview_thumbnail),
+                                bitmap));
+            }
+        }
+
+        return contentPreviewLayout;
+    }
+
+    private static List<ActionRow.Action> createTextPreviewActions(ActionFactory actionFactory) {
+        ArrayList<ActionRow.Action> actions = new ArrayList<>(2);
+        actions.add(actionFactory.createCopyButton());
+        ActionRow.Action nearbyAction = actionFactory.createNearbyButton();
+        if (nearbyAction != null) {
+            actions.add(nearbyAction);
+        }
+        return actions;
+    }
+
+    private static ViewGroup displayImageContentPreview(
+            Intent targetIntent,
+            LayoutInflater layoutInflater,
+            List<ActionRow.Action> actions,
+            ViewGroup parent,
+            ContentPreviewCoordinator previewCoord,
+            Consumer<Boolean> onTransitionTargetReady,
+            ContentResolver contentResolver,
+            ImageMimeTypeClassifier imageClassifier,
+            @LayoutRes int actionRowLayout) {
+        ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+                R.layout.chooser_grid_preview_image, parent, false);
+        ImagePreviewView imagePreview = contentPreviewLayout.findViewById(
+                com.android.internal.R.id.content_preview_image_area);
+
+        final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
+        if (actionRow != null) {
+            actionRow.setActions(actions);
+        }
+
+        final ImagePreviewImageLoader imageLoader = new ImagePreviewImageLoader(previewCoord);
+        final ArrayList<Uri> imageUris = new ArrayList<>();
+        String action = targetIntent.getAction();
+        if (Intent.ACTION_SEND.equals(action)) {
+            // TODO: why don't we use image classifier in this case as well?
+            Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+            imageUris.add(uri);
+        } else {
+            List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+            for (Uri uri : uris) {
+                if (imageClassifier.isImageType(contentResolver.getType(uri))) {
+                    imageUris.add(uri);
+                }
+            }
+        }
+
+        if (imageUris.size() == 0) {
+            Log.i(TAG, "Attempted to display image preview area with zero"
+                    + " available images detected in EXTRA_STREAM list");
+            imagePreview.setVisibility(View.GONE);
+            onTransitionTargetReady.accept(false);
+            return contentPreviewLayout;
+        }
+
+        imagePreview.setSharedElementTransitionTarget(
+                ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME,
+                onTransitionTargetReady);
+        imagePreview.setImages(imageUris, imageLoader);
+
+        return contentPreviewLayout;
+    }
+
+    private static List<ActionRow.Action> createImagePreviewActions(
+            ActionFactory buttonFactory) {
+        ArrayList<ActionRow.Action> actions = new ArrayList<>(2);
+        //TODO: add copy action;
+        ActionRow.Action action = buttonFactory.createNearbyButton();
+        if (action != null) {
+            actions.add(action);
+        }
+        action = buttonFactory.createEditButton();
+        if (action != null) {
+            actions.add(action);
+        }
+        return actions;
+    }
+
+    private static ViewGroup displayFileContentPreview(
+            Intent targetIntent,
+            Resources resources,
+            LayoutInflater layoutInflater,
+            List<ActionRow.Action> actions,
+            ViewGroup parent,
+            ContentPreviewCoordinator previewCoord,
+            ContentResolver contentResolver,
+            @LayoutRes int actionRowLayout) {
+        ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+                R.layout.chooser_grid_preview_file, parent, false);
+
+        final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
+        if (actionRow != null) {
+            actionRow.setActions(actions);
+        }
+
+        String action = targetIntent.getAction();
+        if (Intent.ACTION_SEND.equals(action)) {
+            Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+            loadFileUriIntoView(uri, contentPreviewLayout, previewCoord, contentResolver);
+        } else {
+            List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+            int uriCount = uris.size();
+
+            if (uriCount == 0) {
+                contentPreviewLayout.setVisibility(View.GONE);
+                Log.i(TAG,
+                        "Appears to be no uris available in EXTRA_STREAM, removing "
+                                + "preview area");
+                return contentPreviewLayout;
+            } else if (uriCount == 1) {
+                loadFileUriIntoView(
+                        uris.get(0), contentPreviewLayout, previewCoord, contentResolver);
+            } else {
+                FileInfo fileInfo = extractFileInfo(uris.get(0), contentResolver);
+                int remUriCount = uriCount - 1;
+                Map<String, Object> arguments = new HashMap<>();
+                arguments.put(PLURALS_COUNT, remUriCount);
+                arguments.put(PLURALS_FILE_NAME, fileInfo.name);
+                String fileName =
+                        PluralsMessageFormatter.format(resources, arguments, R.string.file_count);
+
+                TextView fileNameView = contentPreviewLayout.findViewById(
+                        com.android.internal.R.id.content_preview_filename);
+                fileNameView.setText(fileName);
+
+                View thumbnailView = contentPreviewLayout.findViewById(
+                        com.android.internal.R.id.content_preview_file_thumbnail);
+                thumbnailView.setVisibility(View.GONE);
+
+                ImageView fileIconView = contentPreviewLayout.findViewById(
+                        com.android.internal.R.id.content_preview_file_icon);
+                fileIconView.setVisibility(View.VISIBLE);
+                fileIconView.setImageResource(R.drawable.ic_file_copy);
+            }
+        }
+
+        return contentPreviewLayout;
+    }
+
+    private static List<ActionRow.Action> createFilePreviewActions(ActionFactory actionFactory) {
+        List<ActionRow.Action> actions = new ArrayList<>(1);
+        //TODO(b/120417119):
+        // add action buttonFactory.createCopyButton()
+        ActionRow.Action action = actionFactory.createNearbyButton();
+        if (action != null) {
+            actions.add(action);
+        }
+        return actions;
+    }
+
+    private static ActionRow inflateActionRow(ViewGroup parent, @LayoutRes int actionRowLayout) {
+        final ViewStub stub = parent.findViewById(com.android.intentresolver.R.id.action_row_stub);
+        if (stub != null) {
+            stub.setLayoutResource(actionRowLayout);
+            stub.inflate();
+        }
+        return parent.findViewById(com.android.internal.R.id.chooser_action_row);
+    }
+
+    private static 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 "
+                + "desired, consider using Intent#createChooser to launch the ChooserActivity, "
+                + "and set your Intent's clipData and flags in accordance with that method's "
+                + "documentation");
+    }
+
+    private static void loadFileUriIntoView(
+            final Uri uri,
+            final View parent,
+            final ContentPreviewCoordinator previewCoord,
+            final ContentResolver contentResolver) {
+        FileInfo fileInfo = extractFileInfo(uri, contentResolver);
+
+        TextView fileNameView = parent.findViewById(
+                com.android.internal.R.id.content_preview_filename);
+        fileNameView.setText(fileInfo.name);
+
+        if (fileInfo.hasThumbnail) {
+            previewCoord.loadImage(
+                    uri,
+                    (bitmap) -> updateViewWithImage(
+                            parent.findViewById(
+                                    com.android.internal.R.id.content_preview_file_thumbnail),
+                            bitmap));
+        } else {
+            View thumbnailView = parent.findViewById(
+                    com.android.internal.R.id.content_preview_file_thumbnail);
+            thumbnailView.setVisibility(View.GONE);
+
+            ImageView fileIconView = parent.findViewById(
+                    com.android.internal.R.id.content_preview_file_icon);
+            fileIconView.setVisibility(View.VISIBLE);
+            fileIconView.setImageResource(R.drawable.chooser_file_generic);
+        }
+    }
+
+    private static void updateViewWithImage(RoundedRectImageView imageView, Bitmap image) {
+        if (image == null) {
+            imageView.setVisibility(View.GONE);
+            return;
+        }
+        imageView.setVisibility(View.VISIBLE);
+        imageView.setAlpha(0.0f);
+        imageView.setImageBitmap(image);
+
+        ValueAnimator fadeAnim = ObjectAnimator.ofFloat(imageView, "alpha", 0.0f, 1.0f);
+        fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
+        fadeAnim.setDuration(IMAGE_FADE_IN_MILLIS);
+        fadeAnim.start();
+    }
+
+    private static FileInfo extractFileInfo(Uri uri, ContentResolver resolver) {
+        String fileName = null;
+        boolean hasThumbnail = false;
+
+        try (Cursor cursor = queryResolver(resolver, uri)) {
+            if (cursor != null && cursor.getCount() > 0) {
+                int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+                int titleIndex = cursor.getColumnIndex(Downloads.Impl.COLUMN_TITLE);
+                int flagsIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS);
+
+                cursor.moveToFirst();
+                if (nameIndex != -1) {
+                    fileName = cursor.getString(nameIndex);
+                } else if (titleIndex != -1) {
+                    fileName = cursor.getString(titleIndex);
+                }
+
+                if (flagsIndex != -1) {
+                    hasThumbnail = (cursor.getInt(flagsIndex)
+                            & DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
+                }
+            }
+        } catch (SecurityException | NullPointerException e) {
+            logContentPreviewWarning(uri);
+        }
+
+        if (TextUtils.isEmpty(fileName)) {
+            fileName = uri.getPath();
+            int index = fileName.lastIndexOf('/');
+            if (index != -1) {
+                fileName = fileName.substring(index + 1);
+            }
+        }
+
+        return new FileInfo(fileName, hasThumbnail);
+    }
+
+    private static class FileInfo {
+        public final String name;
+        public final boolean hasThumbnail;
+
+        FileInfo(String name, boolean hasThumbnail) {
+            this.name = name;
+            this.hasThumbnail = hasThumbnail;
+        }
+    }
+
+    private ChooserContentPreviewUi() {}
+}
diff --git a/java/src/com/android/intentresolver/ChooserFlags.java b/java/src/com/android/intentresolver/ChooserFlags.java
deleted file mode 100644
index 67f9046..0000000
--- a/java/src/com/android/intentresolver/ChooserFlags.java
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver;
-
-import android.app.prediction.AppPredictionManager;
-
-/**
- * Common flags for {@link ChooserListAdapter} and {@link ChooserActivity}.
- */
-public class ChooserFlags {
-
-    /**
-     * Whether to use {@link AppPredictionManager} to query for direct share targets (as opposed to
-     * talking directly to {@link android.content.pm.ShortcutManager}.
-     */
-    // TODO(b/123089490): Replace with system flag
-    static final boolean USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS = true;
-}
-
diff --git a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
index 7c4b0c1..5f37352 100644
--- a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
+++ b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
@@ -19,8 +19,8 @@
 import android.content.Context;
 import android.util.AttributeSet;
 
-import com.android.internal.widget.GridLayoutManager;
-import com.android.internal.widget.RecyclerView;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
 
 /**
  * For a11y and per {@link RecyclerView#onInitializeAccessibilityNodeInfo}, override
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java
index 6d0c833..699190f 100644
--- a/java/src/com/android/intentresolver/ChooserListAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -19,17 +19,22 @@
 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.AppPredictor;
+import android.app.prediction.AppTarget;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.LabeledIntent;
+import android.content.pm.LauncherApps;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ShortcutInfo;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
 import android.os.AsyncTask;
 import android.os.Trace;
 import android.os.UserHandle;
@@ -42,27 +47,27 @@
 import android.view.ViewGroup;
 import android.widget.TextView;
 
+import androidx.annotation.WorkerThread;
+
 import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
-import com.android.intentresolver.chooser.ChooserTargetInfo;
 import com.android.intentresolver.chooser.DisplayResolveInfo;
 import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
+import com.android.intentresolver.chooser.NotSelectableTargetInfo;
 import com.android.intentresolver.chooser.SelectableTargetInfo;
 import com.android.intentresolver.chooser.TargetInfo;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 public class ChooserListAdapter extends ResolverListAdapter {
     private static final String TAG = "ChooserListAdapter";
     private static final boolean DEBUG = false;
 
-    private boolean mEnableStackedApps = true;
-
     public static final int NO_POSITION = -1;
     public static final int TARGET_BAD = -1;
     public static final int TARGET_CALLER = 0;
@@ -71,40 +76,28 @@
     public static final int TARGET_STANDARD_AZ = 3;
 
     private static final int MAX_SUGGESTED_APP_TARGETS = 4;
-    private static final int MAX_CHOOSER_TARGETS_PER_APP = 2;
 
     /** {@link #getBaseScore} */
     public static final float CALLER_TARGET_SCORE_BOOST = 900.f;
     /** {@link #getBaseScore} */
     public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f;
-    private static final float PINNED_SHORTCUT_TARGET_SCORE_BOOST = 1000.f;
 
-    private final int mMaxShortcutTargetsPerApp;
-    private final ChooserListCommunicator mChooserListCommunicator;
-    private final SelectableTargetInfo.SelectableTargetInfoCommunicator
-            mSelectableTargetInfoCommunicator;
+    private final ChooserRequestParameters mChooserRequest;
+    private final int mMaxRankedTargets;
+
     private final ChooserActivityLogger mChooserActivityLogger;
 
-    private int mNumShortcutResults = 0;
     private final Map<TargetInfo, AsyncTask> mIconLoaders = new HashMap<>();
-    private boolean mApplySharingAppLimits;
 
     // Reserve spots for incoming direct share targets by adding placeholders
-    private ChooserTargetInfo
-            mPlaceHolderTargetInfo = new ChooserActivity.PlaceHolderTargetInfo();
-    private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
+    private final TargetInfo mPlaceHolderTargetInfo;
+    private final List<TargetInfo> mServiceTargets = new ArrayList<>();
     private final List<DisplayResolveInfo> mCallerTargets = new ArrayList<>();
 
-    private final ChooserActivity.BaseChooserTargetComparator mBaseTargetComparator =
-            new ChooserActivity.BaseChooserTargetComparator();
-    private boolean mListViewDataChanged = false;
+    private final ShortcutSelectionLogic mShortcutSelectionLogic;
 
     // Sorted list of DisplayResolveInfos for the alphabetical app section.
     private List<DisplayResolveInfo> mSortedList = new ArrayList<>();
-    private AppPredictor mAppPredictor;
-    private AppPredictor.Callback mAppPredictorCallback;
-
-    private LoadDirectShareIconTaskProvider mTestLoadDirectShareTaskProvider;
 
     // For pinned direct share labels, if the text spans multiple lines, the TextView will consume
     // the full width, even if the characters actually take up less than that. Measure the actual
@@ -137,24 +130,47 @@
                 }
             };
 
-    public ChooserListAdapter(Context context, List<Intent> payloadIntents,
-            Intent[] initialIntents, List<ResolveInfo> rList,
-            boolean filterLastUsed, ResolverListController resolverListController,
-            ChooserListCommunicator chooserListCommunicator,
-            SelectableTargetInfo.SelectableTargetInfoCommunicator selectableTargetInfoCommunicator,
+    public ChooserListAdapter(
+            Context context,
+            List<Intent> payloadIntents,
+            Intent[] initialIntents,
+            List<ResolveInfo> rList,
+            boolean filterLastUsed,
+            ResolverListController resolverListController,
+            UserHandle userHandle,
+            Intent targetIntent,
+            ResolverListCommunicator resolverListCommunicator,
             PackageManager packageManager,
-            ChooserActivityLogger chooserActivityLogger) {
+            ChooserActivityLogger chooserActivityLogger,
+            ChooserRequestParameters chooserRequest,
+            int maxRankedTargets) {
         // Don't send the initial intents through the shared ResolverActivity path,
         // we want to separate them into a different section.
-        super(context, payloadIntents, null, rList, filterLastUsed,
-                resolverListController, chooserListCommunicator, false);
+        super(
+                context,
+                payloadIntents,
+                null,
+                rList,
+                filterLastUsed,
+                resolverListController,
+                userHandle,
+                targetIntent,
+                resolverListCommunicator,
+                false);
 
-        mMaxShortcutTargetsPerApp =
-                context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp);
-        mChooserListCommunicator = chooserListCommunicator;
+        mChooserRequest = chooserRequest;
+        mMaxRankedTargets = maxRankedTargets;
+
+        mPlaceHolderTargetInfo = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context);
         createPlaceHolders();
-        mSelectableTargetInfoCommunicator = selectableTargetInfoCommunicator;
         mChooserActivityLogger = chooserActivityLogger;
+        mShortcutSelectionLogic = new ShortcutSelectionLogic(
+                context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp),
+                DeviceConfig.getBoolean(
+                        DeviceConfig.NAMESPACE_SYSTEMUI,
+                        SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
+                        true)
+        );
 
         if (initialIntents != null) {
             for (int i = 0; i < initialIntents.length; i++) {
@@ -172,7 +188,9 @@
                 final ComponentName cn = ii.getComponent();
                 if (cn != null) {
                     try {
-                        ai = packageManager.getActivityInfo(ii.getComponent(), 0);
+                        ai = packageManager.getActivityInfo(
+                                ii.getComponent(),
+                                PackageManager.ComponentInfoFlags.of(PackageManager.GET_META_DATA));
                         ri = new ResolveInfo();
                         ri.activityInfo = ai;
                     } catch (PackageManager.NameNotFoundException ignored) {
@@ -182,7 +200,9 @@
                 if (ai == null) {
                     // Because of AIDL bug, resolveActivity can't accept subclasses of Intent.
                     final Intent rii = (ii.getClass() == Intent.class) ? ii : new Intent(ii);
-                    ri = packageManager.resolveActivity(rii, PackageManager.MATCH_DEFAULT_ONLY);
+                    ri = packageManager.resolveActivity(
+                            rii,
+                            PackageManager.ResolveInfoFlags.of(PackageManager.MATCH_DEFAULT_ONLY));
                     ai = ri != null ? ri.activityInfo : null;
                 }
                 if (ai == null) {
@@ -203,18 +223,12 @@
                     ri.noResourceId = true;
                     ri.icon = 0;
                 }
-                mCallerTargets.add(new DisplayResolveInfo(ii, ri, ii, makePresentationGetter(ri)));
+                DisplayResolveInfo displayResolveInfo = DisplayResolveInfo.newDisplayResolveInfo(
+                        ii, ri, ii, mPresentationFactory.makePresentationGetter(ri));
+                mCallerTargets.add(displayResolveInfo);
                 if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break;
             }
         }
-        mApplySharingAppLimits = DeviceConfig.getBoolean(
-                DeviceConfig.NAMESPACE_SYSTEMUI,
-                SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
-                true);
-    }
-
-    AppPredictor getAppPredictor() {
-        return mAppPredictor;
     }
 
     @Override
@@ -223,73 +237,54 @@
             Log.d(TAG, "clearing queryTargets on package change");
         }
         createPlaceHolders();
-        mChooserListCommunicator.onHandlePackagesChanged(this);
+        mResolverListCommunicator.onHandlePackagesChanged(this);
 
     }
 
-    @Override
-    public void notifyDataSetChanged() {
-        if (!mListViewDataChanged) {
-            mChooserListCommunicator.sendListViewUpdateMessage(getUserHandle());
-            mListViewDataChanged = true;
-        }
-    }
-
-    void refreshListView() {
-        if (mListViewDataChanged) {
-            super.notifyDataSetChanged();
-        }
-        mListViewDataChanged = false;
-    }
-
     private void createPlaceHolders() {
-        mNumShortcutResults = 0;
         mServiceTargets.clear();
-        for (int i = 0; i < mChooserListCommunicator.getMaxRankedTargets(); i++) {
+        for (int i = 0; i < mMaxRankedTargets; ++i) {
             mServiceTargets.add(mPlaceHolderTargetInfo);
         }
     }
 
     @Override
     View onCreateView(ViewGroup parent) {
-        return mInflater.inflate(
-                R.layout.resolve_grid_item, parent, false);
+        return mInflater.inflate(R.layout.resolve_grid_item, parent, false);
     }
 
+    @VisibleForTesting
     @Override
-    protected void onBindView(View view, TargetInfo info, int position) {
+    public void onBindView(View view, TargetInfo info, int position) {
         final ViewHolder holder = (ViewHolder) view.getTag();
 
         if (info == null) {
-            holder.icon.setImageDrawable(
-                    mContext.getDrawable(R.drawable.resolver_icon_placeholder));
+            holder.icon.setImageDrawable(loadIconPlaceholder());
             return;
         }
 
-        if (info instanceof DisplayResolveInfo) {
+        holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
+        holder.bindIcon(info);
+        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);
+            holder.updateContentDescription(contentDescription);
+            if (!info.hasDisplayIcon()) {
+                loadDirectShareIcon((SelectableTargetInfo) info);
+            }
+        } else if (info.isDisplayResolveInfo()) {
             DisplayResolveInfo dri = (DisplayResolveInfo) info;
-            holder.bindLabel(dri.getDisplayLabel(), dri.getExtendedInfo(), alwaysShowSubLabel());
-            startDisplayResolveInfoIconLoading(holder, dri);
-        } else {
-            holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
-
-            if (info instanceof SelectableTargetInfo) {
-                SelectableTargetInfo selectableInfo = (SelectableTargetInfo) info;
-                // direct share targets should append the application name for a better readout
-                DisplayResolveInfo rInfo = selectableInfo.getDisplayResolveInfo();
-                CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
-                CharSequence extendedInfo = selectableInfo.getExtendedInfo();
-                String contentDescription = String.join(" ", selectableInfo.getDisplayLabel(),
-                        extendedInfo != null ? extendedInfo : "", appName);
-                holder.updateContentDescription(contentDescription);
-                startSelectableTargetInfoIconLoading(holder, selectableInfo);
-            } else {
-                holder.bindIcon(info);
+            if (!dri.hasDisplayIcon()) {
+                loadIcon(dri);
             }
         }
 
         // If target is loading, show a special placeholder shape in the label, make unclickable
-        if (info instanceof ChooserActivity.PlaceHolderTargetInfo) {
+        if (info.isPlaceHolderTargetInfo()) {
             final int maxWidth = mContext.getResources().getDimensionPixelSize(
                     R.dimen.chooser_direct_share_label_placeholder_max_width);
             holder.text.setMaxWidth(maxWidth);
@@ -306,7 +301,7 @@
         // Always remove the spacing listener, attach as needed to direct share targets below.
         holder.text.removeOnLayoutChangeListener(mPinTextSpacingListener);
 
-        if (info instanceof MultiDisplayResolveInfo) {
+        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);
@@ -325,64 +320,47 @@
         }
     }
 
-    private void startDisplayResolveInfoIconLoading(ViewHolder holder, DisplayResolveInfo info) {
-        LoadIconTask task = (LoadIconTask) mIconLoaders.get(info);
-        if (task == null) {
-            task = new LoadIconTask(info, holder);
-            mIconLoaders.put(info, task);
-            task.execute();
-        } else {
-            // The holder was potentially changed as the underlying items were
-            // reshuffled, so reset the target holder
-            task.setViewHolder(holder);
-        }
-    }
-
-    private void startSelectableTargetInfoIconLoading(
-            ViewHolder holder, SelectableTargetInfo info) {
+    private void loadDirectShareIcon(SelectableTargetInfo info) {
         LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info);
         if (task == null) {
-            task = mTestLoadDirectShareTaskProvider == null
-                    ? new LoadDirectShareIconTask(info)
-                    : mTestLoadDirectShareTaskProvider.get();
+            task = createLoadDirectShareIconTask(info);
             mIconLoaders.put(info, task);
             task.loadIcon();
         }
-        task.setViewHolder(holder);
+    }
+
+    @VisibleForTesting
+    protected LoadDirectShareIconTask createLoadDirectShareIconTask(SelectableTargetInfo info) {
+        return new LoadDirectShareIconTask(
+                mContext.createContextAsUser(getUserHandle(), 0),
+                info);
     }
 
     void updateAlphabeticalList() {
+        // TODO: this procedure seems like it should be relatively lightweight. Why does it need to
+        // run in an `AsyncTask`?
         new AsyncTask<Void, Void, List<DisplayResolveInfo>>() {
             @Override
             protected List<DisplayResolveInfo> doInBackground(Void... voids) {
                 List<DisplayResolveInfo> allTargets = new ArrayList<>();
-                allTargets.addAll(mDisplayList);
+                allTargets.addAll(getTargetsInCurrentDisplayList());
                 allTargets.addAll(mCallerTargets);
-                if (!mEnableStackedApps) {
-                    return allTargets;
-                }
+
                 // Consolidate multiple targets from same app.
-                Map<String, DisplayResolveInfo> consolidated = new HashMap<>();
-                for (DisplayResolveInfo info : allTargets) {
-                    String resolvedTarget = info.getResolvedComponentName().getPackageName()
-                            + '#' + info.getDisplayLabel();
-                    DisplayResolveInfo multiDri = consolidated.get(resolvedTarget);
-                    if (multiDri == null) {
-                        consolidated.put(resolvedTarget, info);
-                    } else if (multiDri instanceof MultiDisplayResolveInfo) {
-                        ((MultiDisplayResolveInfo) multiDri).addTarget(info);
-                    } else {
-                        // create consolidated target from the single DisplayResolveInfo
-                        MultiDisplayResolveInfo multiDisplayResolveInfo =
-                                new MultiDisplayResolveInfo(resolvedTarget, multiDri);
-                        multiDisplayResolveInfo.addTarget(info);
-                        consolidated.put(resolvedTarget, multiDisplayResolveInfo);
-                    }
-                }
-                List<DisplayResolveInfo> groupedTargets = new ArrayList<>();
-                groupedTargets.addAll(consolidated.values());
-                Collections.sort(groupedTargets, new ChooserActivity.AzInfoComparator(mContext));
-                return groupedTargets;
+                return allTargets
+                        .stream()
+                        .collect(Collectors.groupingBy(target ->
+                                target.getResolvedComponentName().getPackageName()
+                                + "#" + target.getDisplayLabel()
+                        ))
+                        .values()
+                        .stream()
+                        .map(appTargets ->
+                                (appTargets.size() == 1)
+                                ? appTargets.get(0)
+                                : MultiDisplayResolveInfo.newMultiDisplayResolveInfo(appTargets))
+                        .sorted(new ChooserActivity.AzInfoComparator(mContext))
+                        .collect(Collectors.toList());
             }
             @Override
             protected void onPostExecute(List<DisplayResolveInfo> newList) {
@@ -401,8 +379,9 @@
     @Override
     public int getUnfilteredCount() {
         int appTargets = super.getUnfilteredCount();
-        if (appTargets > mChooserListCommunicator.getMaxRankedTargets()) {
-            appTargets = appTargets + mChooserListCommunicator.getMaxRankedTargets();
+        if (appTargets > mMaxRankedTargets) {
+            // TODO: what does this condition mean?
+            appTargets = appTargets + mMaxRankedTargets;
         }
         return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount();
     }
@@ -417,8 +396,8 @@
      */
     public int getSelectableServiceTargetCount() {
         int count = 0;
-        for (ChooserTargetInfo info : mServiceTargets) {
-            if (info instanceof SelectableTargetInfo) {
+        for (TargetInfo info : mServiceTargets) {
+            if (info.isSelectableTargetInfo()) {
                 count++;
             }
         }
@@ -426,29 +405,37 @@
     }
 
     public int getServiceTargetCount() {
-        if (mChooserListCommunicator.isSendAction(mChooserListCommunicator.getTargetIntent())
-                && !ActivityManager.isLowRamDeviceStatic()) {
-            return Math.min(mServiceTargets.size(), mChooserListCommunicator.getMaxRankedTargets());
+        if (mChooserRequest.isSendActionTarget() && !ActivityManager.isLowRamDeviceStatic()) {
+            return Math.min(mServiceTargets.size(), mMaxRankedTargets);
         }
 
         return 0;
     }
 
-    int getAlphaTargetCount() {
+    public int getAlphaTargetCount() {
         int groupedCount = mSortedList.size();
-        int ungroupedCount = mCallerTargets.size() + mDisplayList.size();
-        return ungroupedCount > mChooserListCommunicator.getMaxRankedTargets() ? groupedCount : 0;
+        int ungroupedCount = mCallerTargets.size() + getDisplayResolveInfoCount();
+        return (ungroupedCount > mMaxRankedTargets) ? groupedCount : 0;
     }
 
     /**
      * Fetch ranked app target count
      */
     public int getRankedTargetCount() {
-        int spacesAvailable =
-                mChooserListCommunicator.getMaxRankedTargets() - getCallerTargetCount();
+        int spacesAvailable = mMaxRankedTargets - getCallerTargetCount();
         return Math.min(spacesAvailable, super.getCount());
     }
 
+    /** Get all the {@link DisplayResolveInfo} data for our targets. */
+    public DisplayResolveInfo[] getDisplayResolveInfos() {
+        int size = getDisplayResolveInfoCount();
+        DisplayResolveInfo[] resolvedTargets = new DisplayResolveInfo[size];
+        for (int i = 0; i < size; i++) {
+            resolvedTargets[i] = getDisplayResolveInfo(i);
+        }
+        return resolvedTargets;
+    }
+
     public int getPositionTargetType(int position) {
         int offset = 0;
 
@@ -483,7 +470,6 @@
         return targetInfoForPosition(position, true);
     }
 
-
     /**
      * Find target info for a given position.
      * Since ChooserActivity displays several sections of content, determine which
@@ -533,8 +519,8 @@
     protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {
         // Checks if this info is already listed in callerTargets.
         for (TargetInfo existingInfo : mCallerTargets) {
-            if (mResolverListCommunicator
-                    .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) {
+            if (mResolverListCommunicator.resolveInfoMatch(
+                    dri.getResolveInfo(), existingInfo.getResolveInfo())) {
                 return false;
             }
         }
@@ -544,10 +530,9 @@
     /**
      * Fetch surfaced direct share target info
      */
-    public List<ChooserTargetInfo> getSurfacedTargetInfo() {
-        int maxSurfacedTargets = mChooserListCommunicator.getMaxRankedTargets();
+    public List<TargetInfo> getSurfacedTargetInfo() {
         return mServiceTargets.subList(0,
-                Math.min(maxSurfacedTargets, getSelectableServiceTargetCount()));
+                Math.min(mMaxRankedTargets, getSelectableServiceTargetCount()));
     }
 
 
@@ -555,83 +540,36 @@
      * Evaluate targets for inclusion in the direct share area. May not be included
      * if score is too low.
      */
-    public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets,
+    public void addServiceResults(
+            @Nullable DisplayResolveInfo origTarget,
+            List<ChooserTarget> targets,
             @ChooserActivity.ShareTargetType int targetType,
-            Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos) {
-        if (DEBUG) {
-            Log.d(TAG, "addServiceResults " + origTarget.getResolvedComponentName() + ", "
-                    + targets.size()
-                    + " targets");
-        }
-        if (targets.size() == 0) {
+            Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos,
+            Map<ChooserTarget, AppTarget> directShareToAppTargets) {
+        // Avoid inserting any potentially late results.
+        if ((mServiceTargets.size() == 1) && mServiceTargets.get(0).isEmptyTargetInfo()) {
             return;
         }
-        final float baseScore = getBaseScore(origTarget, targetType);
-        Collections.sort(targets, mBaseTargetComparator);
-        final boolean isShortcutResult =
-                (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
-                        || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
-        final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp
-                : MAX_CHOOSER_TARGETS_PER_APP;
-        final int targetsLimit = mApplySharingAppLimits ? Math.min(targets.size(), maxTargets)
-                : targets.size();
-        float lastScore = 0;
-        boolean shouldNotify = false;
-        for (int i = 0, count = targetsLimit; i < count; i++) {
-            final ChooserTarget target = targets.get(i);
-            float targetScore = target.getScore();
-            if (mApplySharingAppLimits) {
-                targetScore *= baseScore;
-                if (i > 0 && targetScore >= lastScore) {
-                    // Apply a decay so that the top app can't crowd out everything else.
-                    // This incents ChooserTargetServices to define what's truly better.
-                    targetScore = lastScore * 0.95f;
-                }
-            }
-            ShortcutInfo shortcutInfo = isShortcutResult ? directShareToShortcutInfos.get(target)
-                    : null;
-            if ((shortcutInfo != null) && shortcutInfo.isPinned()) {
-                targetScore += PINNED_SHORTCUT_TARGET_SCORE_BOOST;
-            }
-            UserHandle userHandle = getUserHandle();
-            Context contextAsUser = mContext.createContextAsUser(userHandle, 0 /* flags */);
-            boolean isInserted = insertServiceTarget(new SelectableTargetInfo(contextAsUser,
-                    origTarget, target, targetScore, mSelectableTargetInfoCommunicator,
-                    shortcutInfo));
-
-            if (isInserted && isShortcutResult) {
-                mNumShortcutResults++;
-            }
-
-            shouldNotify |= isInserted;
-
-            if (DEBUG) {
-                Log.d(TAG, " => " + target.toString() + " score=" + targetScore
-                        + " base=" + target.getScore()
-                        + " lastScore=" + lastScore
-                        + " baseScore=" + baseScore
-                        + " applyAppLimit=" + mApplySharingAppLimits);
-            }
-
-            lastScore = targetScore;
-        }
-
-        if (shouldNotify) {
+        boolean isShortcutResult = targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
+                || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
+        boolean isUpdated = mShortcutSelectionLogic.addServiceResults(
+                origTarget,
+                getBaseScore(origTarget, targetType),
+                targets,
+                isShortcutResult,
+                directShareToShortcutInfos,
+                directShareToAppTargets,
+                mContext.createContextAsUser(getUserHandle(), 0),
+                mChooserRequest.getTargetIntent(),
+                mChooserRequest.getReferrerFillInIntent(),
+                mMaxRankedTargets,
+                mServiceTargets);
+        if (isUpdated) {
             notifyDataSetChanged();
         }
     }
 
     /**
-     * The return number have to exceed a minimum limit to make direct share area expandable. When
-     * append direct share targets is enabled, return count of all available targets parking in the
-     * memory; otherwise, it is shortcuts count which will help reduce the amount of visible
-     * shuffling due to older-style direct share targets.
-     */
-    int getNumServiceTargetsForExpand() {
-        return mNumShortcutResults;
-    }
-
-    /**
      * Use the scoring system along with artificial boosts to create up to 4 distinct buckets:
      * <ol>
      *   <li>App-supplied targets
@@ -659,54 +597,14 @@
      * update the direct share area.
      */
     public void completeServiceTargetLoading() {
-        mServiceTargets.removeIf(o -> o instanceof ChooserActivity.PlaceHolderTargetInfo);
+        mServiceTargets.removeIf(o -> o.isPlaceHolderTargetInfo());
         if (mServiceTargets.isEmpty()) {
-            mServiceTargets.add(new ChooserActivity.EmptyTargetInfo());
+            mServiceTargets.add(NotSelectableTargetInfo.newEmptyTargetInfo());
             mChooserActivityLogger.logSharesheetEmptyDirectShareRow();
         }
         notifyDataSetChanged();
     }
 
-    private boolean insertServiceTarget(ChooserTargetInfo chooserTargetInfo) {
-        // Avoid inserting any potentially late results
-        if (mServiceTargets.size() == 1
-                && mServiceTargets.get(0) instanceof ChooserActivity.EmptyTargetInfo) {
-            return false;
-        }
-
-        // Check for duplicates and abort if found
-        for (ChooserTargetInfo otherTargetInfo : mServiceTargets) {
-            if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
-                return false;
-            }
-        }
-
-        int currentSize = mServiceTargets.size();
-        final float newScore = chooserTargetInfo.getModifiedScore();
-        for (int i = 0; i < Math.min(currentSize, mChooserListCommunicator.getMaxRankedTargets());
-                i++) {
-            final ChooserTargetInfo serviceTarget = mServiceTargets.get(i);
-            if (serviceTarget == null) {
-                mServiceTargets.set(i, chooserTargetInfo);
-                return true;
-            } else if (newScore > serviceTarget.getModifiedScore()) {
-                mServiceTargets.add(i, chooserTargetInfo);
-                return true;
-            }
-        }
-
-        if (currentSize < mChooserListCommunicator.getMaxRankedTargets()) {
-            mServiceTargets.add(chooserTargetInfo);
-            return true;
-        }
-
-        return false;
-    }
-
-    public ChooserTarget getChooserTargetForValue(int value) {
-        return mServiceTargets.get(value).getChooserTarget();
-    }
-
     protected boolean alwaysShowSubLabel() {
         // Always show a subLabel for visual consistency across list items. Show an empty
         // subLabel if the subLabel is the same as the label
@@ -728,8 +626,7 @@
             protected List<ResolvedComponentInfo> doInBackground(
                     List<ResolvedComponentInfo>... params) {
                 Trace.beginSection("ChooserListAdapter#SortingTask");
-                mResolverListController.topK(params[0],
-                        mChooserListCommunicator.getMaxRankedTargets());
+                mResolverListController.topK(params[0], mMaxRankedTargets);
                 Trace.endSection();
                 return params[0];
             }
@@ -737,88 +634,95 @@
             protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
                 processSortedList(sortedComponents, doPostProcessing);
                 if (doPostProcessing) {
-                    mChooserListCommunicator.updateProfileViewButton();
+                    mResolverListCommunicator.updateProfileViewButton();
                     notifyDataSetChanged();
                 }
             }
         };
     }
 
-    public void setAppPredictor(AppPredictor appPredictor) {
-        mAppPredictor = appPredictor;
-    }
-
-    public void setAppPredictorCallback(AppPredictor.Callback appPredictorCallback) {
-        mAppPredictorCallback = appPredictorCallback;
-    }
-
-    public void destroyAppPredictor() {
-        if (getAppPredictor() != null) {
-            getAppPredictor().unregisterPredictionUpdates(mAppPredictorCallback);
-            getAppPredictor().destroy();
-            setAppPredictor(null);
-        }
-    }
-
-    /**
-     * An alias for onBindView to use with unit tests.
-     */
-    @VisibleForTesting
-    public void testViewBind(View view, TargetInfo info, int position) {
-        onBindView(view, info, position);
-    }
-
-    @VisibleForTesting
-    public void setTestLoadDirectShareTaskProvider(LoadDirectShareIconTaskProvider provider) {
-        mTestLoadDirectShareTaskProvider = provider;
-    }
-
-    /**
-     * Necessary methods to communicate between {@link ChooserListAdapter}
-     * and {@link ChooserActivity}.
-     */
-    @VisibleForTesting
-    public interface ChooserListCommunicator extends ResolverListCommunicator {
-
-        int getMaxRankedTargets();
-
-        void sendListViewUpdateMessage(UserHandle userHandle);
-
-        boolean isSendAction(Intent targetIntent);
-    }
-
     /**
      * Loads direct share targets icons.
      */
     @VisibleForTesting
-    public class LoadDirectShareIconTask extends AsyncTask<Void, Void, Void> {
+    public class LoadDirectShareIconTask extends AsyncTask<Void, Void, Drawable> {
+        private final Context mContext;
         private final SelectableTargetInfo mTargetInfo;
-        private ViewHolder mViewHolder;
 
-        private LoadDirectShareIconTask(SelectableTargetInfo targetInfo) {
+        private LoadDirectShareIconTask(Context context, SelectableTargetInfo targetInfo) {
+            mContext = context;
             mTargetInfo = targetInfo;
         }
 
         @Override
-        protected Void doInBackground(Void... voids) {
-            mTargetInfo.loadIcon();
-            return null;
+        protected Drawable doInBackground(Void... voids) {
+            Drawable drawable;
+            try {
+                drawable = getChooserTargetIconDrawable(
+                        mContext,
+                        mTargetInfo.getChooserTargetIcon(),
+                        mTargetInfo.getChooserTargetComponentName(),
+                        mTargetInfo.getDirectShareShortcutInfo());
+            } catch (Exception e) {
+                Log.e(TAG,
+                        "Failed to load shortcut icon for "
+                                + mTargetInfo.getChooserTargetComponentName(),
+                        e);
+                drawable = loadIconPlaceholder();
+            }
+            return drawable;
         }
 
         @Override
-        protected void onPostExecute(Void arg) {
-            if (mViewHolder != null) {
-                mViewHolder.bindIcon(mTargetInfo);
+        protected void onPostExecute(@Nullable Drawable icon) {
+            if (icon != null && !mTargetInfo.hasDisplayIcon()) {
+                mTargetInfo.getDisplayIconHolder().setDisplayIcon(icon);
                 notifyDataSetChanged();
             }
         }
 
-        /**
-         * Specifies a view holder that will be updated when the task is completed.
-         */
-        public void setViewHolder(ViewHolder viewHolder) {
-            mViewHolder = viewHolder;
-            mViewHolder.bindIcon(mTargetInfo);
+        @WorkerThread
+        private Drawable getChooserTargetIconDrawable(
+                Context context,
+                @Nullable Icon icon,
+                ComponentName targetComponentName,
+                @Nullable ShortcutInfo shortcutInfo) {
+            Drawable directShareIcon = null;
+
+            // First get the target drawable and associated activity info
+            if (icon != null) {
+                directShareIcon = icon.loadDrawable(context);
+            } else if (shortcutInfo != null) {
+                LauncherApps launcherApps = context.getSystemService(LauncherApps.class);
+                if (launcherApps != null) {
+                    directShareIcon = launcherApps.getShortcutIconDrawable(shortcutInfo, 0);
+                }
+            }
+
+            if (directShareIcon == null) {
+                return null;
+            }
+
+            ActivityInfo info = null;
+            try {
+                info = context.getPackageManager().getActivityInfo(targetComponentName, 0);
+            } catch (PackageManager.NameNotFoundException error) {
+                Log.e(TAG, "Could not find activity associated with ChooserTarget");
+            }
+
+            if (info == null) {
+                return null;
+            }
+
+            // Now fetch app icon and raster with no badging even in work profile
+            Bitmap appIcon = mPresentationFactory.makePresentationGetter(info).getIconBitmap(null);
+
+            // Raster target drawable with appIcon as a badge
+            SimpleIconFactory sif = SimpleIconFactory.obtain(context);
+            Bitmap directShareBadgedIcon = sif.createAppBadgedIconBitmap(directShareIcon, appIcon);
+            sif.recycle();
+
+            return new BitmapDrawable(context.getResources(), directShareBadgedIcon);
         }
 
         /**
@@ -828,16 +732,4 @@
             execute();
         }
     }
-
-    /**
-     * An interface for the unit tests to override icon loading task creation
-     */
-    @VisibleForTesting
-    public interface LoadDirectShareIconTaskProvider {
-        /**
-         * Provides an instance of the task.
-         * @return
-         */
-        LoadDirectShareIconTask get();
-    }
 }
diff --git a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
index da78fc8..39d1fab 100644
--- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
@@ -16,306 +16,159 @@
 
 package com.android.intentresolver;
 
-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.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_PERSONAL_APPS;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE;
-
-import android.annotation.Nullable;
-import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.os.UserHandle;
 import android.view.LayoutInflater;
-import android.view.View;
 import android.view.ViewGroup;
 
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.viewpager.widget.PagerAdapter;
+
+import com.android.intentresolver.grid.ChooserGridAdapter;
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.widget.GridLayoutManager;
-import com.android.internal.widget.PagerAdapter;
-import com.android.internal.widget.RecyclerView;
+
+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 AbstractMultiProfilePagerAdapter {
+public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAdapter<
+        RecyclerView, ChooserGridAdapter, ChooserListAdapter> {
     private static final int SINGLE_CELL_SPAN_SIZE = 1;
 
-    private final ChooserProfileDescriptor[] mItems;
-    private final boolean mIsSendAction;
-    private int mBottomOffset;
-    private int mMaxTargetsPerRow;
+    private final ChooserProfileAdapterBinder mAdapterBinder;
+    private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier;
 
-    ChooserMultiProfilePagerAdapter(Context context,
-            ChooserActivity.ChooserGridAdapter adapter,
-            UserHandle personalProfileUserHandle,
+    ChooserMultiProfilePagerAdapter(
+            Context context,
+            ChooserGridAdapter adapter,
+            EmptyStateProvider emptyStateProvider,
+            QuietModeManager quietModeManager,
             UserHandle workProfileUserHandle,
-            boolean isSendAction, int maxTargetsPerRow) {
-        super(context, /* currentPage */ 0, personalProfileUserHandle, workProfileUserHandle);
-        mItems = new ChooserProfileDescriptor[] {
-                createProfileDescriptor(adapter)
-        };
-        mIsSendAction = isSendAction;
-        mMaxTargetsPerRow = maxTargetsPerRow;
+            int maxTargetsPerRow) {
+        this(
+                context,
+                new ChooserProfileAdapterBinder(maxTargetsPerRow),
+                ImmutableList.of(adapter),
+                emptyStateProvider,
+                quietModeManager,
+                /* defaultProfile= */ 0,
+                workProfileUserHandle,
+                new BottomPaddingOverrideSupplier(context));
     }
 
-    ChooserMultiProfilePagerAdapter(Context context,
-            ChooserActivity.ChooserGridAdapter personalAdapter,
-            ChooserActivity.ChooserGridAdapter workAdapter,
+    ChooserMultiProfilePagerAdapter(
+            Context context,
+            ChooserGridAdapter personalAdapter,
+            ChooserGridAdapter workAdapter,
+            EmptyStateProvider emptyStateProvider,
+            QuietModeManager quietModeManager,
             @Profile int defaultProfile,
-            UserHandle personalProfileUserHandle,
             UserHandle workProfileUserHandle,
-            boolean isSendAction, int maxTargetsPerRow) {
-        super(context, /* currentPage */ defaultProfile, personalProfileUserHandle,
-                workProfileUserHandle);
-        mItems = new ChooserProfileDescriptor[] {
-                createProfileDescriptor(personalAdapter),
-                createProfileDescriptor(workAdapter)
-        };
-        mIsSendAction = isSendAction;
-        mMaxTargetsPerRow = maxTargetsPerRow;
+            int maxTargetsPerRow) {
+        this(
+                context,
+                new ChooserProfileAdapterBinder(maxTargetsPerRow),
+                ImmutableList.of(personalAdapter, workAdapter),
+                emptyStateProvider,
+                quietModeManager,
+                defaultProfile,
+                workProfileUserHandle,
+                new BottomPaddingOverrideSupplier(context));
     }
 
-    private ChooserProfileDescriptor createProfileDescriptor(
-            ChooserActivity.ChooserGridAdapter adapter) {
-        final LayoutInflater inflater = LayoutInflater.from(getContext());
-        final ViewGroup rootView =
-                (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile, null, false);
-        ChooserProfileDescriptor profileDescriptor =
-                new ChooserProfileDescriptor(rootView, adapter);
-        profileDescriptor.recyclerView.setAccessibilityDelegateCompat(
-                new ChooserRecyclerViewAccessibilityDelegate(profileDescriptor.recyclerView));
-        return profileDescriptor;
+    private ChooserMultiProfilePagerAdapter(
+            Context context,
+            ChooserProfileAdapterBinder adapterBinder,
+            ImmutableList<ChooserGridAdapter> gridAdapters,
+            EmptyStateProvider emptyStateProvider,
+            QuietModeManager quietModeManager,
+            @Profile int defaultProfile,
+            UserHandle workProfileUserHandle,
+            BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {
+        super(
+                context,
+                        gridAdapter -> gridAdapter.getListAdapter(),
+                adapterBinder,
+                gridAdapters,
+                emptyStateProvider,
+                quietModeManager,
+                defaultProfile,
+                workProfileUserHandle,
+                        () -> makeProfileView(context),
+                bottomPaddingOverrideSupplier);
+        mAdapterBinder = adapterBinder;
+        mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier;
     }
 
     public void setMaxTargetsPerRow(int maxTargetsPerRow) {
-        mMaxTargetsPerRow = maxTargetsPerRow;
+        mAdapterBinder.setMaxTargetsPerRow(maxTargetsPerRow);
     }
 
-    RecyclerView getListViewForIndex(int index) {
-        return getItem(index).recyclerView;
+    public void setEmptyStateBottomOffset(int bottomOffset) {
+        mBottomPaddingOverrideSupplier.setEmptyStateBottomOffset(bottomOffset);
     }
 
-    @Override
-    ChooserProfileDescriptor getItem(int pageIndex) {
-        return mItems[pageIndex];
+    private static ViewGroup makeProfileView(Context context) {
+        LayoutInflater inflater = LayoutInflater.from(context);
+        ViewGroup rootView = (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
-    int getItemCount() {
-        return mItems.length;
-    }
+    private static class BottomPaddingOverrideSupplier implements Supplier<Optional<Integer>> {
+        private final Context mContext;
+        private int mBottomOffset;
 
-    @Override
-    @VisibleForTesting
-    public ChooserActivity.ChooserGridAdapter getAdapterForIndex(int pageIndex) {
-        return mItems[pageIndex].chooserGridAdapter;
-    }
-
-    @Override
-    @Nullable
-    ChooserListAdapter getListAdapterForUserHandle(UserHandle userHandle) {
-        if (getActiveListAdapter().getUserHandle().equals(userHandle)) {
-            return getActiveListAdapter();
-        } else if (getInactiveListAdapter() != null
-                && getInactiveListAdapter().getUserHandle().equals(userHandle)) {
-            return getInactiveListAdapter();
+        BottomPaddingOverrideSupplier(Context context) {
+            mContext = context;
         }
-        return null;
-    }
 
-    @Override
-    void setupListAdapter(int pageIndex) {
-        final RecyclerView recyclerView = getItem(pageIndex).recyclerView;
-        ChooserActivity.ChooserGridAdapter chooserGridAdapter =
-                getItem(pageIndex).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();
-                    }
-                });
-    }
-
-    @Override
-    @VisibleForTesting
-    public ChooserListAdapter getActiveListAdapter() {
-        return getAdapterForIndex(getCurrentPage()).getListAdapter();
-    }
-
-    @Override
-    @VisibleForTesting
-    public ChooserListAdapter getInactiveListAdapter() {
-        if (getCount() == 1) {
-            return null;
+        public void setEmptyStateBottomOffset(int bottomOffset) {
+            mBottomOffset = bottomOffset;
         }
-        return getAdapterForIndex(1 - getCurrentPage()).getListAdapter();
-    }
 
-    @Override
-    public ResolverListAdapter getPersonalListAdapter() {
-        return getAdapterForIndex(PROFILE_PERSONAL).getListAdapter();
-    }
-
-    @Override
-    @Nullable
-    public ResolverListAdapter getWorkListAdapter() {
-        return getAdapterForIndex(PROFILE_WORK).getListAdapter();
-    }
-
-    @Override
-    ChooserActivity.ChooserGridAdapter getCurrentRootAdapter() {
-        return getAdapterForIndex(getCurrentPage());
-    }
-
-    @Override
-    RecyclerView getActiveAdapterView() {
-        return getListViewForIndex(getCurrentPage());
-    }
-
-    @Override
-    @Nullable
-    RecyclerView getInactiveAdapterView() {
-        if (getCount() == 1) {
-            return null;
-        }
-        return getListViewForIndex(1 - getCurrentPage());
-    }
-
-    @Override
-    String getMetricsCategory() {
-        return ResolverActivity.METRICS_CATEGORY_CHOOSER;
-    }
-
-    @Override
-    protected void showWorkProfileOffEmptyState(ResolverListAdapter activeListAdapter,
-            View.OnClickListener listener) {
-        showEmptyState(activeListAdapter,
-                getWorkAppPausedTitle(),
-                /* subtitle = */ null,
-                listener);
-    }
-
-    @Override
-    protected void showNoPersonalToWorkIntentsEmptyState(ResolverListAdapter activeListAdapter) {
-        if (mIsSendAction) {
-            showEmptyState(activeListAdapter,
-                    getCrossProfileBlockedTitle(),
-                    getCantShareWithWorkMessage());
-        } else {
-            showEmptyState(activeListAdapter,
-                    getCrossProfileBlockedTitle(),
-                    getCantAccessWorkMessage());
+        public Optional<Integer> get() {
+            int initialBottomPadding = mContext.getResources().getDimensionPixelSize(
+                    R.dimen.resolver_empty_state_container_padding_bottom);
+            return Optional.of(initialBottomPadding + mBottomOffset);
         }
     }
 
-    @Override
-    protected void showNoWorkToPersonalIntentsEmptyState(ResolverListAdapter activeListAdapter) {
-        if (mIsSendAction) {
-            showEmptyState(activeListAdapter,
-                    getCrossProfileBlockedTitle(),
-                    getCantShareWithPersonalMessage());
-        } else {
-            showEmptyState(activeListAdapter,
-                    getCrossProfileBlockedTitle(),
-                    getCantAccessPersonalMessage());
+    private static class ChooserProfileAdapterBinder implements
+            AdapterBinder<RecyclerView, ChooserGridAdapter> {
+        private int mMaxTargetsPerRow;
+
+        ChooserProfileAdapterBinder(int maxTargetsPerRow) {
+            mMaxTargetsPerRow = maxTargetsPerRow;
         }
-    }
 
-    @Override
-    protected void showNoPersonalAppsAvailableEmptyState(ResolverListAdapter listAdapter) {
-        showEmptyState(listAdapter, getNoPersonalAppsAvailableMessage(), /* subtitle= */ null);
+        public void setMaxTargetsPerRow(int maxTargetsPerRow) {
+            mMaxTargetsPerRow = maxTargetsPerRow;
+        }
 
-    }
-
-    @Override
-    protected void showNoWorkAppsAvailableEmptyState(ResolverListAdapter listAdapter) {
-        showEmptyState(listAdapter, getNoWorkAppsAvailableMessage(), /* subtitle = */ null);
-    }
-
-    private String getWorkAppPausedTitle() {
-        return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
-                RESOLVER_WORK_PAUSED_TITLE,
-                () -> getContext().getString(R.string.resolver_turn_on_work_apps));
-    }
-
-    private String getCrossProfileBlockedTitle() {
-        return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
-                RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
-                () -> getContext().getString(R.string.resolver_cross_profile_blocked));
-    }
-
-    private String getCantShareWithWorkMessage() {
-        return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
-                RESOLVER_CANT_SHARE_WITH_WORK,
-                () -> getContext().getString(
-                        R.string.resolver_cant_share_with_work_apps_explanation));
-    }
-
-    private String getCantShareWithPersonalMessage() {
-        return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
-                RESOLVER_CANT_SHARE_WITH_PERSONAL,
-                () -> getContext().getString(
-                        R.string.resolver_cant_share_with_personal_apps_explanation));
-    }
-
-    private String getCantAccessWorkMessage() {
-        return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
-                RESOLVER_CANT_ACCESS_WORK,
-                () -> getContext().getString(
-                        R.string.resolver_cant_access_work_apps_explanation));
-    }
-
-    private String getCantAccessPersonalMessage() {
-        return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
-                RESOLVER_CANT_ACCESS_PERSONAL,
-                () -> getContext().getString(
-                        R.string.resolver_cant_access_personal_apps_explanation));
-    }
-
-    private String getNoWorkAppsAvailableMessage() {
-        return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
-                RESOLVER_NO_WORK_APPS,
-                () -> getContext().getString(
-                        R.string.resolver_no_work_apps_available));
-    }
-
-    private String getNoPersonalAppsAvailableMessage() {
-        return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
-                RESOLVER_NO_PERSONAL_APPS,
-                () -> getContext().getString(
-                        R.string.resolver_no_personal_apps_available));
-    }
-
-
-    void setEmptyStateBottomOffset(int bottomOffset) {
-        mBottomOffset = bottomOffset;
-    }
-
-    @Override
-    protected void setupContainerPadding(View container) {
-        int initialBottomPadding = getContext().getResources().getDimensionPixelSize(
-                R.dimen.resolver_empty_state_container_padding_bottom);
-        container.setPadding(container.getPaddingLeft(), container.getPaddingTop(),
-                container.getPaddingRight(), initialBottomPadding + mBottomOffset);
-    }
-
-    class ChooserProfileDescriptor extends ProfileDescriptor {
-        private ChooserActivity.ChooserGridAdapter chooserGridAdapter;
-        private RecyclerView recyclerView;
-        ChooserProfileDescriptor(ViewGroup rootView, ChooserActivity.ChooserGridAdapter adapter) {
-            super(rootView);
-            chooserGridAdapter = adapter;
-            recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list);
+        @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/ChooserRecyclerViewAccessibilityDelegate.java b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
index 67571b4..250b682 100644
--- a/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
+++ b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
@@ -22,8 +22,8 @@
 import android.view.ViewGroup;
 import android.view.accessibility.AccessibilityEvent;
 
-import com.android.internal.widget.RecyclerView;
-import com.android.internal.widget.RecyclerViewAccessibilityDelegate;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.RecyclerViewAccessibilityDelegate;
 
 class ChooserRecyclerViewAccessibilityDelegate extends RecyclerViewAccessibilityDelegate {
     private final Rect mTempRect = new Rect();
diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java
new file mode 100644
index 0000000..81481bf
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java
@@ -0,0 +1,441 @@
+/*
+ * 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;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.IntentSender;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.os.PatternMatcher;
+import android.service.chooser.ChooserTarget;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import com.google.common.collect.ImmutableList;
+
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.stream.Collector;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * Utility to parse and validate parameters from the client-supplied {@link Intent} that launched
+ * the Sharesheet {@link ChooserActivity}. The validated parameters are stored as immutable ivars.
+ *
+ * TODO: field nullability in this class reflects legacy use, and typically would indicate that the
+ * client's intent didn't provide the respective data. In some cases we may be able to provide
+ * defaults instead of nulls -- especially for methods that return nullable lists or arrays, if the
+ * client code could instead handle empty collections equally well.
+ *
+ * TODO: some of these fields (especially getTargetIntent() and any other getters that delegate to
+ * it internally) differ from the legacy model because they're computed directly from the initial
+ * Chooser intent, where in the past they've been relayed up to ResolverActivity and then retrieved
+ * through methods on the base class. The base always seems to return them exactly as they were
+ * provided, so this should be safe -- and clients can reasonably switch to retrieving through these
+ * parameters instead. For now, the other convention is still used in some places. Ideally we'd like
+ * to normalize on a single source of truth, but we'll have to clean up the delegation up to the
+ * resolver (or perhaps this needs to be a subclass of some `ResolverRequestParameters` class?).
+ */
+public class ChooserRequestParameters {
+    private static final String TAG = "ChooserActivity";
+
+    private static final int LAUNCH_FLAGS_FOR_SEND_ACTION =
+            Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
+
+    private final Intent mTarget;
+    private final Pair<CharSequence, Integer> mTitleSpec;
+    private final Intent mReferrerFillInIntent;
+    private final ImmutableList<ComponentName> mFilteredComponentNames;
+    private final ImmutableList<ChooserTarget> mCallerChooserTargets;
+    private final boolean mRetainInOnStop;
+
+    @Nullable
+    private final ImmutableList<Intent> mAdditionalTargets;
+
+    @Nullable
+    private final Bundle mReplacementExtras;
+
+    @Nullable
+    private final ImmutableList<Intent> mInitialIntents;
+
+    @Nullable
+    private final IntentSender mChosenComponentSender;
+
+    @Nullable
+    private final IntentSender mRefinementIntentSender;
+
+    @Nullable
+    private final String mSharedText;
+
+    @Nullable
+    private final IntentFilter mTargetIntentFilter;
+
+    public ChooserRequestParameters(
+            final Intent clientIntent,
+            final Uri referrer,
+            @Nullable final ComponentName nearbySharingComponent) {
+        final Intent requestedTarget = parseTargetIntentExtra(
+                clientIntent.getParcelableExtra(Intent.EXTRA_INTENT));
+        mTarget = intentWithModifiedLaunchFlags(requestedTarget);
+
+        mAdditionalTargets = intentsWithModifiedLaunchFlagsFromExtraIfPresent(
+                clientIntent, Intent.EXTRA_ALTERNATE_INTENTS);
+
+        mReplacementExtras = clientIntent.getBundleExtra(Intent.EXTRA_REPLACEMENT_EXTRAS);
+
+        mTitleSpec = makeTitleSpec(
+                clientIntent.getCharSequenceExtra(Intent.EXTRA_TITLE),
+                isSendAction(mTarget.getAction()));
+
+        mInitialIntents = intentsWithModifiedLaunchFlagsFromExtraIfPresent(
+                clientIntent, Intent.EXTRA_INITIAL_INTENTS);
+
+        mReferrerFillInIntent = new Intent().putExtra(Intent.EXTRA_REFERRER, referrer);
+
+        mChosenComponentSender = clientIntent.getParcelableExtra(
+                Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER);
+        mRefinementIntentSender = clientIntent.getParcelableExtra(
+                Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER);
+
+        mFilteredComponentNames = getFilteredComponentNames(clientIntent, nearbySharingComponent);
+
+        mCallerChooserTargets = parseCallerTargetsFromClientIntent(clientIntent);
+
+        mRetainInOnStop = clientIntent.getBooleanExtra(
+                ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP, false);
+
+        mSharedText = mTarget.getStringExtra(Intent.EXTRA_TEXT);
+
+        mTargetIntentFilter = getTargetIntentFilter(mTarget);
+    }
+
+    public Intent getTargetIntent() {
+        return mTarget;
+    }
+
+    @Nullable
+    public String getTargetAction() {
+        return getTargetIntent().getAction();
+    }
+
+    public boolean isSendActionTarget() {
+        return isSendAction(getTargetAction());
+    }
+
+    @Nullable
+    public String getTargetType() {
+        return getTargetIntent().getType();
+    }
+
+    @Nullable
+    public CharSequence getTitle() {
+        return mTitleSpec.first;
+    }
+
+    public int getDefaultTitleResource() {
+        return mTitleSpec.second;
+    }
+
+    public Intent getReferrerFillInIntent() {
+        return mReferrerFillInIntent;
+    }
+
+    public ImmutableList<ComponentName> getFilteredComponentNames() {
+        return mFilteredComponentNames;
+    }
+
+    public ImmutableList<ChooserTarget> getCallerChooserTargets() {
+        return mCallerChooserTargets;
+    }
+
+    /**
+     * Whether the {@link ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP} behavior was requested.
+     */
+    public boolean shouldRetainInOnStop() {
+        return mRetainInOnStop;
+    }
+
+    /**
+     * TODO: this returns a nullable array for convenience, but if the legacy APIs can be
+     * refactored, returning {@link mAdditionalTargets} directly is simpler and safer.
+     */
+    @Nullable
+    public Intent[] getAdditionalTargets() {
+        return (mAdditionalTargets == null) ? null : mAdditionalTargets.toArray(new Intent[0]);
+    }
+
+    @Nullable
+    public Bundle getReplacementExtras() {
+        return mReplacementExtras;
+    }
+
+    /**
+     * TODO: this returns a nullable array for convenience, but if the legacy APIs can be
+     * refactored, returning {@link mInitialIntents} directly is simpler and safer.
+     */
+    @Nullable
+    public Intent[] getInitialIntents() {
+        return (mInitialIntents == null) ? null : mInitialIntents.toArray(new Intent[0]);
+    }
+
+    @Nullable
+    public IntentSender getChosenComponentSender() {
+        return mChosenComponentSender;
+    }
+
+    @Nullable
+    public IntentSender getRefinementIntentSender() {
+        return mRefinementIntentSender;
+    }
+
+    @Nullable
+    public String getSharedText() {
+        return mSharedText;
+    }
+
+    @Nullable
+    public IntentFilter getTargetIntentFilter() {
+        return mTargetIntentFilter;
+    }
+
+    private static boolean isSendAction(@Nullable String action) {
+        return (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action));
+    }
+
+    private static Intent parseTargetIntentExtra(@Nullable Parcelable targetParcelable) {
+        if (targetParcelable instanceof Uri) {
+            try {
+                targetParcelable = Intent.parseUri(targetParcelable.toString(),
+                        Intent.URI_INTENT_SCHEME);
+            } catch (URISyntaxException ex) {
+                throw new IllegalArgumentException("Failed to parse EXTRA_INTENT from URI", ex);
+            }
+        }
+
+        if (!(targetParcelable instanceof Intent)) {
+            throw new IllegalArgumentException(
+                    "EXTRA_INTENT is neither an Intent nor a Uri: " + targetParcelable);
+        }
+
+        return ((Intent) targetParcelable);
+    }
+
+    private static Intent intentWithModifiedLaunchFlags(Intent intent) {
+        if (isSendAction(intent.getAction())) {
+            intent.addFlags(LAUNCH_FLAGS_FOR_SEND_ACTION);
+        }
+        return intent;
+    }
+
+    /**
+     * Build a pair of values specifying the title to use from the client request. The first
+     * ({@link CharSequence}) value is the client-specified title, if there was one and their
+     * 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
+     * create a real type (not {@link Pair}) to express the semantics described in this comment.
+     */
+    private static Pair<CharSequence, Integer> makeTitleSpec(
+            @Nullable CharSequence requestedTitle, boolean hasSendActionTarget) {
+        if (hasSendActionTarget && (requestedTitle != null)) {
+            // Do not allow the title to be changed when sharing content
+            Log.w(TAG, "Ignoring intent's EXTRA_TITLE, deprecated in P. You may wish to set a"
+                    + " preview title by using EXTRA_TITLE property of the wrapped"
+                    + " EXTRA_INTENT.");
+            requestedTitle = null;
+        }
+
+        int defaultTitleRes =
+                (requestedTitle == null) ? com.android.internal.R.string.chooseActivity : 0;
+
+        return Pair.create(requestedTitle, defaultTitleRes);
+    }
+
+    private static ImmutableList<ComponentName> getFilteredComponentNames(
+            Intent clientIntent, @Nullable ComponentName nearbySharingComponent) {
+        Stream<ComponentName> filteredComponents = streamParcelableArrayExtra(
+                clientIntent, Intent.EXTRA_EXCLUDE_COMPONENTS, ComponentName.class, true, true);
+
+        if (nearbySharingComponent != null) {
+            // Exclude Nearby from main list if chip is present, to avoid duplication.
+            // TODO: we don't have an explicit guarantee that the chip will be displayed just
+            // because we have a non-null component; that's ultimately determined by the preview
+            // layout. Maybe we can make that decision further upstream?
+            filteredComponents = Stream.concat(
+                    filteredComponents, Stream.of(nearbySharingComponent));
+        }
+
+        return filteredComponents.collect(toImmutableList());
+    }
+
+    private static ImmutableList<ChooserTarget> parseCallerTargetsFromClientIntent(
+            Intent clientIntent) {
+        return
+                streamParcelableArrayExtra(
+                        clientIntent, Intent.EXTRA_CHOOSER_TARGETS, ChooserTarget.class, true, true)
+                .collect(toImmutableList());
+    }
+
+    private static <T> Collector<T, ?, ImmutableList<T>> toImmutableList() {
+        return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf);
+    }
+
+    @Nullable
+    private static ImmutableList<Intent> intentsWithModifiedLaunchFlagsFromExtraIfPresent(
+            Intent clientIntent, String extra) {
+        Stream<Intent> intents =
+                streamParcelableArrayExtra(clientIntent, extra, Intent.class, true, false);
+        if (intents == null) {
+            return null;
+        }
+        return intents
+                .map(ChooserRequestParameters::intentWithModifiedLaunchFlags)
+                .collect(toImmutableList());
+    }
+
+    /**
+     * Make a {@link Stream} of the {@link Parcelable} objects given in the provided {@link Intent}
+     * as the optional parcelable array extra with key {@code extra}. The stream elements, if any,
+     * are all of the type specified by {@code clazz}.
+     *
+     * @param intent The intent that may contain the optional extras.
+     * @param extra The extras key to identify the parcelable array.
+     * @param clazz A class that is assignable from any elements in the result stream.
+     * @param warnOnTypeError Whether to log a warning (and ignore) if the client extra doesn't have
+     * 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).
+     * 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.
+     */
+    @Nullable
+    private static <T extends Parcelable> Stream<T> streamParcelableArrayExtra(
+            final Intent intent,
+            String extra,
+            @NonNull Class<T> clazz,
+            boolean warnOnTypeError,
+            boolean streamEmptyIfNull) {
+        T[] result = null;
+
+        try {
+            result = getParcelableArrayExtraIfPresent(intent, extra, clazz);
+        } catch (IllegalArgumentException e) {
+            if (warnOnTypeError) {
+                Log.w(TAG, "Ignoring client-requested " + extra, e);
+            } else {
+                throw e;
+            }
+        }
+
+        if (result != null) {
+            return Arrays.stream(result);
+        } else if (streamEmptyIfNull) {
+            return Stream.empty();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * If the specified {@code extra} is provided in the {@code intent}, cast it to type {@code T[]}
+     * or throw an {@code IllegalArgumentException} if the cast fails. If the {@code extra} isn't
+     * present in the {@code intent}, return null.
+     */
+    @Nullable
+    private static <T extends Parcelable> T[] getParcelableArrayExtraIfPresent(
+            final Intent intent, String extra, @NonNull Class<T> clazz) throws
+                    IllegalArgumentException {
+        if (!intent.hasExtra(extra)) {
+            return null;
+        }
+
+        T[] castResult = intent.getParcelableArrayExtra(extra, clazz);
+        if (castResult == null) {
+            Parcelable[] actualExtrasArray = intent.getParcelableArrayExtra(extra);
+            if (actualExtrasArray != null) {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "%s is not of type %s[]: %s",
+                                extra,
+                                clazz.getSimpleName(),
+                                Arrays.toString(actualExtrasArray)));
+            } else if (intent.getParcelableExtra(extra) != null) {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "%s is not of type %s[] (or any array type): %s",
+                                extra,
+                                clazz.getSimpleName(),
+                                intent.getParcelableExtra(extra)));
+            } else {
+                throw new IllegalArgumentException(
+                        String.format(
+                                "%s is not of type %s (or any Parcelable type): %s",
+                                extra,
+                                clazz.getSimpleName(),
+                                intent.getExtras().get(extra)));
+            }
+        }
+
+        return castResult;
+    }
+
+    private static IntentFilter getTargetIntentFilter(final Intent intent) {
+        try {
+            String dataString = intent.getDataString();
+            if (intent.getType() == null) {
+                if (!TextUtils.isEmpty(dataString)) {
+                    return new IntentFilter(intent.getAction(), dataString);
+                }
+                Log.e(TAG, "Failed to get target intent filter: intent data and type are null");
+                return null;
+            }
+            IntentFilter intentFilter = new IntentFilter(intent.getAction(), intent.getType());
+            List<Uri> contentUris = new ArrayList<>();
+            if (Intent.ACTION_SEND.equals(intent.getAction())) {
+                Uri uri = (Uri) intent.getParcelableExtra(Intent.EXTRA_STREAM);
+                if (uri != null) {
+                    contentUris.add(uri);
+                }
+            } else {
+                List<Uri> uris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+                if (uris != null) {
+                    contentUris.addAll(uris);
+                }
+            }
+            for (Uri uri : contentUris) {
+                intentFilter.addDataScheme(uri.getScheme());
+                intentFilter.addDataAuthority(uri.getAuthority(), null);
+                intentFilter.addDataPath(uri.getPath(), PatternMatcher.PATTERN_LITERAL);
+            }
+            return intentFilter;
+        } catch (Exception e) {
+            Log.e(TAG, "Failed to get target intent filter", e);
+            return null;
+        }
+    }
+}
diff --git a/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
index ae08ace..2cfceea 100644
--- a/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
+++ b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
@@ -20,9 +20,10 @@
 import android.content.DialogInterface;
 import android.content.pm.PackageManager;
 import android.graphics.drawable.Drawable;
-import android.os.Bundle;
 import android.os.UserHandle;
 
+import androidx.fragment.app.FragmentManager;
+
 import com.android.intentresolver.chooser.DisplayResolveInfo;
 import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
 
@@ -30,29 +31,39 @@
  * Shows individual actions for a "stacked" app target - such as an app with multiple posting
  * streams represented in the Sharesheet.
  */
-public class ChooserStackedAppDialogFragment extends ChooserTargetActionsDialogFragment
-        implements DialogInterface.OnClickListener {
+public class ChooserStackedAppDialogFragment extends ChooserTargetActionsDialogFragment {
 
-    static final String WHICH_KEY = "which_key";
-    static final String MULTI_DRI_KEY = "multi_dri_key";
-
-    private MultiDisplayResolveInfo mMultiDisplayResolveInfo;
-    private int mParentWhich;
-
-    public ChooserStackedAppDialogFragment() {}
-
-    void setStateFromBundle(Bundle b) {
-        mMultiDisplayResolveInfo = (MultiDisplayResolveInfo) b.get(MULTI_DRI_KEY);
-        mTargetInfos = mMultiDisplayResolveInfo.getTargets();
-        mUserHandle = (UserHandle) b.get(USER_HANDLE_KEY);
-        mParentWhich = b.getInt(WHICH_KEY);
+    /**
+     * Display a fragment for the user to select one of the members of a target "stack."
+     * @param stackedTarget The display info for the full stack to select within.
+     * @param stackedTargetParentWhich The "which" value that the {@link ChooserActivity} uses to
+     * identify the {@code stackedTarget} as presented in the chooser menu UI. If the user selects
+     * a target in this fragment, the selection will be saved in the {@link MultiDisplayResolveInfo}
+     * and then the {@link ChooserActivity} will receive a {@code #startSelected()} callback using
+     * this "which" value to identify the stack that's now unambiguously resolved.
+     * @param userHandle
+     *
+     * TODO: consider taking a client-provided callback instead of {@code stackedTargetParentWhich}
+     * to avoid coupling with {@link ChooserActivity}'s mechanism for handling the selection.
+     */
+    public static void show(
+            FragmentManager fragmentManager,
+            MultiDisplayResolveInfo stackedTarget,
+            int stackedTargetParentWhich,
+            UserHandle userHandle) {
+        ChooserStackedAppDialogFragment fragment = new ChooserStackedAppDialogFragment(
+                stackedTarget, stackedTargetParentWhich, userHandle);
+        fragment.show(fragmentManager, TARGET_DETAILS_FRAGMENT_TAG);
     }
 
+    private final MultiDisplayResolveInfo mMultiDisplayResolveInfo;
+    private final int mParentWhich;
+
     @Override
-    public void onSaveInstanceState(Bundle outState) {
-        super.onSaveInstanceState(outState);
-        outState.putInt(WHICH_KEY, mParentWhich);
-        outState.putParcelable(MULTI_DRI_KEY, mMultiDisplayResolveInfo);
+    public void onClick(DialogInterface dialog, int which) {
+        mMultiDisplayResolveInfo.setSelected(which);
+        ((ChooserActivity) getActivity()).startSelected(mParentWhich, false, true);
+        dismiss();
     }
 
     @Override
@@ -63,15 +74,16 @@
 
     @Override
     protected Drawable getItemIcon(DisplayResolveInfo dri) {
-
         // Show no icon for the group disambig dialog, null hides the imageview
         return null;
     }
 
-    @Override
-    public void onClick(DialogInterface dialog, int which) {
-        mMultiDisplayResolveInfo.setSelected(which);
-        ((ChooserActivity) getActivity()).startSelected(mParentWhich, false, true);
-        dismiss();
+    private ChooserStackedAppDialogFragment(
+            MultiDisplayResolveInfo stackedTarget,
+            int stackedTargetParentWhich,
+            UserHandle userHandle) {
+        super(stackedTarget.getAllDisplayTargets(), userHandle);
+        mMultiDisplayResolveInfo = stackedTarget;
+        mParentWhich = stackedTargetParentWhich;
     }
 }
diff --git a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
index ffd173c..0aa3250 100644
--- a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
+++ b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
@@ -19,15 +19,12 @@
 
 import static android.content.Context.ACTIVITY_SERVICE;
 
-import static com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;
-
 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.app.DialogFragment;
 import android.content.ComponentName;
 import android.content.Context;
 import android.content.DialogInterface;
@@ -49,11 +46,12 @@
 import android.widget.ImageView;
 import android.widget.TextView;
 
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.recyclerview.widget.RecyclerView;
+
 import com.android.intentresolver.chooser.DisplayResolveInfo;
 
-import com.android.internal.widget.RecyclerView;
-
-import java.util.ArrayList;
 import java.util.List;
 import java.util.Optional;
 import java.util.stream.Collectors;
@@ -64,68 +62,61 @@
 public class ChooserTargetActionsDialogFragment extends DialogFragment
         implements DialogInterface.OnClickListener {
 
-    protected ArrayList<DisplayResolveInfo> mTargetInfos = new ArrayList<>();
-    protected UserHandle mUserHandle;
-    protected String mShortcutId;
-    protected String mShortcutTitle;
-    protected boolean mIsShortcutPinned;
-    protected IntentFilter mIntentFilter;
+    protected final static String TARGET_DETAILS_FRAGMENT_TAG = "targetDetailsFragment";
 
-    public static final String USER_HANDLE_KEY = "user_handle";
-    public static final String TARGET_INFOS_KEY = "target_infos";
-    public static final String SHORTCUT_ID_KEY = "shortcut_id";
-    public static final String SHORTCUT_TITLE_KEY = "shortcut_title";
-    public static final String IS_SHORTCUT_PINNED_KEY = "is_shortcut_pinned";
-    public static final String INTENT_FILTER_KEY = "intent_filter";
+    private final List<DisplayResolveInfo> mTargetInfos;
+    private final UserHandle mUserHandle;
+    private final boolean mIsShortcutPinned;
 
-    public ChooserTargetActionsDialogFragment() {}
+    @Nullable
+    private final String mShortcutId;
+
+    @Nullable
+    private final String mShortcutTitle;
+
+    @Nullable
+    private final IntentFilter mIntentFilter;
+
+    public static void show(
+            FragmentManager fragmentManager,
+            List<DisplayResolveInfo> targetInfos,
+            UserHandle userHandle,
+            @Nullable String shortcutId,
+            @Nullable String shortcutTitle,
+            boolean isShortcutPinned,
+            @Nullable IntentFilter intentFilter) {
+        ChooserTargetActionsDialogFragment fragment = new ChooserTargetActionsDialogFragment(
+                targetInfos,
+                userHandle,
+                shortcutId,
+                shortcutTitle,
+                isShortcutPinned,
+                intentFilter);
+        fragment.show(fragmentManager, TARGET_DETAILS_FRAGMENT_TAG);
+    }
 
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+
         if (savedInstanceState != null) {
-            setStateFromBundle(savedInstanceState);
-        } else {
-            setStateFromBundle(getArguments());
+            // Bail. It's probably not possible to trigger reloading our fragments from a saved
+            // instance since Sharesheet isn't kept in history and the entire session will probably
+            // be lost under any conditions that would've triggered our retention. Nevertheless, if
+            // we ever *did* try to load from a saved state, we wouldn't be able to populate valid
+            // data (since we wouldn't be able to get back our original TargetInfos if we had to
+            // restore them from a Bundle).
+            dismissAllowingStateLoss();
         }
     }
 
-    void setStateFromBundle(Bundle b) {
-        mTargetInfos = (ArrayList<DisplayResolveInfo>) b.get(TARGET_INFOS_KEY);
-        mUserHandle = (UserHandle) b.get(USER_HANDLE_KEY);
-        mShortcutId = b.getString(SHORTCUT_ID_KEY);
-        mShortcutTitle = b.getString(SHORTCUT_TITLE_KEY);
-        mIsShortcutPinned = b.getBoolean(IS_SHORTCUT_PINNED_KEY);
-        mIntentFilter = (IntentFilter) b.get(INTENT_FILTER_KEY);
-    }
-
-    @Override
-    public void onSaveInstanceState(Bundle outState) {
-        super.onSaveInstanceState(outState);
-
-        outState.putParcelable(ChooserTargetActionsDialogFragment.USER_HANDLE_KEY,
-                mUserHandle);
-        outState.putParcelableArrayList(ChooserTargetActionsDialogFragment.TARGET_INFOS_KEY,
-                mTargetInfos);
-        outState.putString(ChooserTargetActionsDialogFragment.SHORTCUT_ID_KEY, mShortcutId);
-        outState.putBoolean(ChooserTargetActionsDialogFragment.IS_SHORTCUT_PINNED_KEY,
-                mIsShortcutPinned);
-        outState.putString(ChooserTargetActionsDialogFragment.SHORTCUT_TITLE_KEY, mShortcutTitle);
-        outState.putParcelable(ChooserTargetActionsDialogFragment.INTENT_FILTER_KEY, mIntentFilter);
-    }
-
     /**
-     * Recreate the layout from scratch to match new Sharesheet redlines
+     * Build the menu UI according to our design spec.
      */
     @Override
     public View onCreateView(LayoutInflater inflater,
             @Nullable ViewGroup container,
             Bundle savedInstanceState) {
-        if (savedInstanceState != null) {
-            setStateFromBundle(savedInstanceState);
-        } else {
-            setStateFromBundle(getArguments());
-        }
         // Make the background transparent to show dialog rounding
         Optional.of(getDialog()).map(Dialog::getWindow)
                 .ifPresent(window -> {
@@ -143,7 +134,7 @@
         ImageView icon = v.findViewById(com.android.internal.R.id.icon);
         RecyclerView rv = v.findViewById(com.android.internal.R.id.listContainer);
 
-        final ResolveInfoPresentationGetter pg = getProvidingAppPresentationGetter();
+        final TargetPresentationGetter pg = getProvidingAppPresentationGetter();
         title.setText(isShortcutTarget() ? mShortcutTitle : pg.getLabel());
         icon.setImageDrawable(pg.getIcon(mUserHandle));
         rv.setAdapter(new VHAdapter(items));
@@ -277,14 +268,14 @@
         return getPinIcon(isPinned(dri));
     }
 
-    private ResolveInfoPresentationGetter getProvidingAppPresentationGetter() {
+    private TargetPresentationGetter getProvidingAppPresentationGetter() {
         final ActivityManager am = (ActivityManager) getContext()
                 .getSystemService(ACTIVITY_SERVICE);
         final int iconDpi = am.getLauncherLargeIconDensity();
 
         // Use the matching application icon and label for the title, any TargetInfo will do
-        return new ResolveInfoPresentationGetter(getContext(), iconDpi,
-                mTargetInfos.get(0).getResolveInfo());
+        return new TargetPresentationGetter.Factory(getContext(), iconDpi)
+                .makePresentationGetter(mTargetInfos.get(0).getResolveInfo());
     }
 
     private boolean isPinned(DisplayResolveInfo dri) {
@@ -294,4 +285,24 @@
     private boolean isShortcutTarget() {
         return mShortcutId != null;
     }
+
+    protected ChooserTargetActionsDialogFragment(
+            List<DisplayResolveInfo> targetInfos, UserHandle userHandle) {
+        this(targetInfos, userHandle, null, null, false, null);
+    }
+
+    private ChooserTargetActionsDialogFragment(
+            List<DisplayResolveInfo> targetInfos,
+            UserHandle userHandle,
+            @Nullable String shortcutId,
+            @Nullable String shortcutTitle,
+            boolean isShortcutPinned,
+            @Nullable IntentFilter intentFilter) {
+        mTargetInfos = targetInfos;
+        mUserHandle = userHandle;
+        mShortcutId = shortcutId;
+        mShortcutTitle = shortcutTitle;
+        mIsShortcutPinned = isShortcutPinned;
+        mIntentFilter = intentFilter;
+    }
 }
diff --git a/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt b/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt
new file mode 100644
index 0000000..a0bf61b
--- /dev/null
+++ b/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver
+
+import android.app.Activity
+import android.app.SharedElementCallback
+import android.view.View
+import com.android.intentresolver.widget.ResolverDrawerLayout
+import java.util.function.Supplier
+
+/**
+ * A helper class to track app's readiness for the scene transition animation.
+ * The app is ready when both the image is laid out and the drawer offset is calculated.
+ */
+internal class EnterTransitionAnimationDelegate(
+    private val activity: Activity,
+    private val resolverDrawerLayoutSupplier: Supplier<ResolverDrawerLayout?>
+) : View.OnLayoutChangeListener {
+    private var removeSharedElements = false
+    private var previewReady = false
+    private var offsetCalculated = false
+
+    init {
+        activity.setEnterSharedElementCallback(
+            object : SharedElementCallback() {
+                override fun onMapSharedElements(
+                    names: MutableList<String>, sharedElements: MutableMap<String, View>
+                ) {
+                    this@EnterTransitionAnimationDelegate.onMapSharedElements(
+                        names, sharedElements
+                    )
+                }
+            })
+    }
+
+    fun postponeTransition() = activity.postponeEnterTransition()
+
+    fun markImagePreviewReady(runTransitionAnimation: Boolean) {
+        if (!runTransitionAnimation) {
+            removeSharedElements = true
+        }
+        if (!previewReady) {
+            previewReady = true
+            maybeStartListenForLayout()
+        }
+    }
+
+    fun markOffsetCalculated() {
+        if (!offsetCalculated) {
+            offsetCalculated = true
+            maybeStartListenForLayout()
+        }
+    }
+
+    private fun onMapSharedElements(
+        names: MutableList<String>,
+        sharedElements: MutableMap<String, View>
+    ) {
+        if (removeSharedElements) {
+            names.remove(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)
+            sharedElements.remove(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)
+        }
+        removeSharedElements = false
+    }
+
+    private fun maybeStartListenForLayout() {
+        val drawer = resolverDrawerLayoutSupplier.get()
+        if (previewReady && offsetCalculated && drawer != null) {
+            if (drawer.isInLayout) {
+                startPostponedEnterTransition()
+            } else {
+                drawer.addOnLayoutChangeListener(this)
+                drawer.requestLayout()
+            }
+        }
+    }
+
+    override fun onLayoutChange(
+        v: View,
+        left: Int, top: Int, right: Int, bottom: Int,
+        oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int
+    ) {
+        v.removeOnLayoutChangeListener(this)
+        startPostponedEnterTransition()
+    }
+
+    private fun startPostponedEnterTransition() {
+        if (!removeSharedElements && activity.isActivityTransitionRunning) {
+            // Disable the window animations as it interferes with the transition animation.
+            activity.window.setWindowAnimations(0)
+        }
+        activity.startPostponedEnterTransition()
+    }
+}
diff --git a/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java
new file mode 100644
index 0000000..9bbdf7c
--- /dev/null
+++ b/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java
@@ -0,0 +1,225 @@
+/*
+ * 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.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,
+            QuietModeManager quietModeManager,
+            @Profile int defaultProfile,
+            UserHandle workProfileUserHandle,
+            Supplier<ViewGroup> pageViewInflater,
+            Supplier<Optional<Integer>> containerBottomPaddingOverrideSupplier) {
+        super(
+                context,
+                /* currentPage= */ defaultProfile,
+                emptyStateProvider,
+                quietModeManager,
+                workProfileUserHandle);
+
+        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 (getActiveListAdapter().getUserHandle().equals(userHandle)) {
+            return getActiveListAdapter();
+        }
+        if ((getInactiveListAdapter() != null) && getInactiveListAdapter().getUserHandle().equals(
+                userHandle)) {
+            return getInactiveListAdapter();
+        }
+        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() {
+        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));
+    }
+
+    // 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/ImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt
new file mode 100644
index 0000000..e68eb66
--- /dev/null
+++ b/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.graphics.Bitmap
+import android.net.Uri
+import kotlinx.coroutines.suspendCancellableCoroutine
+
+// TODO: convert ChooserContentPreviewCoordinator to Kotlin and merge this class into it.
+internal class ImagePreviewImageLoader(
+    private val previewCoordinator: ChooserContentPreviewUi.ContentPreviewCoordinator
+) : suspend (Uri) -> Bitmap? {
+
+    override suspend fun invoke(uri: Uri): Bitmap? =
+        suspendCancellableCoroutine { continuation ->
+            val callback = java.util.function.Consumer<Bitmap?> { bitmap ->
+                try {
+                    continuation.resumeWith(Result.success(bitmap))
+                } catch (ignored: Exception) {
+                }
+            }
+            previewCoordinator.loadImage(uri, callback)
+        }
+}
diff --git a/java/src/com/android/intentresolver/IntentForwarderActivity.java b/java/src/com/android/intentresolver/IntentForwarderActivity.java
index 9b853c9..7824025 100644
--- a/java/src/com/android/intentresolver/IntentForwarderActivity.java
+++ b/java/src/com/android/intentresolver/IntentForwarderActivity.java
@@ -28,7 +28,6 @@
 import android.app.ActivityThread;
 import android.app.AppGlobals;
 import android.app.admin.DevicePolicyManager;
-import android.compat.annotation.UnsupportedAppUsage;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Intent;
@@ -38,7 +37,6 @@
 import android.content.pm.ResolveInfo;
 import android.content.pm.UserInfo;
 import android.metrics.LogMaker;
-import android.os.Build;
 import android.os.Bundle;
 import android.os.RemoteException;
 import android.os.UserHandle;
@@ -65,7 +63,6 @@
  * be passed in and out of a managed profile.
  */
 public class IntentForwarderActivity extends Activity  {
-    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
     public static String TAG = "IntentForwarderActivity";
 
     public static String FORWARD_INTENT_TO_PARENT
diff --git a/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java
new file mode 100644
index 0000000..5bf994d
--- /dev/null
+++ b/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java
@@ -0,0 +1,154 @@
+/*
+ * 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 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;
+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 com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider;
+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 MyUserIdProvider mMyUserIdProvider;
+
+    public NoAppsAvailableEmptyStateProvider(Context context, UserHandle workProfileUserHandle,
+            UserHandle personalProfileUserHandle, String metricsCategory,
+            MyUserIdProvider myUserIdProvider) {
+        mContext = context;
+        mWorkProfileUserHandle = workProfileUserHandle;
+        mPersonalProfileUserHandle = personalProfileUserHandle;
+        mMetricsCategory = metricsCategory;
+        mMyUserIdProvider = myUserIdProvider;
+    }
+
+    @Nullable
+    @Override
+    @SuppressWarnings("ReferenceEquality")
+    public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+        UserHandle listUserHandle = resolverListAdapter.getUserHandle();
+
+        if (mWorkProfileUserHandle != null
+                && (mMyUserIdProvider.getMyUserId() == listUserHandle.getIdentifier()
+                || !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<ResolverActivity.ResolvedComponentInfo> resolversForIntent =
+                adapter.getResolversForUser(UserHandle.of(mMyUserIdProvider.getMyUserId()));
+        for (ResolverActivity.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 String mTitle;
+
+        @NonNull
+        private String mMetricsCategory;
+
+        private boolean mIsPersonalProfile;
+
+        public NoAppsAvailableEmptyState(String title, String metricsCategory,
+                boolean isPersonalProfile) {
+            mTitle = title;
+            mMetricsCategory = metricsCategory;
+            mIsPersonalProfile = isPersonalProfile;
+        }
+
+        @Nullable
+        @Override
+        public String getTitle() {
+            return mTitle;
+        }
+
+        @Override
+        public void onEmptyStateShown() {
+            DevicePolicyEventLogger.createEvent(
+                            DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_APPS_RESOLVED)
+                    .setStrings(mMetricsCategory)
+                    .setBoolean(/*isPersonalProfile*/ mIsPersonalProfile)
+                    .write();
+        }
+    }
+}
\ No newline at end of file
diff --git a/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java b/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java
new file mode 100644
index 0000000..420d26c
--- /dev/null
+++ b/java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java
@@ -0,0 +1,137 @@
+/*
+ * 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.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 com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider;
+
+/**
+ * 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 MyUserIdProvider mUserIdProvider;
+
+    public NoCrossProfileEmptyStateProvider(UserHandle personalUserHandle,
+            EmptyState noWorkToPersonalEmptyState,
+            EmptyState noPersonalToWorkEmptyState,
+            CrossProfileIntentsChecker crossProfileIntentsChecker,
+            MyUserIdProvider myUserIdProvider) {
+        mPersonalProfileUserHandle = personalUserHandle;
+        mNoWorkToPersonalEmptyState = noWorkToPersonalEmptyState;
+        mNoPersonalToWorkEmptyState = noPersonalToWorkEmptyState;
+        mCrossProfileIntentsChecker = crossProfileIntentsChecker;
+        mUserIdProvider = myUserIdProvider;
+    }
+
+    @Nullable
+    @Override
+    public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+        boolean shouldShowBlocker =
+                mUserIdProvider.getMyUserId() != resolverListAdapter.getUserHandle().getIdentifier()
+                        && !mCrossProfileIntentsChecker
+                        .hasCrossProfileIntents(resolverListAdapter.getIntents(),
+                                mUserIdProvider.getMyUserId(),
+                                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(Context context, String devicePolicyStringTitleId,
+                @StringRes int defaultTitleResource, String devicePolicyStringSubtitleId,
+                @StringRes int defaultSubtitleResource,
+                int devicePolicyEventId, 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/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java
index 453a6e8..5573e18 100644
--- a/java/src/com/android/intentresolver/ResolverActivity.java
+++ b/java/src/com/android/intentresolver/ResolverActivity.java
@@ -19,6 +19,9 @@
 import static android.Manifest.permission.INTERACT_ACROSS_PROFILES;
 import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL;
 import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
 import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB;
 import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB_ACCESSIBILITY;
 import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED;
@@ -26,6 +29,8 @@
 import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY;
 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 android.annotation.Nullable;
@@ -39,7 +44,6 @@
 import android.app.VoiceInteractor.Prompt;
 import android.app.admin.DevicePolicyEventLogger;
 import android.app.admin.DevicePolicyManager;
-import android.compat.annotation.UnsupportedAppUsage;
 import android.content.BroadcastReceiver;
 import android.content.ComponentName;
 import android.content.Context;
@@ -55,7 +59,9 @@
 import android.content.res.Configuration;
 import android.content.res.TypedArray;
 import android.graphics.Insets;
+import android.graphics.drawable.Drawable;
 import android.net.Uri;
+import android.os.AsyncTask;
 import android.os.Build;
 import android.os.Bundle;
 import android.os.PatternMatcher;
@@ -90,18 +96,25 @@
 import android.widget.TextView;
 import android.widget.Toast;
 
+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.chooser.ChooserTargetInfo;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager;
+import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
 import com.android.intentresolver.chooser.DisplayResolveInfo;
 import com.android.intentresolver.chooser.TargetInfo;
-
+import com.android.intentresolver.widget.ResolverDrawerLayout;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.content.PackageMonitor;
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto;
 import com.android.internal.util.LatencyTracker;
-import com.android.internal.widget.ResolverDrawerLayout;
-import com.android.internal.widget.ViewPager;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -109,6 +122,7 @@
 import java.util.List;
 import java.util.Objects;
 import java.util.Set;
+import java.util.function.Supplier;
 
 /**
  * This activity is displayed when the system attempts to start an Intent for
@@ -116,10 +130,9 @@
  * which to go to.  It is not normally used directly by application developers.
  */
 @UiThread
-public class ResolverActivity extends Activity implements
+public class ResolverActivity extends FragmentActivity implements
         ResolverListAdapter.ResolverListCommunicator {
 
-    @UnsupportedAppUsage
     public ResolverActivity() {
         mIsIntentPicker = getClass().equals(ResolverActivity.class);
     }
@@ -149,7 +162,6 @@
     @VisibleForTesting
     protected boolean mSupportsAlwaysUseOption;
     protected ResolverDrawerLayout mResolverDrawerLayout;
-    @UnsupportedAppUsage
     protected PackageManager mPm;
     protected int mLaunchedFromUid;
 
@@ -165,17 +177,12 @@
     /** See {@link #setRetainInOnStop}. */
     private boolean mRetainInOnStop;
 
-    private static final String EXTRA_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args";
-    private static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
-    private static final String OPEN_LINKS_COMPONENT_KEY = "app_link_state";
     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;
 
-    @VisibleForTesting
-    public static boolean ENABLE_TABBED_VIEW = true;
     private static final String TAB_TAG_PERSONAL = "personal";
     private static final String TAB_TAG_WORK = "work";
 
@@ -185,6 +192,8 @@
     @VisibleForTesting
     protected AbstractMultiProfilePagerAdapter mMultiProfilePagerAdapter;
 
+    protected QuietModeManager mQuietModeManager;
+
     // Intent extra for connected audio devices
     public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device";
 
@@ -214,7 +223,14 @@
     private BroadcastReceiver mWorkProfileStateReceiver;
     private UserHandle mHeaderCreatorUser;
 
-    private UserHandle mWorkProfileUserHandle;
+    private Supplier<UserHandle> mLazyWorkProfileUserHandle = () -> {
+        final UserHandle result = fetchWorkProfileUserProfile();
+        mLazyWorkProfileUserHandle = () -> result;
+        return result;
+    };
+
+    @Nullable
+    private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
 
     protected final LatencyTracker mLatencyTracker = getLatencyTracker();
 
@@ -360,7 +376,6 @@
      * Compatibility version for other bundled services that use this overload without
      * a default title resource
      */
-    @UnsupportedAppUsage
     protected void onCreate(Bundle savedInstanceState, Intent intent,
             CharSequence title, Intent[] initialIntents,
             List<ResolveInfo> rList, boolean supportsAlwaysUseOption) {
@@ -374,6 +389,8 @@
         setTheme(appliedThemeResId());
         super.onCreate(savedInstanceState);
 
+        mQuietModeManager = createQuietModeManager();
+
         // Determine whether we should show that intent is forwarded
         // from managed profile to owner or other way around.
         setProfileSwitchMessage(intent.getContentUserHint());
@@ -395,7 +412,6 @@
         mDefaultTitleResId = defaultTitleRes;
 
         mSupportsAlwaysUseOption = supportsAlwaysUseOption;
-        mWorkProfileUserHandle = fetchWorkProfileUserProfile();
 
         // 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
@@ -474,6 +490,111 @@
         return resolverMultiProfilePagerAdapter;
     }
 
+    @VisibleForTesting
+    protected MyUserIdProvider createMyUserIdProvider() {
+        return new MyUserIdProvider();
+    }
+
+    @VisibleForTesting
+    protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() {
+        return new CrossProfileIntentsChecker(getContentResolver());
+    }
+
+    @VisibleForTesting
+    protected QuietModeManager createQuietModeManager() {
+        UserManager userManager = getSystemService(UserManager.class);
+        return new QuietModeManager() {
+
+            private boolean mIsWaitingToEnableWorkProfile = false;
+
+            @Override
+            public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) {
+                return userManager.isQuietModeEnabled(workProfileUserHandle);
+            }
+
+            @Override
+            public void requestQuietModeEnabled(boolean enabled, UserHandle workProfileUserHandle) {
+                AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
+                    userManager.requestQuietModeEnabled(enabled, workProfileUserHandle);
+                });
+                mIsWaitingToEnableWorkProfile = true;
+            }
+
+            @Override
+            public void markWorkProfileEnabledBroadcastReceived() {
+                mIsWaitingToEnableWorkProfile = false;
+            }
+
+            @Override
+            public boolean isWaitingToEnableWorkProfile() {
+                return mIsWaitingToEnableWorkProfile;
+            }
+        };
+    }
+
+    protected EmptyStateProvider createBlockerEmptyStateProvider() {
+        final boolean shouldShowNoCrossProfileIntentsEmptyState = getUser().equals(getIntentUser());
+
+        if (!shouldShowNoCrossProfileIntentsEmptyState) {
+            // Implementation that doesn't show any blockers
+            return new EmptyStateProvider() {};
+        }
+
+        final AbstractMultiProfilePagerAdapter.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 AbstractMultiProfilePagerAdapter.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(getPersonalProfileUserHandle(),
+                noWorkToPersonalEmptyState, noPersonalToWorkEmptyState,
+                createCrossProfileIntentsChecker(), createMyUserIdProvider());
+    }
+
+    protected EmptyStateProvider createEmptyStateProvider(
+            @Nullable UserHandle workProfileUserHandle) {
+        final EmptyStateProvider blockerEmptyStateProvider = createBlockerEmptyStateProvider();
+
+        final EmptyStateProvider workProfileOffEmptyStateProvider =
+                new WorkProfilePausedEmptyStateProvider(this, workProfileUserHandle,
+                        mQuietModeManager,
+                        /* onSwitchOnWorkSelectedListener= */
+                        () -> { if (mOnSwitchOnWorkSelectedListener != null) {
+                            mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected();
+                        }},
+                        getMetricsCategory());
+
+        final EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider(
+                this,
+                workProfileUserHandle,
+                getPersonalProfileUserHandle(),
+                getMetricsCategory(),
+                createMyUserIdProvider()
+        );
+
+        // Return composite provider, the order matters (the higher, the more priority)
+        return new CompositeEmptyStateProvider(
+                blockerEmptyStateProvider,
+                workProfileOffEmptyStateProvider,
+                noAppsEmptyStateProvider
+        );
+    }
+
     private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForOneProfile(
             Intent[] initialIntents,
             List<ResolveInfo> rList, boolean filterLastUsed) {
@@ -484,13 +605,21 @@
                 rList,
                 filterLastUsed,
                 /* userHandle */ UserHandle.of(UserHandle.myUserId()));
+        QuietModeManager quietModeManager = createQuietModeManager();
         return new ResolverMultiProfilePagerAdapter(
                 /* context */ this,
                 adapter,
-                getPersonalProfileUserHandle(),
+                createEmptyStateProvider(/* workProfileUserHandle= */ null),
+                quietModeManager,
                 /* workProfileUserHandle= */ null);
     }
 
+    private UserHandle getIntentUser() {
+        return getIntent().hasExtra(EXTRA_CALLING_USER)
+                ? getIntent().getParcelableExtra(EXTRA_CALLING_USER)
+                : getUser();
+    }
+
     private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles(
             Intent[] initialIntents,
             List<ResolveInfo> rList,
@@ -499,9 +628,7 @@
         // 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 = getIntent().hasExtra(EXTRA_CALLING_USER)
-                ? getIntent().getParcelableExtra(EXTRA_CALLING_USER)
-                : getUser();
+        UserHandle intentUser = getIntentUser();
         if (!getUser().equals(intentUser)) {
             if (getPersonalProfileUserHandle().equals(intentUser)) {
                 selectedProfile = PROFILE_PERSONAL;
@@ -534,14 +661,15 @@
                 (filterLastUsed && UserHandle.myUserId()
                         == workProfileUserHandle.getIdentifier()),
                 /* userHandle */ workProfileUserHandle);
+        QuietModeManager quietModeManager = createQuietModeManager();
         return new ResolverMultiProfilePagerAdapter(
                 /* context */ this,
                 personalAdapter,
                 workAdapter,
+                createEmptyStateProvider(getWorkProfileUserHandle()),
+                quietModeManager,
                 selectedProfile,
-                getPersonalProfileUserHandle(),
-                getWorkProfileUserHandle(),
-                /* shouldShowNoCrossProfileIntentsEmptyState= */ getUser().equals(intentUser));
+                getWorkProfileUserHandle());
     }
 
     protected int appliedThemeResId() {
@@ -574,19 +702,25 @@
     protected UserHandle getPersonalProfileUserHandle() {
         return UserHandle.of(ActivityManager.getCurrentUser());
     }
-    protected @Nullable UserHandle getWorkProfileUserHandle() {
-        return mWorkProfileUserHandle;
+
+    @Nullable
+    protected UserHandle getWorkProfileUserHandle() {
+        return mLazyWorkProfileUserHandle.get();
     }
 
-    protected @Nullable UserHandle fetchWorkProfileUserProfile() {
-        mWorkProfileUserHandle = null;
+    @Nullable
+    private UserHandle fetchWorkProfileUserProfile() {
         UserManager userManager = getSystemService(UserManager.class);
+        if (userManager == null) {
+            return null;
+        }
+        UserHandle result = null;
         for (final UserInfo userInfo : userManager.getProfiles(ActivityManager.getCurrentUser())) {
             if (userInfo.isManagedProfile()) {
-                mWorkProfileUserHandle = userInfo.getUserHandle();
+                result = userInfo.getUserHandle();
             }
         }
-        return mWorkProfileUserHandle;
+        return result;
     }
 
     private boolean hasWorkProfile() {
@@ -594,7 +728,7 @@
     }
 
     protected boolean shouldShowTabs() {
-        return hasWorkProfile() && ENABLE_TABBED_VIEW;
+        return hasWorkProfile();
     }
 
     protected void onProfileClick(View v) {
@@ -726,7 +860,6 @@
         }
     }
 
-    @Override // SelectableTargetInfoCommunicator ResolverListCommunicator
     public Intent getTargetIntent() {
         return mIntents.isEmpty() ? null : mIntents.get(0);
     }
@@ -848,9 +981,9 @@
             }
             mRegistered = true;
         }
-        if (shouldShowTabs() && mMultiProfilePagerAdapter.isWaitingToEnableWorkProfile()) {
-            if (mMultiProfilePagerAdapter.isQuietModeEnabled(getWorkProfileUserHandle())) {
-                mMultiProfilePagerAdapter.markWorkProfileEnabledBroadcastReceived();
+        if (shouldShowTabs() && mQuietModeManager.isWaitingToEnableWorkProfile()) {
+            if (mQuietModeManager.isQuietModeEnabled(getWorkProfileUserHandle())) {
+                mQuietModeManager.markWorkProfileEnabledBroadcastReceived();
             }
         }
         mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
@@ -1375,7 +1508,7 @@
                 .createEvent(DevicePolicyEnums.RESOLVER_CROSS_PROFILE_TARGET_OPENED)
                 .setBoolean(currentUserHandle.equals(getPersonalProfileUserHandle()))
                 .setStrings(getMetricsCategory(),
-                        cti instanceof ChooserTargetInfo ? "direct_share" : "other_target")
+                        cti.isInDirectShareMetricsCategory() ? "direct_share" : "other_target")
                 .write();
     }
 
@@ -1407,8 +1540,16 @@
         Intent startIntent = getIntent();
         boolean isAudioCaptureDevice =
                 startIntent.getBooleanExtra(EXTRA_IS_AUDIO_CAPTURE_DEVICE, false);
-        return new ResolverListAdapter(context, payloadIntents, initialIntents, rList,
-                filterLastUsed, createListController(userHandle), this,
+        return new ResolverListAdapter(
+                context,
+                payloadIntents,
+                initialIntents,
+                rList,
+                filterLastUsed,
+                createListController(userHandle),
+                userHandle,
+                getTargetIntent(),
+                this,
                 isAudioCaptureDevice);
     }
 
@@ -1472,17 +1613,25 @@
         setContentView(mLayoutId);
 
         DisplayResolveInfo sameProfileResolveInfo =
-                mMultiProfilePagerAdapter.getActiveListAdapter().mDisplayList.get(0);
+                mMultiProfilePagerAdapter.getActiveListAdapter().getFirstDisplayResolveInfo();
         boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK;
 
-        ResolverListAdapter inactiveAdapter = mMultiProfilePagerAdapter.getInactiveListAdapter();
-        DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0);
+        final ResolverListAdapter inactiveAdapter =
+                mMultiProfilePagerAdapter.getInactiveListAdapter();
+        final DisplayResolveInfo otherProfileResolveInfo =
+                inactiveAdapter.getFirstDisplayResolveInfo();
 
         // Load the icon asynchronously
         ImageView icon = findViewById(com.android.internal.R.id.icon);
-        ResolverListAdapter.LoadIconTask iconTask = inactiveAdapter.new LoadIconTask(
-                        otherProfileResolveInfo, new ResolverListAdapter.ViewHolder(icon));
-        iconTask.execute();
+        inactiveAdapter.new LoadIconTask(otherProfileResolveInfo) {
+            @Override
+            protected void onPostExecute(Drawable drawable) {
+                if (!isDestroyed()) {
+                    otherProfileResolveInfo.getDisplayIconHolder().setDisplayIcon(drawable);
+                    new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo);
+                }
+            }
+        }.execute();
 
         ((TextView) findViewById(com.android.internal.R.id.open_cross_profile)).setText(
                 getResources().getString(
@@ -1521,31 +1670,29 @@
                 || mMultiProfilePagerAdapter.getInactiveListAdapter() == null) {
             return false;
         }
-        List<DisplayResolveInfo> sameProfileList =
-                mMultiProfilePagerAdapter.getActiveListAdapter().mDisplayList;
-        List<DisplayResolveInfo> otherProfileList =
-                mMultiProfilePagerAdapter.getInactiveListAdapter().mDisplayList;
+        ResolverListAdapter sameProfileAdapter =
+                mMultiProfilePagerAdapter.getActiveListAdapter();
+        ResolverListAdapter otherProfileAdapter =
+                mMultiProfilePagerAdapter.getInactiveListAdapter();
 
-        if (sameProfileList.isEmpty()) {
+        if (sameProfileAdapter.getDisplayResolveInfoCount() == 0) {
             Log.d(TAG, "No targets in the current profile");
             return false;
         }
 
-        if (otherProfileList.size() != 1) {
-            Log.d(TAG, "Found " + otherProfileList.size() + " resolvers in the other profile");
+        if (otherProfileAdapter.getDisplayResolveInfoCount() != 1) {
+            Log.d(TAG, "Other-profile count: " + otherProfileAdapter.getDisplayResolveInfoCount());
             return false;
         }
 
-        if (otherProfileList.get(0).getResolveInfo().handleAllWebDataURI) {
+        if (otherProfileAdapter.allResolveInfosHandleAllWebDataUri()) {
             Log.d(TAG, "Other profile is a web browser");
             return false;
         }
 
-        for (DisplayResolveInfo info : sameProfileList) {
-            if (!info.getResolveInfo().handleAllWebDataURI) {
-                Log.d(TAG, "Non-browser found in this profile");
-                return false;
-            }
+        if (!sameProfileAdapter.allResolveInfosHandleAllWebDataUri()) {
+            Log.d(TAG, "Non-browser found in this profile");
+            return false;
         }
 
         return true;
@@ -1800,13 +1947,12 @@
                         onHorizontalSwipeStateChanged(state);
                     }
                 });
-        mMultiProfilePagerAdapter.setOnSwitchOnWorkSelectedListener(
-                () -> {
-                    final View workTab = tabHost.getTabWidget().getChildAt(1);
-                    workTab.setFocusable(true);
-                    workTab.setFocusableInTouchMode(true);
-                    workTab.requestFocus();
-                });
+        mOnSwitchOnWorkSelectedListener = () -> {
+            final View workTab = tabHost.getTabWidget().getChildAt(1);
+            workTab.setFocusable(true);
+            workTab.setFocusableInTouchMode(true);
+            workTab.requestFocus();
+        };
     }
 
     private String getPersonalTabLabel() {
@@ -2067,7 +2213,7 @@
     public void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
         if (listAdapter == mMultiProfilePagerAdapter.getActiveListAdapter()) {
             if (listAdapter.getUserHandle().equals(getWorkProfileUserHandle())
-                    && mMultiProfilePagerAdapter.isWaitingToEnableWorkProfile()) {
+                    && mQuietModeManager.isWaitingToEnableWorkProfile()) {
                 // We have just turned on the work profile and entered the pass code to start it,
                 // now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no
                 // point in reloading the list now, since the work profile user is still
@@ -2119,7 +2265,7 @@
                     }
 
                     mWorkProfileHasBeenEnabled = true;
-                    mMultiProfilePagerAdapter.markWorkProfileEnabledBroadcastReceived();
+                    mQuietModeManager.markWorkProfileEnabledBroadcastReceived();
                 } else {
                     // Must be an UNAVAILABLE broadcast, so we watch for the next availability
                     mWorkProfileHasBeenEnabled = false;
@@ -2135,13 +2281,11 @@
         };
     }
 
-    @VisibleForTesting
     public static final class ResolvedComponentInfo {
         public final ComponentName name;
         private final List<Intent> mIntents = new ArrayList<>();
         private final List<ResolveInfo> mResolveInfos = new ArrayList<>();
         private boolean mPinned;
-        private boolean mFixedAtTop;
 
         public ResolvedComponentInfo(ComponentName name, Intent intent, ResolveInfo info) {
             this.name = name;
@@ -2190,14 +2334,6 @@
         public void setPinned(boolean pinned) {
             mPinned = pinned;
         }
-
-        public boolean isFixedAtTop() {
-            return mFixedAtTop;
-        }
-
-        public void setFixedAtTop(boolean isFixedAtTop) {
-            mFixedAtTop = isFixedAtTop;
-        }
     }
 
     class ItemClickListener implements AdapterView.OnItemClickListener,
@@ -2254,8 +2390,9 @@
 
     }
 
-    static final boolean isSpecificUriMatch(int match) {
-        match = match&IntentFilter.MATCH_CATEGORY_MASK;
+    /** 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;
     }
diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java
index 898d8c8..eecb914 100644
--- a/java/src/com/android/intentresolver/ResolverListAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverListAdapter.java
@@ -26,15 +26,11 @@
 import android.content.Intent;
 import android.content.PermissionChecker;
 import android.content.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
 import android.content.pm.LabeledIntent;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
 import android.graphics.ColorMatrix;
 import android.graphics.ColorMatrixColorFilter;
-import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.os.AsyncTask;
 import android.os.RemoteException;
@@ -54,44 +50,62 @@
 import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
 import com.android.intentresolver.chooser.DisplayResolveInfo;
 import com.android.intentresolver.chooser.TargetInfo;
-
 import com.android.internal.annotations.VisibleForTesting;
 
+import com.google.common.collect.ImmutableList;
+
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 public class ResolverListAdapter extends BaseAdapter {
     private static final String TAG = "ResolverListAdapter";
 
+    @Nullable  // TODO: other model for lazy computation? Or just precompute?
+    private static ColorMatrixColorFilter sSuspendedMatrixColorFilter;
+
+    protected final Context mContext;
+    protected final LayoutInflater mInflater;
+    protected final ResolverListCommunicator mResolverListCommunicator;
+    protected final ResolverListController mResolverListController;
+    protected final TargetPresentationGetter.Factory mPresentationFactory;
+
     private final List<Intent> mIntents;
     private final Intent[] mInitialIntents;
     private final List<ResolveInfo> mBaseResolveList;
     private final PackageManager mPm;
-    protected final Context mContext;
-    private static ColorMatrixColorFilter sSuspendedMatrixColorFilter;
     private final int mIconDpi;
-    protected ResolveInfo mLastChosen;
+    private final boolean mIsAudioCaptureDevice;
+    private final UserHandle mUserHandle;
+    private final Intent mTargetIntent;
+
+    private final Map<DisplayResolveInfo, LoadIconTask> mIconLoaders = new HashMap<>();
+    private final Map<DisplayResolveInfo, LoadLabelTask> mLabelLoaders = new HashMap<>();
+
+    private ResolveInfo mLastChosen;
     private DisplayResolveInfo mOtherProfile;
-    ResolverListController mResolverListController;
     private int mPlaceholderCount;
 
-    protected final LayoutInflater mInflater;
-
     // This one is the list that the Adapter will actually present.
-    List<DisplayResolveInfo> mDisplayList;
+    private List<DisplayResolveInfo> mDisplayList;
     private List<ResolvedComponentInfo> mUnfilteredResolveList;
 
     private int mLastChosenPosition = -1;
     private boolean mFilterLastUsed;
-    final ResolverListCommunicator mResolverListCommunicator;
     private Runnable mPostListReadyRunnable;
-    private final boolean mIsAudioCaptureDevice;
     private boolean mIsTabLoaded;
 
-    public ResolverListAdapter(Context context, List<Intent> payloadIntents,
-            Intent[] initialIntents, List<ResolveInfo> rList,
+    public ResolverListAdapter(
+            Context context,
+            List<Intent> payloadIntents,
+            Intent[] initialIntents,
+            List<ResolveInfo> rList,
             boolean filterLastUsed,
             ResolverListController resolverListController,
+            UserHandle userHandle,
+            Intent targetIntent,
             ResolverListCommunicator resolverListCommunicator,
             boolean isAudioCaptureDevice) {
         mContext = context;
@@ -103,10 +117,21 @@
         mDisplayList = new ArrayList<>();
         mFilterLastUsed = filterLastUsed;
         mResolverListController = resolverListController;
+        mUserHandle = userHandle;
+        mTargetIntent = targetIntent;
         mResolverListCommunicator = resolverListCommunicator;
         mIsAudioCaptureDevice = isAudioCaptureDevice;
         final ActivityManager am = (ActivityManager) mContext.getSystemService(ACTIVITY_SERVICE);
         mIconDpi = am.getLauncherLargeIconDensity();
+        mPresentationFactory = new TargetPresentationGetter.Factory(mContext, mIconDpi);
+    }
+
+    public final DisplayResolveInfo getFirstDisplayResolveInfo() {
+        return mDisplayList.get(0);
+    }
+
+    public final ImmutableList<DisplayResolveInfo> getTargetsInCurrentDisplayList() {
+        return ImmutableList.copyOf(mDisplayList);
     }
 
     public void handlePackagesChanged() {
@@ -258,7 +283,7 @@
         if (mBaseResolveList != null) {
             List<ResolvedComponentInfo> currentResolveList = new ArrayList<>();
             mResolverListController.addResolveListDedupe(currentResolveList,
-                    mResolverListCommunicator.getTargetIntent(),
+                    mTargetIntent,
                     mBaseResolveList);
             return currentResolveList;
         } else {
@@ -334,7 +359,12 @@
 
         if (otherProfileInfo != null) {
             mOtherProfile = makeOtherProfileDisplayResolveInfo(
-                    mContext, otherProfileInfo, mPm, mResolverListCommunicator, mIconDpi);
+                    mContext,
+                    otherProfileInfo,
+                    mPm,
+                    mTargetIntent,
+                    mResolverListCommunicator,
+                    mIconDpi);
         } else {
             mOtherProfile = null;
             try {
@@ -441,8 +471,13 @@
                         ri.icon = 0;
                     }
 
-                    addResolveInfo(new DisplayResolveInfo(ii, ri,
-                            ri.loadLabel(mPm), null, ii, makePresentationGetter(ri)));
+                    addResolveInfo(DisplayResolveInfo.newDisplayResolveInfo(
+                            ii,
+                            ri,
+                            ri.loadLabel(mPm),
+                            null,
+                            ii,
+                            mPresentationFactory.makePresentationGetter(ri)));
                 }
             }
 
@@ -490,10 +525,12 @@
         final Intent replaceIntent =
                 mResolverListCommunicator.getReplacementIntent(add.activityInfo, intent);
         final Intent defaultIntent = mResolverListCommunicator.getReplacementIntent(
-                add.activityInfo, mResolverListCommunicator.getTargetIntent());
-        final DisplayResolveInfo
-                dri = new DisplayResolveInfo(intent, add,
-                replaceIntent != null ? replaceIntent : defaultIntent, makePresentationGetter(add));
+                add.activityInfo, mTargetIntent);
+        final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
+                intent,
+                add,
+                (replaceIntent != null) ? replaceIntent : defaultIntent,
+                mPresentationFactory.makePresentationGetter(add));
         dri.setPinned(rci.isPinned());
         if (rci.isPinned()) {
             Log.i(TAG, "Pinned item: " + rci.name);
@@ -597,11 +634,15 @@
         return position;
     }
 
-    public int getDisplayResolveInfoCount() {
+    public final int getDisplayResolveInfoCount() {
         return mDisplayList.size();
     }
 
-    public DisplayResolveInfo getDisplayResolveInfo(int index) {
+    public final boolean allResolveInfosHandleAllWebDataUri() {
+        return mDisplayList.stream().allMatch(t -> t.getResolveInfo().handleAllWebDataURI);
+    }
+
+    public final DisplayResolveInfo getDisplayResolveInfo(int index) {
         // Used to query services. We only query services for primary targets, not alternates.
         return mDisplayList.get(index);
     }
@@ -634,28 +675,49 @@
     protected void onBindView(View view, TargetInfo info, int position) {
         final ViewHolder holder = (ViewHolder) view.getTag();
         if (info == null) {
-            holder.icon.setImageDrawable(
-                    mContext.getDrawable(R.drawable.resolver_icon_placeholder));
+            holder.icon.setImageDrawable(loadIconPlaceholder());
+            holder.bindLabel("", "", false);
             return;
         }
 
-        if (info instanceof DisplayResolveInfo
-                && !((DisplayResolveInfo) info).hasDisplayLabel()) {
-            getLoadLabelTask((DisplayResolveInfo) info, holder).execute();
-        } else {
-            holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
-        }
-
-        if (info instanceof DisplayResolveInfo
-                && !((DisplayResolveInfo) info).hasDisplayIcon()) {
-            new LoadIconTask((DisplayResolveInfo) info, holder).execute();
-        } else {
+        if (info.isDisplayResolveInfo()) {
+            DisplayResolveInfo dri = (DisplayResolveInfo) info;
+            if (dri.hasDisplayLabel()) {
+                holder.bindLabel(
+                        dri.getDisplayLabel(),
+                        dri.getExtendedInfo(),
+                        alwaysShowSubLabel());
+            } else {
+                holder.bindLabel("", "", false);
+                loadLabel(dri);
+            }
             holder.bindIcon(info);
+            if (!dri.hasDisplayIcon()) {
+                loadIcon(dri);
+            }
         }
     }
 
-    protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
-        return new LoadLabelTask(info, holder);
+    protected final void loadIcon(DisplayResolveInfo info) {
+        LoadIconTask task = mIconLoaders.get(info);
+        if (task == null) {
+            task = new LoadIconTask(info);
+            mIconLoaders.put(info, task);
+            task.execute();
+        }
+    }
+
+    private void loadLabel(DisplayResolveInfo info) {
+        LoadLabelTask task = mLabelLoaders.get(info);
+        if (task == null) {
+            task = createLoadLabelTask(info);
+            mLabelLoaders.put(info, task);
+            task.execute();
+        }
+    }
+
+    protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) {
+        return new LoadLabelTask(info);
     }
 
     public void onDestroy() {
@@ -666,6 +728,16 @@
         if (mResolverListController != null) {
             mResolverListController.destroy();
         }
+        cancelTasks(mIconLoaders.values());
+        cancelTasks(mLabelLoaders.values());
+        mIconLoaders.clear();
+        mLabelLoaders.clear();
+    }
+
+    private <T extends AsyncTask> void cancelTasks(Collection<T> tasks) {
+        for (T task: tasks) {
+            task.cancel(false);
+        }
     }
 
     private static ColorMatrixColorFilter getSuspendedColorMatrix() {
@@ -691,17 +763,13 @@
         return sSuspendedMatrixColorFilter;
     }
 
-    ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo ai) {
-        return new ActivityInfoPresentationGetter(mContext, mIconDpi, ai);
-    }
-
-    ResolveInfoPresentationGetter makePresentationGetter(ResolveInfo ri) {
-        return new ResolveInfoPresentationGetter(mContext, mIconDpi, ri);
-    }
-
     Drawable loadIconForResolveInfo(ResolveInfo ri) {
         // Load icons based on the current process. If in work profile icons should be badged.
-        return makePresentationGetter(ri).getIcon(getUserHandle());
+        return mPresentationFactory.makePresentationGetter(ri).getIcon(getUserHandle());
+    }
+
+    protected final Drawable loadIconPlaceholder() {
+        return mContext.getDrawable(R.drawable.resolver_icon_placeholder);
     }
 
     void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) {
@@ -710,7 +778,15 @@
             new AsyncTask<Void, Void, Drawable>() {
                 @Override
                 protected Drawable doInBackground(Void... params) {
-                    return loadIconForResolveInfo(iconInfo.getResolveInfo());
+                    Drawable drawable;
+                    try {
+                        drawable = loadIconForResolveInfo(iconInfo.getResolveInfo());
+                    } catch (Exception e) {
+                        ComponentName componentName = iconInfo.getResolvedComponentName();
+                        Log.e(TAG, "Failed to load app icon for " + componentName, e);
+                        drawable = loadIconPlaceholder();
+                    }
+                    return drawable;
                 }
 
                 @Override
@@ -721,9 +797,8 @@
         }
     }
 
-    @VisibleForTesting
     public UserHandle getUserHandle() {
-        return mResolverListController.getUserHandle();
+        return mUserHandle;
     }
 
     protected List<ResolvedComponentInfo> getResolversForUser(UserHandle userHandle) {
@@ -779,6 +854,7 @@
             Context context,
             ResolvedComponentInfo resolvedComponentInfo,
             PackageManager pm,
+            Intent targetIntent,
             ResolverListCommunicator resolverListCommunicator,
             int iconDpi) {
         ResolveInfo resolveInfo = resolvedComponentInfo.getResolveInfoAt(0);
@@ -787,13 +863,13 @@
                 resolveInfo.activityInfo,
                 resolvedComponentInfo.getIntentAt(0));
         Intent replacementIntent = resolverListCommunicator.getReplacementIntent(
-                resolveInfo.activityInfo,
-                resolverListCommunicator.getTargetIntent());
+                resolveInfo.activityInfo, targetIntent);
 
-        ResolveInfoPresentationGetter presentationGetter =
-                new ResolveInfoPresentationGetter(context, iconDpi, resolveInfo);
+        TargetPresentationGetter presentationGetter =
+                new TargetPresentationGetter.Factory(context, iconDpi)
+                .makePresentationGetter(resolveInfo);
 
-        return new DisplayResolveInfo(
+        return DisplayResolveInfo.newDisplayResolveInfo(
                 resolvedComponentInfo.getIntentAt(0),
                 resolveInfo,
                 resolveInfo.loadLabel(pm),
@@ -829,13 +905,12 @@
          */
         default boolean shouldGetOnlyDefaultActivities() { return true; };
 
-        Intent getTargetIntent();
-
         void onHandlePackagesChanged(ResolverListAdapter listAdapter);
     }
 
     /**
-     * A view holder.
+     * A view holder keeps a reference to a list view and provides functionality for managing its
+     * state.
      */
     @VisibleForTesting
     public static class ViewHolder {
@@ -877,7 +952,7 @@
         }
 
         public void bindIcon(TargetInfo info) {
-            icon.setImageDrawable(info.getDisplayIcon(itemView.getContext()));
+            icon.setImageDrawable(info.getDisplayIconHolder().getDisplayIcon());
             if (info.isSuspended()) {
                 icon.setColorFilter(getSuspendedColorMatrix());
             } else {
@@ -888,17 +963,15 @@
 
     protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> {
         private final DisplayResolveInfo mDisplayResolveInfo;
-        private final ViewHolder mHolder;
 
-        protected LoadLabelTask(DisplayResolveInfo dri, ViewHolder holder) {
+        protected LoadLabelTask(DisplayResolveInfo dri) {
             mDisplayResolveInfo = dri;
-            mHolder = holder;
         }
 
         @Override
         protected CharSequence[] doInBackground(Void... voids) {
-            ResolveInfoPresentationGetter pg =
-                    makePresentationGetter(mDisplayResolveInfo.getResolveInfo());
+            TargetPresentationGetter pg = mPresentationFactory.makePresentationGetter(
+                    mDisplayResolveInfo.getResolveInfo());
 
             if (mIsAudioCaptureDevice) {
                 // This is an audio capture device, so check record permissions
@@ -930,26 +1003,33 @@
 
         @Override
         protected void onPostExecute(CharSequence[] result) {
+            if (mDisplayResolveInfo.hasDisplayLabel()) {
+                return;
+            }
             mDisplayResolveInfo.setDisplayLabel(result[0]);
             mDisplayResolveInfo.setExtendedInfo(result[1]);
-            mHolder.bindLabel(result[0], result[1], alwaysShowSubLabel());
+            notifyDataSetChanged();
         }
     }
 
     class LoadIconTask extends AsyncTask<Void, Void, Drawable> {
         protected final DisplayResolveInfo mDisplayResolveInfo;
         private final ResolveInfo mResolveInfo;
-        private ViewHolder mHolder;
 
-        LoadIconTask(DisplayResolveInfo dri, ViewHolder holder) {
+        LoadIconTask(DisplayResolveInfo dri) {
             mDisplayResolveInfo = dri;
             mResolveInfo = dri.getResolveInfo();
-            mHolder = holder;
         }
 
         @Override
         protected Drawable doInBackground(Void... params) {
-            return loadIconForResolveInfo(mResolveInfo);
+            try {
+                return loadIconForResolveInfo(mResolveInfo);
+            } catch (Exception e) {
+                ComponentName componentName = mDisplayResolveInfo.getResolvedComponentName();
+                Log.e(TAG, "Failed to load app icon for " + componentName, e);
+                return loadIconPlaceholder();
+            }
         }
 
         @Override
@@ -957,207 +1037,9 @@
             if (getOtherProfile() == mDisplayResolveInfo) {
                 mResolverListCommunicator.updateProfileViewButton();
             } else if (!mDisplayResolveInfo.hasDisplayIcon()) {
-                mDisplayResolveInfo.setDisplayIcon(d);
-                mHolder.bindIcon(mDisplayResolveInfo);
-                // Notify in case view is already bound to resolve the race conditions on
-                // low end devices
+                mDisplayResolveInfo.getDisplayIconHolder().setDisplayIcon(d);
                 notifyDataSetChanged();
             }
         }
-
-        public void setViewHolder(ViewHolder holder) {
-            mHolder = holder;
-            mHolder.bindIcon(mDisplayResolveInfo);
-        }
-    }
-
-    /**
-     * Loads the icon and label for the provided ResolveInfo.
-     */
-    @VisibleForTesting
-    public static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter {
-        private final ResolveInfo mRi;
-        public ResolveInfoPresentationGetter(Context ctx, int iconDpi, ResolveInfo ri) {
-            super(ctx, iconDpi, ri.activityInfo);
-            mRi = ri;
-        }
-
-        @Override
-        Drawable getIconSubstituteInternal() {
-            Drawable dr = null;
-            try {
-                // Do not use ResolveInfo#getIconResource() as it defaults to the app
-                if (mRi.resolvePackageName != null && mRi.icon != 0) {
-                    dr = loadIconFromResource(
-                            mPm.getResourcesForApplication(mRi.resolvePackageName), mRi.icon);
-                }
-            } catch (PackageManager.NameNotFoundException e) {
-                Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
-                        + "couldn't find resources for package", e);
-            }
-
-            // Fall back to ActivityInfo if no icon is found via ResolveInfo
-            if (dr == null) dr = super.getIconSubstituteInternal();
-
-            return dr;
-        }
-
-        @Override
-        String getAppSubLabelInternal() {
-            // Will default to app name if no intent filter or activity label set, make sure to
-            // check if subLabel matches label before final display
-            return mRi.loadLabel(mPm).toString();
-        }
-
-        @Override
-        String getAppLabelForSubstitutePermission() {
-            // Will default to app name if no activity label set
-            return mRi.getComponentInfo().loadLabel(mPm).toString();
-        }
-    }
-
-    /**
-     * Loads the icon and label for the provided ActivityInfo.
-     */
-    @VisibleForTesting
-    public static class ActivityInfoPresentationGetter extends
-            TargetPresentationGetter {
-        private final ActivityInfo mActivityInfo;
-        public ActivityInfoPresentationGetter(Context ctx, int iconDpi,
-                ActivityInfo activityInfo) {
-            super(ctx, iconDpi, activityInfo.applicationInfo);
-            mActivityInfo = activityInfo;
-        }
-
-        @Override
-        Drawable getIconSubstituteInternal() {
-            Drawable dr = null;
-            try {
-                // Do not use ActivityInfo#getIconResource() as it defaults to the app
-                if (mActivityInfo.icon != 0) {
-                    dr = loadIconFromResource(
-                            mPm.getResourcesForApplication(mActivityInfo.applicationInfo),
-                            mActivityInfo.icon);
-                }
-            } catch (PackageManager.NameNotFoundException e) {
-                Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
-                        + "couldn't find resources for package", e);
-            }
-
-            return dr;
-        }
-
-        @Override
-        String getAppSubLabelInternal() {
-            // Will default to app name if no activity label set, make sure to check if subLabel
-            // matches label before final display
-            return (String) mActivityInfo.loadLabel(mPm);
-        }
-
-        @Override
-        String getAppLabelForSubstitutePermission() {
-            return getAppSubLabelInternal();
-        }
-    }
-
-    /**
-     * 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
-     * exception for applications that hold the right permission. Always attempts to use available
-     * resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses
-     * Strings to strip creative formatting.
-     */
-    private abstract static class TargetPresentationGetter {
-        @Nullable abstract Drawable getIconSubstituteInternal();
-        @Nullable abstract String getAppSubLabelInternal();
-        @Nullable abstract String getAppLabelForSubstitutePermission();
-
-        private Context mCtx;
-        private final int mIconDpi;
-        private final boolean mHasSubstitutePermission;
-        private final ApplicationInfo mAi;
-
-        protected PackageManager mPm;
-
-        TargetPresentationGetter(Context ctx, int iconDpi, ApplicationInfo ai) {
-            mCtx = ctx;
-            mPm = ctx.getPackageManager();
-            mAi = ai;
-            mIconDpi = iconDpi;
-            mHasSubstitutePermission = PackageManager.PERMISSION_GRANTED == mPm.checkPermission(
-                    android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON,
-                    mAi.packageName);
-        }
-
-        public Drawable getIcon(UserHandle userHandle) {
-            return new BitmapDrawable(mCtx.getResources(), getIconBitmap(userHandle));
-        }
-
-        public Bitmap getIconBitmap(@Nullable UserHandle userHandle) {
-            Drawable dr = null;
-            if (mHasSubstitutePermission) {
-                dr = getIconSubstituteInternal();
-            }
-
-            if (dr == null) {
-                try {
-                    if (mAi.icon != 0) {
-                        dr = loadIconFromResource(mPm.getResourcesForApplication(mAi), mAi.icon);
-                    }
-                } catch (PackageManager.NameNotFoundException ignore) {
-                }
-            }
-
-            // Fall back to ApplicationInfo#loadIcon if nothing has been loaded
-            if (dr == null) {
-                dr = mAi.loadIcon(mPm);
-            }
-
-            SimpleIconFactory sif = SimpleIconFactory.obtain(mCtx);
-            Bitmap icon = sif.createUserBadgedIconBitmap(dr, userHandle);
-            sif.recycle();
-
-            return icon;
-        }
-
-        public String getLabel() {
-            String label = null;
-            // Apps with the substitute permission will always show the activity label as the
-            // app label if provided
-            if (mHasSubstitutePermission) {
-                label = getAppLabelForSubstitutePermission();
-            }
-
-            if (label == null) {
-                label = (String) mAi.loadLabel(mPm);
-            }
-
-            return label;
-        }
-
-        public String getSubLabel() {
-            // Apps with the substitute permission will always show the resolve info label as the
-            // sublabel if provided
-            if (mHasSubstitutePermission){
-                String appSubLabel = getAppSubLabelInternal();
-                // Use the resolve info label as sublabel if it is set
-                if(!TextUtils.isEmpty(appSubLabel)
-                    && !TextUtils.equals(appSubLabel, getLabel())){
-                    return appSubLabel;
-                }
-                return null;
-            }
-            return getAppSubLabelInternal();
-        }
-
-        protected String loadLabelFromResource(Resources res, int resId) {
-            return res.getString(resId);
-        }
-
-        @Nullable
-        protected Drawable loadIconFromResource(Resources res, int resId) {
-            return res.getDrawableForDensity(resId, mIconDpi);
-        }
-
     }
 }
diff --git a/java/src/com/android/intentresolver/ResolverListController.java b/java/src/com/android/intentresolver/ResolverListController.java
index ff616ce..bfffe0d 100644
--- a/java/src/com/android/intentresolver/ResolverListController.java
+++ b/java/src/com/android/intentresolver/ResolverListController.java
@@ -32,7 +32,8 @@
 import android.util.Log;
 
 import com.android.intentresolver.chooser.DisplayResolveInfo;
-
+import com.android.intentresolver.model.AbstractResolverComparator;
+import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.util.ArrayList;
@@ -187,7 +188,6 @@
                 final ResolverActivity.ResolvedComponentInfo rci =
                         new ResolverActivity.ResolvedComponentInfo(name, intent, newInfo);
                 rci.setPinned(isComponentPinned(name));
-                rci.setFixedAtTop(isFixedAtTop(name));
                 into.add(rci);
             }
         }
@@ -202,14 +202,6 @@
         return false;
     }
 
-    /**
-     * Whether this component is fixed at top in the ranked apps list. Always false for Resolver;
-     * overridden in Chooser.
-     */
-    public boolean isFixedAtTop(ComponentName name) {
-        return false;
-    }
-
     // Filter out any activities that the launched uid does not have permission for.
     // To preserve the inputList, optionally will return the original list if any modification has
     // been made.
@@ -274,19 +266,6 @@
         return listToReturn;
     }
 
-    private class ComputeCallback implements AbstractResolverComparator.AfterCompute {
-
-        private CountDownLatch mFinishComputeSignal;
-
-        public ComputeCallback(CountDownLatch finishComputeSignal) {
-            mFinishComputeSignal = finishComputeSignal;
-        }
-
-        public void afterCompute () {
-            mFinishComputeSignal.countDown();
-        }
-    }
-
     private void compute(List<ResolverActivity.ResolvedComponentInfo> inputList)
             throws InterruptedException {
         if (mResolverComparator == null) {
@@ -294,8 +273,7 @@
             return;
         }
         final CountDownLatch finishComputeSignal = new CountDownLatch(1);
-        ComputeCallback callback = new ComputeCallback(finishComputeSignal);
-        mResolverComparator.setCallBack(callback);
+        mResolverComparator.setCallBack(() -> finishComputeSignal.countDown());
         mResolverComparator.compute(inputList);
         finishComputeSignal.await();
         isComputed = true;
diff --git a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
index 56d326c..65de940 100644
--- a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
@@ -16,264 +16,99 @@
 
 package com.android.intentresolver;
 
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_PERSONAL_APPS;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS;
-import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE;
-
-import android.annotation.Nullable;
-import android.app.admin.DevicePolicyManager;
 import android.content.Context;
 import android.os.UserHandle;
 import android.view.LayoutInflater;
-import android.view.View;
 import android.view.ViewGroup;
 import android.widget.ListView;
 
+import androidx.viewpager.widget.PagerAdapter;
+
 import com.android.internal.annotations.VisibleForTesting;
-import com.android.internal.widget.PagerAdapter;
+
+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 AbstractMultiProfilePagerAdapter {
+public class ResolverMultiProfilePagerAdapter extends
+        GenericMultiProfilePagerAdapter<ListView, ResolverListAdapter, ResolverListAdapter> {
+    private final BottomPaddingOverrideSupplier mBottomPaddingOverrideSupplier;
 
-    private final ResolverProfileDescriptor[] mItems;
-    private final boolean mShouldShowNoCrossProfileIntentsEmptyState;
-    private boolean mUseLayoutWithDefault;
-
-    ResolverMultiProfilePagerAdapter(Context context,
+    ResolverMultiProfilePagerAdapter(
+            Context context,
             ResolverListAdapter adapter,
-            UserHandle personalProfileUserHandle,
+            EmptyStateProvider emptyStateProvider,
+            QuietModeManager quietModeManager,
             UserHandle workProfileUserHandle) {
-        super(context, /* currentPage */ 0, personalProfileUserHandle, workProfileUserHandle);
-        mItems = new ResolverProfileDescriptor[] {
-                createProfileDescriptor(adapter)
-        };
-        mShouldShowNoCrossProfileIntentsEmptyState = true;
+        this(
+                context,
+                ImmutableList.of(adapter),
+                emptyStateProvider,
+                quietModeManager,
+                /* defaultProfile= */ 0,
+                workProfileUserHandle,
+                new BottomPaddingOverrideSupplier());
     }
 
     ResolverMultiProfilePagerAdapter(Context context,
             ResolverListAdapter personalAdapter,
             ResolverListAdapter workAdapter,
+            EmptyStateProvider emptyStateProvider,
+            QuietModeManager quietModeManager,
             @Profile int defaultProfile,
-            UserHandle personalProfileUserHandle,
+            UserHandle workProfileUserHandle) {
+        this(
+                context,
+                ImmutableList.of(personalAdapter, workAdapter),
+                emptyStateProvider,
+                quietModeManager,
+                defaultProfile,
+                workProfileUserHandle,
+                new BottomPaddingOverrideSupplier());
+    }
+
+    private ResolverMultiProfilePagerAdapter(
+            Context context,
+            ImmutableList<ResolverListAdapter> listAdapters,
+            EmptyStateProvider emptyStateProvider,
+            QuietModeManager quietModeManager,
+            @Profile int defaultProfile,
             UserHandle workProfileUserHandle,
-            boolean shouldShowNoCrossProfileIntentsEmptyState) {
-        super(context, /* currentPage */ defaultProfile, personalProfileUserHandle,
-                workProfileUserHandle);
-        mItems = new ResolverProfileDescriptor[] {
-                createProfileDescriptor(personalAdapter),
-                createProfileDescriptor(workAdapter)
-        };
-        mShouldShowNoCrossProfileIntentsEmptyState = shouldShowNoCrossProfileIntentsEmptyState;
+            BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {
+        super(
+                context,
+                        listAdapter -> listAdapter,
+                        (listView, bindAdapter) -> listView.setAdapter(bindAdapter),
+                listAdapters,
+                emptyStateProvider,
+                quietModeManager,
+                defaultProfile,
+                workProfileUserHandle,
+                        () -> (ViewGroup) LayoutInflater.from(context).inflate(
+                                R.layout.resolver_list_per_profile, null, false),
+                bottomPaddingOverrideSupplier);
+        mBottomPaddingOverrideSupplier = bottomPaddingOverrideSupplier;
     }
 
-    private ResolverProfileDescriptor createProfileDescriptor(
-            ResolverListAdapter adapter) {
-        final LayoutInflater inflater = LayoutInflater.from(getContext());
-        final ViewGroup rootView =
-                (ViewGroup) inflater.inflate(R.layout.resolver_list_per_profile, null, false);
-        return new ResolverProfileDescriptor(rootView, adapter);
+    public void setUseLayoutWithDefault(boolean useLayoutWithDefault) {
+        mBottomPaddingOverrideSupplier.setUseLayoutWithDefault(useLayoutWithDefault);
     }
 
-    ListView getListViewForIndex(int index) {
-        return getItem(index).listView;
-    }
+    private static class BottomPaddingOverrideSupplier implements Supplier<Optional<Integer>> {
+        private boolean mUseLayoutWithDefault;
 
-    @Override
-    ResolverProfileDescriptor getItem(int pageIndex) {
-        return mItems[pageIndex];
-    }
-
-    @Override
-    int getItemCount() {
-        return mItems.length;
-    }
-
-    @Override
-    void setupListAdapter(int pageIndex) {
-        final ListView listView = getItem(pageIndex).listView;
-        listView.setAdapter(getItem(pageIndex).resolverListAdapter);
-    }
-
-    @Override
-    @VisibleForTesting
-    public ResolverListAdapter getAdapterForIndex(int pageIndex) {
-        return mItems[pageIndex].resolverListAdapter;
-    }
-
-    @Override
-    public ViewGroup instantiateItem(ViewGroup container, int position) {
-        setupListAdapter(position);
-        return super.instantiateItem(container, position);
-    }
-
-    @Override
-    @Nullable
-    ResolverListAdapter getListAdapterForUserHandle(UserHandle userHandle) {
-        if (getActiveListAdapter().getUserHandle().equals(userHandle)) {
-            return getActiveListAdapter();
-        } else if (getInactiveListAdapter() != null
-                && getInactiveListAdapter().getUserHandle().equals(userHandle)) {
-            return getInactiveListAdapter();
+        public void setUseLayoutWithDefault(boolean useLayoutWithDefault) {
+            mUseLayoutWithDefault = useLayoutWithDefault;
         }
-        return null;
-    }
 
-    @Override
-    @VisibleForTesting
-    public ResolverListAdapter getActiveListAdapter() {
-        return getAdapterForIndex(getCurrentPage());
-    }
-
-    @Override
-    @VisibleForTesting
-    public ResolverListAdapter getInactiveListAdapter() {
-        if (getCount() == 1) {
-            return null;
-        }
-        return getAdapterForIndex(1 - getCurrentPage());
-    }
-
-    @Override
-    public ResolverListAdapter getPersonalListAdapter() {
-        return getAdapterForIndex(PROFILE_PERSONAL);
-    }
-
-    @Override
-    @Nullable
-    public ResolverListAdapter getWorkListAdapter() {
-        return getAdapterForIndex(PROFILE_WORK);
-    }
-
-    @Override
-    ResolverListAdapter getCurrentRootAdapter() {
-        return getActiveListAdapter();
-    }
-
-    @Override
-    ListView getActiveAdapterView() {
-        return getListViewForIndex(getCurrentPage());
-    }
-
-    @Override
-    @Nullable
-    ViewGroup getInactiveAdapterView() {
-        if (getCount() == 1) {
-            return null;
-        }
-        return getListViewForIndex(1 - getCurrentPage());
-    }
-
-    @Override
-    String getMetricsCategory() {
-        return ResolverActivity.METRICS_CATEGORY_RESOLVER;
-    }
-
-    @Override
-    boolean allowShowNoCrossProfileIntentsEmptyState() {
-        return mShouldShowNoCrossProfileIntentsEmptyState;
-    }
-
-    @Override
-    protected void showWorkProfileOffEmptyState(ResolverListAdapter activeListAdapter,
-            View.OnClickListener listener) {
-        showEmptyState(activeListAdapter,
-                getWorkAppPausedTitle(),
-                /* subtitle = */ null,
-                listener);
-    }
-
-    @Override
-    protected void showNoPersonalToWorkIntentsEmptyState(ResolverListAdapter activeListAdapter) {
-        showEmptyState(activeListAdapter,
-                getCrossProfileBlockedTitle(),
-                getCantAccessWorkMessage());
-    }
-
-    @Override
-    protected void showNoWorkToPersonalIntentsEmptyState(ResolverListAdapter activeListAdapter) {
-        showEmptyState(activeListAdapter,
-                getCrossProfileBlockedTitle(),
-                getCantAccessPersonalMessage());
-    }
-
-    @Override
-    protected void showNoPersonalAppsAvailableEmptyState(ResolverListAdapter listAdapter) {
-        showEmptyState(listAdapter,
-                getNoPersonalAppsAvailableMessage(),
-                /* subtitle = */ null);
-    }
-
-    @Override
-    protected void showNoWorkAppsAvailableEmptyState(ResolverListAdapter listAdapter) {
-        showEmptyState(listAdapter,
-                getNoWorkAppsAvailableMessage(),
-                /* subtitle= */ null);
-    }
-
-    private String getWorkAppPausedTitle() {
-        return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
-                RESOLVER_WORK_PAUSED_TITLE,
-                () -> getContext().getString(R.string.resolver_turn_on_work_apps));
-    }
-
-    private String getCrossProfileBlockedTitle() {
-        return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
-                RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
-                () -> getContext().getString(R.string.resolver_cross_profile_blocked));
-    }
-
-    private String getCantAccessWorkMessage() {
-        return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
-                RESOLVER_CANT_ACCESS_WORK,
-                () -> getContext().getString(
-                        R.string.resolver_cant_access_work_apps_explanation));
-    }
-
-    private String getCantAccessPersonalMessage() {
-        return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
-                RESOLVER_CANT_ACCESS_PERSONAL,
-                () -> getContext().getString(
-                        R.string.resolver_cant_access_personal_apps_explanation));
-    }
-
-    private String getNoWorkAppsAvailableMessage() {
-        return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
-                RESOLVER_NO_WORK_APPS,
-                () -> getContext().getString(
-                        R.string.resolver_no_work_apps_available));
-    }
-
-    private String getNoPersonalAppsAvailableMessage() {
-        return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
-                RESOLVER_NO_PERSONAL_APPS,
-                () -> getContext().getString(
-                        R.string.resolver_no_personal_apps_available));
-    }
-
-    void setUseLayoutWithDefault(boolean useLayoutWithDefault) {
-        mUseLayoutWithDefault = useLayoutWithDefault;
-    }
-
-    @Override
-    protected void setupContainerPadding(View container) {
-        int bottom = mUseLayoutWithDefault ? container.getPaddingBottom() : 0;
-        container.setPadding(container.getPaddingLeft(), container.getPaddingTop(),
-                container.getPaddingRight(), bottom);
-    }
-
-    class ResolverProfileDescriptor extends ProfileDescriptor {
-        private ResolverListAdapter resolverListAdapter;
-        final ListView listView;
-        ResolverProfileDescriptor(ViewGroup rootView, ResolverListAdapter adapter) {
-            super(rootView);
-            resolverListAdapter = adapter;
-            listView = rootView.findViewById(com.android.internal.R.id.resolver_list);
+        @Override
+        public Optional<Integer> get() {
+            return mUseLayoutWithDefault ? Optional.empty() : Optional.of(0);
         }
     }
 }
diff --git a/java/src/com/android/intentresolver/ResolverViewPager.java b/java/src/com/android/intentresolver/ResolverViewPager.java
index 1c23452..0804a2b 100644
--- a/java/src/com/android/intentresolver/ResolverViewPager.java
+++ b/java/src/com/android/intentresolver/ResolverViewPager.java
@@ -21,7 +21,7 @@
 import android.view.MotionEvent;
 import android.view.View;
 
-import com.android.internal.widget.ViewPager;
+import androidx.viewpager.widget.ViewPager;
 
 /**
  * A {@link ViewPager} which wraps around its tallest child's height.
@@ -41,15 +41,6 @@
         super(context, attrs);
     }
 
-    public ResolverViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
-        super(context, attrs, defStyleAttr);
-    }
-
-    public ResolverViewPager(Context context, AttributeSet attrs,
-            int defStyleAttr, int defStyleRes) {
-        super(context, attrs, defStyleAttr, defStyleRes);
-    }
-
     @Override
     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
         super.onMeasure(widthMeasureSpec, heightMeasureSpec);
diff --git a/java/src/com/android/intentresolver/ShortcutSelectionLogic.java b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java
new file mode 100644
index 0000000..645b939
--- /dev/null
+++ b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java
@@ -0,0 +1,193 @@
+/*
+ * 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.app.prediction.AppTarget;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.service.chooser.ChooserTarget;
+import android.util.Log;
+
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.SelectableTargetInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+class ShortcutSelectionLogic {
+    private static final String TAG = "ShortcutSelectionLogic";
+    private static final boolean DEBUG = false;
+    private static final float PINNED_SHORTCUT_TARGET_SCORE_BOOST = 1000.f;
+    private static final int MAX_CHOOSER_TARGETS_PER_APP = 2;
+
+    private final int mMaxShortcutTargetsPerApp;
+    private final boolean mApplySharingAppLimits;
+
+    // Descending order
+    private final Comparator<ChooserTarget> mBaseTargetComparator =
+            (lhs, rhs) -> Float.compare(rhs.getScore(), lhs.getScore());
+
+    ShortcutSelectionLogic(
+            int maxShortcutTargetsPerApp,
+            boolean applySharingAppLimits) {
+        mMaxShortcutTargetsPerApp = maxShortcutTargetsPerApp;
+        mApplySharingAppLimits = applySharingAppLimits;
+    }
+
+    /**
+     * Evaluate targets for inclusion in the direct share area. May not be included
+     * if score is too low.
+     */
+    public boolean addServiceResults(
+            @Nullable DisplayResolveInfo origTarget,
+            float origTargetScore,
+            List<ChooserTarget> targets,
+            boolean isShortcutResult,
+            Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos,
+            Map<ChooserTarget, AppTarget> directShareToAppTargets,
+            Context userContext,
+            Intent targetIntent,
+            Intent referrerFillInIntent,
+            int maxRankedTargets,
+            List<TargetInfo> serviceTargets) {
+        if (DEBUG) {
+            Log.d(TAG, "addServiceResults "
+                    + (origTarget == null ? null : origTarget.getResolvedComponentName()) + ", "
+                    + targets.size()
+                    + " targets");
+        }
+        if (targets.size() == 0) {
+            return false;
+        }
+        Collections.sort(targets, mBaseTargetComparator);
+        final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp
+                : MAX_CHOOSER_TARGETS_PER_APP;
+        final int targetsLimit = mApplySharingAppLimits ? Math.min(targets.size(), maxTargets)
+                : targets.size();
+        float lastScore = 0;
+        boolean shouldNotify = false;
+        for (int i = 0, count = targetsLimit; i < count; i++) {
+            final ChooserTarget target = targets.get(i);
+            float targetScore = target.getScore();
+            if (mApplySharingAppLimits) {
+                targetScore *= origTargetScore;
+                if (i > 0 && targetScore >= lastScore) {
+                    // Apply a decay so that the top app can't crowd out everything else.
+                    // This incents ChooserTargetServices to define what's truly better.
+                    targetScore = lastScore * 0.95f;
+                }
+            }
+            ShortcutInfo shortcutInfo = isShortcutResult ? directShareToShortcutInfos.get(target)
+                    : null;
+            if ((shortcutInfo != null) && shortcutInfo.isPinned()) {
+                targetScore += PINNED_SHORTCUT_TARGET_SCORE_BOOST;
+            }
+            ResolveInfo backupResolveInfo;
+            Intent resolvedIntent;
+            if (origTarget == null) {
+                resolvedIntent = createResolvedIntentForCallerTarget(target, targetIntent);
+                backupResolveInfo = userContext.getPackageManager()
+                        .resolveActivity(
+                                resolvedIntent,
+                                PackageManager.ResolveInfoFlags.of(PackageManager.GET_META_DATA));
+            } else {
+                resolvedIntent = origTarget.getResolvedIntent();
+                backupResolveInfo = null;
+            }
+            boolean isInserted = insertServiceTarget(
+                    SelectableTargetInfo.newSelectableTargetInfo(
+                            origTarget,
+                            backupResolveInfo,
+                            resolvedIntent,
+                            target,
+                            targetScore,
+                            shortcutInfo,
+                            directShareToAppTargets.get(target),
+                            referrerFillInIntent),
+                    maxRankedTargets,
+                    serviceTargets);
+
+            shouldNotify |= isInserted;
+
+            if (DEBUG) {
+                Log.d(TAG, " => " + target + " score=" + targetScore
+                        + " base=" + target.getScore()
+                        + " lastScore=" + lastScore
+                        + " baseScore=" + origTargetScore
+                        + " applyAppLimit=" + mApplySharingAppLimits);
+            }
+
+            lastScore = targetScore;
+        }
+
+        return shouldNotify;
+    }
+
+    /**
+     * Creates a resolved intent for a caller-specified target.
+     * @param target, a caller-specified target.
+     * @param targetIntent, a target intent for the Chooser (see {@link Intent#EXTRA_INTENT}).
+     */
+    private static Intent createResolvedIntentForCallerTarget(
+            ChooserTarget target, Intent targetIntent) {
+        final Intent resolvedIntent = new Intent(targetIntent);
+        resolvedIntent.setComponent(target.getComponentName());
+        resolvedIntent.putExtras(target.getIntentExtras());
+        return resolvedIntent;
+    }
+
+    private boolean insertServiceTarget(
+            TargetInfo chooserTargetInfo,
+            int maxRankedTargets,
+            List<TargetInfo> serviceTargets) {
+
+        // Check for duplicates and abort if found
+        for (TargetInfo otherTargetInfo : serviceTargets) {
+            if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
+                return false;
+            }
+        }
+
+        int currentSize = serviceTargets.size();
+        final float newScore = chooserTargetInfo.getModifiedScore();
+        for (int i = 0; i < Math.min(currentSize, maxRankedTargets);
+                i++) {
+            final TargetInfo serviceTarget = serviceTargets.get(i);
+            if (serviceTarget == null) {
+                serviceTargets.set(i, chooserTargetInfo);
+                return true;
+            } else if (newScore > serviceTarget.getModifiedScore()) {
+                serviceTargets.add(i, chooserTargetInfo);
+                return true;
+            }
+        }
+
+        if (currentSize < maxRankedTargets) {
+            serviceTargets.add(chooserTargetInfo);
+            return true;
+        }
+
+        return false;
+    }
+}
diff --git a/java/src/com/android/intentresolver/SimpleIconFactory.java b/java/src/com/android/intentresolver/SimpleIconFactory.java
index b05b4f6..ec5179a 100644
--- a/java/src/com/android/intentresolver/SimpleIconFactory.java
+++ b/java/src/com/android/intentresolver/SimpleIconFactory.java
@@ -50,6 +50,8 @@
 import android.util.Pools.SynchronizedPool;
 import android.util.TypedValue;
 
+import com.android.internal.annotations.VisibleForTesting;
+
 import org.xmlpull.v1.XmlPullParser;
 
 import java.nio.ByteBuffer;
@@ -67,6 +69,7 @@
 
     private static final SynchronizedPool<SimpleIconFactory> sPool =
             new SynchronizedPool<>(Runtime.getRuntime().availableProcessors());
+    private static boolean sPoolEnabled = true;
 
     private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE;
     private static final float BLUR_FACTOR = 1.5f / 48;
@@ -90,7 +93,7 @@
      */
     @Deprecated
     public static SimpleIconFactory obtain(Context ctx) {
-        SimpleIconFactory instance = sPool.acquire();
+        SimpleIconFactory instance = sPoolEnabled ? sPool.acquire() : null;
         if (instance == null) {
             final ActivityManager am = (ActivityManager) ctx.getSystemService(ACTIVITY_SERVICE);
             final int iconDpi = (am == null) ? 0 : am.getLauncherLargeIconDensity();
@@ -104,6 +107,17 @@
         return instance;
     }
 
+    /**
+     * Enables or disables SimpleIconFactory objects pooling. It is enabled in production, you
+     * could use this method in tests and disable the pooling to make the icon rendering more
+     * deterministic because some sizing parameters will not be cached. Please ensure that you
+     * reset this value back after finishing the test.
+     */
+    @VisibleForTesting
+    public static void setPoolEnabled(boolean poolEnabled) {
+        sPoolEnabled = poolEnabled;
+    }
+
     private static int getAttrDimFromContext(Context ctx, @AttrRes int attrId, String errorMsg) {
         final Resources res = ctx.getResources();
         TypedValue outVal = new TypedValue();
diff --git a/java/src/com/android/intentresolver/TargetPresentationGetter.java b/java/src/com/android/intentresolver/TargetPresentationGetter.java
new file mode 100644
index 0000000..f8b3656
--- /dev/null
+++ b/java/src/com/android/intentresolver/TargetPresentationGetter.java
@@ -0,0 +1,267 @@
+/*
+ * 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.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+
+/**
+ * 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
+ * exception for applications that hold the right permission. Always attempts to use available
+ * 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
+ * appropriate concrete type.
+ *
+ * TODO: once this component (and its tests) are merged, it should be possible to refactor and
+ * vastly simplify by precomputing conditional logic at initialization.
+ */
+public abstract class TargetPresentationGetter {
+    private static final String TAG = "ResolverListAdapter";
+
+    /** Helper to build appropriate type-specific {@link TargetPresentationGetter} instances. */
+    public static class Factory {
+        private final Context mContext;
+        private final int mIconDpi;
+
+        public Factory(Context context, int iconDpi) {
+            mContext = context;
+            mIconDpi = iconDpi;
+        }
+
+        /** Make a {@link TargetPresentationGetter} for an {@link ActivityInfo}. */
+        public TargetPresentationGetter makePresentationGetter(ActivityInfo activityInfo) {
+            return new ActivityInfoPresentationGetter(mContext, mIconDpi, activityInfo);
+        }
+
+        /** Make a {@link TargetPresentationGetter} for a {@link ResolveInfo}. */
+        public TargetPresentationGetter makePresentationGetter(ResolveInfo resolveInfo) {
+            return new ResolveInfoPresentationGetter(mContext, mIconDpi, resolveInfo);
+        }
+    }
+
+    @Nullable
+    protected abstract Drawable getIconSubstituteInternal();
+
+    @Nullable
+    protected abstract String getAppSubLabelInternal();
+
+    @Nullable
+    protected abstract String getAppLabelForSubstitutePermission();
+
+    private Context mContext;
+    private final int mIconDpi;
+    private final boolean mHasSubstitutePermission;
+    private final ApplicationInfo mAppInfo;
+
+    protected PackageManager mPm;
+
+    /**
+     * Retrieve the image that should be displayed as the icon when this target is presented to the
+     * specified {@code userHandle}.
+     */
+    public Drawable getIcon(UserHandle userHandle) {
+        return new BitmapDrawable(mContext.getResources(), getIconBitmap(userHandle));
+    }
+
+    /**
+     * Retrieve the image that should be displayed as the icon when this target is presented to the
+     * specified {@code userHandle}.
+     */
+    public Bitmap getIconBitmap(@Nullable UserHandle userHandle) {
+        Drawable drawable = null;
+        if (mHasSubstitutePermission) {
+            drawable = getIconSubstituteInternal();
+        }
+
+        if (drawable == null) {
+            try {
+                if (mAppInfo.icon != 0) {
+                    drawable = loadIconFromResource(
+                            mPm.getResourcesForApplication(mAppInfo), mAppInfo.icon);
+                }
+            } catch (PackageManager.NameNotFoundException ignore) { }
+        }
+
+        // Fall back to ApplicationInfo#loadIcon if nothing has been loaded
+        if (drawable == null) {
+            drawable = mAppInfo.loadIcon(mPm);
+        }
+
+        SimpleIconFactory iconFactory = SimpleIconFactory.obtain(mContext);
+        Bitmap icon = iconFactory.createUserBadgedIconBitmap(drawable, userHandle);
+        iconFactory.recycle();
+
+        return icon;
+    }
+
+    /** Get the label to display for the target. */
+    public String getLabel() {
+        String label = null;
+        // Apps with the substitute permission will always show the activity label as the app label
+        // if provided.
+        if (mHasSubstitutePermission) {
+            label = getAppLabelForSubstitutePermission();
+        }
+
+        if (label == null) {
+            label = (String) mAppInfo.loadLabel(mPm);
+        }
+
+        return label;
+    }
+
+    /**
+     * Get the sublabel to display for the target. Clients are responsible for deduping their
+     * presentation if this returns the same value as {@link #getLabel()}.
+     * TODO: this class should take responsibility for that deduping internally so it's an
+     * authoritative record of exactly the content that should be presented.
+     */
+    public String getSubLabel() {
+        // Apps with the substitute permission will always show the resolve info label as the
+        // sublabel if provided
+        if (mHasSubstitutePermission) {
+            String appSubLabel = getAppSubLabelInternal();
+            // Use the resolve info label as sublabel if it is set
+            if (!TextUtils.isEmpty(appSubLabel) && !TextUtils.equals(appSubLabel, getLabel())) {
+                return appSubLabel;
+            }
+            return null;
+        }
+        return getAppSubLabelInternal();
+    }
+
+    protected String loadLabelFromResource(Resources res, int resId) {
+        return res.getString(resId);
+    }
+
+    @Nullable
+    protected Drawable loadIconFromResource(Resources res, int resId) {
+        return res.getDrawableForDensity(resId, mIconDpi);
+    }
+
+    private TargetPresentationGetter(Context context, int iconDpi, ApplicationInfo appInfo) {
+        mContext = context;
+        mPm = context.getPackageManager();
+        mAppInfo = appInfo;
+        mIconDpi = iconDpi;
+        mHasSubstitutePermission = (PackageManager.PERMISSION_GRANTED == mPm.checkPermission(
+                android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON,
+                mAppInfo.packageName));
+    }
+
+    /** Loads the icon and label for the provided ResolveInfo. */
+    private static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter {
+        private final ResolveInfo mResolveInfo;
+
+        ResolveInfoPresentationGetter(
+                Context context, int iconDpi, ResolveInfo resolveInfo) {
+            super(context, iconDpi, resolveInfo.activityInfo);
+            mResolveInfo = resolveInfo;
+        }
+
+        @Override
+        protected Drawable getIconSubstituteInternal() {
+            Drawable drawable = null;
+            try {
+                // Do not use ResolveInfo#getIconResource() as it defaults to the app
+                if (mResolveInfo.resolvePackageName != null && mResolveInfo.icon != 0) {
+                    drawable = loadIconFromResource(
+                            mPm.getResourcesForApplication(mResolveInfo.resolvePackageName),
+                            mResolveInfo.icon);
+                }
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
+                        + "couldn't find resources for package", e);
+            }
+
+            // Fall back to ActivityInfo if no icon is found via ResolveInfo
+            if (drawable == null) {
+                drawable = super.getIconSubstituteInternal();
+            }
+
+            return drawable;
+        }
+
+        @Override
+        protected String getAppSubLabelInternal() {
+            // Will default to app name if no intent filter or activity label set, make sure to
+            // check if subLabel matches label before final display
+            return mResolveInfo.loadLabel(mPm).toString();
+        }
+
+        @Override
+        protected String getAppLabelForSubstitutePermission() {
+            // Will default to app name if no activity label set
+            return mResolveInfo.getComponentInfo().loadLabel(mPm).toString();
+        }
+    }
+
+    /** Loads the icon and label for the provided {@link ActivityInfo}. */
+    private static class ActivityInfoPresentationGetter extends TargetPresentationGetter {
+        private final ActivityInfo mActivityInfo;
+
+        ActivityInfoPresentationGetter(
+                Context context, int iconDpi, ActivityInfo activityInfo) {
+            super(context, iconDpi, activityInfo.applicationInfo);
+            mActivityInfo = activityInfo;
+        }
+
+        @Override
+        protected Drawable getIconSubstituteInternal() {
+            Drawable drawable = null;
+            try {
+                // Do not use ActivityInfo#getIconResource() as it defaults to the app
+                if (mActivityInfo.icon != 0) {
+                    drawable = loadIconFromResource(
+                            mPm.getResourcesForApplication(mActivityInfo.applicationInfo),
+                            mActivityInfo.icon);
+                }
+            } catch (PackageManager.NameNotFoundException e) {
+                Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
+                        + "couldn't find resources for package", e);
+            }
+
+            return drawable;
+        }
+
+        @Override
+        protected String getAppSubLabelInternal() {
+            // Will default to app name if no activity label set, make sure to check if subLabel
+            // matches label before final display
+            return (String) mActivityInfo.loadLabel(mPm);
+        }
+
+        @Override
+        protected String getAppLabelForSubstitutePermission() {
+            return getAppSubLabelInternal();
+        }
+    }
+}
diff --git a/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java b/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java
new file mode 100644
index 0000000..b7c8990
--- /dev/null
+++ b/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java
@@ -0,0 +1,114 @@
+/*
+ * 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 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.internal.R;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyState;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStateProvider;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.OnSwitchOnWorkSelectedListener;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager;
+
+/**
+ * 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 QuietModeManager mQuietModeManager;
+    private final String mMetricsCategory;
+    private final OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
+    private final Context mContext;
+
+    public WorkProfilePausedEmptyStateProvider(@NonNull Context context,
+            @Nullable UserHandle workProfileUserHandle,
+            @NonNull QuietModeManager quietModeManager,
+            @Nullable OnSwitchOnWorkSelectedListener onSwitchOnWorkSelectedListener,
+            @NonNull String metricsCategory) {
+        mContext = context;
+        mWorkProfileUserHandle = workProfileUserHandle;
+        mQuietModeManager = quietModeManager;
+        mMetricsCategory = metricsCategory;
+        mOnSwitchOnWorkSelectedListener = onSwitchOnWorkSelectedListener;
+    }
+
+    @Nullable
+    @Override
+    public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+        if (!resolverListAdapter.getUserHandle().equals(mWorkProfileUserHandle)
+                || !mQuietModeManager.isQuietModeEnabled(mWorkProfileUserHandle)
+                || 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();
+            }
+            mQuietModeManager.requestQuietModeEnabled(false, mWorkProfileUserHandle);
+        }, 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/chooser/ChooserTargetInfo.java b/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
index 1c76307..8b9bfb3 100644
--- a/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
@@ -16,38 +16,27 @@
 
 package com.android.intentresolver.chooser;
 
-import android.service.chooser.ChooserTarget;
-import android.text.TextUtils;
+import java.util.ArrayList;
+import java.util.Arrays;
 
 /**
  * A TargetInfo for Direct Share. Includes a {@link ChooserTarget} representing the
  * Direct Share deep link into an application.
  */
-public interface ChooserTargetInfo extends TargetInfo {
-    float getModifiedScore();
+public abstract class ChooserTargetInfo implements TargetInfo {
 
-    ChooserTarget getChooserTarget();
+    @Override
+    public final boolean isChooserTargetInfo() {
+        return true;
+    }
 
-    /**
-     * Do not label as 'equals', since this doesn't quite work
-     * as intended with java 8.
-     */
-    default boolean isSimilar(ChooserTargetInfo other) {
-        if (other == null) return false;
-
-        ChooserTarget ct1 = getChooserTarget();
-        ChooserTarget ct2 = other.getChooserTarget();
-
-        // If either is null, there is not enough info to make an informed decision
-        // about equality, so just exit
-        if (ct1 == null || ct2 == null) return false;
-
-        if (ct1.getComponentName().equals(ct2.getComponentName())
-                && TextUtils.equals(getDisplayLabel(), other.getDisplayLabel())
-                && TextUtils.equals(getExtendedInfo(), other.getExtendedInfo())) {
-            return true;
+    @Override
+    public ArrayList<DisplayResolveInfo> getAllDisplayTargets() {
+        // TODO: consider making this the default behavior for all `TargetInfo` implementations
+        // (if it's reasonable for `DisplayResolveInfo.getDisplayResolveInfo()` to return `this`).
+        if (getDisplayResolveInfo() == null) {
+            return new ArrayList<>();
         }
-
-        return false;
+        return new ArrayList<>(Arrays.asList(getDisplayResolveInfo()));
     }
 }
diff --git a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
index e7ffe3c..db5ae0b 100644
--- a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
+++ b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
@@ -20,84 +20,122 @@
 import android.annotation.Nullable;
 import android.app.Activity;
 import android.content.ComponentName;
-import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ActivityInfo;
 import android.content.pm.ApplicationInfo;
 import android.content.pm.ResolveInfo;
-import android.graphics.drawable.Drawable;
 import android.os.Bundle;
-import android.os.Parcel;
-import android.os.Parcelable;
 import android.os.UserHandle;
 
 import com.android.intentresolver.ResolverActivity;
-import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;
+import com.android.intentresolver.TargetPresentationGetter;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.List;
 
 /**
  * A TargetInfo plus additional information needed to render it (such as icon and label) and
  * resolve it to an activity.
  */
-public class DisplayResolveInfo implements TargetInfo, Parcelable {
+public class DisplayResolveInfo implements TargetInfo {
     private final ResolveInfo mResolveInfo;
     private CharSequence mDisplayLabel;
-    private Drawable mDisplayIcon;
     private CharSequence mExtendedInfo;
     private final Intent mResolvedIntent;
     private final List<Intent> mSourceIntents = new ArrayList<>();
-    private boolean mIsSuspended;
-    private ResolveInfoPresentationGetter mResolveInfoPresentationGetter;
+    private final boolean mIsSuspended;
+    private TargetPresentationGetter mPresentationGetter;
     private boolean mPinned = false;
+    private final IconHolder mDisplayIconHolder = new SettableIconHolder();
 
-    public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, Intent pOrigIntent,
-            ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
-        this(originalIntent, pri, null /*mDisplayLabel*/, null /*mExtendedInfo*/, pOrigIntent,
-                resolveInfoPresentationGetter);
+    /** Create a new {@code DisplayResolveInfo} instance. */
+    public static DisplayResolveInfo newDisplayResolveInfo(
+            Intent originalIntent,
+            ResolveInfo resolveInfo,
+            @NonNull Intent resolvedIntent,
+            @Nullable TargetPresentationGetter presentationGetter) {
+        return newDisplayResolveInfo(
+                originalIntent,
+                resolveInfo,
+                /* displayLabel=*/ null,
+                /* extendedInfo=*/ null,
+                resolvedIntent,
+                presentationGetter);
     }
 
-    public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel,
-            CharSequence pInfo, @NonNull Intent resolvedIntent,
-            @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
+    /** Create a new {@code DisplayResolveInfo} instance. */
+    public static DisplayResolveInfo newDisplayResolveInfo(
+            Intent originalIntent,
+            ResolveInfo resolveInfo,
+            CharSequence displayLabel,
+            CharSequence extendedInfo,
+            @NonNull Intent resolvedIntent,
+            @Nullable TargetPresentationGetter presentationGetter) {
+        return new DisplayResolveInfo(
+                originalIntent,
+                resolveInfo,
+                displayLabel,
+                extendedInfo,
+                resolvedIntent,
+                presentationGetter);
+    }
+
+    private DisplayResolveInfo(
+            Intent originalIntent,
+            ResolveInfo resolveInfo,
+            CharSequence displayLabel,
+            CharSequence extendedInfo,
+            @NonNull Intent resolvedIntent,
+            @Nullable TargetPresentationGetter presentationGetter) {
         mSourceIntents.add(originalIntent);
-        mResolveInfo = pri;
-        mDisplayLabel = pLabel;
-        mExtendedInfo = pInfo;
-        mResolveInfoPresentationGetter = resolveInfoPresentationGetter;
+        mResolveInfo = resolveInfo;
+        mDisplayLabel = displayLabel;
+        mExtendedInfo = extendedInfo;
+        mPresentationGetter = presentationGetter;
+
+        final ActivityInfo ai = mResolveInfo.activityInfo;
+        mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
 
         final Intent intent = new Intent(resolvedIntent);
         intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT
                 | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
-        final ActivityInfo ai = mResolveInfo.activityInfo;
         intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name));
-
-        mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
-
         mResolvedIntent = intent;
     }
 
-    private DisplayResolveInfo(DisplayResolveInfo other, Intent fillInIntent, int flags,
-            ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
+    private DisplayResolveInfo(
+            DisplayResolveInfo other,
+            Intent fillInIntent,
+            int flags,
+            TargetPresentationGetter presentationGetter) {
         mSourceIntents.addAll(other.getAllSourceIntents());
         mResolveInfo = other.mResolveInfo;
+        mIsSuspended = other.mIsSuspended;
         mDisplayLabel = other.mDisplayLabel;
-        mDisplayIcon = other.mDisplayIcon;
         mExtendedInfo = other.mExtendedInfo;
         mResolvedIntent = new Intent(other.mResolvedIntent);
         mResolvedIntent.fillIn(fillInIntent, flags);
-        mResolveInfoPresentationGetter = resolveInfoPresentationGetter;
+        mPresentationGetter = presentationGetter;
+
+        mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());
     }
 
-    DisplayResolveInfo(DisplayResolveInfo other) {
+    protected DisplayResolveInfo(DisplayResolveInfo other) {
         mSourceIntents.addAll(other.getAllSourceIntents());
         mResolveInfo = other.mResolveInfo;
+        mIsSuspended = other.mIsSuspended;
         mDisplayLabel = other.mDisplayLabel;
-        mDisplayIcon = other.mDisplayIcon;
         mExtendedInfo = other.mExtendedInfo;
         mResolvedIntent = other.mResolvedIntent;
-        mResolveInfoPresentationGetter = other.mResolveInfoPresentationGetter;
+        mPresentationGetter = other.mPresentationGetter;
+
+        mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());
+    }
+
+    @Override
+    public final boolean isDisplayResolveInfo() {
+        return true;
     }
 
     public ResolveInfo getResolveInfo() {
@@ -105,9 +143,9 @@
     }
 
     public CharSequence getDisplayLabel() {
-        if (mDisplayLabel == null && mResolveInfoPresentationGetter != null) {
-            mDisplayLabel = mResolveInfoPresentationGetter.getLabel();
-            mExtendedInfo = mResolveInfoPresentationGetter.getSubLabel();
+        if (mDisplayLabel == null && mPresentationGetter != null) {
+            mDisplayLabel = mPresentationGetter.getLabel();
+            mExtendedInfo = mPresentationGetter.getSubLabel();
         }
         return mDisplayLabel;
     }
@@ -124,13 +162,14 @@
         mExtendedInfo = extendedInfo;
     }
 
-    public Drawable getDisplayIcon(Context context) {
-        return mDisplayIcon;
+    @Override
+    public IconHolder getDisplayIconHolder() {
+        return mDisplayIconHolder;
     }
 
     @Override
     public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
-        return new DisplayResolveInfo(this, fillInIntent, flags, mResolveInfoPresentationGetter);
+        return new DisplayResolveInfo(this, fillInIntent, flags, mPresentationGetter);
     }
 
     @Override
@@ -138,18 +177,15 @@
         return mSourceIntents;
     }
 
+    @Override
+    public ArrayList<DisplayResolveInfo> getAllDisplayTargets() {
+        return new ArrayList<>(Arrays.asList(this));
+    }
+
     public void addAlternateSourceIntent(Intent alt) {
         mSourceIntents.add(alt);
     }
 
-    public void setDisplayIcon(Drawable icon) {
-        mDisplayIcon = icon;
-    }
-
-    public boolean hasDisplayIcon() {
-        return mDisplayIcon != null;
-    }
-
     public CharSequence getExtendedInfo() {
         return mExtendedInfo;
     }
@@ -172,14 +208,14 @@
 
     @Override
     public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
-        prepareIntentForCrossProfileLaunch(mResolvedIntent, userId);
+        TargetInfo.prepareIntentForCrossProfileLaunch(mResolvedIntent, userId);
         activity.startActivityAsCaller(mResolvedIntent, options, false, userId);
         return true;
     }
 
     @Override
     public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
-        prepareIntentForCrossProfileLaunch(mResolvedIntent, user.getIdentifier());
+        TargetInfo.prepareIntentForCrossProfileLaunch(mResolvedIntent, user.getIdentifier());
         activity.startActivityAsUser(mResolvedIntent, options, user);
         return false;
     }
@@ -196,48 +232,4 @@
     public void setPinned(boolean pinned) {
         mPinned = pinned;
     }
-
-    @Override
-    public int describeContents() {
-        return 0;
-    }
-
-    @Override
-    public void writeToParcel(Parcel dest, int flags) {
-        dest.writeCharSequence(mDisplayLabel);
-        dest.writeCharSequence(mExtendedInfo);
-        dest.writeParcelable(mResolvedIntent, 0);
-        dest.writeTypedList(mSourceIntents);
-        dest.writeBoolean(mIsSuspended);
-        dest.writeBoolean(mPinned);
-        dest.writeParcelable(mResolveInfo, 0);
-    }
-
-    public static final Parcelable.Creator<DisplayResolveInfo> CREATOR =
-            new Parcelable.Creator<DisplayResolveInfo>() {
-        public DisplayResolveInfo createFromParcel(Parcel in) {
-            return new DisplayResolveInfo(in);
-        }
-
-        public DisplayResolveInfo[] newArray(int size) {
-            return new DisplayResolveInfo[size];
-        }
-    };
-
-    private static void prepareIntentForCrossProfileLaunch(Intent intent, int targetUserId) {
-        final int currentUserId = UserHandle.myUserId();
-        if (targetUserId != currentUserId) {
-            intent.fixUris(currentUserId);
-        }
-    }
-
-    private DisplayResolveInfo(Parcel in) {
-        mDisplayLabel = in.readCharSequence();
-        mExtendedInfo = in.readCharSequence();
-        mResolvedIntent = in.readParcelable(null /* ClassLoader */, android.content.Intent.class);
-        in.readTypedList(mSourceIntents, Intent.CREATOR);
-        mIsSuspended = in.readBoolean();
-        mPinned = in.readBoolean();
-        mResolveInfo = in.readParcelable(null /* ClassLoader */, android.content.pm.ResolveInfo.class);
-    }
 }
diff --git a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
index 5133d99..29f00a3 100644
--- a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
+++ b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
@@ -23,6 +23,7 @@
 import com.android.intentresolver.ResolverActivity;
 
 import java.util.ArrayList;
+import java.util.List;
 
 /**
  * Represents a "stack" of chooser targets for various activities within the same component.
@@ -30,18 +31,31 @@
 public class MultiDisplayResolveInfo extends DisplayResolveInfo {
 
     ArrayList<DisplayResolveInfo> mTargetInfos = new ArrayList<>();
-    // We'll use this DRI for basic presentation info - eg icon, name.
-    final DisplayResolveInfo mBaseInfo;
+
     // Index of selected target
     private int mSelected = -1;
 
     /**
-     * @param firstInfo A representative DRI to use for the main icon, title, etc for this Info.
+     * @param targetInfos A list of targets in this stack. The first item is treated as the
+     * "representative" that provides the main icon, title, etc.
      */
-    public MultiDisplayResolveInfo(String packageName, DisplayResolveInfo firstInfo) {
-        super(firstInfo);
-        mBaseInfo = firstInfo;
-        mTargetInfos.add(firstInfo);
+    public static MultiDisplayResolveInfo newMultiDisplayResolveInfo(
+            List<DisplayResolveInfo> targetInfos) {
+        return new MultiDisplayResolveInfo(targetInfos);
+    }
+
+    /**
+     * @param targetInfos A list of targets in this stack. The first item is treated as the
+     * "representative" that provides the main icon, title, etc.
+     */
+    private MultiDisplayResolveInfo(List<DisplayResolveInfo> targetInfos) {
+        super(targetInfos.get(0));
+        mTargetInfos = new ArrayList<>(targetInfos);
+    }
+
+    @Override
+    public final boolean isMultiDisplayResolveInfo() {
+        return true;
     }
 
     @Override
@@ -51,16 +65,12 @@
     }
 
     /**
-     * Add another DisplayResolveInfo to the list included for this target.
+     * List of all {@link DisplayResolveInfo}s included in this target.
+     * TODO: provide as a generic {@code List<DisplayResolveInfo>} once {@link ChooserActivity}
+     * stops requiring the signature to match that of the other "lists" it builds up.
      */
-    public void addTarget(DisplayResolveInfo target) {
-        mTargetInfos.add(target);
-    }
-
-    /**
-     * List of all DisplayResolveInfos included in this target.
-     */
-    public ArrayList<DisplayResolveInfo> getTargets() {
+    @Override
+    public ArrayList<DisplayResolveInfo> getAllDisplayTargets() {
         return mTargetInfos;
     }
 
@@ -96,5 +106,4 @@
     public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
         return mTargetInfos.get(mSelected).startAsUser(activity, options, user);
     }
-
 }
diff --git a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
index 220870f..d633337 100644
--- a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
@@ -18,12 +18,15 @@
 
 import android.app.Activity;
 import android.content.ComponentName;
+import android.content.Context;
 import android.content.Intent;
 import android.content.pm.ResolveInfo;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.UserHandle;
-import android.service.chooser.ChooserTarget;
 
+import com.android.intentresolver.R;
 import com.android.intentresolver.ResolverActivity;
 
 import java.util.List;
@@ -32,7 +35,55 @@
  * Distinguish between targets that selectable by the user, vs those that are
  * placeholders for the system while information is loading in an async manner.
  */
-public abstract class NotSelectableTargetInfo implements ChooserTargetInfo {
+public abstract class NotSelectableTargetInfo extends ChooserTargetInfo {
+    /** Create a non-selectable {@link TargetInfo} with no content. */
+    public static TargetInfo newEmptyTargetInfo() {
+        return new NotSelectableTargetInfo() {
+                @Override
+                public boolean isEmptyTargetInfo() {
+                    return true;
+                }
+            };
+    }
+
+    /**
+     * Create a non-selectable {@link TargetInfo} with placeholder content to be displayed
+     * unless/until it can be replaced by the result of a pending asynchronous load.
+     */
+    public static TargetInfo newPlaceHolderTargetInfo(Context context) {
+        return new NotSelectableTargetInfo() {
+                @Override
+                public boolean isPlaceHolderTargetInfo() {
+                    return true;
+                }
+
+                @Override
+                public IconHolder getDisplayIconHolder() {
+                    return new IconHolder() {
+                        @Override
+                        public Drawable getDisplayIcon() {
+                            AnimatedVectorDrawable avd = (AnimatedVectorDrawable)
+                                    context.getDrawable(
+                                            R.drawable.chooser_direct_share_icon_placeholder);
+                            avd.start();  // Start animation after generation.
+                            return avd;
+                        }
+
+                        @Override
+                        public void setDisplayIcon(Drawable icon) {}
+                    };
+                }
+
+                @Override
+                public boolean hasDisplayIcon() {
+                    return true;
+                }
+            };
+    }
+
+    public final boolean isNotSelectableTargetInfo() {
+        return true;
+    }
 
     public Intent getResolvedIntent() {
         return null;
@@ -78,10 +129,6 @@
         return -0.1f;
     }
 
-    public ChooserTarget getChooserTarget() {
-        return null;
-    }
-
     public boolean isSuspended() {
         return false;
     }
@@ -89,4 +136,17 @@
     public boolean isPinned() {
         return false;
     }
+
+    @Override
+    public IconHolder getDisplayIconHolder() {
+        return new IconHolder() {
+            @Override
+            public Drawable getDisplayIcon() {
+                return null;
+            }
+
+            @Override
+            public void setDisplayIcon(Drawable icon) {}
+        };
+    }
 }
diff --git a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
index 1610d0f..3ab5017 100644
--- a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
@@ -18,31 +18,23 @@
 
 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.pm.ActivityInfo;
-import android.content.pm.ApplicationInfo;
-import android.content.pm.LauncherApps;
-import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.pm.ShortcutInfo;
-import android.graphics.Bitmap;
-import android.graphics.drawable.BitmapDrawable;
-import android.graphics.drawable.Drawable;
 import android.graphics.drawable.Icon;
 import android.os.Bundle;
 import android.os.UserHandle;
+import android.provider.DeviceConfig;
 import android.service.chooser.ChooserTarget;
 import android.text.SpannableStringBuilder;
+import android.util.HashedStringCache;
 import android.util.Log;
 
-import com.android.intentresolver.ChooserActivity;
 import com.android.intentresolver.ResolverActivity;
-import com.android.intentresolver.ResolverListAdapter.ActivityInfoPresentationGetter;
-import com.android.intentresolver.SimpleIconFactory;
-
-import com.android.internal.annotations.GuardedBy;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -51,237 +43,312 @@
  * Live target, currently selectable by the user.
  * @see NotSelectableTargetInfo
  */
-public final class SelectableTargetInfo implements ChooserTargetInfo {
+public final class SelectableTargetInfo extends ChooserTargetInfo {
     private static final String TAG = "SelectableTargetInfo";
 
-    private final Context mContext;
+    private interface TargetHashProvider {
+        HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context);
+    }
+
+    private interface TargetActivityStarter {
+        boolean start(Activity activity, Bundle options);
+        boolean startAsCaller(Activity activity, Bundle options, int userId);
+        boolean startAsUser(Activity activity, Bundle options, UserHandle user);
+    }
+
+    private static final String HASHED_STRING_CACHE_TAG = "ChooserActivity";  // For legacy reasons.
+    private static final int DEFAULT_SALT_EXPIRATION_DAYS = 7;
+
+    private final int mMaxHashSaltDays = DeviceConfig.getInt(
+            DeviceConfig.NAMESPACE_SYSTEMUI,
+            SystemUiDeviceConfigFlags.HASH_SALT_MAX_DAYS,
+            DEFAULT_SALT_EXPIRATION_DAYS);
+
+    @Nullable
     private final DisplayResolveInfo mSourceInfo;
+    @Nullable
     private final ResolveInfo mBackupResolveInfo;
-    private final ChooserTarget mChooserTarget;
+    private final Intent mResolvedIntent;
     private final String mDisplayLabel;
-    private final PackageManager mPm;
-    private final SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator;
-    @GuardedBy("this")
-    private ShortcutInfo mShortcutInfo;
-    private Drawable mBadgeIcon = null;
-    private CharSequence mBadgeContentDescription;
-    @GuardedBy("this")
-    private Drawable mDisplayIcon;
-    private final Intent mFillInIntent;
+    @Nullable
+    private final AppTarget mAppTarget;
+    @Nullable
+    private final ShortcutInfo mShortcutInfo;
+
+    private final ComponentName mChooserTargetComponentName;
+    private final CharSequence mChooserTargetUnsanitizedTitle;
+    private final Icon mChooserTargetIcon;
+    private final Bundle mChooserTargetIntentExtras;
     private final int mFillInFlags;
     private final boolean mIsPinned;
     private final float mModifiedScore;
-    private boolean mIsSuspended = false;
+    private final boolean mIsSuspended;
+    private final ComponentName mResolvedComponentName;
+    private final Intent mBaseIntentToSend;
+    private final ResolveInfo mResolveInfo;
+    private final List<Intent> mAllSourceIntents;
+    private final IconHolder mDisplayIconHolder = new SettableIconHolder();
+    private final TargetHashProvider mHashProvider;
+    private final TargetActivityStarter mActivityStarter;
 
-    public SelectableTargetInfo(Context context, DisplayResolveInfo sourceInfo,
+    /**
+     * A refinement intent from the caller, if any (see
+     * {@link Intent#EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER})
+     */
+    private final Intent mFillInIntent;
+
+    /**
+     * An intent containing referrer URI (see {@link Activity#getReferrer()} (possibly {@code null})
+     * in its extended data under the key {@link Intent#EXTRA_REFERRER}.
+     */
+    private final Intent mReferrerFillInIntent;
+
+    /**
+     * Create a new {@link TargetInfo} instance representing a selectable target. Some target
+     * parameters are copied over from the (deprecated) legacy {@link ChooserTarget} structure.
+     *
+     * @deprecated Use the overload that doesn't call for a {@link ChooserTarget}.
+     */
+    @Deprecated
+    public static TargetInfo newSelectableTargetInfo(
+            @Nullable DisplayResolveInfo sourceInfo,
+            @Nullable ResolveInfo backupResolveInfo,
+            Intent resolvedIntent,
             ChooserTarget chooserTarget,
-            float modifiedScore, SelectableTargetInfoCommunicator selectableTargetInfoComunicator,
-            @Nullable ShortcutInfo shortcutInfo) {
-        mContext = context;
+            float modifiedScore,
+            @Nullable ShortcutInfo shortcutInfo,
+            @Nullable AppTarget appTarget,
+            Intent referrerFillInIntent) {
+        return newSelectableTargetInfo(
+                sourceInfo,
+                backupResolveInfo,
+                resolvedIntent,
+                chooserTarget.getComponentName(),
+                chooserTarget.getTitle(),
+                chooserTarget.getIcon(),
+                chooserTarget.getIntentExtras(),
+                modifiedScore,
+                shortcutInfo,
+                appTarget,
+                referrerFillInIntent);
+    }
+
+    /**
+     * Create a new {@link TargetInfo} instance representing a selectable target. `chooserTarget*`
+     * parameters were historically retrieved from (now-deprecated) {@link ChooserTarget} structures
+     * even when the {@link TargetInfo} was a system (internal) synthesized target that never needed
+     * to be represented as a {@link ChooserTarget}. The values passed here are copied in directly
+     * as if they had been provided in the legacy representation.
+     *
+     * TODO: clarify semantics of how clients use the `getChooserTarget*()` methods; refactor/rename
+     * to avoid making reference to the legacy type; and reflect the improved semantics in the
+     * signature (and documentation) of this method.
+     */
+    public static TargetInfo newSelectableTargetInfo(
+            @Nullable DisplayResolveInfo sourceInfo,
+            @Nullable ResolveInfo backupResolveInfo,
+            Intent resolvedIntent,
+            ComponentName chooserTargetComponentName,
+            CharSequence chooserTargetUnsanitizedTitle,
+            Icon chooserTargetIcon,
+            @Nullable Bundle chooserTargetIntentExtras,
+            float modifiedScore,
+            @Nullable ShortcutInfo shortcutInfo,
+            @Nullable AppTarget appTarget,
+            Intent referrerFillInIntent) {
+        return new SelectableTargetInfo(
+                sourceInfo,
+                backupResolveInfo,
+                resolvedIntent,
+                chooserTargetComponentName,
+                chooserTargetUnsanitizedTitle,
+                chooserTargetIcon,
+                chooserTargetIntentExtras,
+                modifiedScore,
+                shortcutInfo,
+                appTarget,
+                referrerFillInIntent,
+                /* fillInIntent = */ null,
+                /* fillInFlags = */ 0);
+    }
+
+    private SelectableTargetInfo(
+            @Nullable DisplayResolveInfo sourceInfo,
+            @Nullable ResolveInfo backupResolveInfo,
+            Intent resolvedIntent,
+            ComponentName chooserTargetComponentName,
+            CharSequence chooserTargetUnsanitizedTitle,
+            Icon chooserTargetIcon,
+            Bundle chooserTargetIntentExtras,
+            float modifiedScore,
+            @Nullable ShortcutInfo shortcutInfo,
+            @Nullable AppTarget appTarget,
+            Intent referrerFillInIntent,
+            @Nullable Intent fillInIntent,
+            int fillInFlags) {
         mSourceInfo = sourceInfo;
-        mChooserTarget = chooserTarget;
+        mBackupResolveInfo = backupResolveInfo;
+        mResolvedIntent = resolvedIntent;
         mModifiedScore = modifiedScore;
-        mPm = mContext.getPackageManager();
-        mSelectableTargetInfoCommunicator = selectableTargetInfoComunicator;
         mShortcutInfo = shortcutInfo;
-        mIsPinned = shortcutInfo != null && shortcutInfo.isPinned();
-        if (sourceInfo != null) {
-            final ResolveInfo ri = sourceInfo.getResolveInfo();
-            if (ri != null) {
-                final ActivityInfo ai = ri.activityInfo;
-                if (ai != null && ai.applicationInfo != null) {
-                    final PackageManager pm = mContext.getPackageManager();
-                    mBadgeIcon = pm.getApplicationIcon(ai.applicationInfo);
-                    mBadgeContentDescription = pm.getApplicationLabel(ai.applicationInfo);
-                    mIsSuspended =
-                            (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
-                }
-            }
-        }
-
-        if (sourceInfo != null) {
-            mBackupResolveInfo = null;
-        } else {
-            mBackupResolveInfo =
-                    mContext.getPackageManager().resolveActivity(getResolvedIntent(), 0);
-        }
-
-        mFillInIntent = null;
-        mFillInFlags = 0;
-
-        mDisplayLabel = sanitizeDisplayLabel(chooserTarget.getTitle());
-    }
-
-    private SelectableTargetInfo(SelectableTargetInfo other,
-            Intent fillInIntent, int flags) {
-        mContext = other.mContext;
-        mPm = other.mPm;
-        mSelectableTargetInfoCommunicator = other.mSelectableTargetInfoCommunicator;
-        mSourceInfo = other.mSourceInfo;
-        mBackupResolveInfo = other.mBackupResolveInfo;
-        mChooserTarget = other.mChooserTarget;
-        mBadgeIcon = other.mBadgeIcon;
-        mBadgeContentDescription = other.mBadgeContentDescription;
-        synchronized (other) {
-            mShortcutInfo = other.mShortcutInfo;
-            mDisplayIcon = other.mDisplayIcon;
-        }
+        mAppTarget = appTarget;
+        mReferrerFillInIntent = referrerFillInIntent;
         mFillInIntent = fillInIntent;
-        mFillInFlags = flags;
-        mModifiedScore = other.mModifiedScore;
-        mIsPinned = other.mIsPinned;
+        mFillInFlags = fillInFlags;
+        mChooserTargetComponentName = chooserTargetComponentName;
+        mChooserTargetUnsanitizedTitle = chooserTargetUnsanitizedTitle;
+        mChooserTargetIcon = chooserTargetIcon;
+        mChooserTargetIntentExtras = chooserTargetIntentExtras;
 
-        mDisplayLabel = sanitizeDisplayLabel(mChooserTarget.getTitle());
+        mIsPinned = (shortcutInfo != null) && shortcutInfo.isPinned();
+        mDisplayLabel = sanitizeDisplayLabel(mChooserTargetUnsanitizedTitle);
+        mIsSuspended = (mSourceInfo != null) && mSourceInfo.isSuspended();
+        mResolveInfo = (mSourceInfo != null) ? mSourceInfo.getResolveInfo() : mBackupResolveInfo;
+
+        mResolvedComponentName = getResolvedComponentName(mSourceInfo, mBackupResolveInfo);
+
+        mAllSourceIntents = getAllSourceIntents(sourceInfo);
+
+        mBaseIntentToSend = getBaseIntentToSend(
+                mResolvedIntent,
+                mFillInIntent,
+                mFillInFlags,
+                mReferrerFillInIntent);
+
+        mHashProvider = context -> {
+            final String plaintext =
+                    getChooserTargetComponentName().getPackageName()
+                    + mChooserTargetUnsanitizedTitle;
+            return HashedStringCache.getInstance().hashString(
+                    context,
+                    HASHED_STRING_CACHE_TAG,
+                    plaintext,
+                    mMaxHashSaltDays);
+        };
+
+        mActivityStarter = new TargetActivityStarter() {
+            @Override
+            public boolean start(Activity activity, Bundle options) {
+                throw new RuntimeException("ChooserTargets should be started as caller.");
+            }
+
+            @Override
+            public boolean startAsCaller(Activity activity, Bundle options, int userId) {
+                final Intent intent = mBaseIntentToSend;
+                if (intent == null) {
+                    return false;
+                }
+                intent.setComponent(getChooserTargetComponentName());
+                intent.putExtras(mChooserTargetIntentExtras);
+                TargetInfo.prepareIntentForCrossProfileLaunch(intent, userId);
+
+                // Important: we will ignore the target security checks in ActivityManager if and
+                // only if the ChooserTarget's target package is the same package where we got the
+                // ChooserTargetService that provided it. This lets a ChooserTargetService provide
+                // a non-exported or permission-guarded target for the user to pick.
+                //
+                // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere
+                // so we'll obey the caller's normal security checks.
+                final boolean ignoreTargetSecurity = (mSourceInfo != null)
+                        && mSourceInfo.getResolvedComponentName().getPackageName()
+                                .equals(getChooserTargetComponentName().getPackageName());
+                activity.startActivityAsCaller(intent, options, ignoreTargetSecurity, userId);
+                return true;
+            }
+
+            @Override
+            public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
+                throw new RuntimeException("ChooserTargets should be started as caller.");
+            }
+        };
     }
 
-    private String sanitizeDisplayLabel(CharSequence label) {
-        SpannableStringBuilder sb = new SpannableStringBuilder(label);
-        sb.clearSpans();
-        return sb.toString();
+    private SelectableTargetInfo(SelectableTargetInfo other, Intent fillInIntent, int flags) {
+        this(
+                other.mSourceInfo,
+                other.mBackupResolveInfo,
+                other.mResolvedIntent,
+                other.mChooserTargetComponentName,
+                other.mChooserTargetUnsanitizedTitle,
+                other.mChooserTargetIcon,
+                other.mChooserTargetIntentExtras,
+                other.mModifiedScore,
+                other.mShortcutInfo,
+                other.mAppTarget,
+                other.mReferrerFillInIntent,
+                fillInIntent,
+                flags);
     }
 
+    @Override
+    public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
+        return new SelectableTargetInfo(this, fillInIntent, flags);
+    }
+
+    @Override
+    public HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context) {
+        return mHashProvider.getHashedTargetIdForMetrics(context);
+    }
+
+    @Override
+    public boolean isSelectableTargetInfo() {
+        return true;
+    }
+
+    @Override
     public boolean isSuspended() {
         return mIsSuspended;
     }
 
+    @Override
+    @Nullable
     public DisplayResolveInfo getDisplayResolveInfo() {
         return mSourceInfo;
     }
 
-    /**
-     * Load display icon, if needed.
-     */
-    public void loadIcon() {
-        ShortcutInfo shortcutInfo;
-        Drawable icon;
-        synchronized (this) {
-            shortcutInfo = mShortcutInfo;
-            icon = mDisplayIcon;
-        }
-        if (icon == null && shortcutInfo != null) {
-            icon = getChooserTargetIconDrawable(mChooserTarget, shortcutInfo);
-            synchronized (this) {
-                mDisplayIcon = icon;
-                mShortcutInfo = null;
-            }
-        }
-    }
-
-    private Drawable getChooserTargetIconDrawable(ChooserTarget target,
-            @Nullable ShortcutInfo shortcutInfo) {
-        Drawable directShareIcon = null;
-
-        // First get the target drawable and associated activity info
-        final Icon icon = target.getIcon();
-        if (icon != null) {
-            directShareIcon = icon.loadDrawable(mContext);
-        } else if (shortcutInfo != null) {
-            LauncherApps launcherApps = (LauncherApps) mContext.getSystemService(
-                    Context.LAUNCHER_APPS_SERVICE);
-            directShareIcon = launcherApps.getShortcutIconDrawable(shortcutInfo, 0);
-        }
-
-        if (directShareIcon == null) return null;
-
-        ActivityInfo info = null;
-        try {
-            info = mPm.getActivityInfo(target.getComponentName(), 0);
-        } catch (PackageManager.NameNotFoundException error) {
-            Log.e(TAG, "Could not find activity associated with ChooserTarget");
-        }
-
-        if (info == null) return null;
-
-        // Now fetch app icon and raster with no badging even in work profile
-        Bitmap appIcon = mSelectableTargetInfoCommunicator.makePresentationGetter(info)
-                .getIconBitmap(null);
-
-        // Raster target drawable with appIcon as a badge
-        SimpleIconFactory sif = SimpleIconFactory.obtain(mContext);
-        Bitmap directShareBadgedIcon = sif.createAppBadgedIconBitmap(directShareIcon, appIcon);
-        sif.recycle();
-
-        return new BitmapDrawable(mContext.getResources(), directShareBadgedIcon);
-    }
-
+    @Override
     public float getModifiedScore() {
         return mModifiedScore;
     }
 
     @Override
     public Intent getResolvedIntent() {
-        if (mSourceInfo != null) {
-            return mSourceInfo.getResolvedIntent();
-        }
-
-        final Intent targetIntent = new Intent(mSelectableTargetInfoCommunicator.getTargetIntent());
-        targetIntent.setComponent(mChooserTarget.getComponentName());
-        targetIntent.putExtras(mChooserTarget.getIntentExtras());
-        return targetIntent;
+        return mResolvedIntent;
     }
 
     @Override
     public ComponentName getResolvedComponentName() {
-        if (mSourceInfo != null) {
-            return mSourceInfo.getResolvedComponentName();
-        } else if (mBackupResolveInfo != null) {
-            return new ComponentName(mBackupResolveInfo.activityInfo.packageName,
-                    mBackupResolveInfo.activityInfo.name);
-        }
-        return null;
+        return mResolvedComponentName;
     }
 
-    private Intent getBaseIntentToSend() {
-        Intent result = getResolvedIntent();
-        if (result == null) {
-            Log.e(TAG, "ChooserTargetInfo: no base intent available to send");
-        } else {
-            result = new Intent(result);
-            if (mFillInIntent != null) {
-                result.fillIn(mFillInIntent, mFillInFlags);
-            }
-            result.fillIn(mSelectableTargetInfoCommunicator.getReferrerFillInIntent(), 0);
-        }
-        return result;
+    @Override
+    public ComponentName getChooserTargetComponentName() {
+        return mChooserTargetComponentName;
+    }
+
+    @Nullable
+    public Icon getChooserTargetIcon() {
+        return mChooserTargetIcon;
     }
 
     @Override
     public boolean start(Activity activity, Bundle options) {
-        throw new RuntimeException("ChooserTargets should be started as caller.");
+        return mActivityStarter.start(activity, options);
     }
 
     @Override
     public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
-        final Intent intent = getBaseIntentToSend();
-        if (intent == null) {
-            return false;
-        }
-        intent.setComponent(mChooserTarget.getComponentName());
-        intent.putExtras(mChooserTarget.getIntentExtras());
-
-        // Important: we will ignore the target security checks in ActivityManager
-        // if and only if the ChooserTarget's target package is the same package
-        // where we got the ChooserTargetService that provided it. This lets a
-        // ChooserTargetService provide a non-exported or permission-guarded target
-        // to the chooser for the user to pick.
-        //
-        // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere
-        // so we'll obey the caller's normal security checks.
-        final boolean ignoreTargetSecurity = mSourceInfo != null
-                && mSourceInfo.getResolvedComponentName().getPackageName()
-                .equals(mChooserTarget.getComponentName().getPackageName());
-        activity.startActivityAsCaller(intent, options, ignoreTargetSecurity, userId);
-        return true;
+        return mActivityStarter.startAsCaller(activity, options, userId);
     }
 
     @Override
     public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
-        throw new RuntimeException("ChooserTargets should be started as caller.");
+        return mActivityStarter.startAsUser(activity, options, user);
     }
 
     @Override
     public ResolveInfo getResolveInfo() {
-        return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo;
+        return mResolveInfo;
     }
 
     @Override
@@ -296,27 +363,25 @@
     }
 
     @Override
-    public synchronized Drawable getDisplayIcon(Context context) {
-        return mDisplayIcon;
-    }
-
-    public ChooserTarget getChooserTarget() {
-        return mChooserTarget;
+    public IconHolder getDisplayIconHolder() {
+        return mDisplayIconHolder;
     }
 
     @Override
-    public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
-        return new SelectableTargetInfo(this, fillInIntent, flags);
+    @Nullable
+    public ShortcutInfo getDirectShareShortcutInfo() {
+        return mShortcutInfo;
+    }
+
+    @Override
+    @Nullable
+    public AppTarget getDirectShareAppTarget() {
+        return mAppTarget;
     }
 
     @Override
     public List<Intent> getAllSourceIntents() {
-        final List<Intent> results = new ArrayList<>();
-        if (mSourceInfo != null) {
-            // We only queried the service for the first one in our sourceinfo.
-            results.add(mSourceInfo.getAllSourceIntents().get(0));
-        }
-        return results;
+        return mAllSourceIntents;
     }
 
     @Override
@@ -324,16 +389,49 @@
         return mIsPinned;
     }
 
-    /**
-     * Necessary methods to communicate between {@link SelectableTargetInfo}
-     * and {@link ResolverActivity} or {@link ChooserActivity}.
-     */
-    public interface SelectableTargetInfoCommunicator {
+    private static String sanitizeDisplayLabel(CharSequence label) {
+        SpannableStringBuilder sb = new SpannableStringBuilder(label);
+        sb.clearSpans();
+        return sb.toString();
+    }
 
-        ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo info);
+    private static List<Intent> getAllSourceIntents(@Nullable DisplayResolveInfo sourceInfo) {
+        final List<Intent> results = new ArrayList<>();
+        if (sourceInfo != null) {
+            // We only queried the service for the first one in our sourceinfo.
+            results.add(sourceInfo.getAllSourceIntents().get(0));
+        }
+        return results;
+    }
 
-        Intent getTargetIntent();
+    private static ComponentName getResolvedComponentName(
+            @Nullable DisplayResolveInfo sourceInfo, ResolveInfo backupResolveInfo) {
+        if (sourceInfo != null) {
+            return sourceInfo.getResolvedComponentName();
+        } else if (backupResolveInfo != null) {
+            return new ComponentName(
+                    backupResolveInfo.activityInfo.packageName,
+                    backupResolveInfo.activityInfo.name);
+        }
+        return null;
+    }
 
-        Intent getReferrerFillInIntent();
+    @Nullable
+    private static Intent getBaseIntentToSend(
+            @Nullable Intent resolvedIntent,
+            Intent fillInIntent,
+            int fillInFlags,
+            Intent referrerFillInIntent) {
+        Intent result = resolvedIntent;
+        if (result == null) {
+            Log.e(TAG, "ChooserTargetInfo: no base intent available to send");
+        } else {
+            result = new Intent(result);
+            if (fillInIntent != null) {
+                result.fillIn(fillInIntent, fillInFlags);
+            }
+            result.fillIn(referrerFillInIntent, 0);
+        }
+        return result;
     }
 }
diff --git a/java/src/com/android/intentresolver/chooser/TargetInfo.java b/java/src/com/android/intentresolver/chooser/TargetInfo.java
index fabb26c..72dd1b0 100644
--- a/java/src/com/android/intentresolver/chooser/TargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/TargetInfo.java
@@ -17,23 +17,67 @@
 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.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
 import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.UserHandle;
+import android.service.chooser.ChooserTarget;
+import android.text.TextUtils;
+import android.util.HashedStringCache;
 
 import com.android.intentresolver.ResolverActivity;
 
+import java.util.ArrayList;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * A single target as represented in the chooser.
  */
 public interface TargetInfo {
+
+    /**
+     * Container for a {@link TargetInfo}'s (potentially) mutable icon state. This is provided to
+     * encapsulate the state so that the {@link TargetInfo} itself can be "immutable" (in some
+     * sense) as long as it always returns the same {@link IconHolder} instance.
+     *
+     * TODO: move "stateful" responsibilities out to clients; for more info see the Javadoc comment
+     * on {@link #getDisplayIconHolder()}.
+     */
+    interface IconHolder {
+        /** @return the icon (if it's already loaded, or statically available), or null. */
+        @Nullable
+        Drawable getDisplayIcon();
+
+        /**
+         * @param icon the icon to return on subsequent calls to {@link #getDisplayIcon()}.
+         * Implementations may discard this request as a no-op if they don't support setting.
+         */
+        void setDisplayIcon(Drawable icon);
+    }
+
+    /** A simple mutable-container implementation of {@link IconHolder}. */
+    final class SettableIconHolder implements IconHolder {
+        @Nullable
+        private Drawable mDisplayIcon;
+
+        @Nullable
+        public Drawable getDisplayIcon() {
+            return mDisplayIcon;
+        }
+
+        public void setDisplayIcon(Drawable icon) {
+            mDisplayIcon = icon;
+        }
+    }
+
     /**
      * Get the resolved intent that represents this target. Note that this may not be the
      * intent that will be launched by calling one of the <code>start</code> methods provided;
@@ -46,13 +90,34 @@
     /**
      * Get the resolved component name that represents this target. Note that this may not
      * be the component that will be directly launched by calling one of the <code>start</code>
-     * methods provided; this is the component that will be credited with the launch.
+     * methods provided; this is the component that will be credited with the launch. This may be
+     * null if the target was specified by a caller-provided {@link ChooserTarget} that we failed to
+     * resolve to a component on the system.
      *
      * @return the resolved ComponentName for this target
      */
+    @Nullable
     ComponentName getResolvedComponentName();
 
     /**
+     * If this target was historically built from a (now-deprecated) {@link ChooserTarget} record,
+     * get the {@link ComponentName} that would've been provided by that record.
+     *
+     * TODO: for (historical) {@link ChooserTargetInfo} targets, this differs from the result of
+     * {@link #getResolvedComponentName()} only for caller-provided targets that we fail to resolve;
+     * then this returns the name of the component that was requested, and the other returns null.
+     * At the time of writing, this method is only called in contexts where the client knows that
+     * the target was a historical {@link ChooserTargetInfo}. Thus this method could be removed and
+     * all clients consolidated on the other, if we have some alternate mechanism of tracking this
+     * discrepancy; or if we know that the distinction won't apply in the conditions when we call
+     * this method; or if we determine that tracking the distinction isn't a requirement for us.
+     */
+    @Nullable
+    default ComponentName getChooserTargetComponentName() {
+        return null;
+    }
+
+    /**
      * Start the activity referenced by this target.
      *
      * @param activity calling Activity performing the launch
@@ -106,12 +171,23 @@
     CharSequence getExtendedInfo();
 
     /**
-     * @return The drawable that should be used to represent this target including badge
-     * @param context
+     * @return the {@link IconHolder} for the icon used to represent this target, including badge.
+     *
+     * TODO: while the {@link TargetInfo} may be immutable in always returning the same instance of
+     * {@link IconHolder} here, the holder itself is mutable state, and could become a problem if we
+     * ever rely on {@link TargetInfo} immutability elsewhere. Ideally, the {@link TargetInfo}
+     * should provide an immutable "spec" that tells clients <em>how</em> to load the appropriate
+     * icon, while leaving the load itself to some external component.
      */
-    Drawable getDisplayIcon(Context context);
+    IconHolder getDisplayIconHolder();
 
     /**
+     * @return true if display icon is available.
+     */
+    default boolean hasDisplayIcon() {
+        return getDisplayIconHolder().getDisplayIcon() != null;
+    }
+    /**
      * Clone this target with the given fill-in information.
      */
     TargetInfo cloneFilledIn(Intent fillInIntent, int flags);
@@ -122,6 +198,28 @@
     List<Intent> getAllSourceIntents();
 
     /**
+     * @return the one or more {@link DisplayResolveInfo}s that this target represents in the UI.
+     *
+     * TODO: clarify the semantics of the {@link DisplayResolveInfo} branch of {@link TargetInfo}'s
+     * class hierarchy. Why is it that {@link MultiDisplayResolveInfo} can stand in for some
+     * "virtual" {@link DisplayResolveInfo} targets that aren't individually represented in the UI,
+     * but OTOH a {@link ChooserTargetInfo} (which doesn't inherit from {@link DisplayResolveInfo})
+     * can't provide its own UI treatment, and instead needs us to reach into its composed-in
+     * info via {@link #getDisplayResolveInfo()}? It seems like {@link DisplayResolveInfo} may be
+     * required to populate views in our UI, while {@link ChooserTargetInfo} may carry some other
+     * metadata. For non-{@link ChooserTargetInfo} targets (e.g. in {@link ResolverActivity}) the
+     * "naked" {@link DisplayResolveInfo} might also be taken to provide some of this metadata, but
+     * this presents a denormalization hazard since the "UI info" ({@link DisplayResolveInfo}) that
+     * represents a {@link ChooserTargetInfo} might provide different values than its enclosing
+     * {@link ChooserTargetInfo} (as they both implement {@link TargetInfo}). We could try to
+     * address this by splitting {@link DisplayResolveInfo} into two types; one (which implements
+     * the same {@link TargetInfo} interface as {@link ChooserTargetInfo}) provides the previously-
+     * implicit "metadata", and the other provides only the UI treatment for a target of any type
+     * (taking over the respective methods that previously belonged to {@link TargetInfo}).
+     */
+    ArrayList<DisplayResolveInfo> getAllDisplayTargets();
+
+    /**
      * @return true if this target cannot be selected by the user
      */
     boolean isSuspended();
@@ -130,4 +228,220 @@
      * @return true if this target should be pinned to the front by the request of the user
      */
     boolean isPinned();
+
+    /**
+     * Determine whether two targets represent "similar" content that could be de-duped.
+     * Note an earlier version of this code cautioned maintainers,
+     * "do not label as 'equals', since this doesn't quite work as intended with java 8."
+     * This seems to refer to the rule that interfaces can't provide defaults that conflict with the
+     * definitions of "real" methods in {@code java.lang.Object}, and (if desired) it could be
+     * presumably resolved by converting {@code TargetInfo} from an interface to an abstract class.
+     */
+    default boolean isSimilar(TargetInfo other) {
+        if (other == null) {
+            return false;
+        }
+
+        // TODO: audit usage and try to reconcile a behavior that doesn't depend on the legacy
+        // subclass type. Note that the `isSimilar()` method was pulled up from the legacy
+        // `ChooserTargetInfo`, so no legacy behavior currently depends on calling `isSimilar()` on
+        // an instance where `isChooserTargetInfo()` would return false (although technically it may
+        // have been possible for the `other` target to be of a different type). Thus we have
+        // flexibility in defining the similarity conditions between pairs of non "chooser" targets.
+        if (isChooserTargetInfo()) {
+            return other.isChooserTargetInfo()
+                    && Objects.equals(
+                            getChooserTargetComponentName(), other.getChooserTargetComponentName())
+                    && TextUtils.equals(getDisplayLabel(), other.getDisplayLabel())
+                    && TextUtils.equals(getExtendedInfo(), other.getExtendedInfo());
+        } else {
+            return !other.isChooserTargetInfo() && Objects.equals(this, other);
+        }
+    }
+
+    /**
+     * @return the target score, including any Chooser-specific modifications that may have been
+     * applied (either overriding by special-case for "non-selectable" targets, or by twiddling the
+     * scores of "selectable" targets in {@link ChooserListAdapter}). Higher scores are "better."
+     * Targets that aren't intended for ranking/scoring should return a negative value.
+     */
+    default float getModifiedScore() {
+        return -0.1f;
+    }
+
+    /**
+     * @return the {@link ShortcutManager} data for any shortcut associated with this target.
+     */
+    @Nullable
+    default ShortcutInfo getDirectShareShortcutInfo() {
+        return null;
+    }
+
+    /**
+     * @return the ID of the shortcut represented by this target, or null if the target didn't come
+     * from a {@link ShortcutManager} shortcut.
+     */
+    @Nullable
+    default String getDirectShareShortcutId() {
+        ShortcutInfo shortcut = getDirectShareShortcutInfo();
+        if (shortcut == null) {
+            return null;
+        }
+        return shortcut.getId();
+    }
+
+    /**
+     * @return the {@link AppTarget} metadata if this target was sourced from App Prediction
+     * service, or null otherwise.
+     */
+    @Nullable
+    default AppTarget getDirectShareAppTarget() {
+        return null;
+    }
+
+    /**
+     * Get more info about this target in the form of a {@link DisplayResolveInfo}, if available.
+     * TODO: this seems to return non-null only for ChooserTargetInfo subclasses. Determine the
+     * meaning of a TargetInfo (ChooserTargetInfo) embedding another kind of TargetInfo
+     * (DisplayResolveInfo) in this way, and - at least - improve this documentation; OTOH this
+     * probably indicates an opportunity to simplify or better separate these APIs. (For example,
+     * targets that <em>don't</em> descend from ChooserTargetInfo instead descend directly from
+     * DisplayResolveInfo; should they return `this`? Do we always use DisplayResolveInfo to
+     * represent visual properties, and then either assume some implicit metadata properties *or*
+     * embed that visual representation within a ChooserTargetInfo to carry additional metadata? If
+     * that's the case, maybe we could decouple by saying that all TargetInfos compose-in their
+     * visual representation [as a DisplayResolveInfo, now the root of its own class hierarchy] and
+     * then add a new TargetInfo type that explicitly represents the "implicit metadata" that we
+     * previously assumed for "naked DisplayResolveInfo targets" that weren't wrapped as
+     * ChooserTargetInfos. Or does all this complexity disappear once we stop relying on the
+     * deprecated ChooserTarget type?)
+     */
+    @Nullable
+    default DisplayResolveInfo getDisplayResolveInfo() {
+        return null;
+    }
+
+    /**
+     * @return true if this target represents a legacy {@code ChooserTargetInfo}. These objects were
+     * historically documented as representing "[a] TargetInfo for Direct Share." However, not all
+     * of these targets are actually *valid* for direct share; e.g. some represent "empty" items
+     * (although perhaps only for display in the Direct Share UI?). In even earlier versions, these
+     * targets may also have been results from peers in the (now-deprecated/unsupported)
+     * {@code ChooserTargetService} ecosystem; even though we no longer use these services, we're
+     * still shoehorning other target data into the deprecated {@link ChooserTarget} structure for
+     * compatibility with some internal APIs.
+     * TODO: refactor to clarify the semantics of any target for which this method returns true
+     * (e.g., are they characterized by their application in the Direct Share UI?), and to remove
+     * the scaffolding that adapts to and from the {@link ChooserTarget} structure. Eventually, we
+     * expect to remove this method (and others that strictly indicate legacy subclass roles) in
+     * favor of a more semantic design that expresses the purpose and distinctions in those roles.
+     */
+    default boolean isChooserTargetInfo() {
+        return false;
+    }
+
+    /**
+     * @return true if this target represents a legacy {@code DisplayResolveInfo}. These objects
+     * were historically documented as an augmented "TargetInfo plus additional information needed
+     * to render it (such as icon and label) and resolve it to an activity." That description in no
+     * way distinguishes from the base {@code TargetInfo} API. At the time of writing, these objects
+     * are most-clearly defined by their opposite; this returns true for exactly those instances of
+     * {@code TargetInfo} where {@link #isChooserTargetInfo()} returns false (these conditions are
+     * complementary because they correspond to the immediate {@code TargetInfo} child types that
+     * historically partitioned all concrete {@code TargetInfo} implementations). These may(?)
+     * represent any target displayed somewhere other than the Direct Share UI.
+     */
+    default boolean isDisplayResolveInfo() {
+        return false;
+    }
+
+    /**
+     * @return true if this target represents a legacy {@code MultiDisplayResolveInfo}. These
+     * objects were historically documented as representing "a 'stack' of chooser targets for
+     * various activities within the same component." For historical reasons this currently can
+     * return true only if {@link #isDisplayResolveInfo()} returns true (because the legacy classes
+     * shared an inheritance relationship), but new code should avoid relying on that relationship
+     * since these APIs are "in transition."
+     */
+    default boolean isMultiDisplayResolveInfo() {
+        return false;
+    }
+
+    /**
+     * @return true if this target represents a legacy {@code SelectableTargetInfo}. Note that this
+     * is defined for legacy compatibility and may not conform to other notions of a "selectable"
+     * target. For historical reasons, this method and {@link #isNotSelectableTargetInfo()} only
+     * partition the {@code TargetInfo} instances for which {@link #isChooserTargetInfo()} returns
+     * true; otherwise <em>both</em> methods return false.
+     * TODO: define selectability for targets not historically from {@code ChooserTargetInfo},
+     * then attempt to replace this with a new method like {@code TargetInfo#isSelectable()} that
+     * actually partitions <em>all</em> target types (after updating client usage as needed).
+     */
+    default boolean isSelectableTargetInfo() {
+        return false;
+    }
+
+    /**
+     * @return true if this target represents a legacy {@code NotSelectableTargetInfo} (i.e., a
+     * target where {@link #isChooserTargetInfo()} is true but {@link #isSelectableTargetInfo()} is
+     * false). For more information on how this divides the space of targets, see the Javadoc for
+     * {@link #isSelectableTargetInfo()}.
+     */
+    default boolean isNotSelectableTargetInfo() {
+        return false;
+    }
+
+    /**
+     * @return true if this target represents a legacy {@code ChooserActivity#EmptyTargetInfo}. Note
+     * that this is defined for legacy compatibility and may not conform to other notions of an
+     * "empty" target.
+     */
+    default boolean isEmptyTargetInfo() {
+        return false;
+    }
+
+    /**
+     * @return true if this target represents a legacy {@code ChooserActivity#PlaceHolderTargetInfo}
+     * (defined only for compatibility with historic use in {@link ChooserListAdapter}). For
+     * historic reasons (owing to a legacy subclass relationship) this can return true only if
+     * {@link #isNotSelectableTargetInfo()} also returns true.
+     */
+    default boolean isPlaceHolderTargetInfo() {
+        return false;
+    }
+
+    /**
+     * @return true if this target should be logged with the "direct_share" metrics category in
+     * {@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).
+     */
+    default boolean isInDirectShareMetricsCategory() {
+        return isChooserTargetInfo();
+    }
+
+    /**
+     * @param context caller's context, to provide the {@link SharedPreferences} for use by the
+     * {@link HashedStringCache}.
+     * @return a hashed ID that should be logged along with our target-selection metrics, or null.
+     * The contents of the plaintext are defined for historical reasons, "the package name + target
+     * name to answer the question if most users share to mostly the same person
+     * or to a bunch of different people." Clients should consider this as opaque data for logging
+     * only; they should not rely on any particular semantics about the value.
+     */
+    default HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context) {
+        return null;
+    }
+
+    /**
+     * Fix the URIs in {@code intent} if cross-profile sharing is required. This should be called
+     * before launching the intent as another user.
+     */
+    static void prepareIntentForCrossProfileLaunch(Intent intent, int targetUserId) {
+        final int currentUserId = UserHandle.myUserId();
+        if (targetUserId != currentUserId) {
+            intent.fixUris(currentUserId);
+        }
+    }
 }
diff --git a/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
new file mode 100644
index 0000000..1cf5931
--- /dev/null
+++ b/java/src/com/android/intentresolver/grid/ChooserGridAdapter.java
@@ -0,0 +1,604 @@
+/*
+ * 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.grid;
+
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.animation.DecelerateInterpolator;
+import android.widget.Space;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.R;
+import com.android.intentresolver.ResolverListAdapter.ViewHolder;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.android.collect.Lists;
+
+/**
+ * Adapter for all types of items and targets in ShareSheet.
+ * Note that ranked sections like Direct Share - while appearing grid-like - are handled on the
+ * row level by this adapter but not on the item level. Individual targets within the row are
+ * handled by {@link ChooserListAdapter}
+ */
+@VisibleForTesting
+public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+
+    /**
+     * The transition time between placeholders for direct share to a message
+     * indicating that none are available.
+     */
+    public static final int NO_DIRECT_SHARE_ANIM_IN_MILLIS = 200;
+
+    /**
+     * Injectable interface for any considerations that should be delegated to other components
+     * in the {@link ChooserActivity}.
+     * TODO: determine whether any of these methods return parameters that can safely be
+     * precomputed; whether any should be converted to `ChooserGridAdapter` setters to be
+     * invoked by external callbacks; and whether any reflect requirements that should be moved
+     * out of `ChooserGridAdapter` altogether.
+     */
+    public interface ChooserActivityDelegate {
+        /** @return whether we're showing a tabbed (multi-profile) UI. */
+        boolean shouldShowTabs();
+
+        /**
+         * @return a content preview {@link View} that's appropriate for the caller's share
+         * content, constructed for display in the provided {@code parent} group.
+         */
+        View buildContentPreview(ViewGroup parent);
+
+        /** Notify the client that the item with the selected {@code itemIndex} was selected. */
+        void onTargetSelected(int itemIndex);
+
+        /**
+         * Notify the client that the item with the selected {@code itemIndex} was
+         * long-pressed.
+         */
+        void onTargetLongPressed(int itemIndex);
+
+        /**
+         * Notify the client that the provided {@code View} should be configured as the new
+         * "profile view" button. Callers should attach their own click listeners to implement
+         * behaviors on this view.
+         */
+        void updateProfileViewButton(View newButtonFromProfileRow);
+
+        /**
+         * @return the number of "valid" targets in the active list adapter.
+         * TODO: define "valid."
+         */
+        int getValidTargetCount();
+
+        /**
+         * Request that the client update our {@code directShareGroup} to match their desired
+         * state for the "expansion" UI.
+         */
+        void updateDirectShareExpansion(DirectShareViewHolder directShareGroup);
+
+        /**
+         * Request that the client handle a scroll event that should be taken as expanding the
+         * provided {@code directShareGroup}. Note that this currently never happens due to a
+         * hard-coded condition in {@link #canExpandDirectShare()}.
+         */
+        void handleScrollToExpandDirectShare(
+                DirectShareViewHolder directShareGroup, int y, int oldy);
+    }
+
+    private static final int VIEW_TYPE_DIRECT_SHARE = 0;
+    private static final int VIEW_TYPE_NORMAL = 1;
+    private static final int VIEW_TYPE_CONTENT_PREVIEW = 2;
+    private static final int VIEW_TYPE_PROFILE = 3;
+    private static final int VIEW_TYPE_AZ_LABEL = 4;
+    private static final int VIEW_TYPE_CALLER_AND_RANK = 5;
+    private static final int VIEW_TYPE_FOOTER = 6;
+
+    private static final int NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20;
+
+    private final ChooserActivityDelegate mChooserActivityDelegate;
+    private final ChooserListAdapter mChooserListAdapter;
+    private final LayoutInflater mLayoutInflater;
+
+    private final int mMaxTargetsPerRow;
+    private final boolean mShouldShowContentPreview;
+    private final int mChooserWidthPixels;
+    private final int mChooserRowTextOptionTranslatePixelSize;
+    private final boolean mShowAzLabelIfPoss;
+
+    private DirectShareViewHolder mDirectShareViewHolder;
+    private int mChooserTargetWidth = 0;
+
+    private int mFooterHeight = 0;
+
+    public ChooserGridAdapter(
+            Context context,
+            ChooserActivityDelegate chooserActivityDelegate,
+            ChooserListAdapter wrappedAdapter,
+            boolean shouldShowContentPreview,
+            int maxTargetsPerRow,
+            int numSheetExpansions) {
+        super();
+
+        mChooserActivityDelegate = chooserActivityDelegate;
+
+        mChooserListAdapter = wrappedAdapter;
+        mLayoutInflater = LayoutInflater.from(context);
+
+        mShouldShowContentPreview = shouldShowContentPreview;
+        mMaxTargetsPerRow = maxTargetsPerRow;
+
+        mChooserWidthPixels = context.getResources().getDimensionPixelSize(R.dimen.chooser_width);
+        mChooserRowTextOptionTranslatePixelSize = context.getResources().getDimensionPixelSize(
+                R.dimen.chooser_row_text_option_translate);
+
+        mShowAzLabelIfPoss = numSheetExpansions < NUM_EXPANSIONS_TO_HIDE_AZ_LABEL;
+
+        wrappedAdapter.registerDataSetObserver(new DataSetObserver() {
+            @Override
+            public void onChanged() {
+                super.onChanged();
+                notifyDataSetChanged();
+            }
+
+            @Override
+            public void onInvalidated() {
+                super.onInvalidated();
+                notifyDataSetChanged();
+            }
+        });
+    }
+
+    public void setFooterHeight(int height) {
+        mFooterHeight = height;
+    }
+
+    /**
+     * Calculate the chooser target width to maximize space per item
+     *
+     * @param width The new row width to use for recalculation
+     * @return true if the view width has changed
+     */
+    public boolean calculateChooserTargetWidth(int width) {
+        if (width == 0) {
+            return false;
+        }
+
+        // Limit width to the maximum width of the chooser activity
+        int maxWidth = mChooserWidthPixels;
+        width = Math.min(maxWidth, width);
+
+        int newWidth = width / mMaxTargetsPerRow;
+        if (newWidth != mChooserTargetWidth) {
+            mChooserTargetWidth = newWidth;
+            return true;
+        }
+
+        return false;
+    }
+
+    public int getRowCount() {
+        return (int) (
+                getSystemRowCount()
+                        + getProfileRowCount()
+                        + getServiceTargetRowCount()
+                        + getCallerAndRankedTargetRowCount()
+                        + getAzLabelRowCount()
+                        + Math.ceil(
+                        (float) mChooserListAdapter.getAlphaTargetCount()
+                                / mMaxTargetsPerRow)
+            );
+    }
+
+    /**
+     * Whether the "system" row of targets is displayed.
+     * This area includes the content preview (if present) and action row.
+     */
+    public int getSystemRowCount() {
+        // For the tabbed case we show the sticky content preview above the tabs,
+        // please refer to shouldShowStickyContentPreview
+        if (mChooserActivityDelegate.shouldShowTabs()) {
+            return 0;
+        }
+
+        if (!mShouldShowContentPreview) {
+            return 0;
+        }
+
+        if (mChooserListAdapter == null || mChooserListAdapter.getCount() == 0) {
+            return 0;
+        }
+
+        return 1;
+    }
+
+    public int getProfileRowCount() {
+        if (mChooserActivityDelegate.shouldShowTabs()) {
+            return 0;
+        }
+        return mChooserListAdapter.getOtherProfile() == null ? 0 : 1;
+    }
+
+    public int getFooterRowCount() {
+        return 1;
+    }
+
+    public int getCallerAndRankedTargetRowCount() {
+        return (int) Math.ceil(
+                ((float) mChooserListAdapter.getCallerTargetCount()
+                        + mChooserListAdapter.getRankedTargetCount()) / mMaxTargetsPerRow);
+    }
+
+    // There can be at most one row in the listview, that is internally
+    // a ViewGroup with 2 rows
+    public int getServiceTargetRowCount() {
+        if (mShouldShowContentPreview && !ActivityManager.isLowRamDeviceStatic()) {
+            return 1;
+        }
+        return 0;
+    }
+
+    public int getAzLabelRowCount() {
+        // Only show a label if the a-z list is showing
+        return (mShowAzLabelIfPoss && mChooserListAdapter.getAlphaTargetCount() > 0) ? 1 : 0;
+    }
+
+    @Override
+    public int getItemCount() {
+        return (int) (
+                getSystemRowCount()
+                        + getProfileRowCount()
+                        + getServiceTargetRowCount()
+                        + getCallerAndRankedTargetRowCount()
+                        + getAzLabelRowCount()
+                        + mChooserListAdapter.getAlphaTargetCount()
+                        + getFooterRowCount()
+            );
+    }
+
+    @Override
+    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+        switch (viewType) {
+            case VIEW_TYPE_CONTENT_PREVIEW:
+                return new ItemViewHolder(
+                        mChooserActivityDelegate.buildContentPreview(parent),
+                        viewType,
+                        null,
+                        null);
+            case VIEW_TYPE_PROFILE:
+                return new ItemViewHolder(
+                        createProfileView(parent),
+                        viewType,
+                        null,
+                        null);
+            case VIEW_TYPE_AZ_LABEL:
+                return new ItemViewHolder(
+                        createAzLabelView(parent),
+                        viewType,
+                        null,
+                        null);
+            case VIEW_TYPE_NORMAL:
+                return new ItemViewHolder(
+                        mChooserListAdapter.createView(parent),
+                        viewType,
+                        mChooserActivityDelegate::onTargetSelected,
+                        mChooserActivityDelegate::onTargetLongPressed);
+            case VIEW_TYPE_DIRECT_SHARE:
+            case VIEW_TYPE_CALLER_AND_RANK:
+                return createItemGroupViewHolder(viewType, parent);
+            case VIEW_TYPE_FOOTER:
+                Space sp = new Space(parent.getContext());
+                sp.setLayoutParams(new RecyclerView.LayoutParams(
+                        LayoutParams.MATCH_PARENT, mFooterHeight));
+                return new FooterViewHolder(sp, viewType);
+            default:
+                // Since we catch all possible viewTypes above, no chance this is being called.
+                return null;
+        }
+    }
+
+    @Override
+    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+        int viewType = ((ViewHolderBase) holder).getViewType();
+        switch (viewType) {
+            case VIEW_TYPE_DIRECT_SHARE:
+            case VIEW_TYPE_CALLER_AND_RANK:
+                bindItemGroupViewHolder(position, (ItemGroupViewHolder) holder);
+                break;
+            case VIEW_TYPE_NORMAL:
+                bindItemViewHolder(position, (ItemViewHolder) holder);
+                break;
+            default:
+        }
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        int count;
+
+        int countSum = (count = getSystemRowCount());
+        if (count > 0 && position < countSum) return VIEW_TYPE_CONTENT_PREVIEW;
+
+        countSum += (count = getProfileRowCount());
+        if (count > 0 && position < countSum) return VIEW_TYPE_PROFILE;
+
+        countSum += (count = getServiceTargetRowCount());
+        if (count > 0 && position < countSum) return VIEW_TYPE_DIRECT_SHARE;
+
+        countSum += (count = getCallerAndRankedTargetRowCount());
+        if (count > 0 && position < countSum) return VIEW_TYPE_CALLER_AND_RANK;
+
+        countSum += (count = getAzLabelRowCount());
+        if (count > 0 && position < countSum) return VIEW_TYPE_AZ_LABEL;
+
+        if (position == getItemCount() - 1) return VIEW_TYPE_FOOTER;
+
+        return VIEW_TYPE_NORMAL;
+    }
+
+    public int getTargetType(int position) {
+        return mChooserListAdapter.getPositionTargetType(getListPosition(position));
+    }
+
+    private View createProfileView(ViewGroup parent) {
+        View profileRow = mLayoutInflater.inflate(R.layout.chooser_profile_row, parent, false);
+        mChooserActivityDelegate.updateProfileViewButton(profileRow);
+        return profileRow;
+    }
+
+    private View createAzLabelView(ViewGroup parent) {
+        return mLayoutInflater.inflate(R.layout.chooser_az_label_row, parent, false);
+    }
+
+    private ItemGroupViewHolder loadViewsIntoGroup(ItemGroupViewHolder holder) {
+        final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+        final int exactSpec = MeasureSpec.makeMeasureSpec(mChooserTargetWidth, MeasureSpec.EXACTLY);
+        int columnCount = holder.getColumnCount();
+
+        final boolean isDirectShare = holder instanceof DirectShareViewHolder;
+
+        for (int i = 0; i < columnCount; i++) {
+            final View v = mChooserListAdapter.createView(holder.getRowByIndex(i));
+            final int column = i;
+            v.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    mChooserActivityDelegate.onTargetSelected(holder.getItemIndex(column));
+                }
+            });
+
+            // Show menu for both direct share and app share targets after long click.
+            v.setOnLongClickListener(v1 -> {
+                mChooserActivityDelegate.onTargetLongPressed(holder.getItemIndex(column));
+                return true;
+            });
+
+            holder.addView(i, v);
+
+            // Force Direct Share to be 2 lines and auto-wrap to second line via hoz scroll =
+            // false. TextView#setHorizontallyScrolling must be reset after #setLines. Must be
+            // done before measuring.
+            if (isDirectShare) {
+                final ViewHolder vh = (ViewHolder) v.getTag();
+                vh.text.setLines(2);
+                vh.text.setHorizontallyScrolling(false);
+                vh.text2.setVisibility(View.GONE);
+            }
+
+            // Force height to be a given so we don't have visual disruption during scaling.
+            v.measure(exactSpec, spec);
+            setViewBounds(v, v.getMeasuredWidth(), v.getMeasuredHeight());
+        }
+
+        final ViewGroup viewGroup = holder.getViewGroup();
+
+        // Pre-measure and fix height so we can scale later.
+        holder.measure();
+        setViewBounds(viewGroup, LayoutParams.MATCH_PARENT, holder.getMeasuredRowHeight());
+
+        if (isDirectShare) {
+            DirectShareViewHolder dsvh = (DirectShareViewHolder) holder;
+            setViewBounds(dsvh.getRow(0), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight());
+            setViewBounds(dsvh.getRow(1), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight());
+        }
+
+        viewGroup.setTag(holder);
+        return holder;
+    }
+
+    private void setViewBounds(View view, int widthPx, int heightPx) {
+        LayoutParams lp = view.getLayoutParams();
+        if (lp == null) {
+            lp = new LayoutParams(widthPx, heightPx);
+            view.setLayoutParams(lp);
+        } else {
+            lp.height = heightPx;
+            lp.width = widthPx;
+        }
+    }
+
+    ItemGroupViewHolder createItemGroupViewHolder(int viewType, ViewGroup parent) {
+        if (viewType == VIEW_TYPE_DIRECT_SHARE) {
+            ViewGroup parentGroup = (ViewGroup) mLayoutInflater.inflate(
+                    R.layout.chooser_row_direct_share, parent, false);
+            ViewGroup row1 = (ViewGroup) mLayoutInflater.inflate(
+                    R.layout.chooser_row, parentGroup, false);
+            ViewGroup row2 = (ViewGroup) mLayoutInflater.inflate(
+                    R.layout.chooser_row, parentGroup, false);
+            parentGroup.addView(row1);
+            parentGroup.addView(row2);
+
+            mDirectShareViewHolder = new DirectShareViewHolder(parentGroup,
+                    Lists.newArrayList(row1, row2), mMaxTargetsPerRow, viewType,
+                    mChooserActivityDelegate::getValidTargetCount);
+            loadViewsIntoGroup(mDirectShareViewHolder);
+
+            return mDirectShareViewHolder;
+        } else {
+            ViewGroup row = (ViewGroup) mLayoutInflater.inflate(
+                    R.layout.chooser_row, parent, false);
+            ItemGroupViewHolder holder =
+                    new SingleRowViewHolder(row, mMaxTargetsPerRow, viewType);
+            loadViewsIntoGroup(holder);
+
+            return holder;
+        }
+    }
+
+    /**
+     * Need to merge CALLER + ranked STANDARD into a single row and prevent a separator from
+     * showing on top of the AZ list if the AZ label is visible. All other types are placed into
+     * their own row as determined by their target type, and dividers are added in the list to
+     * separate each type.
+     */
+    int getRowType(int rowPosition) {
+        // Merge caller and ranked standard into a single row
+        int positionType = mChooserListAdapter.getPositionTargetType(rowPosition);
+        if (positionType == ChooserListAdapter.TARGET_CALLER) {
+            return ChooserListAdapter.TARGET_STANDARD;
+        }
+
+        // If an A-Z label is shown, prevent a separator from appearing by making the A-Z
+        // row type the same as the suggestion row type
+        if (getAzLabelRowCount() > 0 && positionType == ChooserListAdapter.TARGET_STANDARD_AZ) {
+            return ChooserListAdapter.TARGET_STANDARD;
+        }
+
+        return positionType;
+    }
+
+    void bindItemViewHolder(int position, ItemViewHolder holder) {
+        View v = holder.itemView;
+        int listPosition = getListPosition(position);
+        holder.setListPosition(listPosition);
+        mChooserListAdapter.bindView(listPosition, v);
+    }
+
+    void bindItemGroupViewHolder(int position, ItemGroupViewHolder holder) {
+        final ViewGroup viewGroup = (ViewGroup) holder.itemView;
+        int start = getListPosition(position);
+        int startType = getRowType(start);
+
+        int columnCount = holder.getColumnCount();
+        int end = start + columnCount - 1;
+        while (getRowType(end) != startType && end >= start) {
+            end--;
+        }
+
+        if (end == start && mChooserListAdapter.getItem(start).isEmptyTargetInfo()) {
+            final TextView textView = viewGroup.findViewById(
+                    com.android.internal.R.id.chooser_row_text_option);
+
+            if (textView.getVisibility() != View.VISIBLE) {
+                textView.setAlpha(0.0f);
+                textView.setVisibility(View.VISIBLE);
+                textView.setText(R.string.chooser_no_direct_share_targets);
+
+                ValueAnimator fadeAnim = ObjectAnimator.ofFloat(textView, "alpha", 0.0f, 1.0f);
+                fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
+
+                textView.setTranslationY(mChooserRowTextOptionTranslatePixelSize);
+                ValueAnimator translateAnim =
+                        ObjectAnimator.ofFloat(textView, "translationY", 0.0f);
+                translateAnim.setInterpolator(new DecelerateInterpolator(1.0f));
+
+                AnimatorSet animSet = new AnimatorSet();
+                animSet.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
+                animSet.setStartDelay(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
+                animSet.playTogether(fadeAnim, translateAnim);
+                animSet.start();
+            }
+        }
+
+        for (int i = 0; i < columnCount; i++) {
+            final View v = holder.getView(i);
+
+            if (start + i <= end) {
+                holder.setViewVisibility(i, View.VISIBLE);
+                holder.setItemIndex(i, start + i);
+                mChooserListAdapter.bindView(holder.getItemIndex(i), v);
+            } else {
+                holder.setViewVisibility(i, View.INVISIBLE);
+            }
+        }
+    }
+
+    int getListPosition(int position) {
+        position -= getSystemRowCount() + getProfileRowCount();
+
+        final int serviceCount = mChooserListAdapter.getServiceTargetCount();
+        final int serviceRows = (int) Math.ceil((float) serviceCount / mMaxTargetsPerRow);
+        if (position < serviceRows) {
+            return position * mMaxTargetsPerRow;
+        }
+
+        position -= serviceRows;
+
+        final int callerAndRankedCount =
+                mChooserListAdapter.getCallerTargetCount()
+                + mChooserListAdapter.getRankedTargetCount();
+        final int callerAndRankedRows = getCallerAndRankedTargetRowCount();
+        if (position < callerAndRankedRows) {
+            return serviceCount + position * mMaxTargetsPerRow;
+        }
+
+        position -= getAzLabelRowCount() + callerAndRankedRows;
+
+        return callerAndRankedCount + serviceCount + position;
+    }
+
+    public void handleScroll(View v, int y, int oldy) {
+        boolean canExpandDirectShare = canExpandDirectShare();
+        if (mDirectShareViewHolder != null && canExpandDirectShare) {
+            mChooserActivityDelegate.handleScrollToExpandDirectShare(
+                    mDirectShareViewHolder, y, oldy);
+        }
+    }
+
+    /** Only expand direct share area if there is a minimum number of targets. */
+    private boolean canExpandDirectShare() {
+        // Do not enable until we have confirmed more apps are using sharing shortcuts
+        // Check git history for enablement logic
+        return false;
+    }
+
+    public ChooserListAdapter getListAdapter() {
+        return mChooserListAdapter;
+    }
+
+    public boolean shouldCellSpan(int position) {
+        return getItemViewType(position) == VIEW_TYPE_NORMAL;
+    }
+
+    public void updateDirectShareExpansion() {
+        if (mDirectShareViewHolder == null || !canExpandDirectShare()) {
+            return;
+        }
+        mChooserActivityDelegate.updateDirectShareExpansion(mDirectShareViewHolder);
+    }
+}
diff --git a/java/src/com/android/intentresolver/grid/DirectShareViewHolder.java b/java/src/com/android/intentresolver/grid/DirectShareViewHolder.java
new file mode 100644
index 0000000..316c9f0
--- /dev/null
+++ b/java/src/com/android/intentresolver/grid/DirectShareViewHolder.java
@@ -0,0 +1,197 @@
+/*
+ * 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.grid;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.view.animation.AccelerateInterpolator;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.intentresolver.ChooserActivity;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.function.Supplier;
+
+/** Holder for direct share targets in the {@link ChooserGridAdapter}. */
+public class DirectShareViewHolder extends ItemGroupViewHolder {
+    private final ViewGroup mParent;
+    private final List<ViewGroup> mRows;
+    private int mCellCountPerRow;
+
+    private boolean mHideDirectShareExpansion = false;
+    private int mDirectShareMinHeight = 0;
+    private int mDirectShareCurrHeight = 0;
+    private int mDirectShareMaxHeight = 0;
+
+    private final boolean[] mCellVisibility;
+
+    private final Supplier<Integer> mDeferredTargetCountSupplier;
+
+    public DirectShareViewHolder(
+            ViewGroup parent,
+            List<ViewGroup> rows,
+            int cellCountPerRow,
+            int viewType,
+            Supplier<Integer> deferredTargetCountSupplier) {
+        super(rows.size() * cellCountPerRow, parent, viewType);
+
+        this.mParent = parent;
+        this.mRows = rows;
+        this.mCellCountPerRow = cellCountPerRow;
+        this.mCellVisibility = new boolean[rows.size() * cellCountPerRow];
+        Arrays.fill(mCellVisibility, true);
+        this.mDeferredTargetCountSupplier = deferredTargetCountSupplier;
+    }
+
+    public ViewGroup addView(int index, View v) {
+        ViewGroup row = getRowByIndex(index);
+        row.addView(v);
+        mCells[index] = v;
+
+        return row;
+    }
+
+    public ViewGroup getViewGroup() {
+        return mParent;
+    }
+
+    public ViewGroup getRowByIndex(int index) {
+        return mRows.get(index / mCellCountPerRow);
+    }
+
+    public ViewGroup getRow(int rowNumber) {
+        return mRows.get(rowNumber);
+    }
+
+    public void measure() {
+        final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+        getRow(0).measure(spec, spec);
+        getRow(1).measure(spec, spec);
+
+        mDirectShareMinHeight = getRow(0).getMeasuredHeight();
+        mDirectShareCurrHeight = (mDirectShareCurrHeight > 0)
+                ? mDirectShareCurrHeight : mDirectShareMinHeight;
+        mDirectShareMaxHeight = 2 * mDirectShareMinHeight;
+    }
+
+    public int getMeasuredRowHeight() {
+        return mDirectShareCurrHeight;
+    }
+
+    public int getMinRowHeight() {
+        return mDirectShareMinHeight;
+    }
+
+    public void setViewVisibility(int i, int visibility) {
+        final View v = getView(i);
+        if (visibility == View.VISIBLE) {
+            mCellVisibility[i] = true;
+            v.setVisibility(visibility);
+            v.setAlpha(1.0f);
+        } else if (visibility == View.INVISIBLE && mCellVisibility[i]) {
+            mCellVisibility[i] = false;
+
+            ValueAnimator fadeAnim = ObjectAnimator.ofFloat(v, "alpha", 1.0f, 0f);
+            fadeAnim.setDuration(ChooserGridAdapter.NO_DIRECT_SHARE_ANIM_IN_MILLIS);
+            fadeAnim.setInterpolator(new AccelerateInterpolator(1.0f));
+            fadeAnim.addListener(new AnimatorListenerAdapter() {
+                public void onAnimationEnd(Animator animation) {
+                    v.setVisibility(View.INVISIBLE);
+                }
+            });
+            fadeAnim.start();
+        }
+    }
+
+    public void handleScroll(RecyclerView view, int y, int oldy, int maxTargetsPerRow) {
+        // only exit early if fully collapsed, otherwise onListRebuilt() with shifting
+        // targets can lock us into an expanded mode
+        boolean notExpanded = mDirectShareCurrHeight == mDirectShareMinHeight;
+        if (notExpanded) {
+            if (mHideDirectShareExpansion) {
+                return;
+            }
+
+            // only expand if we have more than maxTargetsPerRow, and delay that decision
+            // until they start to scroll
+            final int validTargets = this.mDeferredTargetCountSupplier.get();
+            if (validTargets <= maxTargetsPerRow) {
+                mHideDirectShareExpansion = true;
+                return;
+            }
+        }
+
+        int yDiff = (int) ((oldy - y) * ChooserActivity.DIRECT_SHARE_EXPANSION_RATE);
+
+        int prevHeight = mDirectShareCurrHeight;
+        int newHeight = Math.min(prevHeight + yDiff, mDirectShareMaxHeight);
+        newHeight = Math.max(newHeight, mDirectShareMinHeight);
+        yDiff = newHeight - prevHeight;
+
+        updateDirectShareRowHeight(view, yDiff, newHeight);
+    }
+
+    public void expand(RecyclerView view) {
+        updateDirectShareRowHeight(
+                view, mDirectShareMaxHeight - mDirectShareCurrHeight, mDirectShareMaxHeight);
+    }
+
+    public void collapse(RecyclerView view) {
+        updateDirectShareRowHeight(
+                view, mDirectShareMinHeight - mDirectShareCurrHeight, mDirectShareMinHeight);
+    }
+
+    private void updateDirectShareRowHeight(RecyclerView view, int yDiff, int newHeight) {
+        if (view == null || view.getChildCount() == 0 || yDiff == 0) {
+            return;
+        }
+
+        // locate the item to expand, and offset the rows below that one
+        boolean foundExpansion = false;
+        for (int i = 0; i < view.getChildCount(); i++) {
+            View child = view.getChildAt(i);
+
+            if (foundExpansion) {
+                child.offsetTopAndBottom(yDiff);
+            } else {
+                if (child.getTag() != null && child.getTag() instanceof DirectShareViewHolder) {
+                    int widthSpec = MeasureSpec.makeMeasureSpec(child.getWidth(),
+                            MeasureSpec.EXACTLY);
+                    int heightSpec = MeasureSpec.makeMeasureSpec(newHeight,
+                            MeasureSpec.EXACTLY);
+                    child.measure(widthSpec, heightSpec);
+                    child.getLayoutParams().height = child.getMeasuredHeight();
+                    child.layout(child.getLeft(), child.getTop(), child.getRight(),
+                            child.getTop() + child.getMeasuredHeight());
+
+                    foundExpansion = true;
+                }
+            }
+        }
+
+        if (foundExpansion) {
+            mDirectShareCurrHeight = newHeight;
+        }
+    }
+}
diff --git a/java/src/com/android/intentresolver/grid/FooterViewHolder.java b/java/src/com/android/intentresolver/grid/FooterViewHolder.java
new file mode 100644
index 0000000..0c94e3e
--- /dev/null
+++ b/java/src/com/android/intentresolver/grid/FooterViewHolder.java
@@ -0,0 +1,28 @@
+/*
+ * 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.grid;
+
+import android.view.View;
+
+/**
+ * A footer on the list, to support scrolling behavior below the navbar.
+ */
+public final class FooterViewHolder extends ViewHolderBase {
+    public FooterViewHolder(View itemView, int viewType) {
+        super(itemView, viewType);
+    }
+}
diff --git a/java/src/com/android/intentresolver/grid/ItemGroupViewHolder.java b/java/src/com/android/intentresolver/grid/ItemGroupViewHolder.java
new file mode 100644
index 0000000..5470506
--- /dev/null
+++ b/java/src/com/android/intentresolver/grid/ItemGroupViewHolder.java
@@ -0,0 +1,76 @@
+/*
+ * 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.grid;
+
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+
+/**
+ * Used to bind types for group of items including:
+ * {@link ChooserGridAdapter#VIEW_TYPE_DIRECT_SHARE},
+ * and {@link ChooserGridAdapter#VIEW_TYPE_CALLER_AND_RANK}.
+ */
+public abstract class ItemGroupViewHolder extends ViewHolderBase {
+    protected int mMeasuredRowHeight;
+    private int[] mItemIndices;
+    protected final View[] mCells;
+    private final int mColumnCount;
+
+    public ItemGroupViewHolder(int cellCount, View itemView, int viewType) {
+        super(itemView, viewType);
+        this.mCells = new View[cellCount];
+        this.mItemIndices = new int[cellCount];
+        this.mColumnCount = cellCount;
+    }
+
+    public abstract ViewGroup addView(int index, View v);
+
+    public abstract ViewGroup getViewGroup();
+
+    public abstract ViewGroup getRowByIndex(int index);
+
+    public abstract ViewGroup getRow(int rowNumber);
+
+    public abstract void setViewVisibility(int i, int visibility);
+
+    public int getColumnCount() {
+        return mColumnCount;
+    }
+
+    public void measure() {
+        final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+        getViewGroup().measure(spec, spec);
+        mMeasuredRowHeight = getViewGroup().getMeasuredHeight();
+    }
+
+    public int getMeasuredRowHeight() {
+        return mMeasuredRowHeight;
+    }
+
+    public void setItemIndex(int itemIndex, int listIndex) {
+        mItemIndices[itemIndex] = listIndex;
+    }
+
+    public int getItemIndex(int itemIndex) {
+        return mItemIndices[itemIndex];
+    }
+
+    public View getView(int index) {
+        return mCells[index];
+    }
+}
diff --git a/java/src/com/android/intentresolver/grid/ItemViewHolder.java b/java/src/com/android/intentresolver/grid/ItemViewHolder.java
new file mode 100644
index 0000000..2ec56b1
--- /dev/null
+++ b/java/src/com/android/intentresolver/grid/ItemViewHolder.java
@@ -0,0 +1,63 @@
+/*
+ * 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.grid;
+
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.ResolverListAdapter;
+
+import java.util.function.Consumer;
+
+/**
+ * Used to bind types of individual item including
+ * {@link ChooserGridAdapter#VIEW_TYPE_NORMAL},
+ * {@link ChooserGridAdapter#VIEW_TYPE_CONTENT_PREVIEW},
+ * {@link ChooserGridAdapter#VIEW_TYPE_PROFILE},
+ * and {@link ChooserGridAdapter#VIEW_TYPE_AZ_LABEL}.
+ */
+public final class ItemViewHolder extends ViewHolderBase {
+    private final ResolverListAdapter.ViewHolder mWrappedViewHolder;
+
+    private int mListPosition = ChooserListAdapter.NO_POSITION;
+
+    public ItemViewHolder(
+            View itemView,
+            int viewType,
+            @Nullable Consumer<Integer> onClick,
+            @Nullable Consumer<Integer> onLongClick) {
+        super(itemView, viewType);
+        mWrappedViewHolder = new ResolverListAdapter.ViewHolder(itemView);
+
+        if (onClick != null) {
+            itemView.setOnClickListener(v -> onClick.accept(mListPosition));
+        }
+
+        if (onLongClick != null) {
+            itemView.setOnLongClickListener(v -> {
+                onLongClick.accept(mListPosition);
+                return true;
+            });
+        }
+    }
+
+    public void setListPosition(int listPosition) {
+        mListPosition = listPosition;
+    }
+}
diff --git a/java/src/com/android/intentresolver/grid/SingleRowViewHolder.java b/java/src/com/android/intentresolver/grid/SingleRowViewHolder.java
new file mode 100644
index 0000000..a72da7a
--- /dev/null
+++ b/java/src/com/android/intentresolver/grid/SingleRowViewHolder.java
@@ -0,0 +1,73 @@
+/*
+ * 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.grid;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+/** Holder for a group of items displayed in a single row of the {@link ChooserGridAdapter}. */
+public final class SingleRowViewHolder extends ItemGroupViewHolder {
+    private final ViewGroup mRow;
+
+    public SingleRowViewHolder(ViewGroup row, int cellCount, int viewType) {
+        super(cellCount, row, viewType);
+
+        this.mRow = row;
+    }
+
+    /** Get the group of all views in this holder. */
+    public ViewGroup getViewGroup() {
+        return mRow;
+    }
+
+    /**
+     * Get the group of views for the row containing the specified cell index.
+     * TODO: unclear if that's what this `index` meant. It doesn't matter for our "single row"
+     * holders, and it doesn't look like this is an override from some other interface; maybe we can
+     * just remove?
+     */
+    public ViewGroup getRowByIndex(int index) {
+        return mRow;
+    }
+
+    /** Get the group of views for the specified {@code rowNumber}, if any. */
+    public ViewGroup getRow(int rowNumber) {
+        if (rowNumber == 0) {
+            return mRow;
+        }
+        return null;
+    }
+
+    /**
+     * @param index the index of the cell to add the view into.
+     * @param v the view to add into the cell.
+     */
+    public ViewGroup addView(int index, View v) {
+        mRow.addView(v);
+        mCells[index] = v;
+
+        return mRow;
+    }
+
+    /**
+     * @param i the index of the cell containing the view to modify.
+     * @param visibility the new visibility to set on the view with the specified index.
+     */
+    public void setViewVisibility(int i, int visibility) {
+        getView(i).setVisibility(visibility);
+    }
+}
diff --git a/java/src/com/android/intentresolver/grid/ViewHolderBase.java b/java/src/com/android/intentresolver/grid/ViewHolderBase.java
new file mode 100644
index 0000000..78e9104
--- /dev/null
+++ b/java/src/com/android/intentresolver/grid/ViewHolderBase.java
@@ -0,0 +1,35 @@
+/*
+ * 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.grid;
+
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+/** Base class for all {@link RecyclerView.ViewHolder} types in the {@link ChooserGridAdapter}. */
+public abstract class ViewHolderBase extends RecyclerView.ViewHolder {
+    private int mViewType;
+
+    ViewHolderBase(View itemView, int viewType) {
+        super(itemView);
+        this.mViewType = viewType;
+    }
+
+    public int getViewType() {
+        return mViewType;
+    }
+}
diff --git a/java/src/com/android/intentresolver/AbstractResolverComparator.java b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
similarity index 91%
rename from java/src/com/android/intentresolver/AbstractResolverComparator.java
rename to java/src/com/android/intentresolver/model/AbstractResolverComparator.java
index 6f80287..271c6f9 100644
--- a/java/src/com/android/intentresolver/AbstractResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.intentresolver;
+package com.android.intentresolver.model;
 
 import android.app.usage.UsageStatsManager;
 import android.content.ComponentName;
@@ -29,6 +29,8 @@
 import android.os.UserHandle;
 import android.util.Log;
 
+import com.android.intentresolver.ChooserActivityLogger;
+import com.android.intentresolver.ResolverActivity;
 import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
 
 import java.text.Collator;
@@ -47,7 +49,7 @@
     private static final boolean DEBUG = true;
     private static final String TAG = "AbstractResolverComp";
 
-    protected AfterCompute mAfterCompute;
+    protected Runnable mAfterCompute;
     protected final PackageManager mPm;
     protected final UsageStatsManager mUsm;
     protected String[] mAnnotations;
@@ -129,15 +131,7 @@
         }
     }
 
-    /**
-     * Callback to be called when {@link #compute(List)} finishes. This signals to stop waiting.
-     */
-    interface AfterCompute {
-
-        void afterCompute();
-    }
-
-    void setCallBack(AfterCompute afterCompute) {
+    public void setCallBack(Runnable afterCompute) {
         mAfterCompute = afterCompute;
     }
 
@@ -150,9 +144,9 @@
     }
 
     protected final void afterCompute() {
-        final AfterCompute afterCompute = mAfterCompute;
+        final Runnable afterCompute = mAfterCompute;
         if (afterCompute != null) {
-            afterCompute.afterCompute();
+            afterCompute.run();
         }
     }
 
@@ -161,11 +155,6 @@
         final ResolveInfo lhs = lhsp.getResolveInfoAt(0);
         final ResolveInfo rhs = rhsp.getResolveInfoAt(0);
 
-        final boolean lFixedAtTop = lhsp.isFixedAtTop();
-        final boolean rFixedAtTop = rhsp.isFixedAtTop();
-        if (lFixedAtTop && !rFixedAtTop) return -1;
-        if (!lFixedAtTop && rFixedAtTop) return 1;
-
         // We want to put the one targeted to another user at the end of the dialog.
         if (lhs.targetUserId != UserHandle.USER_CURRENT) {
             return rhs.targetUserId != UserHandle.USER_CURRENT ? 0 : 1;
@@ -214,7 +203,7 @@
      * ResolvedComponentInfo} by {@link ComponentName}. {@link #beforeCompute()} will be called
      * before doing any computing.
      */
-    final void compute(List<ResolvedComponentInfo> targets) {
+    public final void compute(List<ResolvedComponentInfo> targets) {
         beforeCompute();
         doCompute(targets);
     }
@@ -226,7 +215,7 @@
      * Returns the score that was calculated for the corresponding {@link ResolvedComponentInfo}
      * when {@link #compute(List)} was called before this.
      */
-    abstract float getScore(ComponentName name);
+    public abstract float getScore(ComponentName name);
 
     /** Handles result message sent to mHandler. */
     abstract void handleResultMessage(Message message);
@@ -234,7 +223,7 @@
     /**
      * Reports to UsageStats what was chosen.
      */
-    final void updateChooserCounts(String packageName, int userId, String action) {
+    public final void updateChooserCounts(String packageName, int userId, String action) {
         if (mUsm != null) {
             mUsm.reportChooserSelection(packageName, userId, mContentType, mAnnotations, action);
         }
@@ -248,7 +237,7 @@
      *
      * @param componentName the component that the user clicked
      */
-    void updateModel(ComponentName componentName) {
+    public void updateModel(ComponentName componentName) {
     }
 
     /** Called before {@link #doCompute(List)}. Sets up 500ms timeout. */
@@ -266,7 +255,7 @@
      * this call needs to happen at a different time during destroy, the method should be
      * overridden.
      */
-    void destroy() {
+    public void destroy() {
         mHandler.removeMessages(RANKER_SERVICE_RESULT);
         mHandler.removeMessages(RANKER_RESULT_TIMEOUT);
         afterCompute();
diff --git a/java/src/com/android/intentresolver/AppPredictionServiceResolverComparator.java b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
similarity index 96%
rename from java/src/com/android/intentresolver/AppPredictionServiceResolverComparator.java
rename to java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
index 9b9fc1c..c6bb2b8 100644
--- a/java/src/com/android/intentresolver/AppPredictionServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.intentresolver;
+package com.android.intentresolver.model;
 
 import static android.app.prediction.AppTargetEvent.ACTION_LAUNCH;
 
@@ -31,6 +31,7 @@
 import android.os.UserHandle;
 import android.util.Log;
 
+import com.android.intentresolver.ChooserActivityLogger;
 import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
 
 import java.util.ArrayList;
@@ -45,7 +46,7 @@
  * disabled by returning an empty sorted target list, {@link AppPredictionServiceResolverComparator}
  * will fallback to using a {@link ResolverRankerServiceResolverComparator}.
  */
-class AppPredictionServiceResolverComparator extends AbstractResolverComparator {
+public class AppPredictionServiceResolverComparator extends AbstractResolverComparator {
 
     private static final String TAG = "APSResolverComparator";
 
@@ -62,7 +63,7 @@
     private ResolverRankerServiceResolverComparator mResolverRankerService;
     private AppPredictionServiceComparatorModel mComparatorModel;
 
-    AppPredictionServiceResolverComparator(
+    public AppPredictionServiceResolverComparator(
             Context context,
             Intent intent,
             String referrerPackage,
@@ -166,17 +167,17 @@
     }
 
     @Override
-    float getScore(ComponentName name) {
+    public float getScore(ComponentName name) {
         return mComparatorModel.getScore(name);
     }
 
     @Override
-    void updateModel(ComponentName componentName) {
+    public void updateModel(ComponentName componentName) {
         mComparatorModel.notifyOnTargetSelected(componentName);
     }
 
     @Override
-    void destroy() {
+    public void destroy() {
         if (mResolverRankerService != null) {
             mResolverRankerService.destroy();
             mResolverRankerService = null;
diff --git a/java/src/com/android/intentresolver/ResolverComparatorModel.java b/java/src/com/android/intentresolver/model/ResolverComparatorModel.java
similarity index 97%
rename from java/src/com/android/intentresolver/ResolverComparatorModel.java
rename to java/src/com/android/intentresolver/model/ResolverComparatorModel.java
index 79160c8..3616a85 100644
--- a/java/src/com/android/intentresolver/ResolverComparatorModel.java
+++ b/java/src/com/android/intentresolver/model/ResolverComparatorModel.java
@@ -14,13 +14,12 @@
  * limitations under the License.
  */
 
-package com.android.intentresolver;
+package com.android.intentresolver.model;
 
 import android.content.ComponentName;
 import android.content.pm.ResolveInfo;
 
 import java.util.Comparator;
-import java.util.List;
 
 /**
  * A ranking model for resolver targets, providing ordering and (optionally) numerical scoring.
diff --git a/java/src/com/android/intentresolver/ResolverRankerServiceResolverComparator.java b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
similarity index 96%
rename from java/src/com/android/intentresolver/ResolverRankerServiceResolverComparator.java
rename to java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
index be3e6f1..4382f10 100644
--- a/java/src/com/android/intentresolver/ResolverRankerServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
@@ -15,7 +15,7 @@
  */
 
 
-package com.android.intentresolver;
+package com.android.intentresolver.model;
 
 import android.app.usage.UsageStats;
 import android.content.ComponentName;
@@ -37,8 +37,8 @@
 import android.service.resolver.ResolverTarget;
 import android.util.Log;
 
+import com.android.intentresolver.ChooserActivityLogger;
 import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
-
 import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 
@@ -54,7 +54,7 @@
 /**
  * Ranks and compares packages based on usage stats and uses the {@link ResolverRankerService}.
  */
-class ResolverRankerServiceResolverComparator extends AbstractResolverComparator {
+public class ResolverRankerServiceResolverComparator extends AbstractResolverComparator {
     private static final String TAG = "RRSResolverComparator";
 
     private static final boolean DEBUG = false;
@@ -87,7 +87,7 @@
     private ResolverRankerServiceComparatorModel mComparatorModel;
 
     public ResolverRankerServiceResolverComparator(Context context, Intent intent,
-                String referrerPackage, AfterCompute afterCompute,
+                String referrerPackage, Runnable afterCompute,
                 ChooserActivityLogger chooserActivityLogger) {
         super(context, intent);
         mCollator = Collator.getInstance(context.getResources().getConfiguration().locale);
@@ -191,9 +191,9 @@
                     if (mAction == null) {
                         Log.d(TAG, "Action type is null");
                     } else {
-                        Log.d(TAG, "Chooser Count of " + mAction + ":" +
-                                target.name.getPackageName() + " is " +
-                                Float.toString(chooserScore));
+                        Log.d(TAG, "Chooser Count of " + mAction + ":"
+                                + target.name.getPackageName() + " is "
+                                + Float.toString(chooserScore));
                     }
                 }
                 resolverTarget.setChooserScore(chooserScore);
@@ -333,7 +333,7 @@
     private class ResolverRankerServiceConnection implements ServiceConnection {
         private final CountDownLatch mConnectSignal;
 
-        public ResolverRankerServiceConnection(CountDownLatch connectSignal) {
+        ResolverRankerServiceConnection(CountDownLatch connectSignal) {
             mConnectSignal = connectSignal;
         }
 
@@ -424,8 +424,10 @@
 
     // adds select prob as the default values, according to a pre-trained Logistic Regression model.
     private void addDefaultSelectProbability(ResolverTarget target) {
-        float sum = 2.5543f * target.getLaunchScore() + 2.8412f * target.getTimeSpentScore() +
-                0.269f * target.getRecencyScore() + 4.2222f * target.getChooserScore();
+        float sum = (2.5543f * target.getLaunchScore())
+                + (2.8412f * target.getTimeSpentScore())
+                + (0.269f * target.getRecencyScore())
+                + (4.2222f * target.getChooserScore());
         target.setSelectProbability((float) (1.0 / (1.0 + Math.exp(1.6568f - sum))));
     }
 
@@ -440,8 +442,8 @@
 
     static boolean isPersistentProcess(ResolvedComponentInfo rci) {
         if (rci != null && rci.getCount() > 0) {
-            return (rci.getResolveInfoAt(0).activityInfo.applicationInfo.flags &
-                    ApplicationInfo.FLAG_PERSISTENT) != 0;
+            int flags = rci.getResolveInfoAt(0).activityInfo.applicationInfo.flags;
+            return (flags & ApplicationInfo.FLAG_PERSISTENT) != 0;
         }
         return false;
     }
diff --git a/java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt b/java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt
new file mode 100644
index 0000000..82f40b9
--- /dev/null
+++ b/java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver.shortcuts
+
+import android.app.prediction.AppPredictionContext
+import android.app.prediction.AppPredictionManager
+import android.app.prediction.AppPredictor
+import android.content.Context
+import android.content.IntentFilter
+import android.os.Bundle
+import android.os.UserHandle
+
+// TODO(b/123088566) Share these in a better way.
+private const val APP_PREDICTION_SHARE_UI_SURFACE = "share"
+private const val APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT = 20
+private const val APP_PREDICTION_INTENT_FILTER_KEY = "intent_filter"
+private const val SHARED_TEXT_KEY = "shared_text"
+
+/**
+ * A factory to create an AppPredictor instance for a profile, if available.
+ * @param context, application context
+ * @param sharedText, a shared text associated with the Chooser's target intent
+ * (see [android.content.Intent.EXTRA_TEXT]).
+ * Will be mapped to app predictor's "shared_text" parameter.
+ * @param targetIntentFilter, an IntentFilter to match direct share targets against.
+ * Will be mapped app predictor's "intent_filter" parameter.
+ */
+class AppPredictorFactory(
+    private val context: Context,
+    private val sharedText: String?,
+    private val targetIntentFilter: IntentFilter?
+) {
+    private val mIsComponentAvailable =
+        context.packageManager.appPredictionServicePackageName != null
+
+    /**
+     * Creates an AppPredictor instance for a profile or `null` if app predictor is not available.
+     */
+    fun create(userHandle: UserHandle): AppPredictor? {
+        if (!mIsComponentAvailable) return null
+        val contextAsUser = context.createContextAsUser(userHandle, 0 /* flags */)
+        val extras = Bundle().apply {
+            putParcelable(APP_PREDICTION_INTENT_FILTER_KEY, targetIntentFilter)
+            putString(SHARED_TEXT_KEY, sharedText)
+        }
+        val appPredictionContext = AppPredictionContext.Builder(contextAsUser)
+            .setUiSurface(APP_PREDICTION_SHARE_UI_SURFACE)
+            .setPredictedTargetCount(APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT)
+            .setExtras(extras)
+            .build()
+        return contextAsUser.getSystemService(AppPredictionManager::class.java)
+            ?.createAppPredictionSession(appPredictionContext)
+    }
+}
diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java
new file mode 100644
index 0000000..1cfa2c8
--- /dev/null
+++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java
@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.shortcuts;
+
+import android.app.ActivityManager;
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.IntentFilter;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.ApplicationInfoFlags;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.os.AsyncTask;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.service.chooser.ChooserTarget;
+import android.text.TextUtils;
+import android.util.Log;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+import androidx.annotation.VisibleForTesting;
+import androidx.annotation.WorkerThread;
+
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+/**
+ * Encapsulates shortcuts loading logic from either AppPredictor or ShortcutManager.
+ * <p>
+ *     A ShortcutLoader instance can be viewed as a per-profile singleton hot stream of shortcut
+ * updates. The shortcut loading is triggered by the {@link #queryShortcuts(DisplayResolveInfo[])},
+ * the processing will happen on the {@link #mBackgroundExecutor} and the result is delivered
+ * through the {@link #mCallback} on the {@link #mCallbackExecutor}, the main thread.
+ * </p>
+ * <p>
+ *    The current version does not improve on the legacy in a way that it does not guarantee that
+ * each invocation of the {@link #queryShortcuts(DisplayResolveInfo[])} will be matched by an
+ * invocation of the callback (there are early terminations of the flow). Also, the fetched
+ * shortcuts would be matched against the last known input, i.e. two invocations of
+ * {@link #queryShortcuts(DisplayResolveInfo[])} may result in two callbacks where shortcuts are
+ * processed against the latest input.
+ * </p>
+ */
+public class ShortcutLoader {
+    private static final String TAG = "ChooserActivity";
+
+    private static final Request NO_REQUEST = new Request(new DisplayResolveInfo[0]);
+
+    private final Context mContext;
+    @Nullable
+    private final AppPredictorProxy mAppPredictor;
+    private final UserHandle mUserHandle;
+    @Nullable
+    private final IntentFilter mTargetIntentFilter;
+    private final Executor mBackgroundExecutor;
+    private final Executor mCallbackExecutor;
+    private final boolean mIsPersonalProfile;
+    private final ShortcutToChooserTargetConverter mShortcutToChooserTargetConverter =
+            new ShortcutToChooserTargetConverter();
+    private final UserManager mUserManager;
+    private final AtomicReference<Consumer<Result>> mCallback = new AtomicReference<>();
+    private final AtomicReference<Request> mActiveRequest = new AtomicReference<>(NO_REQUEST);
+
+    @Nullable
+    private final AppPredictor.Callback mAppPredictorCallback;
+
+    @MainThread
+    public ShortcutLoader(
+            Context context,
+            @Nullable AppPredictor appPredictor,
+            UserHandle userHandle,
+            @Nullable IntentFilter targetIntentFilter,
+            Consumer<Result> callback) {
+        this(
+                context,
+                appPredictor == null ? null : new AppPredictorProxy(appPredictor),
+                userHandle,
+                userHandle.equals(UserHandle.of(ActivityManager.getCurrentUser())),
+                targetIntentFilter,
+                AsyncTask.SERIAL_EXECUTOR,
+                context.getMainExecutor(),
+                callback);
+    }
+
+    @VisibleForTesting
+    ShortcutLoader(
+            Context context,
+            @Nullable AppPredictorProxy appPredictor,
+            UserHandle userHandle,
+            boolean isPersonalProfile,
+            @Nullable IntentFilter targetIntentFilter,
+            Executor backgroundExecutor,
+            Executor callbackExecutor,
+            Consumer<Result> callback) {
+        mContext = context;
+        mAppPredictor = appPredictor;
+        mUserHandle = userHandle;
+        mTargetIntentFilter = targetIntentFilter;
+        mBackgroundExecutor = backgroundExecutor;
+        mCallbackExecutor = callbackExecutor;
+        mCallback.set(callback);
+        mIsPersonalProfile = isPersonalProfile;
+        mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+
+        if (mAppPredictor != null) {
+            mAppPredictorCallback = createAppPredictorCallback();
+            mAppPredictor.registerPredictionUpdates(mCallbackExecutor, mAppPredictorCallback);
+        } else {
+            mAppPredictorCallback = null;
+        }
+    }
+
+    /**
+     * Unsubscribe from app predictor if one was provided.
+     */
+    @MainThread
+    public void destroy() {
+        if (mCallback.getAndSet(null) != null) {
+            if (mAppPredictor != null) {
+                mAppPredictor.unregisterPredictionUpdates(mAppPredictorCallback);
+            }
+        }
+    }
+
+    private boolean isDestroyed() {
+        return mCallback.get() == null;
+    }
+
+    /**
+     * Set new resolved targets. This will trigger shortcut loading.
+     * @param appTargets a collection of application targets a loaded set of shortcuts will be
+     *                   grouped against
+     */
+    @MainThread
+    public void queryShortcuts(DisplayResolveInfo[] appTargets) {
+        if (isDestroyed()) {
+            return;
+        }
+        mActiveRequest.set(new Request(appTargets));
+        mBackgroundExecutor.execute(this::loadShortcuts);
+    }
+
+    @WorkerThread
+    private void loadShortcuts() {
+        // no need to query direct share for work profile when its locked or disabled
+        if (!shouldQueryDirectShareTargets()) {
+            return;
+        }
+        Log.d(TAG, "querying direct share targets");
+        queryDirectShareTargets(false);
+    }
+
+    @WorkerThread
+    private void queryDirectShareTargets(boolean skipAppPredictionService) {
+        if (isDestroyed()) {
+            return;
+        }
+        if (!skipAppPredictionService && mAppPredictor != null) {
+            mAppPredictor.requestPredictionUpdate();
+            return;
+        }
+        // Default to just querying ShortcutManager if AppPredictor not present.
+        if (mTargetIntentFilter == null) {
+            return;
+        }
+
+        Context selectedProfileContext = mContext.createContextAsUser(mUserHandle, 0 /* flags */);
+        ShortcutManager sm = (ShortcutManager) selectedProfileContext
+                .getSystemService(Context.SHORTCUT_SERVICE);
+        List<ShortcutManager.ShareShortcutInfo> shortcuts =
+                sm.getShareTargets(mTargetIntentFilter);
+        sendShareShortcutInfoList(shortcuts, false, null);
+    }
+
+    private AppPredictor.Callback createAppPredictorCallback() {
+        return appPredictorTargets -> {
+            if (appPredictorTargets.isEmpty() && shouldQueryDirectShareTargets()) {
+                // APS may be disabled, so try querying targets ourselves.
+                queryDirectShareTargets(true);
+                return;
+            }
+
+            final List<ShortcutManager.ShareShortcutInfo> shortcuts = new ArrayList<>();
+            List<AppTarget> shortcutResults = new ArrayList<>();
+            for (AppTarget appTarget : appPredictorTargets) {
+                if (appTarget.getShortcutInfo() == null) {
+                    continue;
+                }
+                shortcutResults.add(appTarget);
+            }
+            appPredictorTargets = shortcutResults;
+            for (AppTarget appTarget : appPredictorTargets) {
+                shortcuts.add(new ShortcutManager.ShareShortcutInfo(
+                        appTarget.getShortcutInfo(),
+                        new ComponentName(appTarget.getPackageName(), appTarget.getClassName())));
+            }
+            sendShareShortcutInfoList(shortcuts, true, appPredictorTargets);
+        };
+    }
+
+    @WorkerThread
+    private void sendShareShortcutInfoList(
+            List<ShortcutManager.ShareShortcutInfo> shortcuts,
+            boolean isFromAppPredictor,
+            @Nullable List<AppTarget> appPredictorTargets) {
+        if (appPredictorTargets != null && appPredictorTargets.size() != shortcuts.size()) {
+            throw new RuntimeException("resultList and appTargets must have the same size."
+                    + " resultList.size()=" + shortcuts.size()
+                    + " appTargets.size()=" + appPredictorTargets.size());
+        }
+        Context selectedProfileContext = mContext.createContextAsUser(mUserHandle, 0 /* flags */);
+        for (int i = shortcuts.size() - 1; i >= 0; i--) {
+            final String packageName = shortcuts.get(i).getTargetComponent().getPackageName();
+            if (!isPackageEnabled(selectedProfileContext, packageName)) {
+                shortcuts.remove(i);
+                if (appPredictorTargets != null) {
+                    appPredictorTargets.remove(i);
+                }
+            }
+        }
+
+        HashMap<ChooserTarget, AppTarget> directShareAppTargetCache = new HashMap<>();
+        HashMap<ChooserTarget, ShortcutInfo> directShareShortcutInfoCache = new HashMap<>();
+        // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path
+        // for direct share targets. After ShareSheet is refactored we should use the
+        // ShareShortcutInfos directly.
+        final DisplayResolveInfo[] appTargets = mActiveRequest.get().appTargets;
+        List<ShortcutResultInfo> resultRecords = new ArrayList<>();
+        for (DisplayResolveInfo displayResolveInfo : appTargets) {
+            List<ShortcutManager.ShareShortcutInfo> matchingShortcuts =
+                    filterShortcutsByTargetComponentName(
+                            shortcuts, displayResolveInfo.getResolvedComponentName());
+            if (matchingShortcuts.isEmpty()) {
+                continue;
+            }
+
+            List<ChooserTarget> chooserTargets = mShortcutToChooserTargetConverter
+                    .convertToChooserTarget(
+                            matchingShortcuts,
+                            shortcuts,
+                            appPredictorTargets,
+                            directShareAppTargetCache,
+                            directShareShortcutInfoCache);
+
+            ShortcutResultInfo resultRecord =
+                    new ShortcutResultInfo(displayResolveInfo, chooserTargets);
+            resultRecords.add(resultRecord);
+        }
+
+        postReport(
+                new Result(
+                        isFromAppPredictor,
+                        appTargets,
+                        resultRecords.toArray(new ShortcutResultInfo[0]),
+                        directShareAppTargetCache,
+                        directShareShortcutInfoCache));
+    }
+
+    private void postReport(Result result) {
+        mCallbackExecutor.execute(() -> report(result));
+    }
+
+    @MainThread
+    private void report(Result result) {
+        Consumer<Result> callback = mCallback.get();
+        if (callback != null) {
+            callback.accept(result);
+        }
+    }
+
+    /**
+     * Returns {@code false} if {@code userHandle} is the work profile and it's either
+     * in quiet mode or not running.
+     */
+    private boolean shouldQueryDirectShareTargets() {
+        return mIsPersonalProfile || isProfileActive();
+    }
+
+    @VisibleForTesting
+    protected boolean isProfileActive() {
+        return mUserManager.isUserRunning(mUserHandle)
+                && mUserManager.isUserUnlocked(mUserHandle)
+                && !mUserManager.isQuietModeEnabled(mUserHandle);
+    }
+
+    private static boolean isPackageEnabled(Context context, String packageName) {
+        if (TextUtils.isEmpty(packageName)) {
+            return false;
+        }
+        ApplicationInfo appInfo;
+        try {
+            appInfo = context.getPackageManager().getApplicationInfo(
+                    packageName,
+                    ApplicationInfoFlags.of(PackageManager.GET_META_DATA));
+        } catch (NameNotFoundException e) {
+            return false;
+        }
+
+        return appInfo != null && appInfo.enabled
+                && (appInfo.flags & ApplicationInfo.FLAG_SUSPENDED) == 0;
+    }
+
+    private static List<ShortcutManager.ShareShortcutInfo> filterShortcutsByTargetComponentName(
+            List<ShortcutManager.ShareShortcutInfo> allShortcuts, ComponentName requiredTarget) {
+        List<ShortcutManager.ShareShortcutInfo> matchingShortcuts = new ArrayList<>();
+        for (ShortcutManager.ShareShortcutInfo shortcut : allShortcuts) {
+            if (requiredTarget.equals(shortcut.getTargetComponent())) {
+                matchingShortcuts.add(shortcut);
+            }
+        }
+        return matchingShortcuts;
+    }
+
+    private static class Request {
+        public final DisplayResolveInfo[] appTargets;
+
+        Request(DisplayResolveInfo[] targets) {
+            appTargets = targets;
+        }
+    }
+
+    /**
+     * Resolved shortcuts with corresponding app targets.
+     */
+    public static class Result {
+        public final boolean isFromAppPredictor;
+        /**
+         * Input app targets (see {@link ShortcutLoader#queryShortcuts(DisplayResolveInfo[])} the
+         * shortcuts were process against.
+         */
+        public final DisplayResolveInfo[] appTargets;
+        /**
+         * Shortcuts grouped by app target.
+         */
+        public final ShortcutResultInfo[] shortcutsByApp;
+        public final Map<ChooserTarget, AppTarget> directShareAppTargetCache;
+        public final Map<ChooserTarget, ShortcutInfo> directShareShortcutInfoCache;
+
+        @VisibleForTesting
+        public Result(
+                boolean isFromAppPredictor,
+                DisplayResolveInfo[] appTargets,
+                ShortcutResultInfo[] shortcutsByApp,
+                Map<ChooserTarget, AppTarget> directShareAppTargetCache,
+                Map<ChooserTarget, ShortcutInfo> directShareShortcutInfoCache) {
+            this.isFromAppPredictor = isFromAppPredictor;
+            this.appTargets = appTargets;
+            this.shortcutsByApp = shortcutsByApp;
+            this.directShareAppTargetCache = directShareAppTargetCache;
+            this.directShareShortcutInfoCache = directShareShortcutInfoCache;
+        }
+    }
+
+    /**
+     * Shortcuts grouped by app.
+     */
+    public static class ShortcutResultInfo {
+        public final DisplayResolveInfo appTarget;
+        public final List<ChooserTarget> shortcuts;
+
+        public ShortcutResultInfo(DisplayResolveInfo appTarget, List<ChooserTarget> shortcuts) {
+            this.appTarget = appTarget;
+            this.shortcuts = shortcuts;
+        }
+    }
+
+    /**
+     * A wrapper around AppPredictor to facilitate unit-testing.
+     */
+    @VisibleForTesting
+    public static class AppPredictorProxy {
+        private final AppPredictor mAppPredictor;
+
+        AppPredictorProxy(AppPredictor appPredictor) {
+            mAppPredictor = appPredictor;
+        }
+
+        /**
+         * {@link AppPredictor#registerPredictionUpdates}
+         */
+        public void registerPredictionUpdates(
+                Executor callbackExecutor, AppPredictor.Callback callback) {
+            mAppPredictor.registerPredictionUpdates(callbackExecutor, callback);
+        }
+
+        /**
+         * {@link AppPredictor#unregisterPredictionUpdates}
+         */
+        public void unregisterPredictionUpdates(AppPredictor.Callback callback) {
+            mAppPredictor.unregisterPredictionUpdates(callback);
+        }
+
+        /**
+         * {@link AppPredictor#requestPredictionUpdate}
+         */
+        public void requestPredictionUpdate() {
+            mAppPredictor.requestPredictionUpdate();
+        }
+    }
+}
diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java b/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java
new file mode 100644
index 0000000..a37d655
--- /dev/null
+++ b/java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.shortcuts;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.prediction.AppTarget;
+import android.content.Intent;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.os.Bundle;
+import android.service.chooser.ChooserTarget;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+class ShortcutToChooserTargetConverter {
+
+    /**
+     * Converts a list of ShareShortcutInfos to ChooserTargets.
+     * @param matchingShortcuts List of shortcuts, all from the same package, that match the current
+     *                         share intent filter.
+     * @param allShortcuts List of all the shortcuts from all the packages on the device that are
+     *                    returned for the current sharing action.
+     * @param allAppTargets List of AppTargets. Null if the results are not from prediction service.
+     * @param directShareAppTargetCache An optional map to store mapping for the new ChooserTarget
+     *  instances back to original allAppTargets.
+     * @param directShareShortcutInfoCache An optional map to store mapping from the new
+     *  ChooserTarget instances back to the original matchingShortcuts' {@code getShortcutInfo()}
+     * @return A list of ChooserTargets sorted by score in descending order.
+     */
+    @NonNull
+    public List<ChooserTarget> convertToChooserTarget(
+            @NonNull List<ShortcutManager.ShareShortcutInfo> matchingShortcuts,
+            @NonNull List<ShortcutManager.ShareShortcutInfo> allShortcuts,
+            @Nullable List<AppTarget> allAppTargets,
+            @Nullable Map<ChooserTarget, AppTarget> directShareAppTargetCache,
+            @Nullable Map<ChooserTarget, ShortcutInfo> directShareShortcutInfoCache) {
+        // If |appTargets| is not null, results are from AppPredictionService and already sorted.
+        final boolean isFromAppPredictor = allAppTargets != null;
+        // A set of distinct scores for the matched shortcuts. We use index of a rank in the sorted
+        // list instead of the actual rank value when converting a rank to a score.
+        List<Integer> scoreList = new ArrayList<>();
+        if (!isFromAppPredictor) {
+            for (int i = 0; i < matchingShortcuts.size(); i++) {
+                int shortcutRank = matchingShortcuts.get(i).getShortcutInfo().getRank();
+                if (!scoreList.contains(shortcutRank)) {
+                    scoreList.add(shortcutRank);
+                }
+            }
+            Collections.sort(scoreList);
+        }
+
+        List<ChooserTarget> chooserTargetList = new ArrayList<>(matchingShortcuts.size());
+        for (int i = 0; i < matchingShortcuts.size(); i++) {
+            ShortcutInfo shortcutInfo = matchingShortcuts.get(i).getShortcutInfo();
+            int indexInAllShortcuts = allShortcuts.indexOf(matchingShortcuts.get(i));
+
+            float score;
+            if (isFromAppPredictor) {
+                // Incoming results are ordered. Create a score based on index in the original list.
+                score = Math.max(1.0f - (0.01f * indexInAllShortcuts), 0.0f);
+            } else {
+                // Create a score based on the rank of the shortcut.
+                int rankIndex = scoreList.indexOf(shortcutInfo.getRank());
+                score = Math.max(1.0f - (0.01f * rankIndex), 0.0f);
+            }
+
+            Bundle extras = new Bundle();
+            extras.putString(Intent.EXTRA_SHORTCUT_ID, shortcutInfo.getId());
+
+            ChooserTarget chooserTarget = new ChooserTarget(
+                    shortcutInfo.getLabel(),
+                    null, // Icon will be loaded later if this target is selected to be shown.
+                    score, matchingShortcuts.get(i).getTargetComponent().clone(), extras);
+
+            chooserTargetList.add(chooserTarget);
+            if (directShareAppTargetCache != null && allAppTargets != null) {
+                directShareAppTargetCache.put(chooserTarget,
+                        allAppTargets.get(indexInAllShortcuts));
+            }
+            if (directShareShortcutInfoCache != null) {
+                directShareShortcutInfoCache.put(chooserTarget, shortcutInfo);
+            }
+        }
+        // Sort ChooserTargets by score in descending order
+        Comparator<ChooserTarget> byScore =
+                (ChooserTarget a, ChooserTarget b) -> -Float.compare(a.getScore(), b.getScore());
+        Collections.sort(chooserTargetList, byScore);
+        return chooserTargetList;
+    }
+}
diff --git a/java/src/com/android/intentresolver/widget/ActionRow.kt b/java/src/com/android/intentresolver/widget/ActionRow.kt
new file mode 100644
index 0000000..6764d3a
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ActionRow.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.widget
+
+import android.content.res.Resources.ID_NULL
+import android.graphics.drawable.Drawable
+
+interface ActionRow {
+    fun setActions(actions: List<Action>)
+
+    class Action @JvmOverloads constructor(
+        // TODO: apparently, IDs set to this field are used in unit tests only; evaluate whether we
+        //  get rid of them
+        val id: Int = ID_NULL,
+        val label: CharSequence?,
+        val icon: Drawable?,
+        val onClicked: Runnable,
+    )
+}
diff --git a/java/src/com/android/intentresolver/widget/ChooserActionRow.kt b/java/src/com/android/intentresolver/widget/ChooserActionRow.kt
new file mode 100644
index 0000000..a4656bb
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ChooserActionRow.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.widget
+
+import android.annotation.LayoutRes
+import android.content.Context
+import android.os.Parcelable
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.widget.Button
+import android.widget.LinearLayout
+import com.android.intentresolver.R
+import com.android.intentresolver.widget.ActionRow.Action
+
+class ChooserActionRow : LinearLayout, ActionRow {
+    constructor(context: Context) : this(context, null)
+    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
+    constructor(
+        context: Context, attrs: AttributeSet?, defStyleAttr: Int
+    ) : this(context, attrs, defStyleAttr, 0)
+
+    constructor(
+        context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int
+    ) : super(context, attrs, defStyleAttr, defStyleRes) {
+        orientation = HORIZONTAL
+    }
+
+    @LayoutRes
+    private val itemLayout = R.layout.chooser_action_button
+    private val itemMargin =
+        context.resources.getDimensionPixelSize(R.dimen.resolver_icon_margin) / 2
+    private var actions: List<Action> = emptyList()
+
+    override fun onRestoreInstanceState(state: Parcelable?) {
+        super.onRestoreInstanceState(state)
+        setActions(actions)
+    }
+
+    override fun setActions(actions: List<Action>) {
+        removeAllViews()
+        this.actions = ArrayList(actions)
+        for (action in actions) {
+            addAction(action)
+        }
+    }
+
+    private fun addAction(action: Action) {
+        val b = LayoutInflater.from(context).inflate(itemLayout, null) as Button
+        if (action.icon != null) {
+            val size = resources
+                .getDimensionPixelSize(R.dimen.chooser_action_button_icon_size)
+            action.icon.setBounds(0, 0, size, size)
+            b.setCompoundDrawablesRelative(action.icon, null, null, null)
+        }
+        b.text = action.label ?: ""
+        b.setOnClickListener {
+            action.onClicked.run()
+        }
+        b.id = action.id
+        addView(b)
+    }
+
+    override fun generateDefaultLayoutParams(): LayoutParams =
+        super.generateDefaultLayoutParams().apply {
+            setMarginsRelative(itemMargin, 0, itemMargin, 0)
+        }
+}
diff --git a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt
new file mode 100644
index 0000000..a37ef95
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt
@@ -0,0 +1,178 @@
+/*
+ * 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.widget
+
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.graphics.Bitmap
+import android.net.Uri
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewTreeObserver
+import android.view.animation.DecelerateInterpolator
+import android.widget.RelativeLayout
+import androidx.core.view.isVisible
+import com.android.intentresolver.R
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import java.util.function.Consumer
+import com.android.internal.R as IntR
+
+typealias ImageLoader = suspend (Uri) -> Bitmap?
+
+private const val IMAGE_FADE_IN_MILLIS = 150L
+
+class ImagePreviewView : RelativeLayout {
+
+    constructor(context: Context) : this(context, null)
+    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
+
+    constructor(
+        context: Context, attrs: AttributeSet?, defStyleAttr: Int
+    ) : this(context, attrs, defStyleAttr, 0)
+
+    constructor(
+        context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int
+    ) : super(context, attrs, defStyleAttr, defStyleRes)
+
+    private val coroutineScope = MainScope()
+    private lateinit var mainImage: RoundedRectImageView
+    private lateinit var secondLargeImage: RoundedRectImageView
+    private lateinit var secondSmallImage: RoundedRectImageView
+    private lateinit var thirdImage: RoundedRectImageView
+
+    private var loadImageJob: Job? = null
+    private var onTransitionViewReadyCallback: Consumer<Boolean>? = null
+
+    override fun onFinishInflate() {
+        LayoutInflater.from(context).inflate(R.layout.image_preview_view, this, true)
+        mainImage = requireViewById(IntR.id.content_preview_image_1_large)
+        secondLargeImage = requireViewById(IntR.id.content_preview_image_2_large)
+        secondSmallImage = requireViewById(IntR.id.content_preview_image_2_small)
+        thirdImage = requireViewById(IntR.id.content_preview_image_3_small)
+    }
+
+    /**
+     * Specifies a transition animation target name and a readiness callback. The callback will be
+     * invoked once when the view preparation is done i.e. either when an image is loaded into it
+     * and it is laid out (and it is ready to be draw) or image loading has failed.
+     * Should be called before [setImages].
+     * @param name, transition name
+     * @param onViewReady, a callback that will be invoked with `true` if the view is ready to
+     * receive transition animation (the image was loaded successfully) and with `false` otherwise.
+     */
+    fun setSharedElementTransitionTarget(name: String, onViewReady: Consumer<Boolean>) {
+        mainImage.transitionName = name
+        onTransitionViewReadyCallback = onViewReady
+    }
+
+    fun setImages(uris: List<Uri>, imageLoader: ImageLoader) {
+        loadImageJob?.cancel()
+        loadImageJob = coroutineScope.launch {
+            when (uris.size) {
+                0 -> hideAllViews()
+                1 -> showOneImage(uris, imageLoader)
+                2 -> showTwoImages(uris, imageLoader)
+                else -> showThreeImages(uris, imageLoader)
+            }
+        }
+    }
+
+    private fun hideAllViews() {
+        mainImage.isVisible = false
+        secondLargeImage.isVisible = false
+        secondSmallImage.isVisible = false
+        thirdImage.isVisible = false
+        invokeTransitionViewReadyCallback(runTransitionAnimation = false)
+    }
+
+    private suspend fun showOneImage(uris: List<Uri>, imageLoader: ImageLoader) {
+        secondLargeImage.isVisible = false
+        secondSmallImage.isVisible = false
+        thirdImage.isVisible = false
+        showImages(uris, imageLoader, mainImage)
+    }
+
+    private suspend fun showTwoImages(uris: List<Uri>, imageLoader: ImageLoader) {
+        secondSmallImage.isVisible = false
+        thirdImage.isVisible = false
+        showImages(uris, imageLoader, mainImage, secondLargeImage)
+    }
+
+    private suspend fun showThreeImages(uris: List<Uri>, imageLoader: ImageLoader) {
+        secondLargeImage.isVisible = false
+        showImages(uris, imageLoader, mainImage, secondSmallImage, thirdImage)
+        thirdImage.setExtraImageCount(uris.size - 3)
+    }
+
+    private suspend fun showImages(
+        uris: List<Uri>, imageLoader: ImageLoader, vararg views: RoundedRectImageView
+    ) = coroutineScope {
+        for (i in views.indices) {
+            launch {
+                loadImageIntoView(views[i], uris[i], imageLoader)
+            }
+        }
+    }
+
+    private suspend fun loadImageIntoView(
+        view: RoundedRectImageView, uri: Uri, imageLoader: ImageLoader
+    ) {
+        val bitmap = runCatching {
+            imageLoader(uri)
+        }.getOrDefault(null)
+        if (bitmap == null) {
+            view.isVisible = false
+            if (view === mainImage) {
+                invokeTransitionViewReadyCallback(runTransitionAnimation = false)
+            }
+        } else {
+            view.isVisible = true
+            view.setImageBitmap(bitmap)
+
+            view.alpha = 0f
+            ObjectAnimator.ofFloat(view, "alpha", 0.0f, 1.0f).apply {
+                interpolator = DecelerateInterpolator(1.0f)
+                duration = IMAGE_FADE_IN_MILLIS
+                start()
+            }
+            if (view === mainImage && onTransitionViewReadyCallback != null) {
+                setupPreDrawListener(mainImage)
+            }
+        }
+    }
+
+    private fun setupPreDrawListener(view: View) {
+        view.viewTreeObserver.addOnPreDrawListener(
+            object : ViewTreeObserver.OnPreDrawListener {
+                override fun onPreDraw(): Boolean {
+                    view.viewTreeObserver.removeOnPreDrawListener(this)
+                    invokeTransitionViewReadyCallback(runTransitionAnimation = true)
+                    return true
+                }
+            }
+        )
+    }
+
+    private fun invokeTransitionViewReadyCallback(runTransitionAnimation: Boolean) {
+        onTransitionViewReadyCallback?.accept(runTransitionAnimation)
+        onTransitionViewReadyCallback = null
+    }
+}
diff --git a/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
new file mode 100644
index 0000000..f5e2051
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
@@ -0,0 +1,1280 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.intentresolver.widget;
+
+import static android.content.res.Resources.ID_NULL;
+
+import android.annotation.IdRes;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.metrics.LogMaker;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.animation.AnimationUtils;
+import android.widget.AbsListView;
+import android.widget.OverScroller;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.intentresolver.R;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+public class ResolverDrawerLayout extends ViewGroup {
+    private static final String TAG = "ResolverDrawerLayout";
+    private MetricsLogger mMetricsLogger;
+
+    /**
+     * Max width of the whole drawer layout
+     */
+    private final int mMaxWidth;
+
+    /**
+     * Max total visible height of views not marked always-show when in the closed/initial state
+     */
+    private int mMaxCollapsedHeight;
+
+    /**
+     * Max total visible height of views not marked always-show when in the closed/initial state
+     * when a default option is present
+     */
+    private int mMaxCollapsedHeightSmall;
+
+    /**
+     * Whether {@code mMaxCollapsedHeightSmall} was set explicitly as a layout attribute or
+     * inferred by {@code mMaxCollapsedHeight}.
+     */
+    private final boolean mIsMaxCollapsedHeightSmallExplicit;
+
+    private boolean mSmallCollapsed;
+
+    /**
+     * Move views down from the top by this much in px
+     */
+    private float mCollapseOffset;
+
+    /**
+      * Track fractions of pixels from drag calculations. Without this, the view offsets get
+      * out of sync due to frequently dropping fractions of a pixel from '(int) dy' casts.
+      */
+    private float mDragRemainder = 0.0f;
+    private int mHeightUsed;
+    private int mCollapsibleHeight;
+    private int mAlwaysShowHeight;
+
+    /**
+     * The height in pixels of reserved space added to the top of the collapsed UI;
+     * e.g. chooser targets
+     */
+    private int mCollapsibleHeightReserved;
+
+    private int mTopOffset;
+    private boolean mShowAtTop;
+    @IdRes
+    private int mIgnoreOffsetTopLimitViewId = ID_NULL;
+
+    private boolean mIsDragging;
+    private boolean mOpenOnClick;
+    private boolean mOpenOnLayout;
+    private boolean mDismissOnScrollerFinished;
+    private final int mTouchSlop;
+    private final float mMinFlingVelocity;
+    private final OverScroller mScroller;
+    private final VelocityTracker mVelocityTracker;
+
+    private Drawable mScrollIndicatorDrawable;
+
+    private OnDismissedListener mOnDismissedListener;
+    private RunOnDismissedListener mRunOnDismissedListener;
+    private OnCollapsedChangedListener mOnCollapsedChangedListener;
+
+    private boolean mDismissLocked;
+
+    private float mInitialTouchX;
+    private float mInitialTouchY;
+    private float mLastTouchY;
+    private int mActivePointerId = MotionEvent.INVALID_POINTER_ID;
+
+    private final Rect mTempRect = new Rect();
+
+    private AbsListView mNestedListChild;
+    private RecyclerView mNestedRecyclerChild;
+
+    private final ViewTreeObserver.OnTouchModeChangeListener mTouchModeChangeListener =
+            new ViewTreeObserver.OnTouchModeChangeListener() {
+                @Override
+                public void onTouchModeChanged(boolean isInTouchMode) {
+                    if (!isInTouchMode && hasFocus() && isDescendantClipped(getFocusedChild())) {
+                        smoothScrollTo(0, 0);
+                    }
+                }
+            };
+
+    public ResolverDrawerLayout(Context context) {
+        this(context, null);
+    }
+
+    public ResolverDrawerLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public ResolverDrawerLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        super(context, attrs, defStyleAttr);
+
+        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ResolverDrawerLayout,
+                defStyleAttr, 0);
+        mMaxWidth = a.getDimensionPixelSize(R.styleable.ResolverDrawerLayout_android_maxWidth, -1);
+        mMaxCollapsedHeight = a.getDimensionPixelSize(
+                R.styleable.ResolverDrawerLayout_maxCollapsedHeight, 0);
+        mMaxCollapsedHeightSmall = a.getDimensionPixelSize(
+                R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall,
+                mMaxCollapsedHeight);
+        mIsMaxCollapsedHeightSmallExplicit =
+                a.hasValue(R.styleable.ResolverDrawerLayout_maxCollapsedHeightSmall);
+        mShowAtTop = a.getBoolean(R.styleable.ResolverDrawerLayout_showAtTop, false);
+        if (a.hasValue(R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit)) {
+            mIgnoreOffsetTopLimitViewId = a.getResourceId(
+                    R.styleable.ResolverDrawerLayout_ignoreOffsetTopLimit, ID_NULL);
+        }
+        a.recycle();
+
+        mScrollIndicatorDrawable = mContext.getDrawable(
+                com.android.internal.R.drawable.scroll_indicator_material);
+
+        mScroller = new OverScroller(context, AnimationUtils.loadInterpolator(context,
+                android.R.interpolator.decelerate_quint));
+        mVelocityTracker = VelocityTracker.obtain();
+
+        final ViewConfiguration vc = ViewConfiguration.get(context);
+        mTouchSlop = vc.getScaledTouchSlop();
+        mMinFlingVelocity = vc.getScaledMinimumFlingVelocity();
+
+        setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+    }
+
+    /**
+     * Dynamically set the max collapsed height. Note this also updates the small collapsed
+     * height if it wasn't specified explicitly.
+     */
+    public void setMaxCollapsedHeight(int heightInPixels) {
+        if (heightInPixels == mMaxCollapsedHeight) {
+            return;
+        }
+        mMaxCollapsedHeight = heightInPixels;
+        if (!mIsMaxCollapsedHeightSmallExplicit) {
+            mMaxCollapsedHeightSmall = mMaxCollapsedHeight;
+        }
+        requestLayout();
+    }
+
+    public void setSmallCollapsed(boolean smallCollapsed) {
+        if (mSmallCollapsed != smallCollapsed) {
+            mSmallCollapsed = smallCollapsed;
+            requestLayout();
+        }
+    }
+
+    public boolean isSmallCollapsed() {
+        return mSmallCollapsed;
+    }
+
+    public boolean isCollapsed() {
+        return mCollapseOffset > 0;
+    }
+
+    public void setShowAtTop(boolean showOnTop) {
+        if (mShowAtTop != showOnTop) {
+            mShowAtTop = showOnTop;
+            requestLayout();
+        }
+    }
+
+    public boolean getShowAtTop() {
+        return mShowAtTop;
+    }
+
+    public void setCollapsed(boolean collapsed) {
+        if (!isLaidOut()) {
+            mOpenOnLayout = !collapsed;
+        } else {
+            smoothScrollTo(collapsed ? mCollapsibleHeight : 0, 0);
+        }
+    }
+
+    public void setCollapsibleHeightReserved(int heightPixels) {
+        final int oldReserved = mCollapsibleHeightReserved;
+        mCollapsibleHeightReserved = heightPixels;
+        if (oldReserved != mCollapsibleHeightReserved) {
+            requestLayout();
+        }
+
+        final int dReserved = mCollapsibleHeightReserved - oldReserved;
+        if (dReserved != 0 && mIsDragging) {
+            mLastTouchY -= dReserved;
+        }
+
+        final int oldCollapsibleHeight = updateCollapsibleHeight();
+        if (updateCollapseOffset(oldCollapsibleHeight, !isDragging())) {
+            return;
+        }
+
+        invalidate();
+    }
+
+    public void setDismissLocked(boolean locked) {
+        mDismissLocked = locked;
+    }
+
+    private boolean isMoving() {
+        return mIsDragging || !mScroller.isFinished();
+    }
+
+    private boolean isDragging() {
+        return mIsDragging || getNestedScrollAxes() == SCROLL_AXIS_VERTICAL;
+    }
+
+    private boolean updateCollapseOffset(int oldCollapsibleHeight, boolean remainClosed) {
+        if (oldCollapsibleHeight == mCollapsibleHeight) {
+            return false;
+        }
+
+        if (getShowAtTop()) {
+            // Keep the drawer fully open.
+            setCollapseOffset(0);
+            return false;
+        }
+
+        if (isLaidOut()) {
+            final boolean isCollapsedOld = mCollapseOffset != 0;
+            if (remainClosed && (oldCollapsibleHeight < mCollapsibleHeight
+                    && mCollapseOffset == oldCollapsibleHeight)) {
+                // Stay closed even at the new height.
+                setCollapseOffset(mCollapsibleHeight);
+            } else {
+                setCollapseOffset(Math.min(mCollapseOffset, mCollapsibleHeight));
+            }
+            final boolean isCollapsedNew = mCollapseOffset != 0;
+            if (isCollapsedOld != isCollapsedNew) {
+                onCollapsedChanged(isCollapsedNew);
+            }
+        } else {
+            // Start out collapsed at first unless we restored state for otherwise
+            setCollapseOffset(mOpenOnLayout ? 0 : mCollapsibleHeight);
+        }
+        return true;
+    }
+
+    private void setCollapseOffset(float collapseOffset) {
+        if (mCollapseOffset != collapseOffset) {
+            mCollapseOffset = collapseOffset;
+            requestLayout();
+        }
+    }
+
+    private int getMaxCollapsedHeight() {
+        return (isSmallCollapsed() ? mMaxCollapsedHeightSmall : mMaxCollapsedHeight)
+                + mCollapsibleHeightReserved;
+    }
+
+    public void setOnDismissedListener(OnDismissedListener listener) {
+        mOnDismissedListener = listener;
+    }
+
+    private boolean isDismissable() {
+        return mOnDismissedListener != null && !mDismissLocked;
+    }
+
+    public void setOnCollapsedChangedListener(OnCollapsedChangedListener listener) {
+        mOnCollapsedChangedListener = listener;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        final int action = ev.getActionMasked();
+
+        if (action == MotionEvent.ACTION_DOWN) {
+            mVelocityTracker.clear();
+        }
+
+        mVelocityTracker.addMovement(ev);
+
+        switch (action) {
+            case MotionEvent.ACTION_DOWN: {
+                final float x = ev.getX();
+                final float y = ev.getY();
+                mInitialTouchX = x;
+                mInitialTouchY = mLastTouchY = y;
+                mOpenOnClick = isListChildUnderClipped(x, y) && mCollapseOffset > 0;
+            }
+            break;
+
+            case MotionEvent.ACTION_MOVE: {
+                final float x = ev.getX();
+                final float y = ev.getY();
+                final float dy = y - mInitialTouchY;
+                if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null &&
+                        (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
+                    mActivePointerId = ev.getPointerId(0);
+                    mIsDragging = true;
+                    mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
+                            Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
+                }
+            }
+            break;
+
+            case MotionEvent.ACTION_POINTER_UP: {
+                onSecondaryPointerUp(ev);
+            }
+            break;
+
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP: {
+                resetTouch();
+            }
+            break;
+        }
+
+        if (mIsDragging) {
+            abortAnimation();
+        }
+        return mIsDragging || mOpenOnClick;
+    }
+
+    private boolean isNestedListChildScrolled() {
+        return  mNestedListChild != null
+                && mNestedListChild.getChildCount() > 0
+                && (mNestedListChild.getFirstVisiblePosition() > 0
+                        || mNestedListChild.getChildAt(0).getTop() < 0);
+    }
+
+    private boolean isNestedRecyclerChildScrolled() {
+        if (mNestedRecyclerChild != null && mNestedRecyclerChild.getChildCount() > 0) {
+            final RecyclerView.ViewHolder vh =
+                    mNestedRecyclerChild.findViewHolderForAdapterPosition(0);
+            return vh == null || vh.itemView.getTop() < 0;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        final int action = ev.getActionMasked();
+
+        mVelocityTracker.addMovement(ev);
+
+        boolean handled = false;
+        switch (action) {
+            case MotionEvent.ACTION_DOWN: {
+                final float x = ev.getX();
+                final float y = ev.getY();
+                mInitialTouchX = x;
+                mInitialTouchY = mLastTouchY = y;
+                mActivePointerId = ev.getPointerId(0);
+                final boolean hitView = findChildUnder(mInitialTouchX, mInitialTouchY) != null;
+                handled = isDismissable() || mCollapsibleHeight > 0;
+                mIsDragging = hitView && handled;
+                abortAnimation();
+            }
+            break;
+
+            case MotionEvent.ACTION_MOVE: {
+                int index = ev.findPointerIndex(mActivePointerId);
+                if (index < 0) {
+                    Log.e(TAG, "Bad pointer id " + mActivePointerId + ", resetting");
+                    index = 0;
+                    mActivePointerId = ev.getPointerId(0);
+                    mInitialTouchX = ev.getX();
+                    mInitialTouchY = mLastTouchY = ev.getY();
+                }
+                final float x = ev.getX(index);
+                final float y = ev.getY(index);
+                if (!mIsDragging) {
+                    final float dy = y - mInitialTouchY;
+                    if (Math.abs(dy) > mTouchSlop && findChildUnder(x, y) != null) {
+                        handled = mIsDragging = true;
+                        mLastTouchY = Math.max(mLastTouchY - mTouchSlop,
+                                Math.min(mLastTouchY + dy, mLastTouchY + mTouchSlop));
+                    }
+                }
+                if (mIsDragging) {
+                    final float dy = y - mLastTouchY;
+                    if (dy > 0 && isNestedListChildScrolled()) {
+                        mNestedListChild.smoothScrollBy((int) -dy, 0);
+                    } else if (dy > 0 && isNestedRecyclerChildScrolled()) {
+                        mNestedRecyclerChild.scrollBy(0, (int) -dy);
+                    } else {
+                        performDrag(dy);
+                    }
+                }
+                mLastTouchY = y;
+            }
+            break;
+
+            case MotionEvent.ACTION_POINTER_DOWN: {
+                final int pointerIndex = ev.getActionIndex();
+                mActivePointerId = ev.getPointerId(pointerIndex);
+                mInitialTouchX = ev.getX(pointerIndex);
+                mInitialTouchY = mLastTouchY = ev.getY(pointerIndex);
+            }
+            break;
+
+            case MotionEvent.ACTION_POINTER_UP: {
+                onSecondaryPointerUp(ev);
+            }
+            break;
+
+            case MotionEvent.ACTION_UP: {
+                final boolean wasDragging = mIsDragging;
+                mIsDragging = false;
+                if (!wasDragging && findChildUnder(mInitialTouchX, mInitialTouchY) == null &&
+                        findChildUnder(ev.getX(), ev.getY()) == null) {
+                    if (isDismissable()) {
+                        dispatchOnDismissed();
+                        resetTouch();
+                        return true;
+                    }
+                }
+                if (mOpenOnClick && Math.abs(ev.getX() - mInitialTouchX) < mTouchSlop &&
+                        Math.abs(ev.getY() - mInitialTouchY) < mTouchSlop) {
+                    smoothScrollTo(0, 0);
+                    return true;
+                }
+                mVelocityTracker.computeCurrentVelocity(1000);
+                final float yvel = mVelocityTracker.getYVelocity(mActivePointerId);
+                if (Math.abs(yvel) > mMinFlingVelocity) {
+                    if (getShowAtTop()) {
+                        if (isDismissable() && yvel < 0) {
+                            abortAnimation();
+                            dismiss();
+                        } else {
+                            smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
+                        }
+                    } else {
+                        if (isDismissable()
+                                && yvel > 0 && mCollapseOffset > mCollapsibleHeight) {
+                            smoothScrollTo(mHeightUsed, yvel);
+                            mDismissOnScrollerFinished = true;
+                        } else {
+                            scrollNestedScrollableChildBackToTop();
+                            smoothScrollTo(yvel < 0 ? 0 : mCollapsibleHeight, yvel);
+                        }
+                    }
+                }else {
+                    smoothScrollTo(
+                            mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
+                }
+                resetTouch();
+            }
+            break;
+
+            case MotionEvent.ACTION_CANCEL: {
+                if (mIsDragging) {
+                    smoothScrollTo(
+                            mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
+                }
+                resetTouch();
+                return true;
+            }
+        }
+
+        return handled;
+    }
+
+    /**
+     * Scroll nested scrollable child back to top if it has been scrolled.
+     */
+    public void scrollNestedScrollableChildBackToTop() {
+        if (isNestedListChildScrolled()) {
+            mNestedListChild.smoothScrollToPosition(0);
+        } else if (isNestedRecyclerChildScrolled()) {
+            mNestedRecyclerChild.smoothScrollToPosition(0);
+        }
+    }
+
+    private void onSecondaryPointerUp(MotionEvent ev) {
+        final int pointerIndex = ev.getActionIndex();
+        final int pointerId = ev.getPointerId(pointerIndex);
+        if (pointerId == mActivePointerId) {
+            // This was our active pointer going up. Choose a new
+            // active pointer and adjust accordingly.
+            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+            mInitialTouchX = ev.getX(newPointerIndex);
+            mInitialTouchY = mLastTouchY = ev.getY(newPointerIndex);
+            mActivePointerId = ev.getPointerId(newPointerIndex);
+        }
+    }
+
+    private void resetTouch() {
+        mActivePointerId = MotionEvent.INVALID_POINTER_ID;
+        mIsDragging = false;
+        mOpenOnClick = false;
+        mInitialTouchX = mInitialTouchY = mLastTouchY = 0;
+        mVelocityTracker.clear();
+    }
+
+    private void dismiss() {
+        mRunOnDismissedListener = new RunOnDismissedListener();
+        post(mRunOnDismissedListener);
+    }
+
+    @Override
+    public void computeScroll() {
+        super.computeScroll();
+        if (mScroller.computeScrollOffset()) {
+            final boolean keepGoing = !mScroller.isFinished();
+            performDrag(mScroller.getCurrY() - mCollapseOffset);
+            if (keepGoing) {
+                postInvalidateOnAnimation();
+            } else if (mDismissOnScrollerFinished && mOnDismissedListener != null) {
+                dismiss();
+            }
+        }
+    }
+
+    private void abortAnimation() {
+        mScroller.abortAnimation();
+        mRunOnDismissedListener = null;
+        mDismissOnScrollerFinished = false;
+    }
+
+    private float performDrag(float dy) {
+        if (getShowAtTop()) {
+            return 0;
+        }
+
+        final float newPos = Math.max(0, Math.min(mCollapseOffset + dy, mHeightUsed));
+        if (newPos != mCollapseOffset) {
+            dy = newPos - mCollapseOffset;
+
+            mDragRemainder += dy - (int) dy;
+            if (mDragRemainder >= 1.0f) {
+                mDragRemainder -= 1.0f;
+                dy += 1.0f;
+            } else if (mDragRemainder <= -1.0f) {
+                mDragRemainder += 1.0f;
+                dy -= 1.0f;
+            }
+
+            boolean isIgnoreOffsetLimitSet = false;
+            int ignoreOffsetLimit = 0;
+            View ignoreOffsetLimitView = findIgnoreOffsetLimitView();
+            if (ignoreOffsetLimitView != null) {
+                LayoutParams lp = (LayoutParams) ignoreOffsetLimitView.getLayoutParams();
+                ignoreOffsetLimit = ignoreOffsetLimitView.getBottom() + lp.bottomMargin;
+                isIgnoreOffsetLimitSet = true;
+            }
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                if (child.getVisibility() == View.GONE) {
+                    continue;
+                }
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (!lp.ignoreOffset) {
+                    child.offsetTopAndBottom((int) dy);
+                } else if (isIgnoreOffsetLimitSet) {
+                    int top = child.getTop();
+                    int targetTop = Math.max(
+                            (int) (ignoreOffsetLimit + lp.topMargin + dy),
+                            lp.mFixedTop);
+                    if (top != targetTop) {
+                        child.offsetTopAndBottom(targetTop - top);
+                    }
+                    ignoreOffsetLimit = child.getBottom() + lp.bottomMargin;
+                }
+            }
+            final boolean isCollapsedOld = mCollapseOffset != 0;
+            mCollapseOffset = newPos;
+            mTopOffset += dy;
+            final boolean isCollapsedNew = newPos != 0;
+            if (isCollapsedOld != isCollapsedNew) {
+                onCollapsedChanged(isCollapsedNew);
+                getMetricsLogger().write(
+                        new LogMaker(MetricsEvent.ACTION_SHARESHEET_COLLAPSED_CHANGED)
+                        .setSubtype(isCollapsedNew ? 1 : 0));
+            }
+            onScrollChanged(0, (int) newPos, 0, (int) (newPos - dy));
+            postInvalidateOnAnimation();
+            return dy;
+        }
+        return 0;
+    }
+
+    private void onCollapsedChanged(boolean isCollapsed) {
+        notifyViewAccessibilityStateChangedIfNeeded(
+                AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+
+        if (mScrollIndicatorDrawable != null) {
+            setWillNotDraw(!isCollapsed);
+        }
+
+        if (mOnCollapsedChangedListener != null) {
+            mOnCollapsedChangedListener.onCollapsedChanged(isCollapsed);
+        }
+    }
+
+    void dispatchOnDismissed() {
+        if (mOnDismissedListener != null) {
+            mOnDismissedListener.onDismissed();
+        }
+        if (mRunOnDismissedListener != null) {
+            removeCallbacks(mRunOnDismissedListener);
+            mRunOnDismissedListener = null;
+        }
+    }
+
+    private void smoothScrollTo(int yOffset, float velocity) {
+        abortAnimation();
+        final int sy = (int) mCollapseOffset;
+        int dy = yOffset - sy;
+        if (dy == 0) {
+            return;
+        }
+
+        final int height = getHeight();
+        final int halfHeight = height / 2;
+        final float distanceRatio = Math.min(1f, 1.0f * Math.abs(dy) / height);
+        final float distance = halfHeight + halfHeight *
+                distanceInfluenceForSnapDuration(distanceRatio);
+
+        int duration = 0;
+        velocity = Math.abs(velocity);
+        if (velocity > 0) {
+            duration = 4 * Math.round(1000 * Math.abs(distance / velocity));
+        } else {
+            final float pageDelta = (float) Math.abs(dy) / height;
+            duration = (int) ((pageDelta + 1) * 100);
+        }
+        duration = Math.min(duration, 300);
+
+        mScroller.startScroll(0, sy, 0, dy, duration);
+        postInvalidateOnAnimation();
+    }
+
+    private float distanceInfluenceForSnapDuration(float f) {
+        f -= 0.5f; // center the values about 0.
+        f *= 0.3f * Math.PI / 2.0f;
+        return (float) Math.sin(f);
+    }
+
+    /**
+     * Note: this method doesn't take Z into account for overlapping views
+     * since it is only used in contexts where this doesn't affect the outcome.
+     */
+    private View findChildUnder(float x, float y) {
+        return findChildUnder(this, x, y);
+    }
+
+    private static View findChildUnder(ViewGroup parent, float x, float y) {
+        final int childCount = parent.getChildCount();
+        for (int i = childCount - 1; i >= 0; i--) {
+            final View child = parent.getChildAt(i);
+            if (isChildUnder(child, x, y)) {
+                return child;
+            }
+        }
+        return null;
+    }
+
+    private View findListChildUnder(float x, float y) {
+        View v = findChildUnder(x, y);
+        while (v != null) {
+            x -= v.getX();
+            y -= v.getY();
+            if (v instanceof AbsListView) {
+                // One more after this.
+                return findChildUnder((ViewGroup) v, x, y);
+            }
+            v = v instanceof ViewGroup ? findChildUnder((ViewGroup) v, x, y) : null;
+        }
+        return v;
+    }
+
+    /**
+     * This only checks clipping along the bottom edge.
+     */
+    private boolean isListChildUnderClipped(float x, float y) {
+        final View listChild = findListChildUnder(x, y);
+        return listChild != null && isDescendantClipped(listChild);
+    }
+
+    private boolean isDescendantClipped(View child) {
+        mTempRect.set(0, 0, child.getWidth(), child.getHeight());
+        offsetDescendantRectToMyCoords(child, mTempRect);
+        View directChild;
+        if (child.getParent() == this) {
+            directChild = child;
+        } else {
+            View v = child;
+            ViewParent p = child.getParent();
+            while (p != this) {
+                v = (View) p;
+                p = v.getParent();
+            }
+            directChild = v;
+        }
+
+        // ResolverDrawerLayout lays out vertically in child order;
+        // the next view and forward is what to check against.
+        int clipEdge = getHeight() - getPaddingBottom();
+        final int childCount = getChildCount();
+        for (int i = indexOfChild(directChild) + 1; i < childCount; i++) {
+            final View nextChild = getChildAt(i);
+            if (nextChild.getVisibility() == GONE) {
+                continue;
+            }
+            clipEdge = Math.min(clipEdge, nextChild.getTop());
+        }
+        return mTempRect.bottom > clipEdge;
+    }
+
+    private static boolean isChildUnder(View child, float x, float y) {
+        final float left = child.getX();
+        final float top = child.getY();
+        final float right = left + child.getWidth();
+        final float bottom = top + child.getHeight();
+        return x >= left && y >= top && x < right && y < bottom;
+    }
+
+    @Override
+    public void requestChildFocus(View child, View focused) {
+        super.requestChildFocus(child, focused);
+        if (!isInTouchMode() && isDescendantClipped(focused)) {
+            smoothScrollTo(0, 0);
+        }
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        getViewTreeObserver().addOnTouchModeChangeListener(mTouchModeChangeListener);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        getViewTreeObserver().removeOnTouchModeChangeListener(mTouchModeChangeListener);
+        abortAnimation();
+    }
+
+    @Override
+    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
+        if ((nestedScrollAxes & View.SCROLL_AXIS_VERTICAL) != 0) {
+            if (target instanceof AbsListView) {
+                mNestedListChild = (AbsListView) target;
+            }
+            if (target instanceof RecyclerView) {
+                mNestedRecyclerChild = (RecyclerView) target;
+            }
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void onNestedScrollAccepted(View child, View target, int axes) {
+        super.onNestedScrollAccepted(child, target, axes);
+    }
+
+    @Override
+    public void onStopNestedScroll(View child) {
+        super.onStopNestedScroll(child);
+        if (mScroller.isFinished()) {
+            smoothScrollTo(mCollapseOffset < mCollapsibleHeight / 2 ? 0 : mCollapsibleHeight, 0);
+        }
+    }
+
+    @Override
+    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
+            int dxUnconsumed, int dyUnconsumed) {
+        if (dyUnconsumed < 0) {
+            performDrag(-dyUnconsumed);
+        }
+    }
+
+    @Override
+    public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
+        if (dy > 0) {
+            consumed[1] = (int) -performDrag(-dy);
+        }
+    }
+
+    @Override
+    public boolean onNestedPreFling(View target, float velocityX, float velocityY) {
+        if (!getShowAtTop() && velocityY > mMinFlingVelocity && mCollapseOffset != 0) {
+            smoothScrollTo(0, velocityY);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+        if (!consumed && Math.abs(velocityY) > mMinFlingVelocity) {
+            if (getShowAtTop()) {
+                if (isDismissable() && velocityY > 0) {
+                    abortAnimation();
+                    dismiss();
+                } else {
+                    smoothScrollTo(velocityY < 0 ? mCollapsibleHeight : 0, velocityY);
+                }
+            } else {
+                if (isDismissable()
+                        && velocityY < 0 && mCollapseOffset > mCollapsibleHeight) {
+                    smoothScrollTo(mHeightUsed, velocityY);
+                    mDismissOnScrollerFinished = true;
+                } else {
+                    smoothScrollTo(velocityY > 0 ? 0 : mCollapsibleHeight, velocityY);
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    private boolean performAccessibilityActionCommon(int action) {
+        switch (action) {
+            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+            case AccessibilityNodeInfo.ACTION_EXPAND:
+            case com.android.internal.R.id.accessibilityActionScrollDown:
+                if (mCollapseOffset != 0) {
+                    smoothScrollTo(0, 0);
+                    return true;
+                }
+                break;
+            case AccessibilityNodeInfo.ACTION_COLLAPSE:
+                if (mCollapseOffset < mCollapsibleHeight) {
+                    smoothScrollTo(mCollapsibleHeight, 0);
+                    return true;
+                }
+                break;
+            case AccessibilityNodeInfo.ACTION_DISMISS:
+                if ((mCollapseOffset < mHeightUsed) && isDismissable()) {
+                    smoothScrollTo(mHeightUsed, 0);
+                    mDismissOnScrollerFinished = true;
+                    return true;
+                }
+                break;
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean onNestedPrePerformAccessibilityAction(View target, int action, Bundle args) {
+        if (super.onNestedPrePerformAccessibilityAction(target, action, args)) {
+            return true;
+        }
+
+        return performAccessibilityActionCommon(action);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        // Since we support scrolling, make this ViewGroup look like a
+        // ScrollView. This is kind of a hack until we have support for
+        // specifying auto-scroll behavior.
+        return android.widget.ScrollView.class.getName();
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+
+        if (isEnabled()) {
+            if (mCollapseOffset != 0) {
+                info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
+                info.addAction(AccessibilityAction.ACTION_EXPAND);
+                info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN);
+                info.setScrollable(true);
+            }
+            if ((mCollapseOffset < mHeightUsed)
+                    && ((mCollapseOffset < mCollapsibleHeight) || isDismissable())) {
+                info.addAction(AccessibilityAction.ACTION_SCROLL_UP);
+                info.setScrollable(true);
+            }
+            if (mCollapseOffset < mCollapsibleHeight) {
+                info.addAction(AccessibilityAction.ACTION_COLLAPSE);
+            }
+            if (mCollapseOffset < mHeightUsed && isDismissable()) {
+                info.addAction(AccessibilityAction.ACTION_DISMISS);
+            }
+        }
+
+        // This view should never get accessibility focus, but it's interactive
+        // via nested scrolling, so we can't hide it completely.
+        info.removeAction(AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS);
+    }
+
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (action == AccessibilityAction.ACTION_ACCESSIBILITY_FOCUS.getId()) {
+            // This view should never get accessibility focus.
+            return false;
+        }
+
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+
+        return performAccessibilityActionCommon(action);
+    }
+
+    @Override
+    public void onDrawForeground(Canvas canvas) {
+        if (mScrollIndicatorDrawable != null) {
+            mScrollIndicatorDrawable.draw(canvas);
+        }
+
+        super.onDrawForeground(canvas);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int sourceWidth = MeasureSpec.getSize(widthMeasureSpec);
+        int widthSize = sourceWidth;
+        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+        // Single-use layout; just ignore the mode and use available space.
+        // Clamp to maxWidth.
+        if (mMaxWidth >= 0) {
+            widthSize = Math.min(widthSize, mMaxWidth + getPaddingLeft() + getPaddingRight());
+        }
+
+        final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
+        final int heightSpec = MeasureSpec.makeMeasureSpec(heightSize, MeasureSpec.EXACTLY);
+
+        // Currently we allot more height than is really needed so that the entirety of the
+        // sheet may be pulled up.
+        // TODO: Restrict the height here to be the right value.
+        int heightUsed = 0;
+
+        // Measure always-show children first.
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if (lp.alwaysShow && child.getVisibility() != GONE) {
+                if (lp.maxHeight != -1) {
+                    final int remainingHeight = heightSize - heightUsed;
+                    measureChildWithMargins(child, widthSpec, 0,
+                            MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
+                            lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
+                } else {
+                    measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
+                }
+                heightUsed += child.getMeasuredHeight();
+            }
+        }
+
+        mAlwaysShowHeight = heightUsed;
+
+        // And now the rest.
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if (!lp.alwaysShow && child.getVisibility() != GONE) {
+                if (lp.maxHeight != -1) {
+                    final int remainingHeight = heightSize - heightUsed;
+                    measureChildWithMargins(child, widthSpec, 0,
+                            MeasureSpec.makeMeasureSpec(lp.maxHeight, MeasureSpec.AT_MOST),
+                            lp.maxHeight > remainingHeight ? lp.maxHeight - remainingHeight : 0);
+                } else {
+                    measureChildWithMargins(child, widthSpec, 0, heightSpec, heightUsed);
+                }
+                heightUsed += child.getMeasuredHeight();
+            }
+        }
+
+        mHeightUsed = heightUsed;
+        int oldCollapsibleHeight = updateCollapsibleHeight();
+        updateCollapseOffset(oldCollapsibleHeight, !isDragging());
+
+        if (getShowAtTop()) {
+            mTopOffset = 0;
+        } else {
+            mTopOffset = Math.max(0, heightSize - mHeightUsed) + (int) mCollapseOffset;
+        }
+
+        setMeasuredDimension(sourceWidth, heightSize);
+    }
+
+    private int updateCollapsibleHeight() {
+        final int oldCollapsibleHeight = mCollapsibleHeight;
+        mCollapsibleHeight = Math.max(0, mHeightUsed - mAlwaysShowHeight - getMaxCollapsedHeight());
+        return oldCollapsibleHeight;
+    }
+
+    /**
+      * @return The space reserved by views with 'alwaysShow=true'
+      */
+    public int getAlwaysShowHeight() {
+        return mAlwaysShowHeight;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        final int width = getWidth();
+
+        View indicatorHost = null;
+
+        int ypos = mTopOffset;
+        final int leftEdge = getPaddingLeft();
+        final int rightEdge = width - getPaddingRight();
+        final int widthAvailable = rightEdge - leftEdge;
+
+        boolean isIgnoreOffsetLimitSet = false;
+        int ignoreOffsetLimit = 0;
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if (lp.hasNestedScrollIndicator) {
+                indicatorHost = child;
+            }
+
+            if (child.getVisibility() == GONE) {
+                continue;
+            }
+
+            if (mIgnoreOffsetTopLimitViewId != ID_NULL && !isIgnoreOffsetLimitSet) {
+                if (mIgnoreOffsetTopLimitViewId == child.getId()) {
+                    ignoreOffsetLimit = child.getBottom() + lp.bottomMargin;
+                    isIgnoreOffsetLimitSet = true;
+                }
+            }
+
+            int top = ypos + lp.topMargin;
+            if (lp.ignoreOffset) {
+                if (!isDragging()) {
+                    lp.mFixedTop = (int) (top - mCollapseOffset);
+                }
+                if (isIgnoreOffsetLimitSet) {
+                    top = Math.max(ignoreOffsetLimit + lp.topMargin, (int) (top - mCollapseOffset));
+                    ignoreOffsetLimit = top + child.getMeasuredHeight() + lp.bottomMargin;
+                } else {
+                    top -= mCollapseOffset;
+                }
+            }
+            final int bottom = top + child.getMeasuredHeight();
+
+            final int childWidth = child.getMeasuredWidth();
+            final int left = leftEdge + (widthAvailable - childWidth) / 2;
+            final int right = left + childWidth;
+
+            child.layout(left, top, right, bottom);
+
+            ypos = bottom + lp.bottomMargin;
+        }
+
+        if (mScrollIndicatorDrawable != null) {
+            if (indicatorHost != null) {
+                final int left = indicatorHost.getLeft();
+                final int right = indicatorHost.getRight();
+                final int bottom = indicatorHost.getTop();
+                final int top = bottom - mScrollIndicatorDrawable.getIntrinsicHeight();
+                mScrollIndicatorDrawable.setBounds(left, top, right, bottom);
+                setWillNotDraw(!isCollapsed());
+            } else {
+                mScrollIndicatorDrawable = null;
+                setWillNotDraw(true);
+            }
+        }
+    }
+
+    @Override
+    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new LayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+        if (p instanceof LayoutParams) {
+            return new LayoutParams((LayoutParams) p);
+        } else if (p instanceof MarginLayoutParams) {
+            return new LayoutParams((MarginLayoutParams) p);
+        }
+        return new LayoutParams(p);
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        final SavedState ss = new SavedState(super.onSaveInstanceState());
+        ss.open = mCollapsibleHeight > 0 && mCollapseOffset == 0;
+        ss.mCollapsibleHeightReserved = mCollapsibleHeightReserved;
+        return ss;
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        final SavedState ss = (SavedState) state;
+        super.onRestoreInstanceState(ss.getSuperState());
+        mOpenOnLayout = ss.open;
+        mCollapsibleHeightReserved = ss.mCollapsibleHeightReserved;
+    }
+
+    private View findIgnoreOffsetLimitView() {
+        if (mIgnoreOffsetTopLimitViewId == ID_NULL) {
+            return null;
+        }
+        View v = findViewById(mIgnoreOffsetTopLimitViewId);
+        if (v != null && v != this && v.getParent() == this && v.getVisibility() != View.GONE) {
+            return v;
+        }
+        return null;
+    }
+
+    public static class LayoutParams extends MarginLayoutParams {
+        public boolean alwaysShow;
+        public boolean ignoreOffset;
+        public boolean hasNestedScrollIndicator;
+        public int maxHeight;
+        int mFixedTop;
+
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+
+            final TypedArray a = c.obtainStyledAttributes(attrs,
+                    R.styleable.ResolverDrawerLayout_LayoutParams);
+            alwaysShow = a.getBoolean(
+                    R.styleable.ResolverDrawerLayout_LayoutParams_layout_alwaysShow,
+                    false);
+            ignoreOffset = a.getBoolean(
+                    R.styleable.ResolverDrawerLayout_LayoutParams_layout_ignoreOffset,
+                    false);
+            hasNestedScrollIndicator = a.getBoolean(
+                    R.styleable.ResolverDrawerLayout_LayoutParams_layout_hasNestedScrollIndicator,
+                    false);
+            maxHeight = a.getDimensionPixelSize(
+                    R.styleable.ResolverDrawerLayout_LayoutParams_layout_maxHeight, -1);
+            a.recycle();
+        }
+
+        public LayoutParams(int width, int height) {
+            super(width, height);
+        }
+
+        public LayoutParams(LayoutParams source) {
+            super(source);
+            this.alwaysShow = source.alwaysShow;
+            this.ignoreOffset = source.ignoreOffset;
+            this.hasNestedScrollIndicator = source.hasNestedScrollIndicator;
+            this.maxHeight = source.maxHeight;
+        }
+
+        public LayoutParams(MarginLayoutParams source) {
+            super(source);
+        }
+
+        public LayoutParams(ViewGroup.LayoutParams source) {
+            super(source);
+        }
+    }
+
+    static class SavedState extends BaseSavedState {
+        boolean open;
+        private int mCollapsibleHeightReserved;
+
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        private SavedState(Parcel in) {
+            super(in);
+            open = in.readInt() != 0;
+            mCollapsibleHeightReserved = in.readInt();
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeInt(open ? 1 : 0);
+            out.writeInt(mCollapsibleHeightReserved);
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR =
+                new Parcelable.Creator<SavedState>() {
+            @Override
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            @Override
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    /**
+     * Listener for sheet dismissed events.
+     */
+    public interface OnDismissedListener {
+        /**
+         * Callback when the sheet is dismissed by the user.
+         */
+        void onDismissed();
+    }
+
+    /**
+     * Listener for sheet collapsed / expanded events.
+     */
+    public interface OnCollapsedChangedListener {
+        /**
+         * Callback when the sheet is either fully expanded or collapsed.
+         * @param isCollapsed true when collapsed, false when expanded.
+         */
+        void onCollapsedChanged(boolean isCollapsed);
+    }
+
+    private class RunOnDismissedListener implements Runnable {
+        @Override
+        public void run() {
+            dispatchOnDismissed();
+        }
+    }
+
+    private MetricsLogger getMetricsLogger() {
+        if (mMetricsLogger == null) {
+            mMetricsLogger = new MetricsLogger();
+        }
+        return mMetricsLogger;
+    }
+}
diff --git a/java/src/com/android/intentresolver/widget/RoundedRectImageView.java b/java/src/com/android/intentresolver/widget/RoundedRectImageView.java
new file mode 100644
index 0000000..8538041
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/RoundedRectImageView.java
@@ -0,0 +1,132 @@
+/*
+ * 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.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.util.AttributeSet;
+import android.widget.ImageView;
+
+import com.android.intentresolver.R;
+
+/**
+ * {@link ImageView} that rounds the corners around the presented image while obeying view padding.
+ */
+public class RoundedRectImageView extends ImageView {
+    private int mRadius = 0;
+    private Path mPath = new Path();
+    private Paint mOverlayPaint = new Paint(0);
+    private Paint mRoundRectPaint = new Paint(0);
+    private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+    private String mExtraImageCount = null;
+
+    public RoundedRectImageView(Context context) {
+        super(context);
+    }
+
+    public RoundedRectImageView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public RoundedRectImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public RoundedRectImageView(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        mRadius = context.getResources().getDimensionPixelSize(R.dimen.chooser_corner_radius);
+
+        mOverlayPaint.setColor(0x99000000);
+        mOverlayPaint.setStyle(Paint.Style.FILL);
+
+        mRoundRectPaint.setColor(context.getResources().getColor(R.color.chooser_row_divider));
+        mRoundRectPaint.setStyle(Paint.Style.STROKE);
+        mRoundRectPaint.setStrokeWidth(context.getResources()
+                .getDimensionPixelSize(R.dimen.chooser_preview_image_border));
+
+        mTextPaint.setColor(Color.WHITE);
+        mTextPaint.setTextSize(context.getResources()
+                .getDimensionPixelSize(R.dimen.chooser_preview_image_font_size));
+        mTextPaint.setTextAlign(Paint.Align.CENTER);
+    }
+
+    private void updatePath(int width, int height) {
+        mPath.reset();
+
+        int imageWidth = width - getPaddingRight() - getPaddingLeft();
+        int imageHeight = height - getPaddingBottom() - getPaddingTop();
+        mPath.addRoundRect(getPaddingLeft(), getPaddingTop(), imageWidth, imageHeight, mRadius,
+                mRadius, Path.Direction.CW);
+    }
+
+    /**
+      * Sets the corner radius on all corners
+      *
+      * param radius 0 for no radius, &gt; 0 for a visible corner radius
+      */
+    public void setRadius(int radius) {
+        mRadius = radius;
+        updatePath(getWidth(), getHeight());
+    }
+
+    /**
+      * Display an overlay with extra image count on 3rd image
+      */
+    public void setExtraImageCount(int count) {
+        if (count > 0) {
+            this.mExtraImageCount = "+" + count;
+        } else {
+            this.mExtraImageCount = null;
+        }
+        invalidate();
+    }
+
+    @Override
+    protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
+        super.onSizeChanged(width, height, oldWidth, oldHeight);
+        updatePath(width, height);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (mRadius != 0) {
+            canvas.clipPath(mPath);
+        }
+
+        super.onDraw(canvas);
+
+        int x = getPaddingLeft();
+        int y = getPaddingRight();
+        int width = getWidth() - getPaddingRight() - getPaddingLeft();
+        int height = getHeight() - getPaddingBottom() - getPaddingTop();
+        if (mExtraImageCount != null) {
+            canvas.drawRect(x, y, width, height, mOverlayPaint);
+
+            int xPos = canvas.getWidth() / 2;
+            int yPos = (int) ((canvas.getHeight() / 2.0f)
+                    - ((mTextPaint.descent() + mTextPaint.ascent()) / 2.0f));
+
+            canvas.drawText(mExtraImageCount, xPos, yPos, mTextPaint);
+        }
+
+        canvas.drawRoundRect(x, y, width, height, mRadius, mRadius, mRoundRectPaint);
+    }
+}
diff --git a/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt b/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt
new file mode 100644
index 0000000..a941b97
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt
@@ -0,0 +1,130 @@
+/*
+ * 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.widget
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.intentresolver.R
+
+class ScrollableActionRow : RecyclerView, ActionRow {
+    constructor(context: Context) : this(context, null)
+    constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
+    constructor(
+        context: Context, attrs: AttributeSet?, defStyleAttr: Int
+    ) : super(context, attrs, defStyleAttr) {
+        layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+        adapter = Adapter(context)
+    }
+
+    private val actionsAdapter get() = adapter as Adapter
+
+    override fun setActions(actions: List<ActionRow.Action>) {
+        actionsAdapter.setActions(actions)
+    }
+
+    override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+        super.onLayout(changed, l, t, r, b)
+        setOverScrollMode(
+            if (areAllChildrenVisible) View.OVER_SCROLL_NEVER else View.OVER_SCROLL_ALWAYS
+        )
+    }
+
+    private val areAllChildrenVisible: Boolean
+        get() {
+            val count = getChildCount()
+            if (count == 0) return true
+            val first = getChildAt(0)
+            val last = getChildAt(count - 1)
+            return getChildAdapterPosition(first) == 0
+                && getChildAdapterPosition(last) == actionsAdapter.itemCount - 1
+                && isFullyVisible(first)
+                && isFullyVisible(last)
+        }
+
+    private fun isFullyVisible(view: View): Boolean =
+        view.left >= paddingLeft && view.right <= width - paddingRight
+
+    private class Adapter(private val context: Context) : RecyclerView.Adapter<ViewHolder>() {
+        private val iconSize: Int =
+            context.resources.getDimensionPixelSize(R.dimen.chooser_action_view_icon_size)
+        private val itemLayout = R.layout.chooser_action_view
+        private var actions: List<ActionRow.Action> = emptyList()
+
+        override fun onCreateViewHolder(parent: ViewGroup, type: Int): ViewHolder =
+            ViewHolder(
+                LayoutInflater.from(context).inflate(itemLayout, null) as TextView,
+                iconSize
+            )
+
+        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
+            holder.bind(actions[position])
+        }
+
+        override fun getItemCount() = actions.size
+
+        override fun onViewRecycled(holder: ViewHolder) {
+            holder.unbind()
+        }
+
+        override fun onFailedToRecycleView(holder: ViewHolder): Boolean {
+            holder.unbind()
+            return super.onFailedToRecycleView(holder)
+        }
+
+        fun setActions(actions: List<ActionRow.Action>) {
+            this.actions = ArrayList(actions)
+            notifyDataSetChanged()
+        }
+    }
+
+    private class ViewHolder(
+        private val view: TextView, private val iconSize: Int
+    ) : RecyclerView.ViewHolder(view) {
+
+        fun bind(action: ActionRow.Action) {
+            if (action.icon != null) {
+                action.icon.setBounds(0, 0, iconSize, iconSize)
+                // some drawables (edit) does not gets tinted when set to the top of the text
+                // with TextView#setCompoundDrawableRelative
+                view.setCompoundDrawablesRelative(null, action.icon, null, null)
+            }
+            view.text = action.label ?: ""
+            view.setOnClickListener {
+                action.onClicked.run()
+            }
+            view.id = action.id
+        }
+
+        fun unbind() {
+            view.setOnClickListener(null)
+        }
+
+        private fun tintIcon(drawable: Drawable, view: TextView) {
+            val tintList = view.compoundDrawableTintList ?: return
+            drawable.setTintList(tintList)
+            view.compoundDrawableTintMode?.let { drawable.setTintMode(it) }
+            view.compoundDrawableTintBlendMode?.let { drawable.setTintBlendMode(it) }
+        }
+    }
+}
diff --git a/java/tests/Android.bp b/java/tests/Android.bp
index fdabc4e..2913d12 100644
--- a/java/tests/Android.bp
+++ b/java/tests/Android.bp
@@ -7,7 +7,7 @@
     name: "IntentResolverUnitTests",
 
     // Include all test java files.
-    srcs: ["src/**/*.java"],
+    srcs: ["src/**/*.java", "src/**/*.kt"],
 
     libs: [
         "android.test.runner",
@@ -19,8 +19,8 @@
 
     static_libs: [
         "IntentResolver-core",
-        "ChooserActivityTestsLib",
         "androidx.test.rules",
+        "androidx.test.ext.junit",
         "mockito-target-minus-junit4",
         "androidx.test.espresso.core",
         "truth-prebuilt",
diff --git a/java/tests/AndroidManifest.xml b/java/tests/AndroidManifest.xml
index bfe3a39..306eccb 100644
--- a/java/tests/AndroidManifest.xml
+++ b/java/tests/AndroidManifest.xml
@@ -23,10 +23,12 @@
     <uses-permission android:name="android.permission.QUERY_USERS"/>
     <uses-permission android:name="android.permission.READ_CLIPBOARD_IN_BACKGROUND"/>
     <uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG"/>
+    <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
 
-    <application>
+    <application android:name="com.android.intentresolver.TestApplication">
         <uses-library android:name="android.test.runner" />
         <activity android:name="com.android.intentresolver.ChooserWrapperActivity" />
+        <activity android:name="com.android.intentresolver.ResolverWrapperActivity" />
     </application>
 
     <instrumentation android:name="android.testing.TestableInstrumentation"
diff --git a/java/tests/AndroidTest.xml b/java/tests/AndroidTest.xml
index f4e75c4..d1d77c1 100644
--- a/java/tests/AndroidTest.xml
+++ b/java/tests/AndroidTest.xml
@@ -14,7 +14,7 @@
      limitations under the License.
 -->
 <configuration description="Run IntentResolver Tests.">
-    <!--<target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
+    <target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
         <option name="test-file-name" value="IntentResolverUnitTests.apk" />
     </target_preparer>
 
@@ -24,5 +24,5 @@
         <option name="package" value="com.android.intentresolver.tests" />
         <option name="runner" value="android.testing.TestableInstrumentation" />
         <option name="hidden-api-checks" value="false"/>
-    </test>-->
+    </test>
 </configuration>
diff --git a/java/tests/res/drawable/test320x240.png b/java/tests/res/drawable/test320x240.png
new file mode 100644
index 0000000..9b5800d
--- /dev/null
+++ b/java/tests/res/drawable/test320x240.png
Binary files differ
diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerFake.java b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerFake.java
deleted file mode 100644
index e4146cc..0000000
--- a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerFake.java
+++ /dev/null
@@ -1,134 +0,0 @@
-/*
- * Copyright (C) 2020 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver;
-
-import com.android.internal.logging.InstanceId;
-import com.android.internal.logging.UiEventLogger;
-import com.android.internal.util.FrameworkStatsLog;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class ChooserActivityLoggerFake implements ChooserActivityLogger {
-    static class CallRecord {
-        // shared fields between all logs
-        public int atomId;
-        public String packageName;
-        public InstanceId instanceId;
-
-        // generic log field
-        public UiEventLogger.UiEventEnum event;
-
-        // share started fields
-        public String mimeType;
-        public int appProvidedDirect;
-        public int appProvidedApp;
-        public boolean isWorkprofile;
-        public int previewType;
-        public String intent;
-
-        // share completed fields
-        public int targetType;
-        public int positionPicked;
-        public boolean isPinned;
-
-        CallRecord(int atomId, UiEventLogger.UiEventEnum eventId,
-                String packageName, InstanceId instanceId) {
-            this.atomId = atomId;
-            this.packageName = packageName;
-            this.instanceId = instanceId;
-            this.event = eventId;
-        }
-
-        CallRecord(int atomId, String packageName, InstanceId instanceId, String mimeType,
-                int appProvidedDirect, int appProvidedApp, boolean isWorkprofile, int previewType,
-                String intent) {
-            this.atomId = atomId;
-            this.packageName = packageName;
-            this.instanceId = instanceId;
-            this.mimeType = mimeType;
-            this.appProvidedDirect = appProvidedDirect;
-            this.appProvidedApp = appProvidedApp;
-            this.isWorkprofile = isWorkprofile;
-            this.previewType = previewType;
-            this.intent = intent;
-        }
-
-        CallRecord(int atomId, String packageName, InstanceId instanceId, int targetType,
-                int positionPicked, boolean isPinned) {
-            this.atomId = atomId;
-            this.packageName = packageName;
-            this.instanceId = instanceId;
-            this.targetType = targetType;
-            this.positionPicked = positionPicked;
-            this.isPinned = isPinned;
-        }
-
-    }
-    private List<CallRecord> mCalls = new ArrayList<>();
-
-    public int numCalls() {
-        return mCalls.size();
-    }
-
-    List<CallRecord> getCalls() {
-        return mCalls;
-    }
-
-    CallRecord get(int index) {
-        return mCalls.get(index);
-    }
-
-    UiEventLogger.UiEventEnum event(int index) {
-        return mCalls.get(index).event;
-    }
-
-    public void removeCallsForUiEventsOfType(int uiEventType) {
-        mCalls.removeIf(
-                call ->
-                        (call.atomId == FrameworkStatsLog.UI_EVENT_REPORTED)
-                                && (call.event.getId() == uiEventType));
-    }
-
-    @Override
-    public void logShareStarted(int eventId, String packageName, String mimeType,
-            int appProvidedDirect, int appProvidedApp, boolean isWorkprofile, int previewType,
-            String intent) {
-        mCalls.add(new CallRecord(FrameworkStatsLog.SHARESHEET_STARTED, packageName,
-                getInstanceId(), mimeType, appProvidedDirect, appProvidedApp, isWorkprofile,
-                previewType, intent));
-    }
-
-    @Override
-    public void logShareTargetSelected(int targetType, String packageName, int positionPicked,
-            boolean isPinned) {
-        mCalls.add(new CallRecord(FrameworkStatsLog.RANKING_SELECTED, packageName, getInstanceId(),
-                SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(), positionPicked,
-                isPinned));
-    }
-
-    @Override
-    public void log(UiEventLogger.UiEventEnum event, InstanceId instanceId) {
-        mCalls.add(new CallRecord(FrameworkStatsLog.UI_EVENT_REPORTED,
-                    event, "", instanceId));
-    }
-
-    @Override
-    public InstanceId getInstanceId() {
-        return InstanceId.fakeInstanceId(-1);
-    }
-}
diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java
new file mode 100644
index 0000000..705a322
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java
@@ -0,0 +1,406 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.AdditionalMatchers.gt;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+import android.content.Intent;
+import android.metrics.LogMaker;
+
+import com.android.intentresolver.ChooserActivityLogger.FrameworkStatsLogger;
+import com.android.intentresolver.ChooserActivityLogger.SharesheetStandardEvent;
+import com.android.intentresolver.ChooserActivityLogger.SharesheetStartedEvent;
+import com.android.intentresolver.ChooserActivityLogger.SharesheetTargetSelectedEvent;
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.logging.UiEventLogger.UiEventEnum;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.util.FrameworkStatsLog;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnitRunner;
+
+@RunWith(MockitoJUnitRunner.class)
+public final class ChooserActivityLoggerTest {
+    @Mock private UiEventLogger mUiEventLog;
+    @Mock private FrameworkStatsLogger mFrameworkLog;
+    @Mock private MetricsLogger mMetricsLogger;
+
+    private ChooserActivityLogger mChooserLogger;
+
+    @Before
+    public void setUp() {
+        //Mockito.reset(mUiEventLog, mFrameworkLog, mMetricsLogger);
+        mChooserLogger = new ChooserActivityLogger(mUiEventLog, mFrameworkLog, mMetricsLogger);
+    }
+
+    @After
+    public void tearDown() {
+        verifyNoMoreInteractions(mUiEventLog);
+        verifyNoMoreInteractions(mFrameworkLog);
+        verifyNoMoreInteractions(mMetricsLogger);
+    }
+
+    @Test
+    public void testLogChooserActivityShown_personalProfile() {
+        final boolean isWorkProfile = false;
+        final String mimeType = "application/TestType";
+        final long systemCost = 456;
+
+        mChooserLogger.logChooserActivityShown(isWorkProfile, mimeType, systemCost);
+
+        ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class);
+        verify(mMetricsLogger).write(eventCaptor.capture());
+        LogMaker event = eventCaptor.getValue();
+
+        assertThat(event.getCategory()).isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN);
+        assertThat(event.getSubtype()).isEqualTo(MetricsEvent.PARENT_PROFILE);
+        assertThat(event.getTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE)).isEqualTo(mimeType);
+        assertThat(event.getTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS))
+                .isEqualTo(systemCost);
+    }
+
+    @Test
+    public void testLogChooserActivityShown_workProfile() {
+        final boolean isWorkProfile = true;
+        final String mimeType = "application/TestType";
+        final long systemCost = 456;
+
+        mChooserLogger.logChooserActivityShown(isWorkProfile, mimeType, systemCost);
+
+        ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class);
+        verify(mMetricsLogger).write(eventCaptor.capture());
+        LogMaker event = eventCaptor.getValue();
+
+        assertThat(event.getCategory()).isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN);
+        assertThat(event.getSubtype()).isEqualTo(MetricsEvent.MANAGED_PROFILE);
+        assertThat(event.getTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE)).isEqualTo(mimeType);
+        assertThat(event.getTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS))
+                .isEqualTo(systemCost);
+    }
+
+    @Test
+    public void testLogShareStarted() {
+        final int eventId = -1;  // Passed-in eventId is unused. TODO: remove from method signature.
+        final String packageName = "com.test.foo";
+        final String mimeType = "text/plain";
+        final int appProvidedDirectTargets = 123;
+        final int appProvidedAppTargets = 456;
+        final boolean workProfile = true;
+        final int previewType = ChooserContentPreviewUi.CONTENT_PREVIEW_FILE;
+        final String intentAction = Intent.ACTION_SENDTO;
+
+        mChooserLogger.logShareStarted(
+                eventId,
+                packageName,
+                mimeType,
+                appProvidedDirectTargets,
+                appProvidedAppTargets,
+                workProfile,
+                previewType,
+                intentAction);
+
+        verify(mFrameworkLog).write(
+                eq(FrameworkStatsLog.SHARESHEET_STARTED),
+                eq(SharesheetStartedEvent.SHARE_STARTED.getId()),
+                eq(packageName),
+                /* instanceId=*/ gt(0),
+                eq(mimeType),
+                eq(appProvidedDirectTargets),
+                eq(appProvidedAppTargets),
+                eq(workProfile),
+                eq(FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_FILE),
+                eq(FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_SENDTO));
+    }
+
+    @Test
+    public void testLogShareTargetSelected() {
+        final int targetType = ChooserActivityLogger.SELECTION_TYPE_SERVICE;
+        final String packageName = "com.test.foo";
+        final int positionPicked = 123;
+        final int directTargetAlsoRanked = -1;
+        final int callerTargetCount = 0;
+        final boolean isPinned = true;
+        final boolean isSuccessfullySelected = true;
+        final long selectionCost = 456;
+
+        mChooserLogger.logShareTargetSelected(
+                targetType,
+                packageName,
+                positionPicked,
+                directTargetAlsoRanked,
+                callerTargetCount,
+                /* directTargetHashed= */ null,
+                isPinned,
+                isSuccessfullySelected,
+                selectionCost);
+
+        verify(mFrameworkLog).write(
+                eq(FrameworkStatsLog.RANKING_SELECTED),
+                eq(SharesheetTargetSelectedEvent.SHARESHEET_SERVICE_TARGET_SELECTED.getId()),
+                eq(packageName),
+                /* instanceId=*/ gt(0),
+                eq(positionPicked),
+                eq(isPinned));
+
+        ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class);
+        verify(mMetricsLogger).write(eventCaptor.capture());
+        LogMaker event = eventCaptor.getValue();
+        assertThat(event.getCategory()).isEqualTo(
+                MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET);
+        assertThat(event.getSubtype()).isEqualTo(positionPicked);
+    }
+
+    @Test
+    public void testLogActionSelected() {
+        mChooserLogger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_COPY);
+
+        verify(mFrameworkLog).write(
+                eq(FrameworkStatsLog.RANKING_SELECTED),
+                eq(SharesheetTargetSelectedEvent.SHARESHEET_COPY_TARGET_SELECTED.getId()),
+                eq(""),
+                /* instanceId=*/ gt(0),
+                eq(-1),
+                eq(false));
+
+        ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class);
+        verify(mMetricsLogger).write(eventCaptor.capture());
+        LogMaker event = eventCaptor.getValue();
+        assertThat(event.getCategory()).isEqualTo(
+                MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SYSTEM_TARGET);
+        assertThat(event.getSubtype()).isEqualTo(1);
+    }
+
+    @Test
+    public void testLogDirectShareTargetReceived() {
+        final int category = MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER;
+        final int latency = 123;
+
+        mChooserLogger.logDirectShareTargetReceived(category, latency);
+
+        ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class);
+        verify(mMetricsLogger).write(eventCaptor.capture());
+        LogMaker event = eventCaptor.getValue();
+        assertThat(event.getCategory()).isEqualTo(category);
+        assertThat(event.getSubtype()).isEqualTo(latency);
+    }
+
+    @Test
+    public void testLogActionShareWithPreview() {
+        final int previewType = ChooserContentPreviewUi.CONTENT_PREVIEW_TEXT;
+
+        mChooserLogger.logActionShareWithPreview(previewType);
+
+        ArgumentCaptor<LogMaker> eventCaptor = ArgumentCaptor.forClass(LogMaker.class);
+        verify(mMetricsLogger).write(eventCaptor.capture());
+        LogMaker event = eventCaptor.getValue();
+        assertThat(event.getCategory()).isEqualTo(MetricsEvent.ACTION_SHARE_WITH_PREVIEW);
+        assertThat(event.getSubtype()).isEqualTo(previewType);
+    }
+
+    @Test
+    public void testLogSharesheetTriggered() {
+        mChooserLogger.logSharesheetTriggered();
+        verify(mUiEventLog).logWithInstanceId(
+                eq(SharesheetStandardEvent.SHARESHEET_TRIGGERED), eq(0), isNull(), any());
+    }
+
+    @Test
+    public void testLogSharesheetAppLoadComplete() {
+        mChooserLogger.logSharesheetAppLoadComplete();
+        verify(mUiEventLog).logWithInstanceId(
+                eq(SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE), eq(0), isNull(), any());
+    }
+
+    @Test
+    public void testLogSharesheetDirectLoadComplete() {
+        mChooserLogger.logSharesheetDirectLoadComplete();
+        verify(mUiEventLog).logWithInstanceId(
+                eq(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_COMPLETE),
+                eq(0),
+                isNull(),
+                any());
+    }
+
+    @Test
+    public void testLogSharesheetDirectLoadTimeout() {
+        mChooserLogger.logSharesheetDirectLoadTimeout();
+        verify(mUiEventLog).logWithInstanceId(
+                eq(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_TIMEOUT), eq(0), isNull(), any());
+    }
+
+    @Test
+    public void testLogSharesheetProfileChanged() {
+        mChooserLogger.logSharesheetProfileChanged();
+        verify(mUiEventLog).logWithInstanceId(
+                eq(SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED), eq(0), isNull(), any());
+    }
+
+    @Test
+    public void testLogSharesheetExpansionChanged_collapsed() {
+        mChooserLogger.logSharesheetExpansionChanged(/* isCollapsed=*/ true);
+        verify(mUiEventLog).logWithInstanceId(
+                eq(SharesheetStandardEvent.SHARESHEET_COLLAPSED), eq(0), isNull(), any());
+    }
+
+    @Test
+    public void testLogSharesheetExpansionChanged_expanded() {
+        mChooserLogger.logSharesheetExpansionChanged(/* isCollapsed=*/ false);
+        verify(mUiEventLog).logWithInstanceId(
+                eq(SharesheetStandardEvent.SHARESHEET_EXPANDED), eq(0), isNull(), any());
+    }
+
+    @Test
+    public void testLogSharesheetAppShareRankingTimeout() {
+        mChooserLogger.logSharesheetAppShareRankingTimeout();
+        verify(mUiEventLog).logWithInstanceId(
+                eq(SharesheetStandardEvent.SHARESHEET_APP_SHARE_RANKING_TIMEOUT),
+                eq(0),
+                isNull(),
+                any());
+    }
+
+    @Test
+    public void testLogSharesheetEmptyDirectShareRow() {
+        mChooserLogger.logSharesheetEmptyDirectShareRow();
+        verify(mUiEventLog).logWithInstanceId(
+                eq(SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW),
+                eq(0),
+                isNull(),
+                any());
+    }
+
+    @Test
+    public void testDifferentLoggerInstancesUseDifferentInstanceIds() {
+        ArgumentCaptor<Integer> idIntCaptor = ArgumentCaptor.forClass(Integer.class);
+        ChooserActivityLogger chooserLogger2 =
+                new ChooserActivityLogger(mUiEventLog, mFrameworkLog, mMetricsLogger);
+
+        final int targetType = ChooserActivityLogger.SELECTION_TYPE_COPY;
+        final String packageName = "com.test.foo";
+        final int positionPicked = 123;
+        final int directTargetAlsoRanked = -1;
+        final int callerTargetCount = 0;
+        final boolean isPinned = true;
+        final boolean isSuccessfullySelected = true;
+        final long selectionCost = 456;
+
+        mChooserLogger.logShareTargetSelected(
+                targetType,
+                packageName,
+                positionPicked,
+                directTargetAlsoRanked,
+                callerTargetCount,
+                /* directTargetHashed= */ null,
+                isPinned,
+                isSuccessfullySelected,
+                selectionCost);
+
+        chooserLogger2.logShareTargetSelected(
+                targetType,
+                packageName,
+                positionPicked,
+                directTargetAlsoRanked,
+                callerTargetCount,
+                /* directTargetHashed= */ null,
+                isPinned,
+                isSuccessfullySelected,
+                selectionCost);
+
+        verify(mFrameworkLog, times(2)).write(
+                anyInt(), anyInt(), anyString(), idIntCaptor.capture(), anyInt(), anyBoolean());
+
+        int id1 = idIntCaptor.getAllValues().get(0);
+        int id2 = idIntCaptor.getAllValues().get(1);
+
+        assertThat(id1).isGreaterThan(0);
+        assertThat(id2).isGreaterThan(0);
+        assertThat(id1).isNotEqualTo(id2);
+    }
+
+    @Test
+    public void testUiAndFrameworkEventsUseSameInstanceIdForSameLoggerInstance() {
+        ArgumentCaptor<Integer> idIntCaptor = ArgumentCaptor.forClass(Integer.class);
+        ArgumentCaptor<InstanceId> idObjectCaptor = ArgumentCaptor.forClass(InstanceId.class);
+
+        final int targetType = ChooserActivityLogger.SELECTION_TYPE_COPY;
+        final String packageName = "com.test.foo";
+        final int positionPicked = 123;
+        final int directTargetAlsoRanked = -1;
+        final int callerTargetCount = 0;
+        final boolean isPinned = true;
+        final boolean isSuccessfullySelected = true;
+        final long selectionCost = 456;
+
+        mChooserLogger.logShareTargetSelected(
+                targetType,
+                packageName,
+                positionPicked,
+                directTargetAlsoRanked,
+                callerTargetCount,
+                /* directTargetHashed= */ null,
+                isPinned,
+                isSuccessfullySelected,
+                selectionCost);
+
+        verify(mFrameworkLog).write(
+                anyInt(), anyInt(), anyString(), idIntCaptor.capture(), anyInt(), anyBoolean());
+
+        mChooserLogger.logSharesheetTriggered();
+        verify(mUiEventLog).logWithInstanceId(
+                any(UiEventEnum.class), anyInt(), any(), idObjectCaptor.capture());
+
+        assertThat(idIntCaptor.getValue()).isGreaterThan(0);
+        assertThat(idObjectCaptor.getValue().getId()).isEqualTo(idIntCaptor.getValue());
+    }
+
+    @Test
+    public void testTargetSelectionCategories() {
+        assertThat(ChooserActivityLogger.getTargetSelectionCategory(
+                ChooserActivityLogger.SELECTION_TYPE_SERVICE))
+                        .isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET);
+        assertThat(ChooserActivityLogger.getTargetSelectionCategory(
+                ChooserActivityLogger.SELECTION_TYPE_APP))
+                        .isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET);
+        assertThat(ChooserActivityLogger.getTargetSelectionCategory(
+                ChooserActivityLogger.SELECTION_TYPE_STANDARD))
+                        .isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET);
+        assertThat(ChooserActivityLogger.getTargetSelectionCategory(
+                ChooserActivityLogger.SELECTION_TYPE_COPY)).isEqualTo(0);
+        assertThat(ChooserActivityLogger.getTargetSelectionCategory(
+                ChooserActivityLogger.SELECTION_TYPE_NEARBY)).isEqualTo(0);
+        assertThat(ChooserActivityLogger.getTargetSelectionCategory(
+                ChooserActivityLogger.SELECTION_TYPE_EDIT)).isEqualTo(0);
+    }
+}
diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
index 080f1e4..5df0d4a 100644
--- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
+++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
@@ -16,21 +16,28 @@
 
 package com.android.intentresolver;
 
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
 import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
 
-import android.content.Intent;
 import android.content.pm.PackageManager;
 import android.content.res.Resources;
 import android.database.Cursor;
 import android.graphics.Bitmap;
 import android.os.UserHandle;
 
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager;
 import com.android.intentresolver.chooser.TargetInfo;
-import com.android.internal.logging.MetricsLogger;
+import com.android.intentresolver.shortcuts.ShortcutLoader;
 
-import java.util.List;
+import java.util.function.Consumer;
 import java.util.function.Function;
 
+import kotlin.jvm.functions.Function2;
+
 /**
  * Singleton providing overrides to be applied by any {@code IChooserWrapper} used in testing.
  * We cannot directly mock the activity created since instrumentation creates it, so instead we use
@@ -49,7 +56,8 @@
     @SuppressWarnings("Since15")
     public Function<PackageManager, PackageManager> createPackageManager;
     public Function<TargetInfo, Boolean> onSafelyStartCallback;
-    public Function<ChooserListAdapter, Void> onQueryDirectShareTargets;
+    public Function2<UserHandle, Consumer<ShortcutLoader.Result>, ShortcutLoader>
+            shortcutLoaderFactory = (userHandle, callback) -> null;
     public ResolverListController resolverListController;
     public ResolverListController workResolverListController;
     public Boolean isVoiceInteraction;
@@ -57,21 +65,20 @@
     public Cursor resolverCursor;
     public boolean resolverForceException;
     public Bitmap previewThumbnail;
-    public MetricsLogger metricsLogger;
     public ChooserActivityLogger chooserActivityLogger;
     public int alternateProfileSetting;
     public Resources resources;
     public UserHandle workProfileUserHandle;
     public boolean hasCrossProfileIntents;
     public boolean isQuietModeEnabled;
-    public boolean isWorkProfileUserRunning;
-    public boolean isWorkProfileUserUnlocked;
-    public AbstractMultiProfilePagerAdapter.Injector multiPagerAdapterInjector;
+    public Integer myUserId;
+    public QuietModeManager mQuietModeManager;
+    public MyUserIdProvider mMyUserIdProvider;
+    public CrossProfileIntentsChecker mCrossProfileIntentsChecker;
     public PackageManager packageManager;
 
     public void reset() {
         onSafelyStartCallback = null;
-        onQueryDirectShareTargets = null;
         isVoiceInteraction = null;
         createPackageManager = null;
         previewThumbnail = null;
@@ -80,23 +87,15 @@
         resolverForceException = false;
         resolverListController = mock(ResolverListController.class);
         workResolverListController = mock(ResolverListController.class);
-        metricsLogger = mock(MetricsLogger.class);
-        chooserActivityLogger = new ChooserActivityLoggerFake();
+        chooserActivityLogger = mock(ChooserActivityLogger.class);
         alternateProfileSetting = 0;
         resources = null;
         workProfileUserHandle = null;
         hasCrossProfileIntents = true;
         isQuietModeEnabled = false;
-        isWorkProfileUserRunning = true;
-        isWorkProfileUserUnlocked = true;
+        myUserId = null;
         packageManager = null;
-        multiPagerAdapterInjector = new AbstractMultiProfilePagerAdapter.Injector() {
-            @Override
-            public boolean hasCrossProfileIntents(List<Intent> intents, int sourceUserId,
-                    int targetUserId) {
-                return hasCrossProfileIntents;
-            }
-
+        mQuietModeManager = new QuietModeManager() {
             @Override
             public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) {
                 return isQuietModeEnabled;
@@ -107,7 +106,28 @@
                     UserHandle workProfileUserHandle) {
                 isQuietModeEnabled = enabled;
             }
+
+            @Override
+            public void markWorkProfileEnabledBroadcastReceived() {
+            }
+
+            @Override
+            public boolean isWaitingToEnableWorkProfile() {
+                return false;
+            }
         };
+        shortcutLoaderFactory = ((userHandle, resultConsumer) -> null);
+
+        mMyUserIdProvider = new MyUserIdProvider() {
+            @Override
+            public int getMyUserId() {
+                return myUserId != null ? myUserId : UserHandle.myUserId();
+            }
+        };
+
+        mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class);
+        when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt()))
+                .thenAnswer(invocation -> hasCrossProfileIntents);
     }
 
     private ChooserActivityOverrideData() {}
diff --git a/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt b/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt
new file mode 100644
index 0000000..58f6b73
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt
@@ -0,0 +1,155 @@
+/*
+ * 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.content.ComponentName
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.ResolveInfoFlags
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.ImageView
+import android.widget.TextView
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.intentresolver.ChooserListAdapter.LoadDirectShareIconTask
+import com.android.intentresolver.chooser.DisplayResolveInfo
+import com.android.intentresolver.chooser.SelectableTargetInfo
+import com.android.intentresolver.chooser.TargetInfo
+import com.android.internal.R
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class ChooserListAdapterTest {
+    private val packageManager = mock<PackageManager> {
+        whenever(
+            resolveActivity(any(), any<ResolveInfoFlags>())
+        ).thenReturn(mock())
+    }
+    private val context = InstrumentationRegistry.getInstrumentation().getContext()
+    private val resolverListController = mock<ResolverListController>()
+    private val chooserActivityLogger = mock<ChooserActivityLogger>()
+
+    private fun createChooserListAdapter(
+        taskProvider: (TargetInfo?) -> LoadDirectShareIconTask
+    ) = object : ChooserListAdapter(
+            context,
+            emptyList(),
+            emptyArray(),
+            emptyList(),
+            false,
+            resolverListController,
+            null,
+            Intent(),
+            mock(),
+            packageManager,
+            chooserActivityLogger,
+            mock(),
+            0
+        ) {
+            override fun createLoadDirectShareIconTask(
+                info: SelectableTargetInfo
+            ): LoadDirectShareIconTask = taskProvider(info)
+        }
+
+    @Before
+    fun setup() {
+        // ChooserListAdapter reads DeviceConfig and needs a permission for that.
+        InstrumentationRegistry
+            .getInstrumentation()
+            .getUiAutomation()
+            .adoptShellPermissionIdentity("android.permission.READ_DEVICE_CONFIG")
+    }
+
+    @Test
+    fun testDirectShareTargetLoadingIconIsStarted() {
+        val view = createView()
+        val viewHolder = ResolverListAdapter.ViewHolder(view)
+        view.tag = viewHolder
+        val targetInfo = createSelectableTargetInfo()
+        val iconTask = mock<LoadDirectShareIconTask>()
+        val testSubject = createChooserListAdapter { iconTask }
+        testSubject.onBindView(view, targetInfo, 0)
+
+        verify(iconTask, times(1)).loadIcon()
+    }
+
+    @Test
+    fun testOnlyOneTaskPerTarget() {
+        val view = createView()
+        val viewHolderOne = ResolverListAdapter.ViewHolder(view)
+        view.tag = viewHolderOne
+        val targetInfo = createSelectableTargetInfo()
+        val iconTaskOne = mock<LoadDirectShareIconTask>()
+        val testTaskProvider = mock<() -> LoadDirectShareIconTask> {
+            whenever(invoke()).thenReturn(iconTaskOne)
+        }
+        val testSubject = createChooserListAdapter { testTaskProvider.invoke() }
+        testSubject.onBindView(view, targetInfo, 0)
+
+        val viewHolderTwo = ResolverListAdapter.ViewHolder(view)
+        view.tag = viewHolderTwo
+        whenever(testTaskProvider()).thenReturn(mock())
+
+        testSubject.onBindView(view, targetInfo, 0)
+
+        verify(iconTaskOne, times(1)).loadIcon()
+        verify(testTaskProvider, times(1)).invoke()
+    }
+
+    private fun createSelectableTargetInfo(): TargetInfo =
+        SelectableTargetInfo.newSelectableTargetInfo(
+            /* sourceInfo = */ DisplayResolveInfo.newDisplayResolveInfo(
+                Intent(),
+                ResolverDataProvider.createResolveInfo(2, 0),
+                "label",
+                "extended info",
+                Intent(),
+                /* resolveInfoPresentationGetter= */ null
+            ),
+            /* backupResolveInfo = */ mock(),
+            /* resolvedIntent = */ Intent(),
+            /* chooserTarget = */ createChooserTarget(
+                "Target", 0.5f, ComponentName("pkg", "Class"), "id-1"
+            ),
+            /* modifiedScore = */ 1f,
+            /* shortcutInfo = */ createShortcutInfo("id-1", ComponentName("pkg", "Class"), 1),
+            /* appTarget */ null,
+            /* referrerFillInIntent = */ Intent()
+        )
+
+    private fun createView(): View {
+        val view = FrameLayout(context)
+        TextView(context).apply {
+            id = R.id.text1
+            view.addView(this)
+        }
+        TextView(context).apply {
+            id = R.id.text2
+            view.addView(this)
+        }
+        ImageView(context).apply {
+            id = R.id.icon
+            view.addView(this)
+        }
+        return view
+    }
+}
diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
index 0e9f010..97de97f 100644
--- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
+++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
@@ -19,11 +19,13 @@
 import static org.mockito.Mockito.when;
 
 import android.annotation.Nullable;
+import android.app.prediction.AppPredictor;
 import android.app.usage.UsageStatsManager;
 import android.content.ComponentName;
 import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
+import android.content.IntentFilter;
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.content.res.Resources;
@@ -33,19 +35,18 @@
 import android.os.UserHandle;
 import android.util.Size;
 
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter;
-import com.android.intentresolver.ChooserActivityLogger;
-import com.android.intentresolver.ChooserActivityOverrideData;
-import com.android.intentresolver.ChooserListAdapter;
-import com.android.intentresolver.IChooserWrapper;
-import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;
-import com.android.intentresolver.ResolverListController;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager;
 import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.NotSelectableTargetInfo;
 import com.android.intentresolver.chooser.TargetInfo;
-import com.android.internal.logging.MetricsLogger;
+import com.android.intentresolver.grid.ChooserGridAdapter;
+import com.android.intentresolver.shortcuts.ShortcutLoader;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
 
 import java.util.List;
+import java.util.function.Consumer;
 
 /**
  * Simple wrapper around chooser activity to be able to initiate it under test. For more
@@ -64,25 +65,34 @@
     }
 
     @Override
-    protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter(
-            Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed) {
-        AbstractMultiProfilePagerAdapter multiProfilePagerAdapter =
-                super.createMultiProfilePagerAdapter(initialIntents, rList, filterLastUsed);
-        multiProfilePagerAdapter.setInjector(sOverrides.multiPagerAdapterInjector);
-        return multiProfilePagerAdapter;
-    }
-
-    @Override
-    public ChooserListAdapter createChooserListAdapter(Context context, List<Intent> payloadIntents,
-            Intent[] initialIntents, List<ResolveInfo> rList, boolean filterLastUsed,
-            ResolverListController resolverListController) {
+    public ChooserListAdapter createChooserListAdapter(
+            Context context,
+            List<Intent> payloadIntents,
+            Intent[] initialIntents,
+            List<ResolveInfo> rList,
+            boolean filterLastUsed,
+            ResolverListController resolverListController,
+            UserHandle userHandle,
+            Intent targetIntent,
+            ChooserRequestParameters chooserRequest,
+            int maxTargetsPerRow) {
         PackageManager packageManager =
                 sOverrides.packageManager == null ? context.getPackageManager()
                         : sOverrides.packageManager;
-        return new ChooserListAdapter(context, payloadIntents, initialIntents, rList,
-                filterLastUsed, resolverListController,
-                this, this, packageManager,
-                getChooserActivityLogger());
+        return new ChooserListAdapter(
+                context,
+                payloadIntents,
+                initialIntents,
+                rList,
+                filterLastUsed,
+                resolverListController,
+                userHandle,
+                targetIntent,
+                this,
+                packageManager,
+                getChooserActivityLogger(),
+                chooserRequest,
+                maxTargetsPerRow);
     }
 
     @Override
@@ -119,7 +129,7 @@
 
     @Override
     protected TargetInfo getNearbySharingTarget(Intent originalIntent) {
-        return new ChooserWrapperActivity.EmptyTargetInfo();
+        return NotSelectableTargetInfo.newEmptyTargetInfo();
     }
 
     @Override
@@ -139,6 +149,30 @@
     }
 
     @Override
+    protected MyUserIdProvider createMyUserIdProvider() {
+        if (sOverrides.mMyUserIdProvider != null) {
+            return sOverrides.mMyUserIdProvider;
+        }
+        return super.createMyUserIdProvider();
+    }
+
+    @Override
+    protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() {
+        if (sOverrides.mCrossProfileIntentsChecker != null) {
+            return sOverrides.mCrossProfileIntentsChecker;
+        }
+        return super.createCrossProfileIntentsChecker();
+    }
+
+    @Override
+    protected QuietModeManager createQuietModeManager() {
+        if (sOverrides.mQuietModeManager != null) {
+            return sOverrides.mQuietModeManager;
+        }
+        return super.createQuietModeManager();
+    }
+
+    @Override
     public void safelyStartActivity(com.android.intentresolver.chooser.TargetInfo cti) {
         if (sOverrides.onSafelyStartCallback != null
                 && sOverrides.onSafelyStartCallback.apply(cti)) {
@@ -187,11 +221,6 @@
     }
 
     @Override
-    protected MetricsLogger getMetricsLogger() {
-        return sOverrides.metricsLogger;
-    }
-
-    @Override
     public ChooserActivityLogger getChooserActivityLogger() {
         return sOverrides.chooserActivityLogger;
     }
@@ -220,8 +249,13 @@
     @Override
     public DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri,
             CharSequence pLabel, CharSequence pInfo, Intent replacementIntent,
-            @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
-        return new DisplayResolveInfo(originalIntent, pri, pLabel, pInfo, replacementIntent,
+            @Nullable TargetPresentationGetter resolveInfoPresentationGetter) {
+        return DisplayResolveInfo.newDisplayResolveInfo(
+                originalIntent,
+                pri,
+                pLabel,
+                pInfo,
+                replacementIntent,
                 resolveInfoPresentationGetter);
     }
 
@@ -242,32 +276,18 @@
     }
 
     @Override
-    protected void queryDirectShareTargets(ChooserListAdapter adapter,
-            boolean skipAppPredictionService) {
-        if (sOverrides.onQueryDirectShareTargets != null) {
-            sOverrides.onQueryDirectShareTargets.apply(adapter);
+    protected ShortcutLoader createShortcutLoader(
+            Context context,
+            AppPredictor appPredictor,
+            UserHandle userHandle,
+            IntentFilter targetIntentFilter,
+            Consumer<ShortcutLoader.Result> callback) {
+        ShortcutLoader shortcutLoader =
+                sOverrides.shortcutLoaderFactory.invoke(userHandle, callback);
+        if (shortcutLoader != null) {
+            return shortcutLoader;
         }
-        super.queryDirectShareTargets(adapter, skipAppPredictionService);
-    }
-
-    @Override
-    protected boolean isQuietModeEnabled(UserHandle userHandle) {
-        return sOverrides.isQuietModeEnabled;
-    }
-
-    @Override
-    protected boolean isUserRunning(UserHandle userHandle) {
-        if (userHandle.equals(UserHandle.SYSTEM)) {
-            return super.isUserRunning(userHandle);
-        }
-        return sOverrides.isWorkProfileUserRunning;
-    }
-
-    @Override
-    protected boolean isUserUnlocked(UserHandle userHandle) {
-        if (userHandle.equals(UserHandle.SYSTEM)) {
-            return super.isUserUnlocked(userHandle);
-        }
-        return sOverrides.isWorkProfileUserUnlocked;
+        return super.createShortcutLoader(
+                context, appPredictor, userHandle, targetIntentFilter, callback);
     }
 }
diff --git a/java/tests/src/com/android/intentresolver/IChooserWrapper.java b/java/tests/src/com/android/intentresolver/IChooserWrapper.java
index f81cd02..af897a4 100644
--- a/java/tests/src/com/android/intentresolver/IChooserWrapper.java
+++ b/java/tests/src/com/android/intentresolver/IChooserWrapper.java
@@ -22,9 +22,10 @@
 import android.content.pm.ResolveInfo;
 import android.os.UserHandle;
 
-import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;
 import com.android.intentresolver.chooser.DisplayResolveInfo;
 
+import java.util.concurrent.Executor;
+
 /**
  * Test-only extended API capabilities that an instrumented ChooserActivity subclass provides in
  * order to expose the internals for override/inspection. Implementations should apply the overrides
@@ -38,7 +39,8 @@
     UsageStatsManager getUsageStatsManager();
     DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri,
             CharSequence pLabel, CharSequence pInfo, Intent replacementIntent,
-            @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter);
+            @Nullable TargetPresentationGetter resolveInfoPresentationGetter);
     UserHandle getCurrentUserHandle();
     ChooserActivityLogger getChooserActivityLogger();
+    Executor getMainExecutor();
 }
diff --git a/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt b/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt
new file mode 100644
index 0000000..159c6d6
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt
@@ -0,0 +1,146 @@
+/*
+ * 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
+
+/**
+ * Kotlin versions of popular mockito methods that can return null in situations when Kotlin expects
+ * a non-null value. Kotlin will throw an IllegalStateException when this takes place ("x must not
+ * be null"). To fix this, we can use methods that modify the return type to be nullable. This
+ * causes Kotlin to skip the null checks.
+ * Cloned from frameworks/base/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
+ */
+
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatcher
+import org.mockito.Mockito
+import org.mockito.stubbing.OngoingStubbing
+
+/**
+ * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when
+ * null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> eq(obj: T): T = Mockito.eq<T>(obj)
+
+/**
+ * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when
+ * null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
+inline fun <reified T> any(): T = any(T::class.java)
+
+/**
+ * Returns Mockito.argThat() as nullable type to avoid java.lang.IllegalStateException when
+ * null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> argThat(matcher: ArgumentMatcher<T>): T = Mockito.argThat(matcher)
+
+/**
+ * Kotlin type-inferred version of Mockito.nullable()
+ */
+inline fun <reified T> nullable(): T? = Mockito.nullable(T::class.java)
+
+/**
+ * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException
+ * when null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+
+/**
+ * Helper function for creating an argumentCaptor in kotlin.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> =
+    ArgumentCaptor.forClass(T::class.java)
+
+/**
+ * Helper function for creating new mocks, without the need to pass in a [Class] instance.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ *
+ * @param apply builder function to simplify stub configuration by improving type inference.
+ */
+inline fun <reified T : Any> mock(apply: T.() -> Unit = {}): T = Mockito.mock(T::class.java)
+    .apply(apply)
+
+/**
+ * Helper function for stubbing methods without the need to use backticks.
+ *
+ * @see Mockito.when
+ */
+fun <T> whenever(methodCall: T): OngoingStubbing<T> = Mockito.`when`(methodCall)
+
+/**
+ * A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when
+ * kotlin tests are mocking kotlin objects and the methods take non-null parameters:
+ *
+ *     java.lang.NullPointerException: capture() must not be null
+ */
+class KotlinArgumentCaptor<T> constructor(clazz: Class<T>) {
+    private val wrapped: ArgumentCaptor<T> = ArgumentCaptor.forClass(clazz)
+    fun capture(): T = wrapped.capture()
+    val value: T
+        get() = wrapped.value
+    val allValues: List<T>
+        get() = wrapped.allValues
+}
+
+/**
+ * Helper function for creating an argumentCaptor in kotlin.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+inline fun <reified T : Any> kotlinArgumentCaptor(): KotlinArgumentCaptor<T> =
+    KotlinArgumentCaptor(T::class.java)
+
+/**
+ * Helper function for creating and using a single-use ArgumentCaptor in kotlin.
+ *
+ *    val captor = argumentCaptor<Foo>()
+ *    verify(...).someMethod(captor.capture())
+ *    val captured = captor.value
+ *
+ * becomes:
+ *
+ *    val captured = withArgCaptor<Foo> { verify(...).someMethod(capture()) }
+ *
+ * NOTE: this uses the KotlinArgumentCaptor to avoid the NullPointerException.
+ */
+inline fun <reified T : Any> withArgCaptor(block: KotlinArgumentCaptor<T>.() -> Unit): T =
+    kotlinArgumentCaptor<T>().apply { block() }.value
+
+/**
+ * Variant of [withArgCaptor] for capturing multiple arguments.
+ *
+ *    val captor = argumentCaptor<Foo>()
+ *    verify(...).someMethod(captor.capture())
+ *    val captured: List<Foo> = captor.allValues
+ *
+ * becomes:
+ *
+ *    val capturedList = captureMany<Foo> { verify(...).someMethod(capture()) }
+ */
+inline fun <reified T : Any> captureMany(block: KotlinArgumentCaptor<T>.() -> Unit): List<T> =
+    kotlinArgumentCaptor<T>().apply{ block() }.allValues
diff --git a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java
new file mode 100644
index 0000000..62c16ff
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java
@@ -0,0 +1,858 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.action.ViewActions.swipeUp;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.isEnabled;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import static com.android.intentresolver.MatcherUtils.first;
+import static com.android.intentresolver.ResolverWrapperActivity.sOverrides;
+
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+import static org.testng.Assert.assertFalse;
+import static org.testng.Assert.fail;
+
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.espresso.Espresso;
+import androidx.test.espresso.NoMatchingViewException;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
+import com.android.intentresolver.widget.ResolverDrawerLayout;
+import com.android.internal.R;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Resolver activity instrumentation tests
+ */
+@RunWith(AndroidJUnit4.class)
+public class ResolverActivityTest {
+    protected Intent getConcreteIntentForLaunch(Intent clientIntent) {
+        clientIntent.setClass(
+                androidx.test.platform.app.InstrumentationRegistry.getInstrumentation().getTargetContext(),
+                ResolverWrapperActivity.class);
+        return clientIntent;
+    }
+
+    @Rule
+    public ActivityTestRule<ResolverWrapperActivity> mActivityRule =
+            new ActivityTestRule<>(ResolverWrapperActivity.class, false, false);
+
+    @Before
+    public void setup() {
+        // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the
+        // permissions we require (which we'll read from the manifest at runtime).
+        androidx.test.platform.app.InstrumentationRegistry
+                .getInstrumentation()
+                .getUiAutomation()
+                .adoptShellPermissionIdentity();
+
+        sOverrides.reset();
+    }
+
+    @Test
+    public void twoOptionsAndUserSelectsOne() throws InterruptedException {
+        Intent sendIntent = createSendImageIntent();
+        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource());
+        waitForIdle();
+
+        assertThat(activity.getAdapter().getCount(), is(2));
+
+        ResolveInfo[] chosen = new ResolveInfo[1];
+        sOverrides.onSafelyStartCallback = targetInfo -> {
+            chosen[0] = targetInfo.getResolveInfo();
+            return true;
+        };
+
+        ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0);
+        onView(withText(toChoose.activityInfo.name))
+                .perform(click());
+        onView(withId(R.id.button_once))
+                .perform(click());
+        waitForIdle();
+        assertThat(chosen[0], is(toChoose));
+    }
+
+    @Ignore // Failing - b/144929805
+    @Test
+    public void setMaxHeight() throws Exception {
+        Intent sendIntent = createSendImageIntent();
+        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+        waitForIdle();
+
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        final View viewPager = activity.findViewById(R.id.profile_pager);
+        final int initialResolverHeight = viewPager.getHeight();
+
+        activity.runOnUiThread(() -> {
+            ResolverDrawerLayout layout = (ResolverDrawerLayout)
+                    activity.findViewById(
+                            R.id.contentPanel);
+            ((ResolverDrawerLayout.LayoutParams) viewPager.getLayoutParams()).maxHeight
+                    = initialResolverHeight - 1;
+            // Force a relayout
+            layout.invalidate();
+            layout.requestLayout();
+        });
+        waitForIdle();
+        assertThat("Drawer should be capped at maxHeight",
+                viewPager.getHeight() == (initialResolverHeight - 1));
+
+        activity.runOnUiThread(() -> {
+            ResolverDrawerLayout layout = (ResolverDrawerLayout)
+                    activity.findViewById(
+                            R.id.contentPanel);
+            ((ResolverDrawerLayout.LayoutParams) viewPager.getLayoutParams()).maxHeight
+                    = initialResolverHeight + 1;
+            // Force a relayout
+            layout.invalidate();
+            layout.requestLayout();
+        });
+        waitForIdle();
+        assertThat("Drawer should not change height if its height is less than maxHeight",
+                viewPager.getHeight() == initialResolverHeight);
+    }
+
+    @Ignore // Failing - b/144929805
+    @Test
+    public void setShowAtTopToTrue() throws Exception {
+        Intent sendIntent = createSendImageIntent();
+        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+        waitForIdle();
+
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        final View viewPager = activity.findViewById(R.id.profile_pager);
+        final View divider = activity.findViewById(R.id.divider);
+        final RelativeLayout profileView =
+                (RelativeLayout) activity.findViewById(R.id.profile_button).getParent();
+        assertThat("Drawer should show at bottom by default",
+                profileView.getBottom() + divider.getHeight() == viewPager.getTop()
+                        && profileView.getTop() > 0);
+
+        activity.runOnUiThread(() -> {
+            ResolverDrawerLayout layout = (ResolverDrawerLayout)
+                    activity.findViewById(
+                            R.id.contentPanel);
+            layout.setShowAtTop(true);
+        });
+        waitForIdle();
+        assertThat("Drawer should show at top with new attribute",
+                profileView.getBottom() + divider.getHeight() == viewPager.getTop()
+                        && profileView.getTop() == 0);
+    }
+
+    @Test
+    public void hasLastChosenActivity() throws Exception {
+        Intent sendIntent = createSendImageIntent();
+        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+        ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0);
+
+        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+        when(sOverrides.resolverListController.getLastChosen())
+                .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0));
+
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+
+        // The other entry is filtered to the last used slot
+        assertThat(activity.getAdapter().getCount(), is(1));
+        assertThat(activity.getAdapter().getPlaceholderCount(), is(1));
+
+        ResolveInfo[] chosen = new ResolveInfo[1];
+        sOverrides.onSafelyStartCallback = targetInfo -> {
+            chosen[0] = targetInfo.getResolveInfo();
+            return true;
+        };
+
+        onView(withId(R.id.button_once)).perform(click());
+        waitForIdle();
+        assertThat(chosen[0], is(toChoose));
+    }
+
+    @Test
+    public void hasOtherProfileOneOption() throws Exception {
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10);
+        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        markWorkProfileUserAvailable();
+
+        ResolveInfo toChoose = personalResolvedComponentInfos.get(1).getResolveInfoAt(0);
+        Intent sendIntent = createSendImageIntent();
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource());
+        waitForIdle();
+
+        // The other entry is filtered to the last used slot
+        assertThat(activity.getAdapter().getCount(), is(1));
+
+        ResolveInfo[] chosen = new ResolveInfo[1];
+        sOverrides.onSafelyStartCallback = targetInfo -> {
+            chosen[0] = targetInfo.getResolveInfo();
+            return true;
+        };
+        // Make a stable copy of the components as the original list may be modified
+        List<ResolvedComponentInfo> stableCopy =
+                createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10);
+        // We pick the first one as there is another one in the work profile side
+        onView(first(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)))
+                .perform(click());
+        onView(withId(R.id.button_once))
+                .perform(click());
+        waitForIdle();
+        assertThat(chosen[0], is(toChoose));
+    }
+
+    @Test
+    public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception {
+        Intent sendIntent = createSendImageIntent();
+        List<ResolvedComponentInfo> resolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(3);
+        ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0);
+
+        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource());
+        waitForIdle();
+
+        // The other entry is filtered to the other profile slot
+        assertThat(activity.getAdapter().getCount(), is(2));
+
+        ResolveInfo[] chosen = new ResolveInfo[1];
+        sOverrides.onSafelyStartCallback = targetInfo -> {
+            chosen[0] = targetInfo.getResolveInfo();
+            return true;
+        };
+
+        // Confirm that the button bar is disabled by default
+        onView(withId(R.id.button_once)).check(matches(not(isEnabled())));
+
+        // Make a stable copy of the components as the original list may be modified
+        List<ResolvedComponentInfo> stableCopy =
+                createResolvedComponentsForTestWithOtherProfile(2);
+
+        onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))
+                .perform(click());
+        onView(withId(R.id.button_once)).perform(click());
+        waitForIdle();
+        assertThat(chosen[0], is(toChoose));
+    }
+
+
+    @Test
+    public void hasLastChosenActivityAndOtherProfile() throws Exception {
+        // In this case we prefer the other profile and don't display anything about the last
+        // chosen activity.
+        Intent sendIntent = createSendImageIntent();
+        List<ResolvedComponentInfo> resolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(3);
+        ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0);
+
+        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+        when(sOverrides.resolverListController.getLastChosen())
+                .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0));
+
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource());
+        waitForIdle();
+
+        // The other entry is filtered to the other profile slot
+        assertThat(activity.getAdapter().getCount(), is(2));
+
+        ResolveInfo[] chosen = new ResolveInfo[1];
+        sOverrides.onSafelyStartCallback = targetInfo -> {
+            chosen[0] = targetInfo.getResolveInfo();
+            return true;
+        };
+
+        // Confirm that the button bar is disabled by default
+        onView(withId(R.id.button_once)).check(matches(not(isEnabled())));
+
+        // Make a stable copy of the components as the original list may be modified
+        List<ResolvedComponentInfo> stableCopy =
+                createResolvedComponentsForTestWithOtherProfile(2);
+
+        onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))
+                .perform(click());
+        onView(withId(R.id.button_once)).perform(click());
+        waitForIdle();
+        assertThat(chosen[0], is(toChoose));
+    }
+
+    @Test
+    public void testWorkTab_displayedWhenWorkProfileUserAvailable() {
+        Intent sendIntent = createSendImageIntent();
+        markWorkProfileUserAvailable();
+
+        mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+
+        onView(withId(R.id.tabs)).check(matches(isDisplayed()));
+    }
+
+    @Test
+    public void testWorkTab_hiddenWhenWorkProfileUserNotAvailable() {
+        Intent sendIntent = createSendImageIntent();
+
+        mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+
+        onView(withId(R.id.tabs)).check(matches(not(isDisplayed())));
+    }
+
+    @Test
+    public void testWorkTab_workTabListPopulatedBeforeGoingToTab() throws InterruptedException {
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(3, /* userId = */ 10);
+        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
+        setupResolverControllers(personalResolvedComponentInfos,
+                new ArrayList<>(workResolvedComponentInfos));
+        Intent sendIntent = createSendImageIntent();
+        markWorkProfileUserAvailable();
+
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+
+        assertThat(activity.getCurrentUserHandle().getIdentifier(), is(0));
+        // The work list adapter must be populated in advance before tapping the other tab
+        assertThat(activity.getWorkListAdapter().getCount(), is(4));
+    }
+
+    @Test
+    public void testWorkTab_workTabUsesExpectedAdapter() {
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        Intent sendIntent = createSendImageIntent();
+        markWorkProfileUserAvailable();
+
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        onView(withText(R.string.resolver_work_tab)).perform(click());
+
+        assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10));
+        assertThat(activity.getWorkListAdapter().getCount(), is(4));
+    }
+
+    @Test
+    public void testWorkTab_personalTabUsesExpectedAdapter() {
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(3);
+        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        Intent sendIntent = createSendImageIntent();
+        markWorkProfileUserAvailable();
+
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        onView(withText(R.string.resolver_work_tab)).perform(click());
+
+        assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10));
+        assertThat(activity.getPersonalListAdapter().getCount(), is(2));
+    }
+
+    @Test
+    public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException {
+        markWorkProfileUserAvailable();
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        Intent sendIntent = createSendImageIntent();
+
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+
+        onView(withText(R.string.resolver_work_tab))
+                .perform(click());
+        waitForIdle();
+        assertThat(activity.getWorkListAdapter().getCount(), is(4));
+    }
+
+    @Test
+    public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() throws InterruptedException {
+        markWorkProfileUserAvailable();
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        Intent sendIntent = createSendImageIntent();
+        ResolveInfo[] chosen = new ResolveInfo[1];
+        sOverrides.onSafelyStartCallback = targetInfo -> {
+            chosen[0] = targetInfo.getResolveInfo();
+            return true;
+        };
+
+        mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        onView(withText(R.string.resolver_work_tab))
+                .perform(click());
+        waitForIdle();
+        onView(first(allOf(withText(workResolvedComponentInfos.get(0)
+                .getResolveInfoAt(0).activityInfo.applicationInfo.name), isCompletelyDisplayed())))
+                .perform(click());
+        onView(withId(R.id.button_once))
+                .perform(click());
+
+        waitForIdle();
+        assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0)));
+    }
+
+    @Test
+    public void testWorkTab_noPersonalApps_workTabHasExpectedNumberOfTargets()
+            throws InterruptedException {
+        markWorkProfileUserAvailable();
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(1);
+        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        Intent sendIntent = createSendImageIntent();
+
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        onView(withText(R.string.resolver_work_tab))
+                .perform(click());
+
+        waitForIdle();
+        assertThat(activity.getWorkListAdapter().getCount(), is(4));
+    }
+
+    @Test
+    public void testWorkTab_headerIsVisibleInPersonalTab() {
+        markWorkProfileUserAvailable();
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(1);
+        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        Intent sendIntent = createOpenWebsiteIntent();
+
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        TextView headerText = activity.findViewById(R.id.title);
+        String initialText = headerText.getText().toString();
+        assertFalse(initialText.isEmpty(), "Header text is empty.");
+        assertThat(headerText.getVisibility(), is(View.VISIBLE));
+    }
+
+    @Test
+    public void testWorkTab_switchTabs_headerStaysSame() {
+        markWorkProfileUserAvailable();
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(1);
+        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        Intent sendIntent = createOpenWebsiteIntent();
+
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        TextView headerText = activity.findViewById(R.id.title);
+        String initialText = headerText.getText().toString();
+        onView(withText(R.string.resolver_work_tab))
+                .perform(click());
+
+        waitForIdle();
+        String currentText = headerText.getText().toString();
+        assertThat(headerText.getVisibility(), is(View.VISIBLE));
+        assertThat(String.format("Header text is not the same when switching tabs, personal profile"
+                        + " header was %s but work profile header is %s", initialText, currentText),
+                TextUtils.equals(initialText, currentText));
+    }
+
+    @Test
+    public void testWorkTab_noPersonalApps_canStartWorkApps()
+            throws InterruptedException {
+        markWorkProfileUserAvailable();
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(3, /* userId= */ 10);
+        List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        Intent sendIntent = createSendImageIntent();
+        ResolveInfo[] chosen = new ResolveInfo[1];
+        sOverrides.onSafelyStartCallback = targetInfo -> {
+            chosen[0] = targetInfo.getResolveInfo();
+            return true;
+        };
+
+        mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        onView(withText(R.string.resolver_work_tab))
+                .perform(click());
+        waitForIdle();
+        onView(first(allOf(
+                withText(workResolvedComponentInfos.get(0)
+                        .getResolveInfoAt(0).activityInfo.applicationInfo.name),
+                isDisplayed())))
+                .perform(click());
+        onView(withId(R.id.button_once))
+                .perform(click());
+        waitForIdle();
+
+        assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0)));
+    }
+
+    @Test
+    public void testWorkTab_crossProfileIntentsDisabled_personalToWork_emptyStateShown() {
+        markWorkProfileUserAvailable();
+        int workProfileTargets = 4;
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+        List<ResolvedComponentInfo> workResolvedComponentInfos =
+                createResolvedComponentsForTest(workProfileTargets);
+        sOverrides.hasCrossProfileIntents = false;
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        Intent sendIntent = createSendImageIntent();
+        sendIntent.setType("TestType");
+
+        mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        onView(withText(R.string.resolver_work_tab)).perform(click());
+        waitForIdle();
+        onView(withId(R.id.contentPanel))
+                .perform(swipeUp());
+
+        onView(withText(R.string.resolver_cross_profile_blocked))
+                .check(matches(isDisplayed()));
+    }
+
+    @Test
+    public void testWorkTab_workProfileDisabled_emptyStateShown() {
+        markWorkProfileUserAvailable();
+        int workProfileTargets = 4;
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+        List<ResolvedComponentInfo> workResolvedComponentInfos =
+                createResolvedComponentsForTest(workProfileTargets);
+        sOverrides.isQuietModeEnabled = true;
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        Intent sendIntent = createSendImageIntent();
+        sendIntent.setType("TestType");
+
+        mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        onView(withId(R.id.contentPanel))
+                .perform(swipeUp());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
+        waitForIdle();
+
+        onView(withText(R.string.resolver_turn_on_work_apps))
+                .check(matches(isDisplayed()));
+    }
+
+    @Test
+    public void testWorkTab_noWorkAppsAvailable_emptyStateShown() {
+        markWorkProfileUserAvailable();
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTest(3);
+        List<ResolvedComponentInfo> workResolvedComponentInfos =
+                createResolvedComponentsForTest(0);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        Intent sendIntent = createSendImageIntent();
+        sendIntent.setType("TestType");
+
+        mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        onView(withId(R.id.contentPanel))
+                .perform(swipeUp());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
+        waitForIdle();
+
+        onView(withText(R.string.resolver_no_work_apps_available))
+                .check(matches(isDisplayed()));
+    }
+
+    @Test
+    public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() {
+        markWorkProfileUserAvailable();
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTest(3);
+        List<ResolvedComponentInfo> workResolvedComponentInfos =
+                createResolvedComponentsForTest(0);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        Intent sendIntent = createSendImageIntent();
+        sendIntent.setType("TestType");
+        sOverrides.isQuietModeEnabled = true;
+        sOverrides.hasCrossProfileIntents = false;
+
+        mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        onView(withId(R.id.contentPanel))
+                .perform(swipeUp());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
+        waitForIdle();
+
+        onView(withText(R.string.resolver_cross_profile_blocked))
+                .check(matches(isDisplayed()));
+    }
+
+    @Test
+    public void testMiniResolver() {
+        markWorkProfileUserAvailable();
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTest(1);
+        List<ResolvedComponentInfo> workResolvedComponentInfos =
+                createResolvedComponentsForTest(1);
+        // Personal profile only has a browser
+        personalResolvedComponentInfos.get(0).getResolveInfoAt(0).handleAllWebDataURI = true;
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        Intent sendIntent = createSendImageIntent();
+        sendIntent.setType("TestType");
+
+        mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        onView(withId(R.id.open_cross_profile)).check(matches(isDisplayed()));
+    }
+
+    @Test
+    public void testMiniResolver_noCurrentProfileTarget() {
+        markWorkProfileUserAvailable();
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTest(0);
+        List<ResolvedComponentInfo> workResolvedComponentInfos =
+                createResolvedComponentsForTest(1);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        Intent sendIntent = createSendImageIntent();
+        sendIntent.setType("TestType");
+
+        mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+
+        // Need to ensure mini resolver doesn't trigger here.
+        assertNotMiniResolver();
+    }
+
+    private void assertNotMiniResolver() {
+        try {
+            onView(withId(R.id.open_cross_profile)).check(matches(isDisplayed()));
+        } catch (NoMatchingViewException e) {
+            return;
+        }
+        fail("Mini resolver present but shouldn't be");
+    }
+
+    @Test
+    public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() {
+        markWorkProfileUserAvailable();
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTest(3);
+        List<ResolvedComponentInfo> workResolvedComponentInfos =
+                createResolvedComponentsForTest(0);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        Intent sendIntent = createSendImageIntent();
+        sendIntent.setType("TestType");
+        sOverrides.isQuietModeEnabled = true;
+
+        mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+        onView(withId(R.id.contentPanel))
+                .perform(swipeUp());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
+        waitForIdle();
+
+        onView(withText(R.string.resolver_no_work_apps_available))
+                .check(matches(isDisplayed()));
+    }
+
+    @Test
+    public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_autolaunch() {
+        markWorkProfileUserAvailable();
+        int workProfileTargets = 4;
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10);
+        List<ResolvedComponentInfo> workResolvedComponentInfos =
+                createResolvedComponentsForTest(workProfileTargets);
+        sOverrides.hasCrossProfileIntents = false;
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+        Intent sendIntent = createSendImageIntent();
+        sendIntent.setType("TestType");
+        ResolveInfo[] chosen = new ResolveInfo[1];
+        sOverrides.onSafelyStartCallback = targetInfo -> {
+            chosen[0] = targetInfo.getResolveInfo();
+            return true;
+        };
+
+        mActivityRule.launchActivity(sendIntent);
+        waitForIdle();
+
+        assertThat(chosen[0], is(personalResolvedComponentInfos.get(1).getResolveInfoAt(0)));
+    }
+
+    @Test
+    public void testLayoutWithDefault_withWorkTab_neverShown() throws RemoteException {
+        markWorkProfileUserAvailable();
+
+        // In this case we prefer the other profile and don't display anything about the last
+        // chosen activity.
+        Intent sendIntent = createSendImageIntent();
+        List<ResolvedComponentInfo> resolvedComponentInfos =
+                createResolvedComponentsForTest(2);
+
+        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+        when(sOverrides.resolverListController.getLastChosen())
+                .thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0));
+
+        final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+        Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource());
+        waitForIdle();
+
+        // The other entry is filtered to the last used slot
+        assertThat(activity.getAdapter().hasFilteredItem(), is(false));
+        assertThat(activity.getAdapter().getCount(), is(2));
+        assertThat(activity.getAdapter().getPlaceholderCount(), is(2));
+    }
+
+    private Intent createSendImageIntent() {
+        Intent sendIntent = new Intent();
+        sendIntent.setAction(Intent.ACTION_SEND);
+        sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending");
+        sendIntent.setType("image/jpeg");
+        return sendIntent;
+    }
+
+    private Intent createOpenWebsiteIntent() {
+        Intent sendIntent = new Intent();
+        sendIntent.setAction(Intent.ACTION_VIEW);
+        sendIntent.setData(Uri.parse("https://google.com"));
+        return sendIntent;
+    }
+
+    private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) {
+        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+        for (int i = 0; i < numberOfResults; i++) {
+            infoList.add(ResolverDataProvider.createResolvedComponentInfo(i));
+        }
+        return infoList;
+    }
+
+    private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile(
+            int numberOfResults) {
+        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+        for (int i = 0; i < numberOfResults; i++) {
+            if (i == 0) {
+                infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i));
+            } else {
+                infoList.add(ResolverDataProvider.createResolvedComponentInfo(i));
+            }
+        }
+        return infoList;
+    }
+
+    private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile(
+            int numberOfResults, int userId) {
+        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+        for (int i = 0; i < numberOfResults; i++) {
+            if (i == 0) {
+                infoList.add(
+                        ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId));
+            } else {
+                infoList.add(ResolverDataProvider.createResolvedComponentInfo(i));
+            }
+        }
+        return infoList;
+    }
+
+    private void waitForIdle() {
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+    }
+
+    private void markWorkProfileUserAvailable() {
+        ResolverWrapperActivity.sOverrides.workProfileUserHandle = UserHandle.of(10);
+    }
+
+    private void setupResolverControllers(
+            List<ResolvedComponentInfo> personalResolvedComponentInfos,
+            List<ResolvedComponentInfo> workResolvedComponentInfos) {
+        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class)))
+                .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+        when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos);
+        when(sOverrides.workResolverListController.getResolversForIntentAsUser(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class),
+                eq(UserHandle.SYSTEM)))
+                .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+    }
+}
diff --git a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
index 33e7123..fb928e0 100644
--- a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
+++ b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
@@ -32,7 +32,7 @@
 /**
  * Utility class used by resolver tests to create mock data
  */
-class ResolverDataProvider {
+public class ResolverDataProvider {
 
     static private int USER_SOMEONE_ELSE = 10;
 
@@ -52,12 +52,12 @@
                 createResolverIntent(i), createResolveInfo(i, userId));
     }
 
-    static ComponentName createComponentName(int i) {
+    public static ComponentName createComponentName(int i) {
         final String name = "component" + i;
         return new ComponentName("foo.bar." + name, name);
     }
 
-    static ResolveInfo createResolveInfo(int i, int userId) {
+    public static ResolveInfo createResolveInfo(int i, int userId) {
         final ResolveInfo resolveInfo = new ResolveInfo();
         resolveInfo.activityInfo = createActivityInfo(i);
         resolveInfo.targetUserId = userId;
@@ -93,11 +93,17 @@
         public String setResolveInfoLabel;
     }
 
+    /** Create a {@link PackageManagerMockedInfo} with all distinct labels. */
     static PackageManagerMockedInfo createPackageManagerMockedInfo(boolean hasOverridePermission) {
-        final String appLabel = "app_label";
-        final String activityLabel = "activity_label";
-        final String resolveInfoLabel = "resolve_info_label";
+        return createPackageManagerMockedInfo(
+                hasOverridePermission, "app_label", "activity_label", "resolve_info_label");
+    }
 
+    static PackageManagerMockedInfo createPackageManagerMockedInfo(
+            boolean hasOverridePermission,
+            String appLabel,
+            String activityLabel,
+            String resolveInfoLabel) {
         MockContext ctx = new MockContext() {
             @Override
             public PackageManager getPackageManager() {
diff --git a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java
new file mode 100644
index 0000000..239bffe
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.app.usage.UsageStatsManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.UserHandle;
+
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager;
+import com.android.intentresolver.chooser.TargetInfo;
+
+import java.util.List;
+import java.util.function.Function;
+
+/*
+ * Simple wrapper around chooser activity to be able to initiate it under test
+ */
+public class ResolverWrapperActivity extends ResolverActivity {
+    static final OverrideData sOverrides = new OverrideData();
+    private UsageStatsManager mUsm;
+
+    public ResolverWrapperActivity() {
+        super(/* isIntentPicker= */ true);
+    }
+
+    // ResolverActivity inspects the launched-from UID at onCreate and needs to see some
+    // non-negative value in the test.
+    @Override
+    public int getLaunchedFromUid() {
+        return 1234;
+    }
+
+    @Override
+    public ResolverListAdapter createResolverListAdapter(Context context,
+            List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList,
+            boolean filterLastUsed, UserHandle userHandle) {
+        return new ResolverWrapperAdapter(
+                context,
+                payloadIntents,
+                initialIntents,
+                rList,
+                filterLastUsed,
+                createListController(userHandle),
+                userHandle,
+                payloadIntents.get(0),  // TODO: extract upstream
+                this);
+    }
+
+    @Override
+    protected MyUserIdProvider createMyUserIdProvider() {
+        if (sOverrides.mMyUserIdProvider != null) {
+            return sOverrides.mMyUserIdProvider;
+        }
+        return super.createMyUserIdProvider();
+    }
+
+    @Override
+    protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() {
+        if (sOverrides.mCrossProfileIntentsChecker != null) {
+            return sOverrides.mCrossProfileIntentsChecker;
+        }
+        return super.createCrossProfileIntentsChecker();
+    }
+
+    @Override
+    protected QuietModeManager createQuietModeManager() {
+        if (sOverrides.mQuietModeManager != null) {
+            return sOverrides.mQuietModeManager;
+        }
+        return super.createQuietModeManager();
+    }
+
+    ResolverWrapperAdapter getAdapter() {
+        return (ResolverWrapperAdapter) mMultiProfilePagerAdapter.getActiveListAdapter();
+    }
+
+    ResolverListAdapter getPersonalListAdapter() {
+        return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(0));
+    }
+
+    ResolverListAdapter getWorkListAdapter() {
+        if (mMultiProfilePagerAdapter.getInactiveListAdapter() == null) {
+            return null;
+        }
+        return ((ResolverListAdapter) mMultiProfilePagerAdapter.getAdapterForIndex(1));
+    }
+
+    @Override
+    public boolean isVoiceInteraction() {
+        if (sOverrides.isVoiceInteraction != null) {
+            return sOverrides.isVoiceInteraction;
+        }
+        return super.isVoiceInteraction();
+    }
+
+    @Override
+    public void safelyStartActivity(TargetInfo cti) {
+        if (sOverrides.onSafelyStartCallback != null &&
+                sOverrides.onSafelyStartCallback.apply(cti)) {
+            return;
+        }
+        super.safelyStartActivity(cti);
+    }
+
+    @Override
+    protected ResolverListController createListController(UserHandle userHandle) {
+        if (userHandle == UserHandle.SYSTEM) {
+            when(sOverrides.resolverListController.getUserHandle()).thenReturn(UserHandle.SYSTEM);
+            return sOverrides.resolverListController;
+        }
+        when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle);
+        return sOverrides.workResolverListController;
+    }
+
+    @Override
+    public PackageManager getPackageManager() {
+        if (sOverrides.createPackageManager != null) {
+            return sOverrides.createPackageManager.apply(super.getPackageManager());
+        }
+        return super.getPackageManager();
+    }
+
+    protected UserHandle getCurrentUserHandle() {
+        return mMultiProfilePagerAdapter.getCurrentUserHandle();
+    }
+
+    @Override
+    protected UserHandle getWorkProfileUserHandle() {
+        return sOverrides.workProfileUserHandle;
+    }
+
+    @Override
+    public void startActivityAsUser(Intent intent, Bundle options, UserHandle user) {
+        super.startActivityAsUser(intent, options, user);
+    }
+
+    /**
+     * We cannot directly mock the activity created since instrumentation creates it.
+     * <p>
+     * Instead, we use static instances of this object to modify behavior.
+     */
+    static class OverrideData {
+        @SuppressWarnings("Since15")
+        public Function<PackageManager, PackageManager> createPackageManager;
+        public Function<TargetInfo, Boolean> onSafelyStartCallback;
+        public ResolverListController resolverListController;
+        public ResolverListController workResolverListController;
+        public Boolean isVoiceInteraction;
+        public UserHandle workProfileUserHandle;
+        public Integer myUserId;
+        public boolean hasCrossProfileIntents;
+        public boolean isQuietModeEnabled;
+        public QuietModeManager mQuietModeManager;
+        public MyUserIdProvider mMyUserIdProvider;
+        public CrossProfileIntentsChecker mCrossProfileIntentsChecker;
+
+        public void reset() {
+            onSafelyStartCallback = null;
+            isVoiceInteraction = null;
+            createPackageManager = null;
+            resolverListController = mock(ResolverListController.class);
+            workResolverListController = mock(ResolverListController.class);
+            workProfileUserHandle = null;
+            myUserId = null;
+            hasCrossProfileIntents = true;
+            isQuietModeEnabled = false;
+
+            mQuietModeManager = new QuietModeManager() {
+                @Override
+                public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) {
+                    return isQuietModeEnabled;
+                }
+
+                @Override
+                public void requestQuietModeEnabled(boolean enabled,
+                        UserHandle workProfileUserHandle) {
+                    isQuietModeEnabled = enabled;
+                }
+
+                @Override
+                public void markWorkProfileEnabledBroadcastReceived() {
+                }
+
+                @Override
+                public boolean isWaitingToEnableWorkProfile() {
+                    return false;
+                }
+            };
+
+            mMyUserIdProvider = new MyUserIdProvider() {
+                @Override
+                public int getMyUserId() {
+                    return myUserId != null ? myUserId : UserHandle.myUserId();
+                }
+            };
+
+            mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class);
+            when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt()))
+                    .thenAnswer(invocation -> hasCrossProfileIntents);
+        }
+    }
+}
diff --git a/java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java b/java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java
new file mode 100644
index 0000000..a53b41d
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+
+import androidx.test.espresso.idling.CountingIdlingResource;
+
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+
+import java.util.List;
+
+public class ResolverWrapperAdapter extends ResolverListAdapter {
+
+    private CountingIdlingResource mLabelIdlingResource =
+            new CountingIdlingResource("LoadLabelTask");
+
+    public ResolverWrapperAdapter(
+            Context context,
+            List<Intent> payloadIntents,
+            Intent[] initialIntents,
+            List<ResolveInfo> rList,
+            boolean filterLastUsed,
+            ResolverListController resolverListController,
+            UserHandle userHandle,
+            Intent targetIntent,
+            ResolverListCommunicator resolverListCommunicator) {
+        super(
+                context,
+                payloadIntents,
+                initialIntents,
+                rList,
+                filterLastUsed,
+                resolverListController,
+                userHandle,
+                targetIntent,
+                resolverListCommunicator,
+                false);
+    }
+
+    public CountingIdlingResource getLabelIdlingResource() {
+        return mLabelIdlingResource;
+    }
+
+    @Override
+    protected LoadLabelTask createLoadLabelTask(DisplayResolveInfo info) {
+        return new LoadLabelWrapperTask(info);
+    }
+
+    class LoadLabelWrapperTask extends LoadLabelTask {
+
+        protected LoadLabelWrapperTask(DisplayResolveInfo dri) {
+            super(dri);
+        }
+
+        @Override
+        protected void onPreExecute() {
+            mLabelIdlingResource.increment();
+        }
+
+        @Override
+        protected void onPostExecute(CharSequence[] result) {
+            super.onPostExecute(result);
+            mLabelIdlingResource.decrement();
+        }
+    }
+}
diff --git a/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt b/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt
new file mode 100644
index 0000000..a8d6f97
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt
@@ -0,0 +1,308 @@
+/*
+ * 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.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ResolveInfo
+import android.content.pm.ShortcutInfo
+import android.service.chooser.ChooserTarget
+import com.android.intentresolver.chooser.DisplayResolveInfo
+import com.android.intentresolver.chooser.TargetInfo
+import androidx.test.filters.SmallTest
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+private const val PACKAGE_A = "package.a"
+private const val PACKAGE_B = "package.b"
+private const val CLASS_NAME = "./MainActivity"
+
+@SmallTest
+class ShortcutSelectionLogicTest {
+    private val packageTargets = HashMap<String, Array<ChooserTarget>>().apply {
+        arrayOf(PACKAGE_A, PACKAGE_B).forEach { pkg ->
+            // shortcuts in reverse priority order
+            val targets = Array(3) { i ->
+                createChooserTarget(
+                    "Shortcut $i",
+                    (i + 1).toFloat() / 10f,
+                    ComponentName(pkg, CLASS_NAME),
+                    pkg.shortcutId(i),
+                )
+            }
+            this[pkg] = targets
+        }
+    }
+
+    private val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo(
+            Intent(),
+            ResolverDataProvider.createResolveInfo(3, 0),
+            "label",
+            "extended info",
+            Intent(),
+            /* resolveInfoPresentationGetter= */ null)
+
+    private val otherBaseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo(
+            Intent(),
+            ResolverDataProvider.createResolveInfo(4, 0),
+            "label 2",
+            "extended info 2",
+            Intent(),
+            /* resolveInfoPresentationGetter= */ null)
+
+    private operator fun Map<String, Array<ChooserTarget>>.get(pkg: String, idx: Int) =
+        this[pkg]?.get(idx) ?: error("missing package $pkg")
+
+    @Test
+    fun testAddShortcuts_no_limits() {
+        val serviceResults = ArrayList<TargetInfo>()
+        val sc1 = packageTargets[PACKAGE_A, 0]
+        val sc2 = packageTargets[PACKAGE_A, 1]
+        val testSubject = ShortcutSelectionLogic(
+            /* maxShortcutTargetsPerApp = */ 1,
+            /* applySharingAppLimits = */ false
+        )
+
+        val isUpdated = testSubject.addServiceResults(
+            /* origTarget = */ baseDisplayInfo,
+            /* origTargetScore = */ 0.1f,
+            /* targets = */ listOf(sc1, sc2),
+            /* isShortcutResult = */ true,
+            /* directShareToShortcutInfos = */ emptyMap(),
+            /* directShareToAppTargets = */ emptyMap(),
+            /* userContext = */ mock(),
+            /* targetIntent = */ mock(),
+            /* refererFillInIntent = */ mock(),
+            /* maxRankedTargets = */ 4,
+            /* serviceTargets = */ serviceResults
+        )
+
+        assertTrue("Updates are expected", isUpdated)
+        assertShortcutsInOrder(
+            listOf(sc2, sc1),
+            serviceResults,
+            "Two shortcuts are expected as we do not apply per-app shortcut limit"
+        )
+    }
+
+    @Test
+    fun testAddShortcuts_same_package_with_per_package_limit() {
+        val serviceResults = ArrayList<TargetInfo>()
+        val sc1 = packageTargets[PACKAGE_A, 0]
+        val sc2 = packageTargets[PACKAGE_A, 1]
+        val testSubject = ShortcutSelectionLogic(
+            /* maxShortcutTargetsPerApp = */ 1,
+            /* applySharingAppLimits = */ true
+        )
+
+        val isUpdated = testSubject.addServiceResults(
+            /* origTarget = */ baseDisplayInfo,
+            /* origTargetScore = */ 0.1f,
+            /* targets = */ listOf(sc1, sc2),
+            /* isShortcutResult = */ true,
+            /* directShareToShortcutInfos = */ emptyMap(),
+            /* directShareToAppTargets = */ emptyMap(),
+            /* userContext = */ mock(),
+            /* targetIntent = */ mock(),
+            /* refererFillInIntent = */ mock(),
+            /* maxRankedTargets = */ 4,
+            /* serviceTargets = */ serviceResults
+        )
+
+        assertTrue("Updates are expected", isUpdated)
+        assertShortcutsInOrder(
+            listOf(sc2),
+            serviceResults,
+            "One shortcut is expected as we apply per-app shortcut limit"
+        )
+    }
+
+    @Test
+    fun testAddShortcuts_same_package_no_per_app_limit_with_target_limit() {
+        val serviceResults = ArrayList<TargetInfo>()
+        val sc1 = packageTargets[PACKAGE_A, 0]
+        val sc2 = packageTargets[PACKAGE_A, 1]
+        val testSubject = ShortcutSelectionLogic(
+            /* maxShortcutTargetsPerApp = */ 1,
+            /* applySharingAppLimits = */ false
+        )
+
+        val isUpdated = testSubject.addServiceResults(
+            /* origTarget = */ baseDisplayInfo,
+            /* origTargetScore = */ 0.1f,
+            /* targets = */ listOf(sc1, sc2),
+            /* isShortcutResult = */ true,
+            /* directShareToShortcutInfos = */ emptyMap(),
+            /* directShareToAppTargets = */ emptyMap(),
+            /* userContext = */ mock(),
+            /* targetIntent = */ mock(),
+            /* refererFillInIntent = */ mock(),
+            /* maxRankedTargets = */ 1,
+            /* serviceTargets = */ serviceResults
+        )
+
+        assertTrue("Updates are expected", isUpdated)
+        assertShortcutsInOrder(
+            listOf(sc2),
+            serviceResults,
+            "One shortcut is expected as we apply overall shortcut limit"
+        )
+    }
+
+    @Test
+    fun testAddShortcuts_different_packages_with_per_package_limit() {
+        val serviceResults = ArrayList<TargetInfo>()
+        val pkgAsc1 = packageTargets[PACKAGE_A, 0]
+        val pkgAsc2 = packageTargets[PACKAGE_A, 1]
+        val pkgBsc1 = packageTargets[PACKAGE_B, 0]
+        val pkgBsc2 = packageTargets[PACKAGE_B, 1]
+        val testSubject = ShortcutSelectionLogic(
+            /* maxShortcutTargetsPerApp = */ 1,
+            /* applySharingAppLimits = */ true
+        )
+
+        testSubject.addServiceResults(
+            /* origTarget = */ baseDisplayInfo,
+            /* origTargetScore = */ 0.1f,
+            /* targets = */ listOf(pkgAsc1, pkgAsc2),
+            /* isShortcutResult = */ true,
+            /* directShareToShortcutInfos = */ emptyMap(),
+            /* directShareToAppTargets = */ emptyMap(),
+            /* userContext = */ mock(),
+            /* targetIntent = */ mock(),
+            /* refererFillInIntent = */ mock(),
+            /* maxRankedTargets = */ 4,
+            /* serviceTargets = */ serviceResults
+        )
+        testSubject.addServiceResults(
+            /* origTarget = */ otherBaseDisplayInfo,
+            /* origTargetScore = */ 0.2f,
+            /* targets = */ listOf(pkgBsc1, pkgBsc2),
+            /* isShortcutResult = */ true,
+            /* directShareToShortcutInfos = */ emptyMap(),
+            /* directShareToAppTargets = */ emptyMap(),
+            /* userContext = */ mock(),
+            /* targetIntent = */ mock(),
+            /* refererFillInIntent = */ mock(),
+            /* maxRankedTargets = */ 4,
+            /* serviceTargets = */ serviceResults
+        )
+
+        assertShortcutsInOrder(
+            listOf(pkgBsc2, pkgAsc2),
+            serviceResults,
+            "Two shortcuts are expected as we apply per-app shortcut limit"
+        )
+    }
+
+    @Test
+    fun testAddShortcuts_pinned_shortcut() {
+        val serviceResults = ArrayList<TargetInfo>()
+        val sc1 = packageTargets[PACKAGE_A, 0]
+        val sc2 = packageTargets[PACKAGE_A, 1]
+        val testSubject = ShortcutSelectionLogic(
+            /* maxShortcutTargetsPerApp = */ 1,
+            /* applySharingAppLimits = */ false
+        )
+
+        val isUpdated = testSubject.addServiceResults(
+            /* origTarget = */ baseDisplayInfo,
+            /* origTargetScore = */ 0.1f,
+            /* targets = */ listOf(sc1, sc2),
+            /* isShortcutResult = */ true,
+            /* directShareToShortcutInfos = */ mapOf(
+                sc1 to createShortcutInfo(
+                    PACKAGE_A.shortcutId(1),
+                    sc1.componentName, 1).apply {
+                        addFlags(ShortcutInfo.FLAG_PINNED)
+                    }
+            ),
+            /* directShareToAppTargets = */ emptyMap(),
+            /* userContext = */ mock(),
+            /* targetIntent = */ mock(),
+            /* refererFillInIntent = */ mock(),
+            /* maxRankedTargets = */ 4,
+            /* serviceTargets = */ serviceResults
+        )
+
+        assertTrue("Updates are expected", isUpdated)
+        assertShortcutsInOrder(
+            listOf(sc1, sc2),
+            serviceResults,
+            "Two shortcuts are expected as we do not apply per-app shortcut limit"
+        )
+    }
+
+    @Test
+    fun test_available_caller_shortcuts_count_is_limited() {
+        val serviceResults = ArrayList<TargetInfo>()
+        val sc1 = packageTargets[PACKAGE_A, 0]
+        val sc2 = packageTargets[PACKAGE_A, 1]
+        val sc3 = packageTargets[PACKAGE_A, 2]
+        val testSubject = ShortcutSelectionLogic(
+            /* maxShortcutTargetsPerApp = */ 1,
+            /* applySharingAppLimits = */ true
+        )
+        val context = mock<Context> {
+            whenever(packageManager).thenReturn(mock())
+        }
+
+        testSubject.addServiceResults(
+            /* origTarget = */ baseDisplayInfo,
+            /* origTargetScore = */ 0f,
+            /* targets = */ listOf(sc1, sc2, sc3),
+            /* isShortcutResult = */ false,
+            /* directShareToShortcutInfos = */ emptyMap(),
+            /* directShareToAppTargets = */ emptyMap(),
+            /* userContext = */ context,
+            /* targetIntent = */ mock(),
+            /* refererFillInIntent = */ mock(),
+            /* maxRankedTargets = */ 4,
+            /* serviceTargets = */ serviceResults
+        )
+
+        assertShortcutsInOrder(
+            listOf(sc3, sc2),
+            serviceResults,
+            "At most two caller-provided shortcuts are allowed"
+        )
+    }
+
+    // TODO: consider renaming. Not all `ChooserTarget`s are "shortcuts" and many of our test cases
+    // add results with `isShortcutResult = false` and `directShareToShortcutInfos = emptyMap()`.
+    private fun assertShortcutsInOrder(
+        expected: List<ChooserTarget>, actual: List<TargetInfo>, msg: String? = ""
+    ) {
+        assertEquals(msg, expected.size, actual.size)
+        for (i in expected.indices) {
+            assertEquals(
+                "Unexpected item at position $i",
+                expected[i].componentName,
+                actual[i].chooserTargetComponentName
+            )
+            assertEquals(
+                "Unexpected item at position $i",
+                expected[i].title,
+                actual[i].displayLabel
+            )
+        }
+    }
+
+    private fun String.shortcutId(id: Int) = "$this.$id"
+}
diff --git a/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt b/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt
new file mode 100644
index 0000000..e62672a
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/TargetPresentationGetterTest.kt
@@ -0,0 +1,204 @@
+/*
+ * 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 com.android.intentresolver.ResolverDataProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+/**
+ * Unit tests for the various implementations of {@link TargetPresentationGetter}.
+ * TODO: consider expanding to cover icon logic (not just labels/sublabels).
+ * TODO: these are conceptually "acceptance tests" that provide comprehensive coverage of the
+ * apparent variations in the legacy implementation. The tests probably don't have to be so
+ * exhaustive if we're able to impose a simpler design on the implementation.
+ */
+class TargetPresentationGetterTest {
+  fun makeResolveInfoPresentationGetter(
+          withSubstitutePermission: Boolean,
+          appLabel: String,
+          activityLabel: String,
+          resolveInfoLabel: String): TargetPresentationGetter {
+      val testPackageInfo = ResolverDataProvider.createPackageManagerMockedInfo(
+              withSubstitutePermission, appLabel, activityLabel, resolveInfoLabel)
+      val factory = TargetPresentationGetter.Factory(testPackageInfo.ctx, 100)
+      return factory.makePresentationGetter(testPackageInfo.resolveInfo)
+  }
+
+  fun makeActivityInfoPresentationGetter(
+          withSubstitutePermission: Boolean,
+          appLabel: String?,
+          activityLabel: String?): TargetPresentationGetter {
+      val testPackageInfo = ResolverDataProvider.createPackageManagerMockedInfo(
+              withSubstitutePermission, appLabel, activityLabel, "")
+      val factory = TargetPresentationGetter.Factory(testPackageInfo.ctx, 100)
+      return factory.makePresentationGetter(testPackageInfo.activityInfo)
+  }
+
+  @Test
+  fun testActivityInfoLabels_noSubstitutePermission_distinctRequestedLabelAndSublabel() {
+      val presentationGetter = makeActivityInfoPresentationGetter(
+              false, "app_label", "activity_label")
+      assertThat(presentationGetter.getLabel()).isEqualTo("app_label")
+      assertThat(presentationGetter.getSubLabel()).isEqualTo("activity_label")
+  }
+
+  @Test
+  fun testActivityInfoLabels_noSubstitutePermission_sameRequestedLabelAndSublabel() {
+      val presentationGetter = makeActivityInfoPresentationGetter(
+              false, "app_label", "app_label")
+      assertThat(presentationGetter.getLabel()).isEqualTo("app_label")
+      // Without the substitute permission, there's no logic to dedupe the labels.
+      // TODO: this matches our observations in the legacy code, but is it the right behavior? It
+      // seems like {@link ResolverListAdapter.ViewHolder#bindLabel()} has some logic to dedupe in
+      // the UI at least, but maybe that logic should be pulled back to the "presentation"?
+      assertThat(presentationGetter.getSubLabel()).isEqualTo("app_label")
+  }
+
+  @Test
+  fun testActivityInfoLabels_noSubstitutePermission_nullRequestedLabel() {
+      val presentationGetter = makeActivityInfoPresentationGetter(false, null, "activity_label")
+      assertThat(presentationGetter.getLabel()).isNull()
+      assertThat(presentationGetter.getSubLabel()).isEqualTo("activity_label")
+  }
+
+  @Test
+  fun testActivityInfoLabels_noSubstitutePermission_emptyRequestedLabel() {
+      val presentationGetter = makeActivityInfoPresentationGetter(false, "", "activity_label")
+      assertThat(presentationGetter.getLabel()).isEqualTo("")
+      assertThat(presentationGetter.getSubLabel()).isEqualTo("activity_label")
+  }
+
+  @Test
+  fun testActivityInfoLabels_noSubstitutePermission_emptyRequestedSublabel() {
+      val presentationGetter = makeActivityInfoPresentationGetter(false, "app_label", "")
+      assertThat(presentationGetter.getLabel()).isEqualTo("app_label")
+      // Without the substitute permission, empty sublabels are passed through as-is.
+      assertThat(presentationGetter.getSubLabel()).isEqualTo("")
+  }
+
+  @Test
+  fun testActivityInfoLabels_withSubstitutePermission_distinctRequestedLabelAndSublabel() {
+      val presentationGetter = makeActivityInfoPresentationGetter(
+              true, "app_label", "activity_label")
+      assertThat(presentationGetter.getLabel()).isEqualTo("activity_label")
+      // With the substitute permission, the same ("activity") label is requested as both the label
+      // and sublabel, even though the other value ("app_label") was distinct. Thus this behaves the
+      // same as a dupe.
+      assertThat(presentationGetter.getSubLabel()).isEqualTo(null)
+  }
+
+  @Test
+  fun testActivityInfoLabels_withSubstitutePermission_sameRequestedLabelAndSublabel() {
+      val presentationGetter = makeActivityInfoPresentationGetter(
+              true, "app_label", "app_label")
+      assertThat(presentationGetter.getLabel()).isEqualTo("app_label")
+      // With the substitute permission, duped sublabels get converted to nulls.
+      assertThat(presentationGetter.getSubLabel()).isNull()
+  }
+
+  @Test
+  fun testActivityInfoLabels_withSubstitutePermission_nullRequestedLabel() {
+      val presentationGetter = makeActivityInfoPresentationGetter(true, "app_label", null)
+      assertThat(presentationGetter.getLabel()).isEqualTo("app_label")
+      // With the substitute permission, null inputs are a special case that produces null outputs
+      // (i.e., they're not simply passed-through from the inputs).
+      assertThat(presentationGetter.getSubLabel()).isNull()
+  }
+
+  @Test
+  fun testActivityInfoLabels_withSubstitutePermission_emptyRequestedLabel() {
+      val presentationGetter = makeActivityInfoPresentationGetter(true, "app_label", "")
+      // Empty "labels" are taken as-is and (unlike nulls) don't prompt a fallback to the sublabel.
+      // Thus (as in the previous case with substitute permission & "distinct" labels), this is
+      // treated as a dupe.
+      assertThat(presentationGetter.getLabel()).isEqualTo("")
+      assertThat(presentationGetter.getSubLabel()).isNull()
+  }
+
+  @Test
+  fun testActivityInfoLabels_withSubstitutePermission_emptyRequestedSublabel() {
+      val presentationGetter = makeActivityInfoPresentationGetter(true, "", "activity_label")
+      assertThat(presentationGetter.getLabel()).isEqualTo("activity_label")
+      // With the substitute permission, empty sublabels get converted to nulls.
+      assertThat(presentationGetter.getSubLabel()).isNull()
+  }
+
+  @Test
+  fun testResolveInfoLabels_noSubstitutePermission_distinctRequestedLabelAndSublabel() {
+      val presentationGetter = makeResolveInfoPresentationGetter(
+              false, "app_label", "activity_label", "resolve_info_label")
+      assertThat(presentationGetter.getLabel()).isEqualTo("app_label")
+      assertThat(presentationGetter.getSubLabel()).isEqualTo("resolve_info_label")
+  }
+
+  @Test
+  fun testResolveInfoLabels_noSubstitutePermission_sameRequestedLabelAndSublabel() {
+      val presentationGetter = makeResolveInfoPresentationGetter(
+              false, "app_label", "activity_label", "app_label")
+      assertThat(presentationGetter.getLabel()).isEqualTo("app_label")
+      // Without the substitute permission, there's no logic to dedupe the labels.
+      // TODO: this matches our observations in the legacy code, but is it the right behavior? It
+      // seems like {@link ResolverListAdapter.ViewHolder#bindLabel()} has some logic to dedupe in
+      // the UI at least, but maybe that logic should be pulled back to the "presentation"?
+      assertThat(presentationGetter.getSubLabel()).isEqualTo("app_label")
+  }
+
+  @Test
+  fun testResolveInfoLabels_noSubstitutePermission_emptyRequestedSublabel() {
+      val presentationGetter = makeResolveInfoPresentationGetter(
+              false, "app_label", "activity_label", "")
+      assertThat(presentationGetter.getLabel()).isEqualTo("app_label")
+      // Without the substitute permission, empty sublabels are passed through as-is.
+      assertThat(presentationGetter.getSubLabel()).isEqualTo("")
+  }
+
+  @Test
+  fun testResolveInfoLabels_withSubstitutePermission_distinctRequestedLabelAndSublabel() {
+      val presentationGetter = makeResolveInfoPresentationGetter(
+              true, "app_label", "activity_label", "resolve_info_label")
+      assertThat(presentationGetter.getLabel()).isEqualTo("activity_label")
+      assertThat(presentationGetter.getSubLabel()).isEqualTo("resolve_info_label")
+  }
+
+  @Test
+  fun testResolveInfoLabels_withSubstitutePermission_sameRequestedLabelAndSublabel() {
+      val presentationGetter = makeResolveInfoPresentationGetter(
+              true, "app_label", "activity_label", "activity_label")
+      assertThat(presentationGetter.getLabel()).isEqualTo("activity_label")
+      // With the substitute permission, duped sublabels get converted to nulls.
+      assertThat(presentationGetter.getSubLabel()).isNull()
+  }
+
+  @Test
+  fun testResolveInfoLabels_withSubstitutePermission_emptyRequestedSublabel() {
+      val presentationGetter = makeResolveInfoPresentationGetter(
+              true, "app_label", "activity_label", "")
+      assertThat(presentationGetter.getLabel()).isEqualTo("activity_label")
+      // With the substitute permission, empty sublabels get converted to nulls.
+      assertThat(presentationGetter.getSubLabel()).isNull()
+  }
+
+  @Test
+  fun testResolveInfoLabels_withSubstitutePermission_emptyRequestedLabelAndSublabel() {
+      val presentationGetter = makeResolveInfoPresentationGetter(
+              true, "app_label", "", "")
+      assertThat(presentationGetter.getLabel()).isEqualTo("")
+      // With the substitute permission, empty sublabels get converted to nulls.
+      assertThat(presentationGetter.getSubLabel()).isNull()
+  }
+}
diff --git a/java/tests/src/com/android/intentresolver/TestApplication.kt b/java/tests/src/com/android/intentresolver/TestApplication.kt
new file mode 100644
index 0000000..849cfba
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/TestApplication.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.app.Application
+import android.content.Context
+import android.os.UserHandle
+
+class TestApplication : Application() {
+
+    // return the current context as a work profile doesn't really exist in these tests
+    override fun createContextAsUser(user: UserHandle, flags: Int): Context = this
+}
\ No newline at end of file
diff --git a/java/tests/src/com/android/intentresolver/TestHelpers.kt b/java/tests/src/com/android/intentresolver/TestHelpers.kt
new file mode 100644
index 0000000..5b583fe
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/TestHelpers.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.app.prediction.AppTarget
+import android.app.prediction.AppTargetId
+import android.content.ComponentName
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.content.pm.ShortcutManager.ShareShortcutInfo
+import android.os.Bundle
+import android.service.chooser.ChooserTarget
+import org.mockito.Mockito.`when` as whenever
+
+internal fun createShareShortcutInfo(
+    id: String,
+    componentName: ComponentName,
+    rank: Int
+): ShareShortcutInfo =
+    ShareShortcutInfo(
+        createShortcutInfo(id, componentName, rank),
+        componentName
+    )
+
+internal fun createShortcutInfo(
+    id: String,
+    componentName: ComponentName,
+    rank: Int
+): ShortcutInfo {
+    val context = mock<Context>()
+    whenever(context.packageName).thenReturn(componentName.packageName)
+    return ShortcutInfo.Builder(context, id)
+        .setShortLabel("Short Label $id")
+        .setLongLabel("Long Label $id")
+        .setActivity(componentName)
+        .setRank(rank)
+        .build()
+}
+
+internal fun createAppTarget(shortcutInfo: ShortcutInfo) =
+    AppTarget(
+        AppTargetId(shortcutInfo.id),
+        shortcutInfo,
+        shortcutInfo.activity?.className ?: error("missing activity info")
+    )
+
+fun createChooserTarget(
+    title: String, score: Float, componentName: ComponentName, shortcutId: String
+): ChooserTarget =
+    ChooserTarget(
+        title,
+        null,
+        score,
+        componentName,
+        Bundle().apply { putString(Intent.EXTRA_SHORTCUT_ID, shortcutId) }
+    )
diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
index b901fc1..af2557e 100644
--- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
+++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
@@ -38,21 +38,18 @@
 
 import static com.google.common.truth.Truth.assertThat;
 
-import static junit.framework.Assert.assertFalse;
 import static junit.framework.Assert.assertNull;
-import static junit.framework.Assert.assertTrue;
 
 import static org.hamcrest.CoreMatchers.allOf;
 import static org.hamcrest.CoreMatchers.is;
 import static org.hamcrest.CoreMatchers.not;
-import static org.hamcrest.CoreMatchers.notNullValue;
 import static org.hamcrest.MatcherAssert.assertThat;
-import static org.hamcrest.Matchers.greaterThan;
 import static org.junit.Assert.assertEquals;
 import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
 import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyLong;
 import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.atLeastOnce;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.times;
 import static org.mockito.Mockito.verify;
@@ -78,26 +75,29 @@
 import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.drawable.Icon;
-import android.metrics.LogMaker;
 import android.net.Uri;
+import android.os.Bundle;
 import android.os.UserHandle;
 import android.provider.DeviceConfig;
 import android.service.chooser.ChooserTarget;
+import android.util.HashedStringCache;
+import android.util.Pair;
+import android.util.SparseArray;
 import android.view.View;
 
 import androidx.annotation.CallSuper;
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
 import androidx.test.espresso.matcher.BoundedDiagnosingMatcher;
 import androidx.test.platform.app.InstrumentationRegistry;
 import androidx.test.rule.ActivityTestRule;
 
 import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
 import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.shortcuts.ShortcutLoader;
 import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
-import com.android.internal.logging.MetricsLogger;
 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.internal.util.FrameworkStatsLog;
-import com.android.internal.widget.GridLayoutManager;
-import com.android.internal.widget.RecyclerView;
 
 import org.hamcrest.Description;
 import org.hamcrest.Matcher;
@@ -117,6 +117,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Consumer;
 import java.util.function.Function;
 
 /**
@@ -130,7 +131,6 @@
  * TODO: this can simply be renamed to "ChooserActivityTest" if that's ever unambiguous (i.e., if
  * there's no risk of confusion with the framework tests that currently share the same name).
  */
-@Ignore("investigate b/241944046 and re-enabled")
 @RunWith(Parameterized.class)
 public class UnbundledChooserActivityTest {
 
@@ -252,13 +252,31 @@
         mTestNum = testNum;
     }
 
+    private void setDeviceConfigProperty(
+            @NonNull String propertyName,
+            @NonNull String value) {
+        // TODO: consider running with {@link #runWithShellPermissionIdentity()} to more narrowly
+        // request WRITE_DEVICE_CONFIG permissions if we get rid of the broad grant we currently
+        // configure in {@link #setup()}.
+        // TODO: is it really appropriate that this is always set with makeDefault=true?
+        boolean valueWasSet = DeviceConfig.setProperty(
+                DeviceConfig.NAMESPACE_SYSTEMUI,
+                propertyName,
+                value,
+                true /* makeDefault */);
+        if (!valueWasSet) {
+            throw new IllegalStateException(
+                        "Could not set " + propertyName + " to " + value);
+        }
+    }
+
     public void cleanOverrideData() {
         ChooserActivityOverrideData.getInstance().reset();
         ChooserActivityOverrideData.getInstance().createPackageManager = mPackageManagerOverride;
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
+
+        setDeviceConfigProperty(
                 SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
-                Boolean.toString(true),
-                true /* makeDefault*/);
+                Boolean.toString(true));
     }
 
     @Test
@@ -282,7 +300,7 @@
         waitForIdle();
         assertThat(activity.getAdapter().getCount(), is(2));
         assertThat(activity.getAdapter().getServiceTargetCount(), is(0));
-        onView(withIdFromRuntimeResource("title")).check(matches(withText("chooser test")));
+        onView(withId(android.R.id.title)).check(matches(withText("chooser test")));
     }
 
     @Test
@@ -302,8 +320,8 @@
                 .thenReturn(resolvedComponentInfos);
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, "chooser test"));
         waitForIdle();
-        onView(withIdFromRuntimeResource("title"))
-                .check(matches(withTextFromRuntimeResource("whichSendApplication")));
+        onView(withId(android.R.id.title))
+                .check(matches(withText(com.android.internal.R.string.whichSendApplication)));
     }
 
     @Test
@@ -323,8 +341,8 @@
                 .thenReturn(resolvedComponentInfos);
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
-        onView(withIdFromRuntimeResource("title"))
-                .check(matches(withTextFromRuntimeResource("whichSendApplication")));
+        onView(withId(android.R.id.title))
+                .check(matches(withText(com.android.internal.R.string.whichSendApplication)));
     }
 
     @Test
@@ -344,9 +362,9 @@
                 .thenReturn(resolvedComponentInfos);
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
-        onView(withIdFromRuntimeResource("content_preview_title"))
+        onView(withId(com.android.internal.R.id.content_preview_title))
                 .check(matches(not(isDisplayed())));
-        onView(withIdFromRuntimeResource("content_preview_thumbnail"))
+        onView(withId(com.android.internal.R.id.content_preview_thumbnail))
                 .check(matches(not(isDisplayed())));
     }
 
@@ -368,11 +386,11 @@
                 .thenReturn(resolvedComponentInfos);
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
-        onView(withIdFromRuntimeResource("content_preview_title"))
+        onView(withId(com.android.internal.R.id.content_preview_title))
                 .check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("content_preview_title"))
+        onView(withId(com.android.internal.R.id.content_preview_title))
                 .check(matches(withText(previewTitle)));
-        onView(withIdFromRuntimeResource("content_preview_thumbnail"))
+        onView(withId(com.android.internal.R.id.content_preview_thumbnail))
                 .check(matches(not(isDisplayed())));
     }
 
@@ -395,8 +413,9 @@
                 .thenReturn(resolvedComponentInfos);
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
-        onView(withIdFromRuntimeResource("content_preview_title")).check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("content_preview_thumbnail"))
+        onView(withId(com.android.internal.R.id.content_preview_title))
+                .check(matches(isDisplayed()));
+        onView(withId(com.android.internal.R.id.content_preview_thumbnail))
                 .check(matches(not(isDisplayed())));
     }
 
@@ -405,7 +424,7 @@
         String previewTitle = "My Content Preview Title";
         Intent sendIntent = createSendTextIntentWithPreview(previewTitle,
                 Uri.parse("android.resource://com.android.frameworks.coretests/"
-                        + com.android.frameworks.coretests.R.drawable.test320x240));
+                        + R.drawable.test320x240));
         ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap();
         List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
 
@@ -421,8 +440,9 @@
                 .thenReturn(resolvedComponentInfos);
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
-        onView(withIdFromRuntimeResource("content_preview_title")).check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("content_preview_thumbnail"))
+        onView(withId(com.android.internal.R.id.content_preview_title))
+                .check(matches(isDisplayed()));
+        onView(withId(com.android.internal.R.id.content_preview_thumbnail))
                 .check(matches(isDisplayed()));
     }
 
@@ -447,7 +467,7 @@
         waitForIdle();
 
         assertThat(activity.getAdapter().getCount(), is(2));
-        onView(withIdFromRuntimeResource("profile_button")).check(doesNotExist());
+        onView(withId(com.android.internal.R.id.profile_button)).check(doesNotExist());
 
         ResolveInfo[] chosen = new ResolveInfo[1];
         ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
@@ -580,8 +600,8 @@
         waitForIdle();
         assertThat(activity.isFinishing(), is(false));
 
-        onView(withIdFromRuntimeResource("empty")).check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("profile_pager")).check(matches(not(isDisplayed())));
+        onView(withId(android.R.id.empty)).check(matches(isDisplayed()));
+        onView(withId(com.android.internal.R.id.profile_pager)).check(matches(not(isDisplayed())));
         InstrumentationRegistry.getInstrumentation().runOnMainSync(
                 () -> wrapper.getAdapter().handlePackagesChanged()
         );
@@ -619,9 +639,7 @@
     }
 
     @Test @Ignore
-    public void hasOtherProfileOneOption() throws Exception {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
+    public void hasOtherProfileOneOption() {
         List<ResolvedComponentInfo> personalResolvedComponentInfos =
                 createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10);
         List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
@@ -647,7 +665,6 @@
         List<ResolvedComponentInfo> stableCopy =
                 createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10);
         waitForIdle();
-        Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
 
         onView(first(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)))
                 .perform(click());
@@ -657,9 +674,6 @@
 
     @Test @Ignore
     public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
-
         Intent sendIntent = createSendTextIntent();
         List<ResolvedComponentInfo> resolvedComponentInfos =
                 createResolvedComponentsForTestWithOtherProfile(3);
@@ -697,9 +711,6 @@
 
     @Test @Ignore
     public void hasLastChosenActivityAndOtherProfile() throws Exception {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
-
         Intent sendIntent = createSendTextIntent();
         List<ResolvedComponentInfo> resolvedComponentInfos =
                 createResolvedComponentsForTestWithOtherProfile(3);
@@ -748,8 +759,8 @@
                 mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
 
-        onView(withIdFromRuntimeResource("chooser_copy_button")).check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("chooser_copy_button")).perform(click());
+        onView(withId(com.android.internal.R.id.chooser_copy_button)).check(matches(isDisplayed()));
+        onView(withId(com.android.internal.R.id.chooser_copy_button)).perform(click());
         ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(
                 Context.CLIPBOARD_SERVICE);
         ClipData clipData = clipboard.getPrimaryClip();
@@ -762,7 +773,7 @@
     }
 
     @Test
-    public void copyTextToClipboardLogging() throws Exception {
+    public void copyTextToClipboardLogging() {
         Intent sendIntent = createSendTextIntent();
         List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
 
@@ -772,24 +783,17 @@
             Mockito.anyBoolean(),
             Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
 
-        MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
-        ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
-
-        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+        final IChooserWrapper activity = (IChooserWrapper)
+                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
 
-        onView(withIdFromRuntimeResource("chooser_copy_button")).check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("chooser_copy_button")).perform(click());
+        onView(withId(com.android.internal.R.id.chooser_copy_button)).check(matches(isDisplayed()));
+        onView(withId(com.android.internal.R.id.chooser_copy_button)).perform(click());
 
-        verify(mockLogger, atLeastOnce()).write(logMakerCaptor.capture());
-
-        // The last captured event is the selection of the target.
-        assertThat(logMakerCaptor.getValue().getCategory(),
-                is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SYSTEM_TARGET));
-        assertThat(logMakerCaptor.getValue().getSubtype(), is(1));
+        ChooserActivityLogger logger = activity.getChooserActivityLogger();
+        verify(logger, times(1)).logActionSelected(eq(ChooserActivityLogger.SELECTION_TYPE_COPY));
     }
 
-
     @Test
     @Ignore
     public void testNearbyShareLogging() throws Exception {
@@ -806,52 +810,11 @@
                 mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
 
-        onView(withIdFromRuntimeResource("chooser_nearby_button")).check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("chooser_nearby_button")).perform(click());
-
-        ChooserActivityLoggerFake logger =
-                (ChooserActivityLoggerFake) activity.getChooserActivityLogger();
+        onView(withId(com.android.internal.R.id.chooser_nearby_button))
+                .check(matches(isDisplayed()));
+        onView(withId(com.android.internal.R.id.chooser_nearby_button)).perform(click());
 
         // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
-        logger.removeCallsForUiEventsOfType(
-                ChooserActivityLogger.SharesheetStandardEvent
-                        .SHARESHEET_DIRECT_LOAD_COMPLETE.getId());
-
-        // SHARESHEET_TRIGGERED:
-        assertThat(logger.event(0).getId(),
-                is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId()));
-
-        // SHARESHEET_STARTED:
-        assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED));
-        assertThat(logger.get(1).intent, is(Intent.ACTION_SEND));
-        assertThat(logger.get(1).mimeType, is("text/plain"));
-        assertThat(logger.get(1).packageName, is(
-                InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()));
-        assertThat(logger.get(1).appProvidedApp, is(0));
-        assertThat(logger.get(1).appProvidedDirect, is(0));
-        assertThat(logger.get(1).isWorkprofile, is(false));
-        assertThat(logger.get(1).previewType, is(3));
-
-        // SHARESHEET_APP_LOAD_COMPLETE:
-        assertThat(logger.event(2).getId(),
-                is(ChooserActivityLogger
-                        .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId()));
-
-        // Next are just artifacts of test set-up:
-        assertThat(logger.event(3).getId(),
-                is(ChooserActivityLogger
-                        .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId()));
-        assertThat(logger.event(4).getId(),
-                is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_EXPANDED.getId()));
-
-        // SHARESHEET_NEARBY_TARGET_SELECTED:
-        assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED));
-        assertThat(logger.get(5).targetType,
-                is(ChooserActivityLogger
-                        .SharesheetTargetSelectedEvent.SHARESHEET_NEARBY_TARGET_SELECTED.getId()));
-
-        // No more events.
-        assertThat(logger.numCalls(), is(6));
     }
 
 
@@ -860,7 +823,7 @@
     public void testEditImageLogs() throws Exception {
         Intent sendIntent = createSendImageIntent(
                 Uri.parse("android.resource://com.android.frameworks.coretests/"
-                        + com.android.frameworks.coretests.R.drawable.test320x240));
+                        + R.drawable.test320x240));
 
         ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap();
         ChooserActivityOverrideData.getInstance().isImageType = true;
@@ -877,59 +840,17 @@
                 mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
 
-        onView(withIdFromRuntimeResource("chooser_edit_button")).check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("chooser_edit_button")).perform(click());
-
-        ChooserActivityLoggerFake logger =
-                (ChooserActivityLoggerFake) activity.getChooserActivityLogger();
+        onView(withId(com.android.internal.R.id.chooser_edit_button)).check(matches(isDisplayed()));
+        onView(withId(com.android.internal.R.id.chooser_edit_button)).perform(click());
 
         // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
-        logger.removeCallsForUiEventsOfType(
-                ChooserActivityLogger.SharesheetStandardEvent
-                        .SHARESHEET_DIRECT_LOAD_COMPLETE.getId());
-
-        // SHARESHEET_TRIGGERED:
-        assertThat(logger.event(0).getId(),
-                is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId()));
-
-        // SHARESHEET_STARTED:
-        assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED));
-        assertThat(logger.get(1).intent, is(Intent.ACTION_SEND));
-        assertThat(logger.get(1).mimeType, is("image/png"));
-        assertThat(logger.get(1).packageName, is(
-                InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()));
-        assertThat(logger.get(1).appProvidedApp, is(0));
-        assertThat(logger.get(1).appProvidedDirect, is(0));
-        assertThat(logger.get(1).isWorkprofile, is(false));
-        assertThat(logger.get(1).previewType, is(1));
-
-        // SHARESHEET_APP_LOAD_COMPLETE:
-        assertThat(logger.event(2).getId(),
-                is(ChooserActivityLogger
-                        .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId()));
-
-        // Next are just artifacts of test set-up:
-        assertThat(logger.event(3).getId(),
-                is(ChooserActivityLogger
-                        .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId()));
-        assertThat(logger.event(4).getId(),
-                is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_EXPANDED.getId()));
-
-        // SHARESHEET_EDIT_TARGET_SELECTED:
-        assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED));
-        assertThat(logger.get(5).targetType,
-                is(ChooserActivityLogger
-                        .SharesheetTargetSelectedEvent.SHARESHEET_EDIT_TARGET_SELECTED.getId()));
-
-        // No more events.
-        assertThat(logger.numCalls(), is(6));
     }
 
 
     @Test
     public void oneVisibleImagePreview() throws InterruptedException {
         Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/"
-                + com.android.frameworks.coretests.R.drawable.test320x240);
+                + R.drawable.test320x240);
 
         ArrayList<Uri> uris = new ArrayList<>();
         uris.add(uri);
@@ -952,20 +873,20 @@
                 .thenReturn(resolvedComponentInfos);
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
-        onView(withIdFromRuntimeResource("content_preview_image_1_large"))
+        onView(withId(com.android.internal.R.id.content_preview_image_1_large))
                 .check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("content_preview_image_2_large"))
+        onView(withId(com.android.internal.R.id.content_preview_image_2_large))
                 .check(matches(not(isDisplayed())));
-        onView(withIdFromRuntimeResource("content_preview_image_2_small"))
+        onView(withId(com.android.internal.R.id.content_preview_image_2_small))
                 .check(matches(not(isDisplayed())));
-        onView(withIdFromRuntimeResource("content_preview_image_3_small"))
+        onView(withId(com.android.internal.R.id.content_preview_image_3_small))
                 .check(matches(not(isDisplayed())));
     }
 
     @Test
     public void twoVisibleImagePreview() throws InterruptedException {
         Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/"
-                + com.android.frameworks.coretests.R.drawable.test320x240);
+                + R.drawable.test320x240);
 
         ArrayList<Uri> uris = new ArrayList<>();
         uris.add(uri);
@@ -989,20 +910,20 @@
                 .thenReturn(resolvedComponentInfos);
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
-        onView(withIdFromRuntimeResource("content_preview_image_1_large"))
+        onView(withId(com.android.internal.R.id.content_preview_image_1_large))
                 .check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("content_preview_image_2_large"))
+        onView(withId(com.android.internal.R.id.content_preview_image_2_large))
                 .check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("content_preview_image_2_small"))
+        onView(withId(com.android.internal.R.id.content_preview_image_2_small))
                 .check(matches(not(isDisplayed())));
-        onView(withIdFromRuntimeResource("content_preview_image_3_small"))
+        onView(withId(com.android.internal.R.id.content_preview_image_3_small))
                 .check(matches(not(isDisplayed())));
     }
 
     @Test
     public void threeOrMoreVisibleImagePreview() throws InterruptedException {
         Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/"
-                + com.android.frameworks.coretests.R.drawable.test320x240);
+                + R.drawable.test320x240);
 
         ArrayList<Uri> uris = new ArrayList<>();
         uris.add(uri);
@@ -1029,13 +950,13 @@
                 .thenReturn(resolvedComponentInfos);
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
-        onView(withIdFromRuntimeResource("content_preview_image_1_large"))
+        onView(withId(com.android.internal.R.id.content_preview_image_1_large))
                 .check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("content_preview_image_2_large"))
+        onView(withId(com.android.internal.R.id.content_preview_image_2_large))
                 .check(matches(not(isDisplayed())));
-        onView(withIdFromRuntimeResource("content_preview_image_2_small"))
+        onView(withId(com.android.internal.R.id.content_preview_image_2_small))
                 .check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("content_preview_image_3_small"))
+        onView(withId(com.android.internal.R.id.content_preview_image_3_small))
                 .check(matches(isDisplayed()));
     }
 
@@ -1044,25 +965,12 @@
         Intent sendIntent = createSendTextIntent();
         sendIntent.setType(TEST_MIME_TYPE);
 
-        MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
-        ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
-        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
+        final IChooserWrapper activity = (IChooserWrapper)
+                mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
+        ChooserActivityLogger logger = activity.getChooserActivityLogger();
         waitForIdle();
-        verify(mockLogger, atLeastOnce()).write(logMakerCaptor.capture());
-        assertThat(logMakerCaptor.getAllValues().get(0).getCategory(),
-                is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN));
-        assertThat(logMakerCaptor
-                .getAllValues().get(0)
-                .getTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS),
-                is(notNullValue()));
-        assertThat(logMakerCaptor
-                .getAllValues().get(0)
-                .getTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE),
-                is(TEST_MIME_TYPE));
-        assertThat(logMakerCaptor
-                        .getAllValues().get(0)
-                        .getSubtype(),
-                is(MetricsEvent.PARENT_PROFILE));
+
+        verify(logger).logChooserActivityShown(eq(false), eq(TEST_MIME_TYPE), anyLong());
     }
 
     @Test
@@ -1071,49 +979,32 @@
         sendIntent.setType(TEST_MIME_TYPE);
         ChooserActivityOverrideData.getInstance().alternateProfileSetting =
                 MetricsEvent.MANAGED_PROFILE;
-        MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
-        ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
-        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
+
+        final IChooserWrapper activity = (IChooserWrapper)
+                mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
+        ChooserActivityLogger logger = activity.getChooserActivityLogger();
         waitForIdle();
-        verify(mockLogger, atLeastOnce()).write(logMakerCaptor.capture());
-        assertThat(logMakerCaptor.getAllValues().get(0).getCategory(),
-                is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN));
-        assertThat(logMakerCaptor
-                        .getAllValues().get(0)
-                        .getTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS),
-                is(notNullValue()));
-        assertThat(logMakerCaptor
-                        .getAllValues().get(0)
-                        .getTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE),
-                is(TEST_MIME_TYPE));
-        assertThat(logMakerCaptor
-                        .getAllValues().get(0)
-                        .getSubtype(),
-                is(MetricsEvent.MANAGED_PROFILE));
+
+        verify(logger).logChooserActivityShown(eq(true), eq(TEST_MIME_TYPE), anyLong());
     }
 
     @Test
     public void testEmptyPreviewLogging() {
         Intent sendIntent = createSendTextIntentWithPreview(null, null);
 
-        MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
-        ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
-        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "empty preview logger test"));
+        final IChooserWrapper activity = (IChooserWrapper)
+                mActivityRule.launchActivity(
+                        Intent.createChooser(sendIntent, "empty preview logger test"));
+        ChooserActivityLogger logger = activity.getChooserActivityLogger();
         waitForIdle();
 
-        verify(mockLogger, Mockito.times(1)).write(logMakerCaptor.capture());
-        // First invocation is from onCreate
-        assertThat(logMakerCaptor.getAllValues().get(0).getCategory(),
-                is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN));
+        verify(logger).logChooserActivityShown(eq(false), eq(null), anyLong());
     }
 
     @Test
     public void testTitlePreviewLogging() {
         Intent sendIntent = createSendTextIntentWithPreview("TestTitle", null);
 
-        MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
-        ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
-
         List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
 
         when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent(
@@ -1122,20 +1013,19 @@
             Mockito.anyBoolean(),
             Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
 
-        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+        final IChooserWrapper activity = (IChooserWrapper)
+                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
+
         // Second invocation is from onCreate
-        verify(mockLogger, Mockito.times(2)).write(logMakerCaptor.capture());
-        assertThat(logMakerCaptor.getAllValues().get(0).getSubtype(),
-                is(CONTENT_PREVIEW_TEXT));
-        assertThat(logMakerCaptor.getAllValues().get(0).getCategory(),
-                is(MetricsEvent.ACTION_SHARE_WITH_PREVIEW));
+        ChooserActivityLogger logger = activity.getChooserActivityLogger();
+        Mockito.verify(logger, times(1)).logActionShareWithPreview(eq(CONTENT_PREVIEW_TEXT));
     }
 
     @Test
     public void testImagePreviewLogging() {
         Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/"
-                + com.android.frameworks.coretests.R.drawable.test320x240);
+                + R.drawable.test320x240);
 
         ArrayList<Uri> uris = new ArrayList<>();
         uris.add(uri);
@@ -1157,16 +1047,11 @@
                                 Mockito.isA(List.class)))
                 .thenReturn(resolvedComponentInfos);
 
-        MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
-        ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
-        mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+        final IChooserWrapper activity = (IChooserWrapper)
+                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
-        verify(mockLogger, Mockito.times(2)).write(logMakerCaptor.capture());
-        // First invocation is from onCreate
-        assertThat(logMakerCaptor.getAllValues().get(0).getSubtype(),
-                is(CONTENT_PREVIEW_IMAGE));
-        assertThat(logMakerCaptor.getAllValues().get(0).getCategory(),
-                is(MetricsEvent.ACTION_SHARE_WITH_PREVIEW));
+        ChooserActivityLogger logger = activity.getChooserActivityLogger();
+        Mockito.verify(logger, times(1)).logActionShareWithPreview(eq(CONTENT_PREVIEW_IMAGE));
     }
 
     @Test
@@ -1192,10 +1077,11 @@
                 .thenReturn(resolvedComponentInfos);
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
-        onView(withIdFromRuntimeResource("content_preview_filename")).check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("content_preview_filename"))
+        onView(withId(com.android.internal.R.id.content_preview_filename))
+                .check(matches(isDisplayed()));
+        onView(withId(com.android.internal.R.id.content_preview_filename))
                 .check(matches(withText("app.pdf")));
-        onView(withIdFromRuntimeResource("content_preview_file_icon"))
+        onView(withId(com.android.internal.R.id.content_preview_file_icon))
                 .check(matches(isDisplayed()));
     }
 
@@ -1225,11 +1111,11 @@
                 .thenReturn(resolvedComponentInfos);
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
-        onView(withIdFromRuntimeResource("content_preview_filename"))
+        onView(withId(com.android.internal.R.id.content_preview_filename))
                 .check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("content_preview_filename"))
+        onView(withId(com.android.internal.R.id.content_preview_filename))
                 .check(matches(withText("app.pdf + 2 files")));
-        onView(withIdFromRuntimeResource("content_preview_file_icon"))
+        onView(withId(com.android.internal.R.id.content_preview_file_icon))
                 .check(matches(isDisplayed()));
     }
 
@@ -1258,10 +1144,11 @@
 
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
-        onView(withIdFromRuntimeResource("content_preview_filename")).check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("content_preview_filename"))
+        onView(withId(com.android.internal.R.id.content_preview_filename))
+                .check(matches(isDisplayed()));
+        onView(withId(com.android.internal.R.id.content_preview_filename))
                 .check(matches(withText("app.pdf")));
-        onView(withIdFromRuntimeResource("content_preview_file_icon"))
+        onView(withId(com.android.internal.R.id.content_preview_file_icon))
                 .check(matches(isDisplayed()));
     }
 
@@ -1297,10 +1184,11 @@
 
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
-        onView(withIdFromRuntimeResource("content_preview_filename")).check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("content_preview_filename"))
+        onView(withId(com.android.internal.R.id.content_preview_filename))
+                .check(matches(isDisplayed()));
+        onView(withId(com.android.internal.R.id.content_preview_filename))
                 .check(matches(withText("app.pdf + 1 file")));
-        onView(withIdFromRuntimeResource("content_preview_file_icon"))
+        onView(withId(com.android.internal.R.id.content_preview_file_icon))
                 .check(matches(isDisplayed()));
     }
 
@@ -1347,95 +1235,9 @@
                 is(testBaseScore * SHORTCUT_TARGET_SCORE_BOOST));
     }
 
-    @Test
-    public void testConvertToChooserTarget_predictionService() {
-        Intent sendIntent = createSendTextIntent();
-        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
-        when(
-                ChooserActivityOverrideData
-                        .getInstance()
-                        .resolverListController
-                        .getResolversForIntent(
-                                Mockito.anyBoolean(),
-                                Mockito.anyBoolean(),
-                                Mockito.anyBoolean(),
-                                Mockito.isA(List.class)))
-                .thenReturn(resolvedComponentInfos);
-
-        final ChooserActivity activity =
-                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
-        waitForIdle();
-
-        List<ShareShortcutInfo> shortcuts = createShortcuts(activity);
-
-        int[] expectedOrderAllShortcuts = {0, 1, 2, 3};
-        float[] expectedScoreAllShortcuts = {1.0f, 0.99f, 0.98f, 0.97f};
-
-        List<ChooserTarget> chooserTargets = activity.convertToChooserTarget(shortcuts, shortcuts,
-                null, TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
-        assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets,
-                expectedOrderAllShortcuts, expectedScoreAllShortcuts);
-
-        List<ShareShortcutInfo> subset = new ArrayList<>();
-        subset.add(shortcuts.get(1));
-        subset.add(shortcuts.get(2));
-        subset.add(shortcuts.get(3));
-
-        int[] expectedOrderSubset = {1, 2, 3};
-        float[] expectedScoreSubset = {0.99f, 0.98f, 0.97f};
-
-        chooserTargets = activity.convertToChooserTarget(subset, shortcuts, null,
-                TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
-        assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets,
-                expectedOrderSubset, expectedScoreSubset);
-    }
-
-    @Test
-    public void testConvertToChooserTarget_shortcutManager() {
-        Intent sendIntent = createSendTextIntent();
-        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
-        when(
-                ChooserActivityOverrideData
-                        .getInstance()
-                        .resolverListController
-                        .getResolversForIntent(
-                                Mockito.anyBoolean(),
-                                Mockito.anyBoolean(),
-                                Mockito.anyBoolean(),
-                                Mockito.isA(List.class)))
-                .thenReturn(resolvedComponentInfos);
-
-        final ChooserActivity activity =
-                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
-        waitForIdle();
-
-        List<ShareShortcutInfo> shortcuts = createShortcuts(activity);
-
-        int[] expectedOrderAllShortcuts = {2, 0, 3, 1};
-        float[] expectedScoreAllShortcuts = {1.0f, 0.99f, 0.99f, 0.98f};
-
-        List<ChooserTarget> chooserTargets = activity.convertToChooserTarget(shortcuts, shortcuts,
-                null, TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER);
-        assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets,
-                expectedOrderAllShortcuts, expectedScoreAllShortcuts);
-
-        List<ShareShortcutInfo> subset = new ArrayList<>();
-        subset.add(shortcuts.get(1));
-        subset.add(shortcuts.get(2));
-        subset.add(shortcuts.get(3));
-
-        int[] expectedOrderSubset = {2, 3, 1};
-        float[] expectedScoreSubset = {1.0f, 0.99f, 0.98f};
-
-        chooserTargets = activity.convertToChooserTarget(subset, shortcuts, null,
-                TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER);
-        assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets,
-                expectedOrderSubset, expectedScoreSubset);
-    }
-
     // This test is too long and too slow and should not be taken as an example for future tests.
-    @Test @Ignore
-    public void testDirectTargetSelectionLogging() throws InterruptedException {
+    @Test
+    public void testDirectTargetSelectionLogging() {
         Intent sendIntent = createSendTextIntent();
         // We need app targets for direct targets to get displayed
         List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -1450,44 +1252,54 @@
                                 Mockito.isA(List.class)))
                 .thenReturn(resolvedComponentInfos);
 
-        // Set up resources
-        MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
-        ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
-        // Create direct share target
+        // create test shortcut loader factory, remember loaders and their callbacks
+        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+                createShortcutLoaderFactory();
+
+        // Start activity
+        final IChooserWrapper activity = (IChooserWrapper)
+                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+        waitForIdle();
+
+        // verify that ShortcutLoader was queried
+        ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+                ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+        verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture());
+
+        // send shortcuts
+        assertThat(
+                "Wrong number of app targets",
+                appTargets.getValue().length,
+                is(resolvedComponentInfos.size()));
         List<ChooserTarget> serviceTargets = createDirectShareTargets(1, "");
-        ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
-
-        // Start activity
-        final IChooserWrapper activity = (IChooserWrapper)
-                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
-
-        // Insert the direct share target
-        Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
-        directShareToShortcutInfos.put(serviceTargets.get(0), null);
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                () -> activity.getAdapter().addServiceResults(
-                        activity.createTestDisplayResolveInfo(sendIntent,
-                                ri,
-                                "testLabel",
-                                "testInfo",
-                                sendIntent,
-                                /* resolveInfoPresentationGetter */ null),
-                        serviceTargets,
-                        TARGET_TYPE_CHOOSER_TARGET,
-                        directShareToShortcutInfos)
+        ShortcutLoader.Result result = new ShortcutLoader.Result(
+                true,
+                appTargets.getValue(),
+                new ShortcutLoader.ShortcutResultInfo[] {
+                        new ShortcutLoader.ShortcutResultInfo(
+                                appTargets.getValue()[0],
+                                serviceTargets
+                        )
+                },
+                new HashMap<>(),
+                new HashMap<>()
         );
+        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+        waitForIdle();
 
-        // Thread.sleep shouldn't be a thing in an integration test but it's
-        // necessary here because of the way the code is structured
-        // TODO: restructure the tests b/129870719
-        Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
-
-        assertThat("Chooser should have 3 targets (2 apps, 1 direct)",
-                activity.getAdapter().getCount(), is(3));
-        assertThat("Chooser should have exactly one selectable direct target",
-                activity.getAdapter().getSelectableServiceTargetCount(), is(1));
-        assertThat("The resolver info must match the resolver info used to create the target",
-                activity.getAdapter().getItem(0).getResolveInfo(), is(ri));
+        final ChooserListAdapter activeAdapter = activity.getAdapter();
+        assertThat(
+                "Chooser should have 3 targets (2 apps, 1 direct)",
+                activeAdapter.getCount(),
+                is(3));
+        assertThat(
+                "Chooser should have exactly one selectable direct target",
+                activeAdapter.getSelectableServiceTargetCount(),
+                is(1));
+        assertThat(
+                "The resolver info must match the resolver info used to create the target",
+                activeAdapter.getItem(0).getResolveInfo(),
+                is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
 
         // Click on the direct target
         String name = serviceTargets.get(0).getTitle().toString();
@@ -1495,24 +1307,27 @@
                 .perform(click());
         waitForIdle();
 
-        // Currently we're seeing 3 invocations
-        //      1. ChooserActivity.onCreate()
-        //      2. ChooserActivity$ChooserRowAdapter.createContentPreviewView()
-        //      3. ChooserActivity.startSelected -- which is the one we're after
-        verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture());
-        assertThat(logMakerCaptor.getAllValues().get(2).getCategory(),
-                is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET));
-        String hashedName = (String) logMakerCaptor
-                .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_HASHED_TARGET_NAME);
-        assertThat("Hash is not predictable but must be obfuscated",
+        ArgumentCaptor<HashedStringCache.HashResult> hashCaptor =
+                ArgumentCaptor.forClass(HashedStringCache.HashResult.class);
+        verify(activity.getChooserActivityLogger(), times(1)).logShareTargetSelected(
+                eq(ChooserActivityLogger.SELECTION_TYPE_SERVICE),
+                /* packageName= */ any(),
+                /* positionPicked= */ anyInt(),
+                /* directTargetAlsoRanked= */ eq(-1),
+                /* numCallerProvided= */ anyInt(),
+                /* directTargetHashed= */ hashCaptor.capture(),
+                /* isPinned= */ anyBoolean(),
+                /* successfullySelected= */ anyBoolean(),
+                /* selectionCost= */ anyLong());
+        String hashedName = hashCaptor.getValue().hashedString;
+        assertThat(
+                "Hash is not predictable but must be obfuscated",
                 hashedName, is(not(name)));
-        assertThat("The packages shouldn't match for app target and direct target", logMakerCaptor
-                .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(-1));
     }
 
     // This test is too long and too slow and should not be taken as an example for future tests.
-    @Test @Ignore
-    public void testDirectTargetLoggingWithRankedAppTarget() throws InterruptedException {
+    @Test
+    public void testDirectTargetLoggingWithRankedAppTarget() {
         Intent sendIntent = createSendTextIntent();
         // We need app targets for direct targets to get displayed
         List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -1527,44 +1342,56 @@
                                 Mockito.isA(List.class)))
                 .thenReturn(resolvedComponentInfos);
 
-        // Set up resources
-        MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
-        ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
-        // Create direct share target
-        List<ChooserTarget> serviceTargets = createDirectShareTargets(1,
-                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
-        ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
+        // create test shortcut loader factory, remember loaders and their callbacks
+        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+                createShortcutLoaderFactory();
 
         // Start activity
         final IChooserWrapper activity = (IChooserWrapper)
                 mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+        waitForIdle();
 
-        // Insert the direct share target
-        Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
-        directShareToShortcutInfos.put(serviceTargets.get(0), null);
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                () -> activity.getAdapter().addServiceResults(
-                        activity.createTestDisplayResolveInfo(sendIntent,
-                                ri,
-                                "testLabel",
-                                "testInfo",
-                                sendIntent,
-                                /* resolveInfoPresentationGetter */ null),
-                        serviceTargets,
-                        TARGET_TYPE_CHOOSER_TARGET,
-                        directShareToShortcutInfos)
+        // verify that ShortcutLoader was queried
+        ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+                ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+        verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture());
+
+        // send shortcuts
+        assertThat(
+                "Wrong number of app targets",
+                appTargets.getValue().length,
+                is(resolvedComponentInfos.size()));
+        List<ChooserTarget> serviceTargets = createDirectShareTargets(
+                1,
+                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+        ShortcutLoader.Result result = new ShortcutLoader.Result(
+                true,
+                appTargets.getValue(),
+                new ShortcutLoader.ShortcutResultInfo[] {
+                        new ShortcutLoader.ShortcutResultInfo(
+                                appTargets.getValue()[0],
+                                serviceTargets
+                        )
+                },
+                new HashMap<>(),
+                new HashMap<>()
         );
-        // Thread.sleep shouldn't be a thing in an integration test but it's
-        // necessary here because of the way the code is structured
-        // TODO: restructure the tests b/129870719
-        Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
+        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+        waitForIdle();
 
-        assertThat("Chooser should have 3 targets (2 apps, 1 direct)",
-                activity.getAdapter().getCount(), is(3));
-        assertThat("Chooser should have exactly one selectable direct target",
-                activity.getAdapter().getSelectableServiceTargetCount(), is(1));
-        assertThat("The resolver info must match the resolver info used to create the target",
-                activity.getAdapter().getItem(0).getResolveInfo(), is(ri));
+        final ChooserListAdapter activeAdapter = activity.getAdapter();
+        assertThat(
+                "Chooser should have 3 targets (2 apps, 1 direct)",
+                activeAdapter.getCount(),
+                is(3));
+        assertThat(
+                "Chooser should have exactly one selectable direct target",
+                activeAdapter.getSelectableServiceTargetCount(),
+                is(1));
+        assertThat(
+                "The resolver info must match the resolver info used to create the target",
+                activeAdapter.getItem(0).getResolveInfo(),
+                is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
 
         // Click on the direct target
         String name = serviceTargets.get(0).getTitle().toString();
@@ -1572,19 +1399,20 @@
                 .perform(click());
         waitForIdle();
 
-        // Currently we're seeing 3 invocations
-        //      1. ChooserActivity.onCreate()
-        //      2. ChooserActivity$ChooserRowAdapter.createContentPreviewView()
-        //      3. ChooserActivity.startSelected -- which is the one we're after
-        verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture());
-        assertThat(logMakerCaptor.getAllValues().get(2).getCategory(),
-                is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET));
-        assertThat("The packages should match for app target and direct target", logMakerCaptor
-                .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(0));
+        verify(activity.getChooserActivityLogger(), times(1)).logShareTargetSelected(
+                eq(ChooserActivityLogger.SELECTION_TYPE_SERVICE),
+                /* packageName= */ any(),
+                /* positionPicked= */ anyInt(),
+                /* directTargetAlsoRanked= */ eq(0),
+                /* numCallerProvided= */ anyInt(),
+                /* directTargetHashed= */ any(),
+                /* isPinned= */ anyBoolean(),
+                /* successfullySelected= */ anyBoolean(),
+                /* selectionCost= */ anyLong());
     }
 
-    @Test @Ignore
-    public void testShortcutTargetWithApplyAppLimits() throws InterruptedException {
+    @Test
+    public void testShortcutTargetWithApplyAppLimits() {
         // Set up resources
         ChooserActivityOverrideData.getInstance().resources = Mockito.spy(
                 InstrumentationRegistry.getInstrumentation().getContext().getResources());
@@ -1592,8 +1420,7 @@
                 ChooserActivityOverrideData
                         .getInstance()
                         .resources
-                        .getInteger(
-                              getRuntimeResourceId("config_maxShortcutTargetsPerApp", "integer")))
+                        .getInteger(R.integer.config_maxShortcutTargetsPerApp))
                 .thenReturn(1);
         Intent sendIntent = createSendTextIntent();
         // We need app targets for direct targets to get displayed
@@ -1608,56 +1435,68 @@
                                 Mockito.anyBoolean(),
                                 Mockito.isA(List.class)))
                 .thenReturn(resolvedComponentInfos);
-        // Create direct share target
-        List<ChooserTarget> serviceTargets = createDirectShareTargets(2,
-                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
-        ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
+
+        // create test shortcut loader factory, remember loaders and their callbacks
+        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+                createShortcutLoaderFactory();
 
         // Start activity
-        final ChooserActivity activity =
-                mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
-        final IChooserWrapper wrapper = (IChooserWrapper) activity;
+        final IChooserWrapper activity = (IChooserWrapper) mActivityRule
+                .launchActivity(Intent.createChooser(sendIntent, null));
+        waitForIdle();
 
-        // Insert the direct share target
-        Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
-        List<ShareShortcutInfo> shortcutInfos = createShortcuts(activity);
-        directShareToShortcutInfos.put(serviceTargets.get(0),
-                shortcutInfos.get(0).getShortcutInfo());
-        directShareToShortcutInfos.put(serviceTargets.get(1),
-                shortcutInfos.get(1).getShortcutInfo());
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                () -> wrapper.getAdapter().addServiceResults(
-                        wrapper.createTestDisplayResolveInfo(sendIntent,
-                                ri,
-                                "testLabel",
-                                "testInfo",
-                                sendIntent,
-                                /* resolveInfoPresentationGetter */ null),
-                        serviceTargets,
-                        TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE,
-                        directShareToShortcutInfos)
+        // verify that ShortcutLoader was queried
+        ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+                ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+        verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture());
+
+        // send shortcuts
+        assertThat(
+                "Wrong number of app targets",
+                appTargets.getValue().length,
+                is(resolvedComponentInfos.size()));
+        List<ChooserTarget> serviceTargets = createDirectShareTargets(
+                2,
+                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+        ShortcutLoader.Result result = new ShortcutLoader.Result(
+                true,
+                appTargets.getValue(),
+                new ShortcutLoader.ShortcutResultInfo[] {
+                        new ShortcutLoader.ShortcutResultInfo(
+                                appTargets.getValue()[0],
+                                serviceTargets
+                        )
+                },
+                new HashMap<>(),
+                new HashMap<>()
         );
-        // Thread.sleep shouldn't be a thing in an integration test but it's
-        // necessary here because of the way the code is structured
-        // TODO: restructure the tests b/129870719
-        Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
+        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+        waitForIdle();
 
-        assertThat("Chooser should have 3 targets (2 apps, 1 direct)",
-                wrapper.getAdapter().getCount(), is(3));
-        assertThat("Chooser should have exactly one selectable direct target",
-                wrapper.getAdapter().getSelectableServiceTargetCount(), is(1));
-        assertThat("The resolver info must match the resolver info used to create the target",
-                wrapper.getAdapter().getItem(0).getResolveInfo(), is(ri));
-        assertThat("The display label must match",
-                wrapper.getAdapter().getItem(0).getDisplayLabel(), is("testTitle0"));
+        final ChooserListAdapter activeAdapter = activity.getAdapter();
+        assertThat(
+                "Chooser should have 3 targets (2 apps, 1 direct)",
+                activeAdapter.getCount(),
+                is(3));
+        assertThat(
+                "Chooser should have exactly one selectable direct target",
+                activeAdapter.getSelectableServiceTargetCount(),
+                is(1));
+        assertThat(
+                "The resolver info must match the resolver info used to create the target",
+                activeAdapter.getItem(0).getResolveInfo(),
+                is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
+        assertThat(
+                "The display label must match",
+                activeAdapter.getItem(0).getDisplayLabel(),
+                is("testTitle0"));
     }
 
-    @Test @Ignore
-    public void testShortcutTargetWithoutApplyAppLimits() throws InterruptedException {
-        DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
+    @Test
+    public void testShortcutTargetWithoutApplyAppLimits() {
+        setDeviceConfigProperty(
                 SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
-                Boolean.toString(false),
-                true /* makeDefault*/);
+                Boolean.toString(false));
         // Set up resources
         ChooserActivityOverrideData.getInstance().resources = Mockito.spy(
                 InstrumentationRegistry.getInstrumentation().getContext().getResources());
@@ -1665,8 +1504,7 @@
                 ChooserActivityOverrideData
                         .getInstance()
                         .resources
-                        .getInteger(
-                              getRuntimeResourceId("config_maxShortcutTargetsPerApp", "integer")))
+                        .getInteger(R.integer.config_maxShortcutTargetsPerApp))
                 .thenReturn(1);
         Intent sendIntent = createSendTextIntent();
         // We need app targets for direct targets to get displayed
@@ -1681,50 +1519,149 @@
                                 Mockito.anyBoolean(),
                                 Mockito.isA(List.class)))
                 .thenReturn(resolvedComponentInfos);
-        // Create direct share target
-        List<ChooserTarget> serviceTargets = createDirectShareTargets(2,
-                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
-        ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
+
+        // create test shortcut loader factory, remember loaders and their callbacks
+        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+                createShortcutLoaderFactory();
 
         // Start activity
-        final ChooserActivity activity =
+        final IChooserWrapper activity = (IChooserWrapper)
                 mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
-        final IChooserWrapper wrapper = (IChooserWrapper) activity;
+        waitForIdle();
 
-        // Insert the direct share target
-        Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
-        List<ShareShortcutInfo> shortcutInfos = createShortcuts(activity);
-        directShareToShortcutInfos.put(serviceTargets.get(0),
-                shortcutInfos.get(0).getShortcutInfo());
-        directShareToShortcutInfos.put(serviceTargets.get(1),
-                shortcutInfos.get(1).getShortcutInfo());
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                () -> wrapper.getAdapter().addServiceResults(
-                        wrapper.createTestDisplayResolveInfo(sendIntent,
-                                ri,
-                                "testLabel",
-                                "testInfo",
-                                sendIntent,
-                                /* resolveInfoPresentationGetter */ null),
-                        serviceTargets,
-                        TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE,
-                        directShareToShortcutInfos)
+        // verify that ShortcutLoader was queried
+        ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+                ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+        verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture());
+
+        // send shortcuts
+        assertThat(
+                "Wrong number of app targets",
+                appTargets.getValue().length,
+                is(resolvedComponentInfos.size()));
+        List<ChooserTarget> serviceTargets = createDirectShareTargets(
+                2,
+                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+        ShortcutLoader.Result result = new ShortcutLoader.Result(
+                true,
+                appTargets.getValue(),
+                new ShortcutLoader.ShortcutResultInfo[] {
+                        new ShortcutLoader.ShortcutResultInfo(
+                                appTargets.getValue()[0],
+                                serviceTargets
+                        )
+                },
+                new HashMap<>(),
+                new HashMap<>()
         );
-        // Thread.sleep shouldn't be a thing in an integration test but it's
-        // necessary here because of the way the code is structured
-        // TODO: restructure the tests b/129870719
-        Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
+        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+        waitForIdle();
 
-        assertThat("Chooser should have 4 targets (2 apps, 2 direct)",
-                wrapper.getAdapter().getCount(), is(4));
-        assertThat("Chooser should have exactly two selectable direct target",
-                wrapper.getAdapter().getSelectableServiceTargetCount(), is(2));
-        assertThat("The resolver info must match the resolver info used to create the target",
-                wrapper.getAdapter().getItem(0).getResolveInfo(), is(ri));
-        assertThat("The display label must match",
-                wrapper.getAdapter().getItem(0).getDisplayLabel(), is("testTitle0"));
-        assertThat("The display label must match",
-                wrapper.getAdapter().getItem(1).getDisplayLabel(), is("testTitle1"));
+        final ChooserListAdapter activeAdapter = activity.getAdapter();
+        assertThat(
+                "Chooser should have 4 targets (2 apps, 2 direct)",
+                activeAdapter.getCount(),
+                is(4));
+        assertThat(
+                "Chooser should have exactly two selectable direct target",
+                activeAdapter.getSelectableServiceTargetCount(),
+                is(2));
+        assertThat(
+                "The resolver info must match the resolver info used to create the target",
+                activeAdapter.getItem(0).getResolveInfo(),
+                is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
+        assertThat(
+                "The display label must match",
+                activeAdapter.getItem(0).getDisplayLabel(),
+                is("testTitle0"));
+        assertThat(
+                "The display label must match",
+                activeAdapter.getItem(1).getDisplayLabel(),
+                is("testTitle1"));
+    }
+
+    @Test
+    public void testLaunchWithCallerProvidedTarget() {
+        setDeviceConfigProperty(
+                SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
+                Boolean.toString(false));
+        // Set up resources
+        ChooserActivityOverrideData.getInstance().resources = Mockito.spy(
+                InstrumentationRegistry.getInstrumentation().getContext().getResources());
+        when(
+                ChooserActivityOverrideData
+                        .getInstance()
+                        .resources
+                        .getInteger(R.integer.config_maxShortcutTargetsPerApp))
+                .thenReturn(1);
+
+        // We need app targets for direct targets to get displayed
+        List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+        when(
+                ChooserActivityOverrideData
+                        .getInstance()
+                        .resolverListController
+                        .getResolversForIntent(
+                                Mockito.anyBoolean(),
+                                Mockito.anyBoolean(),
+                                Mockito.anyBoolean(),
+                                Mockito.isA(List.class)))
+                .thenReturn(resolvedComponentInfos);
+
+        // set caller-provided target
+        Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null);
+        String callerTargetLabel = "Caller Target";
+        ChooserTarget[] targets = new ChooserTarget[] {
+                new ChooserTarget(
+                        callerTargetLabel,
+                        Icon.createWithBitmap(createBitmap()),
+                        0.1f,
+                        resolvedComponentInfos.get(0).name,
+                        new Bundle())
+        };
+        chooserIntent.putExtra(Intent.EXTRA_CHOOSER_TARGETS, targets);
+
+        // create test shortcut loader factory, remember loaders and their callbacks
+        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+                createShortcutLoaderFactory();
+
+        // Start activity
+        final IChooserWrapper activity = (IChooserWrapper)
+                mActivityRule.launchActivity(chooserIntent);
+        waitForIdle();
+
+        // verify that ShortcutLoader was queried
+        ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+                ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+        verify(shortcutLoaders.get(0).first, times(1)).queryShortcuts(appTargets.capture());
+
+        // send shortcuts
+        assertThat(
+                "Wrong number of app targets",
+                appTargets.getValue().length,
+                is(resolvedComponentInfos.size()));
+        ShortcutLoader.Result result = new ShortcutLoader.Result(
+                true,
+                appTargets.getValue(),
+                new ShortcutLoader.ShortcutResultInfo[0],
+                new HashMap<>(),
+                new HashMap<>());
+        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+        waitForIdle();
+
+        final ChooserListAdapter activeAdapter = activity.getAdapter();
+        assertThat(
+                "Chooser should have 3 targets (2 apps, 1 direct)",
+                activeAdapter.getCount(),
+                is(3));
+        assertThat(
+                "Chooser should have exactly two selectable direct target",
+                activeAdapter.getSelectableServiceTargetCount(),
+                is(1));
+        assertThat(
+                "The display label must match",
+                activeAdapter.getItem(0).getDisplayLabel(),
+                is(callerTargetLabel));
     }
 
     @Test
@@ -1742,7 +1679,7 @@
                                 .getContext().getResources().getConfiguration()));
 
         waitForIdle();
-        onView(withIdFromRuntimeResource("resolver_list"))
+        onView(withId(com.android.internal.R.id.resolver_list))
                 .check(matches(withGridColumnCount(6)));
     }
 
@@ -1760,8 +1697,7 @@
     }
 
     private void testDirectTargetLoggingWithAppTargetNotRanked(
-            int orientation, int appTargetsExpected
-    ) throws InterruptedException {
+            int orientation, int appTargetsExpected) {
         Configuration configuration =
                 new Configuration(InstrumentationRegistry.getInstrumentation().getContext()
                         .getResources().getConfiguration());
@@ -1790,18 +1726,14 @@
                                 Mockito.isA(List.class)))
                 .thenReturn(resolvedComponentInfos);
 
-        // Set up resources
-        MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
-        ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
         // Create direct share target
         List<ChooserTarget> serviceTargets = createDirectShareTargets(1,
                 resolvedComponentInfos.get(14).getResolveInfoAt(0).activityInfo.packageName);
         ResolveInfo ri = ResolverDataProvider.createResolveInfo(16, 0);
 
         // Start activity
-        final IChooserWrapper activity = (IChooserWrapper)
+        final IChooserWrapper wrapper = (IChooserWrapper)
                 mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
-        final IChooserWrapper wrapper = (IChooserWrapper) activity;
         // Insert the direct share target
         Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
         directShareToShortcutInfos.put(serviceTargets.get(0), null);
@@ -1815,12 +1747,9 @@
                                 /* resolveInfoPresentationGetter */ null),
                         serviceTargets,
                         TARGET_TYPE_CHOOSER_TARGET,
-                        directShareToShortcutInfos)
+                        directShareToShortcutInfos,
+                        /* directShareToAppTargets */ null)
         );
-        // Thread.sleep shouldn't be a thing in an integration test but it's
-        // necessary here because of the way the code is structured
-        // TODO: restructure the tests b/129870719
-        Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
 
         assertThat(
                 String.format("Chooser should have %d targets (%d apps, 1 direct, 15 A-Z)",
@@ -1837,21 +1766,22 @@
                 .perform(click());
         waitForIdle();
 
-        // Currently we're seeing 3 invocations
-        //      1. ChooserActivity.onCreate()
-        //      2. ChooserActivity$ChooserRowAdapter.createContentPreviewView()
-        //      3. ChooserActivity.startSelected -- which is the one we're after
-        verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture());
-        assertThat(logMakerCaptor.getAllValues().get(2).getCategory(),
-                is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET));
-        assertThat("The packages shouldn't match for app target and direct target", logMakerCaptor
-                .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(-1));
+        ChooserActivityLogger logger = wrapper.getChooserActivityLogger();
+        verify(logger, times(1)).logShareTargetSelected(
+                eq(ChooserActivityLogger.SELECTION_TYPE_SERVICE),
+                /* packageName= */ any(),
+                /* positionPicked= */ anyInt(),
+                // The packages sholdn't match for app target and direct target:
+                /* directTargetAlsoRanked= */ eq(-1),
+                /* numCallerProvided= */ anyInt(),
+                /* directTargetHashed= */ any(),
+                /* isPinned= */ anyBoolean(),
+                /* successfullySelected= */ anyBoolean(),
+                /* selectionCost= */ anyLong());
     }
 
     @Test
     public void testWorkTab_displayedWhenWorkProfileUserAvailable() {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
         Intent sendIntent = createSendTextIntent();
         sendIntent.setType(TEST_MIME_TYPE);
         markWorkProfileUserAvailable();
@@ -1859,26 +1789,22 @@
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
         waitForIdle();
 
-        onView(withIdFromRuntimeResource("tabs")).check(matches(isDisplayed()));
+        onView(withId(android.R.id.tabs)).check(matches(isDisplayed()));
     }
 
     @Test
     public void testWorkTab_hiddenWhenWorkProfileUserNotAvailable() {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
         Intent sendIntent = createSendTextIntent();
         sendIntent.setType(TEST_MIME_TYPE);
 
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
         waitForIdle();
 
-        onView(withIdFromRuntimeResource("tabs")).check(matches(not(isDisplayed())));
+        onView(withId(android.R.id.tabs)).check(matches(not(isDisplayed())));
     }
 
     @Test
     public void testWorkTab_eachTabUsesExpectedAdapter() {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
         int personalProfileTargets = 3;
         int otherProfileTargets = 1;
         List<ResolvedComponentInfo> personalResolvedComponentInfos =
@@ -1897,7 +1823,7 @@
         waitForIdle();
 
         assertThat(activity.getCurrentUserHandle().getIdentifier(), is(0));
-        onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
         assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10));
         assertThat(activity.getPersonalListAdapter().getCount(), is(personalProfileTargets));
         assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets));
@@ -1905,8 +1831,6 @@
 
     @Test
     public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
         markWorkProfileUserAvailable();
         int workProfileTargets = 4;
         List<ResolvedComponentInfo> personalResolvedComponentInfos =
@@ -1920,16 +1844,14 @@
         final IChooserWrapper activity = (IChooserWrapper)
                 mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
         waitForIdle();
-        onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
         waitForIdle();
 
         assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets));
     }
 
     @Test @Ignore
-    public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() throws InterruptedException {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
+    public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() {
         markWorkProfileUserAvailable();
         List<ResolvedComponentInfo> personalResolvedComponentInfos =
                 createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
@@ -1945,13 +1867,10 @@
             return true;
         };
 
-        final IChooserWrapper activity = (IChooserWrapper)
-                mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
         waitForIdle();
-        onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
         waitForIdle();
-        // wait for the share sheet to expand
-        Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
 
         onView(first(allOf(
                 withText(workResolvedComponentInfos.get(0)
@@ -1964,8 +1883,6 @@
 
     @Test
     public void testWorkTab_crossProfileIntentsDisabled_personalToWork_emptyStateShown() {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
         markWorkProfileUserAvailable();
         int workProfileTargets = 4;
         List<ResolvedComponentInfo> personalResolvedComponentInfos =
@@ -1979,18 +1896,17 @@
 
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
         waitForIdle();
-        onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
         waitForIdle();
-        onView(withIdFromRuntimeResource("contentPanel"))
+        onView(withId(com.android.internal.R.id.contentPanel))
                 .perform(swipeUp());
 
-        onView(withTextFromRuntimeResource("resolver_cross_profile_blocked"))
+        onView(withText(R.string.resolver_cross_profile_blocked))
                 .check(matches(isDisplayed()));
     }
 
     @Test
     public void testWorkTab_workProfileDisabled_emptyStateShown() {
-        // enable the work tab feature flag
         markWorkProfileUserAvailable();
         int workProfileTargets = 4;
         List<ResolvedComponentInfo> personalResolvedComponentInfos =
@@ -2002,22 +1918,19 @@
         Intent sendIntent = createSendTextIntent();
         sendIntent.setType(TEST_MIME_TYPE);
 
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
         waitForIdle();
-        onView(withIdFromRuntimeResource("contentPanel"))
+        onView(withId(com.android.internal.R.id.contentPanel))
                 .perform(swipeUp());
-        onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
         waitForIdle();
 
-        onView(withTextFromRuntimeResource("resolver_turn_on_work_apps"))
+        onView(withText(R.string.resolver_turn_on_work_apps))
                 .check(matches(isDisplayed()));
     }
 
     @Test
     public void testWorkTab_noWorkAppsAvailable_emptyStateShown() {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
         markWorkProfileUserAvailable();
         List<ResolvedComponentInfo> personalResolvedComponentInfos =
                 createResolvedComponentsForTest(3);
@@ -2029,20 +1942,18 @@
 
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
         waitForIdle();
-        onView(withIdFromRuntimeResource("contentPanel"))
+        onView(withId(com.android.internal.R.id.contentPanel))
                 .perform(swipeUp());
-        onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
         waitForIdle();
 
-        onView(withTextFromRuntimeResource("resolver_no_work_apps_available"))
+        onView(withText(R.string.resolver_no_work_apps_available))
                 .check(matches(isDisplayed()));
     }
 
     @Ignore // b/220067877
     @Test
     public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
         markWorkProfileUserAvailable();
         List<ResolvedComponentInfo> personalResolvedComponentInfos =
                 createResolvedComponentsForTest(3);
@@ -2056,19 +1967,17 @@
 
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
         waitForIdle();
-        onView(withIdFromRuntimeResource("contentPanel"))
+        onView(withId(com.android.internal.R.id.contentPanel))
                 .perform(swipeUp());
-        onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
         waitForIdle();
 
-        onView(withTextFromRuntimeResource("resolver_cross_profile_blocked"))
+        onView(withText(R.string.resolver_cross_profile_blocked))
                 .check(matches(isDisplayed()));
     }
 
     @Test
     public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
         markWorkProfileUserAvailable();
         List<ResolvedComponentInfo> personalResolvedComponentInfos =
                 createResolvedComponentsForTest(3);
@@ -2081,12 +1990,12 @@
 
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
         waitForIdle();
-        onView(withIdFromRuntimeResource("contentPanel"))
+        onView(withId(com.android.internal.R.id.contentPanel))
                 .perform(swipeUp());
-        onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
         waitForIdle();
 
-        onView(withTextFromRuntimeResource("resolver_no_work_apps_available"))
+        onView(withText(R.string.resolver_no_work_apps_available))
                 .check(matches(isDisplayed()));
     }
 
@@ -2115,7 +2024,7 @@
         // timeout everywhere instead of introducing one to fix this particular test.
 
         assertThat(activity.getAdapter().getCount(), is(2));
-        onView(withIdFromRuntimeResource("profile_button")).check(doesNotExist());
+        onView(withId(com.android.internal.R.id.profile_button)).check(doesNotExist());
 
         ResolveInfo[] chosen = new ResolveInfo[1];
         ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
@@ -2128,53 +2037,11 @@
                 .perform(click());
         waitForIdle();
 
-        ChooserActivityLoggerFake logger =
-                (ChooserActivityLoggerFake) activity.getChooserActivityLogger();
-
         // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
-        logger.removeCallsForUiEventsOfType(
-                ChooserActivityLogger.SharesheetStandardEvent
-                        .SHARESHEET_DIRECT_LOAD_COMPLETE.getId());
-
-        // SHARESHEET_TRIGGERED:
-        assertThat(logger.event(0).getId(),
-                is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId()));
-
-        // SHARESHEET_STARTED:
-        assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED));
-        assertThat(logger.get(1).intent, is(Intent.ACTION_SEND));
-        assertThat(logger.get(1).mimeType, is("text/plain"));
-        assertThat(logger.get(1).packageName, is(
-                InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()));
-        assertThat(logger.get(1).appProvidedApp, is(0));
-        assertThat(logger.get(1).appProvidedDirect, is(0));
-        assertThat(logger.get(1).isWorkprofile, is(false));
-        assertThat(logger.get(1).previewType, is(3));
-
-        // SHARESHEET_APP_LOAD_COMPLETE:
-        assertThat(logger.event(2).getId(),
-                is(ChooserActivityLogger
-                        .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId()));
-
-        // Next are just artifacts of test set-up:
-        assertThat(logger.event(3).getId(),
-                is(ChooserActivityLogger
-                        .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId()));
-        assertThat(logger.event(4).getId(),
-                is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_EXPANDED.getId()));
-
-        // SHARESHEET_APP_TARGET_SELECTED:
-        assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED));
-        assertThat(logger.get(5).targetType,
-                is(ChooserActivityLogger
-                        .SharesheetTargetSelectedEvent.SHARESHEET_APP_TARGET_SELECTED.getId()));
-
-        // No more events.
-        assertThat(logger.numCalls(), is(6));
     }
 
-    @Test @Ignore
-    public void testDirectTargetLogging() throws InterruptedException {
+    @Test
+    public void testDirectTargetLogging() {
         Intent sendIntent = createSendTextIntent();
         // We need app targets for direct targets to get displayed
         List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -2189,41 +2056,59 @@
                                 Mockito.isA(List.class)))
                 .thenReturn(resolvedComponentInfos);
 
-        // Create direct share target
-        List<ChooserTarget> serviceTargets = createDirectShareTargets(1,
-                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
-        ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
+        // create test shortcut loader factory, remember loaders and their callbacks
+        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+                new SparseArray<>();
+        ChooserActivityOverrideData.getInstance().shortcutLoaderFactory =
+                (userHandle, callback) -> {
+                    Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>> pair =
+                            new Pair<>(mock(ShortcutLoader.class), callback);
+                    shortcutLoaders.put(userHandle.getIdentifier(), pair);
+                    return pair.first;
+                };
 
         // Start activity
         final IChooserWrapper activity = (IChooserWrapper)
                 mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+        waitForIdle();
 
-        // Insert the direct share target
-        Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
-        directShareToShortcutInfos.put(serviceTargets.get(0), null);
-        InstrumentationRegistry.getInstrumentation().runOnMainSync(
-                () -> activity.getAdapter().addServiceResults(
-                        activity.createTestDisplayResolveInfo(sendIntent,
-                                ri,
-                                "testLabel",
-                                "testInfo",
-                                sendIntent,
-                                /* resolveInfoPresentationGetter */ null),
-                        serviceTargets,
-                        TARGET_TYPE_CHOOSER_TARGET,
-                        directShareToShortcutInfos)
+        // verify that ShortcutLoader was queried
+        ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+                ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+        verify(shortcutLoaders.get(0).first, times(1))
+                .queryShortcuts(appTargets.capture());
+
+        // send shortcuts
+        assertThat(
+                "Wrong number of app targets",
+                appTargets.getValue().length,
+                is(resolvedComponentInfos.size()));
+        List<ChooserTarget> serviceTargets = createDirectShareTargets(1,
+                resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+        ShortcutLoader.Result result = new ShortcutLoader.Result(
+                // TODO: test another value as well
+                false,
+                appTargets.getValue(),
+                new ShortcutLoader.ShortcutResultInfo[] {
+                        new ShortcutLoader.ShortcutResultInfo(
+                                appTargets.getValue()[0],
+                                serviceTargets
+                        )
+                },
+                new HashMap<>(),
+                new HashMap<>()
         );
-        // Thread.sleep shouldn't be a thing in an integration test but it's
-        // necessary here because of the way the code is structured
-        // TODO: restructure the tests b/129870719
-        Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
+        activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+        waitForIdle();
 
         assertThat("Chooser should have 3 targets (2 apps, 1 direct)",
                 activity.getAdapter().getCount(), is(3));
         assertThat("Chooser should have exactly one selectable direct target",
                 activity.getAdapter().getSelectableServiceTargetCount(), is(1));
-        assertThat("The resolver info must match the resolver info used to create the target",
-                activity.getAdapter().getItem(0).getResolveInfo(), is(ri));
+        assertThat(
+                "The resolver info must match the resolver info used to create the target",
+                activity.getAdapter().getItem(0).getResolveInfo(),
+                is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
 
         // Click on the direct target
         String name = serviceTargets.get(0).getTitle().toString();
@@ -2231,34 +2116,18 @@
                 .perform(click());
         waitForIdle();
 
-        ChooserActivityLoggerFake logger =
-                (ChooserActivityLoggerFake) activity.getChooserActivityLogger();
-        assertThat(logger.numCalls(), is(6));
-        // first one should be SHARESHEET_TRIGGERED uievent
-        assertThat(logger.get(0).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED));
-        assertThat(logger.get(0).event.getId(),
-                is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId()));
-        // second one should be SHARESHEET_STARTED event
-        assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED));
-        assertThat(logger.get(1).intent, is(Intent.ACTION_SEND));
-        assertThat(logger.get(1).mimeType, is("text/plain"));
-        assertThat(logger.get(1).packageName, is(
-                InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()));
-        assertThat(logger.get(1).appProvidedApp, is(0));
-        assertThat(logger.get(1).appProvidedDirect, is(0));
-        assertThat(logger.get(1).isWorkprofile, is(false));
-        assertThat(logger.get(1).previewType, is(3));
-        // third one should be SHARESHEET_APP_LOAD_COMPLETE uievent
-        assertThat(logger.get(2).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED));
-        assertThat(logger.get(2).event.getId(),
-                is(ChooserActivityLogger
-                        .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId()));
-        // fourth and fifth are just artifacts of test set-up
-        // sixth one should be ranking atom with SHARESHEET_COPY_TARGET_SELECTED event
-        assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED));
-        assertThat(logger.get(5).targetType,
-                is(ChooserActivityLogger
-                        .SharesheetTargetSelectedEvent.SHARESHEET_SERVICE_TARGET_SELECTED.getId()));
+        ChooserActivityLogger logger = activity.getChooserActivityLogger();
+        ArgumentCaptor<Integer> typeCaptor = ArgumentCaptor.forClass(Integer.class);
+        verify(logger, times(1)).logShareTargetSelected(
+                eq(ChooserActivityLogger.SELECTION_TYPE_SERVICE),
+                /* packageName= */ any(),
+                /* positionPicked= */ anyInt(),
+                /* directTargetAlsoRanked= */ anyInt(),
+                /* numCallerProvided= */ anyInt(),
+                /* directTargetHashed= */ any(),
+                /* isPinned= */ anyBoolean(),
+                /* successfullySelected= */ anyBoolean(),
+                /* selectionCost= */ anyLong());
     }
 
     @Test @Ignore
@@ -2290,44 +2159,7 @@
         assertThat("Chooser should have no direct targets",
                 activity.getAdapter().getSelectableServiceTargetCount(), is(0));
 
-        ChooserActivityLoggerFake logger =
-                (ChooserActivityLoggerFake) activity.getChooserActivityLogger();
-
         // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
-        logger.removeCallsForUiEventsOfType(
-                ChooserActivityLogger.SharesheetStandardEvent
-                        .SHARESHEET_DIRECT_LOAD_COMPLETE.getId());
-
-        // SHARESHEET_TRIGGERED:
-        assertThat(logger.event(0).getId(),
-                is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId()));
-
-        // SHARESHEET_STARTED:
-        assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED));
-        assertThat(logger.get(1).intent, is(Intent.ACTION_SEND));
-        assertThat(logger.get(1).mimeType, is("text/plain"));
-        assertThat(logger.get(1).packageName, is(
-                InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()));
-        assertThat(logger.get(1).appProvidedApp, is(0));
-        assertThat(logger.get(1).appProvidedDirect, is(0));
-        assertThat(logger.get(1).isWorkprofile, is(false));
-        assertThat(logger.get(1).previewType, is(3));
-
-        // SHARESHEET_APP_LOAD_COMPLETE:
-        assertThat(logger.event(2).getId(),
-                is(ChooserActivityLogger
-                        .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId()));
-
-        // SHARESHEET_EMPTY_DIRECT_SHARE_ROW:
-        assertThat(logger.event(3).getId(),
-                is(ChooserActivityLogger
-                        .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId()));
-
-        // Next is just an artifact of test set-up:
-        assertThat(logger.event(4).getId(),
-                is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_EXPANDED.getId()));
-
-        assertThat(logger.numCalls(), is(5));
     }
 
     @Ignore // b/220067877
@@ -2351,58 +2183,14 @@
                 mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
         waitForIdle();
 
-        onView(withIdFromRuntimeResource("chooser_copy_button")).check(matches(isDisplayed()));
-        onView(withIdFromRuntimeResource("chooser_copy_button")).perform(click());
-
-        ChooserActivityLoggerFake logger =
-                (ChooserActivityLoggerFake) activity.getChooserActivityLogger();
+        onView(withId(com.android.internal.R.id.chooser_copy_button)).check(matches(isDisplayed()));
+        onView(withId(com.android.internal.R.id.chooser_copy_button)).perform(click());
 
         // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
-        logger.removeCallsForUiEventsOfType(
-                ChooserActivityLogger.SharesheetStandardEvent
-                        .SHARESHEET_DIRECT_LOAD_COMPLETE.getId());
-
-        // SHARESHEET_TRIGGERED:
-        assertThat(logger.event(0).getId(),
-                is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId()));
-
-        // SHARESHEET_STARTED:
-        assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED));
-        assertThat(logger.get(1).intent, is(Intent.ACTION_SEND));
-        assertThat(logger.get(1).mimeType, is("text/plain"));
-        assertThat(logger.get(1).packageName, is(
-                InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()));
-        assertThat(logger.get(1).appProvidedApp, is(0));
-        assertThat(logger.get(1).appProvidedDirect, is(0));
-        assertThat(logger.get(1).isWorkprofile, is(false));
-        assertThat(logger.get(1).previewType, is(3));
-
-        // SHARESHEET_APP_LOAD_COMPLETE:
-        assertThat(logger.event(2).getId(),
-                is(ChooserActivityLogger
-                        .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId()));
-
-        // Next are just artifacts of test set-up:
-        assertThat(logger.event(3).getId(),
-                is(ChooserActivityLogger
-                        .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId()));
-        assertThat(logger.event(4).getId(),
-                is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_EXPANDED.getId()));
-
-        // SHARESHEET_COPY_TARGET_SELECTED:
-        assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED));
-        assertThat(logger.get(5).targetType,
-                is(ChooserActivityLogger
-                        .SharesheetTargetSelectedEvent.SHARESHEET_COPY_TARGET_SELECTED.getId()));
-
-        // No more events.
-        assertThat(logger.numCalls(), is(6));
     }
 
     @Test @Ignore("b/222124533")
     public void testSwitchProfileLogging() throws InterruptedException {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
         markWorkProfileUserAvailable();
         int workProfileTargets = 4;
         List<ResolvedComponentInfo> personalResolvedComponentInfos =
@@ -2416,134 +2204,16 @@
         final IChooserWrapper activity = (IChooserWrapper)
                 mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
         waitForIdle();
-        onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
         waitForIdle();
-        onView(withTextFromRuntimeResource("resolver_personal_tab")).perform(click());
+        onView(withText(R.string.resolver_personal_tab)).perform(click());
         waitForIdle();
 
-        ChooserActivityLoggerFake logger =
-                (ChooserActivityLoggerFake) activity.getChooserActivityLogger();
-
         // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
-        logger.removeCallsForUiEventsOfType(
-                ChooserActivityLogger.SharesheetStandardEvent
-                        .SHARESHEET_DIRECT_LOAD_COMPLETE.getId());
-
-        // SHARESHEET_TRIGGERED:
-        assertThat(logger.event(0).getId(),
-                is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId()));
-
-        // SHARESHEET_STARTED:
-        assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED));
-        assertThat(logger.get(1).intent, is(Intent.ACTION_SEND));
-        assertThat(logger.get(1).mimeType, is(TEST_MIME_TYPE));
-        assertThat(logger.get(1).packageName, is(
-                InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()));
-        assertThat(logger.get(1).appProvidedApp, is(0));
-        assertThat(logger.get(1).appProvidedDirect, is(0));
-        assertThat(logger.get(1).isWorkprofile, is(false));
-        assertThat(logger.get(1).previewType, is(3));
-
-        // SHARESHEET_APP_LOAD_COMPLETE:
-        assertThat(logger.event(2).getId(),
-                is(ChooserActivityLogger
-                        .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId()));
-
-        // Next is just an artifact of test set-up:
-        assertThat(logger.event(3).getId(),
-                is(ChooserActivityLogger
-                        .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId()));
-
-        // SHARESHEET_PROFILE_CHANGED:
-        assertThat(logger.event(4).getId(),
-                is(ChooserActivityLogger.SharesheetStandardEvent
-                        .SHARESHEET_PROFILE_CHANGED.getId()));
-
-        // Repeat the loading steps in the new profile:
-
-        // SHARESHEET_APP_LOAD_COMPLETE:
-        assertThat(logger.event(5).getId(),
-                is(ChooserActivityLogger
-                        .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId()));
-
-        // Next is again an artifact of test set-up:
-        assertThat(logger.event(6).getId(),
-                is(ChooserActivityLogger
-                        .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId()));
-
-        // SHARESHEET_PROFILE_CHANGED:
-        assertThat(logger.event(7).getId(),
-                is(ChooserActivityLogger.SharesheetStandardEvent
-                        .SHARESHEET_PROFILE_CHANGED.getId()));
-
-        // No more events (this profile was already loaded).
-        assertThat(logger.numCalls(), is(8));
-    }
-
-    @Test
-    public void testAutolaunch_singleTarget_wifthWorkProfileAndTabbedViewOff_noAutolaunch() {
-        ResolverActivity.ENABLE_TABBED_VIEW = false;
-        List<ResolvedComponentInfo> personalResolvedComponentInfos =
-                createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10);
-        when(
-                ChooserActivityOverrideData
-                        .getInstance()
-                        .resolverListController
-                        .getResolversForIntent(
-                                Mockito.anyBoolean(),
-                                Mockito.anyBoolean(),
-                                Mockito.anyBoolean(),
-                                Mockito.isA(List.class)))
-                .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
-        Intent sendIntent = createSendTextIntent();
-        sendIntent.setType(TEST_MIME_TYPE);
-        ResolveInfo[] chosen = new ResolveInfo[1];
-        ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
-            chosen[0] = targetInfo.getResolveInfo();
-            return true;
-        };
-        waitForIdle();
-
-        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
-        waitForIdle();
-
-        assertTrue(chosen[0] == null);
-    }
-
-    @Test
-    public void testAutolaunch_singleTarget_noWorkProfile_autolaunch() {
-        ResolverActivity.ENABLE_TABBED_VIEW = false;
-        List<ResolvedComponentInfo> personalResolvedComponentInfos =
-                createResolvedComponentsForTest(1);
-        when(
-                ChooserActivityOverrideData
-                        .getInstance()
-                        .resolverListController
-                        .getResolversForIntent(
-                                Mockito.anyBoolean(),
-                                Mockito.anyBoolean(),
-                                Mockito.anyBoolean(),
-                                Mockito.isA(List.class)))
-                .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
-        Intent sendIntent = createSendTextIntent();
-        sendIntent.setType(TEST_MIME_TYPE);
-        ResolveInfo[] chosen = new ResolveInfo[1];
-        ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
-            chosen[0] = targetInfo.getResolveInfo();
-            return true;
-        };
-        waitForIdle();
-
-        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
-        waitForIdle();
-
-        assertThat(chosen[0], is(personalResolvedComponentInfos.get(0).getResolveInfoAt(0)));
     }
 
     @Test
     public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_autolaunch() {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
         markWorkProfileUserAvailable();
         int workProfileTargets = 4;
         List<ResolvedComponentInfo> personalResolvedComponentInfos =
@@ -2559,7 +2229,7 @@
             return true;
         };
 
-        mActivityRule.launchActivity(sendIntent);
+        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test"));
         waitForIdle();
 
         assertThat(chosen[0], is(personalResolvedComponentInfos.get(1).getResolveInfoAt(0)));
@@ -2591,7 +2261,7 @@
         when(
                 ChooserActivityOverrideData
                         .getInstance().packageManager
-                        .resolveActivity(any(Intent.class), anyInt()))
+                        .resolveActivity(any(Intent.class), any()))
                 .thenReturn(ri);
         waitForIdle();
 
@@ -2605,8 +2275,6 @@
 
     @Test
     public void testWorkTab_withInitialIntents_workTabDoesNotIncludePersonalInitialIntents() {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
         markWorkProfileUserAvailable();
         int workProfileTargets = 1;
         List<ResolvedComponentInfo> personalResolvedComponentInfos =
@@ -2624,7 +2292,7 @@
                 ChooserActivityOverrideData
                         .getInstance()
                         .packageManager
-                        .resolveActivity(any(Intent.class), anyInt()))
+                        .resolveActivity(any(Intent.class), any()))
                 .thenReturn(createFakeResolveInfo());
         waitForIdle();
 
@@ -2637,8 +2305,6 @@
 
     @Test
     public void testWorkTab_xProfileIntentsDisabled_personalToWork_nonSendIntent_emptyStateShown() {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
         markWorkProfileUserAvailable();
         int workProfileTargets = 4;
         List<ResolvedComponentInfo> personalResolvedComponentInfos =
@@ -2657,24 +2323,22 @@
                 ChooserActivityOverrideData
                         .getInstance()
                         .packageManager
-                        .resolveActivity(any(Intent.class), anyInt()))
+                        .resolveActivity(any(Intent.class), any()))
                 .thenReturn(createFakeResolveInfo());
 
         mActivityRule.launchActivity(chooserIntent);
         waitForIdle();
-        onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
         waitForIdle();
-        onView(withIdFromRuntimeResource("contentPanel"))
+        onView(withId(com.android.internal.R.id.contentPanel))
                 .perform(swipeUp());
 
-        onView(withTextFromRuntimeResource("resolver_cross_profile_blocked"))
+        onView(withText(R.string.resolver_cross_profile_blocked))
                 .check(matches(isDisplayed()));
     }
 
     @Test
     public void testWorkTab_noWorkAppsAvailable_nonSendIntent_emptyStateShown() {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
         markWorkProfileUserAvailable();
         List<ResolvedComponentInfo> personalResolvedComponentInfos =
                 createResolvedComponentsForTest(3);
@@ -2691,17 +2355,17 @@
                 ChooserActivityOverrideData
                         .getInstance()
                         .packageManager
-                        .resolveActivity(any(Intent.class), anyInt()))
+                        .resolveActivity(any(Intent.class), any()))
                 .thenReturn(createFakeResolveInfo());
 
         mActivityRule.launchActivity(chooserIntent);
         waitForIdle();
-        onView(withIdFromRuntimeResource("contentPanel"))
+        onView(withId(com.android.internal.R.id.contentPanel))
                 .perform(swipeUp());
-        onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
         waitForIdle();
 
-        onView(withTextFromRuntimeResource("resolver_no_work_apps_available"))
+        onView(withText(R.string.resolver_no_work_apps_available))
                 .check(matches(isDisplayed()));
     }
 
@@ -2726,7 +2390,7 @@
                 ChooserActivityOverrideData
                         .getInstance()
                         .packageManager
-                        .resolveActivity(any(Intent.class), anyInt()))
+                        .resolveActivity(any(Intent.class), any()))
                 .thenReturn(ri);
         waitForIdle();
 
@@ -2740,150 +2404,35 @@
     }
 
     @Test
-    public void testWorkTab_selectingWorkTabWithPausedWorkProfile_directShareTargetsNotQueried() {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
+    public void test_query_shortcut_loader_for_the_selected_tab() {
         markWorkProfileUserAvailable();
         List<ResolvedComponentInfo> personalResolvedComponentInfos =
                 createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
         List<ResolvedComponentInfo> workResolvedComponentInfos =
                 createResolvedComponentsForTest(3);
         setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
-        ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true;
-        boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false };
-        ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets =
-                chooserListAdapter -> {
-                    isQueryDirectShareCalledOnWorkProfile[0] =
-                            (chooserListAdapter.getUserHandle().getIdentifier() == 10);
-                    return null;
-                };
+        ShortcutLoader personalProfileShortcutLoader = mock(ShortcutLoader.class);
+        ShortcutLoader workProfileShortcutLoader = mock(ShortcutLoader.class);
+        final SparseArray<ShortcutLoader> shortcutLoaders = new SparseArray<>();
+        shortcutLoaders.put(0, personalProfileShortcutLoader);
+        shortcutLoaders.put(10, workProfileShortcutLoader);
+        ChooserActivityOverrideData.getInstance().shortcutLoaderFactory =
+                (userHandle, callback) -> shortcutLoaders.get(userHandle.getIdentifier(), null);
         Intent sendIntent = createSendTextIntent();
         sendIntent.setType(TEST_MIME_TYPE);
 
         mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
         waitForIdle();
-        onView(withIdFromRuntimeResource("contentPanel"))
+        onView(withId(com.android.internal.R.id.contentPanel))
                 .perform(swipeUp());
-        onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
         waitForIdle();
 
-        assertFalse("Direct share targets were queried on a paused work profile",
-                isQueryDirectShareCalledOnWorkProfile[0]);
-    }
+        verify(personalProfileShortcutLoader, times(1)).queryShortcuts(any());
 
-    @Test
-    public void testWorkTab_selectingWorkTabWithNotRunningWorkUser_directShareTargetsNotQueried() {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
-        markWorkProfileUserAvailable();
-        List<ResolvedComponentInfo> personalResolvedComponentInfos =
-                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
-        List<ResolvedComponentInfo> workResolvedComponentInfos =
-                createResolvedComponentsForTest(3);
-        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
-        ChooserActivityOverrideData.getInstance().isWorkProfileUserRunning = false;
-        boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false };
-        ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets =
-                chooserListAdapter -> {
-                    isQueryDirectShareCalledOnWorkProfile[0] =
-                            (chooserListAdapter.getUserHandle().getIdentifier() == 10);
-                    return null;
-                };
-        Intent sendIntent = createSendTextIntent();
-        sendIntent.setType(TEST_MIME_TYPE);
-
-        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
-        waitForIdle();
-        onView(withIdFromRuntimeResource("contentPanel"))
-                .perform(swipeUp());
-        onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+        onView(withText(R.string.resolver_work_tab)).perform(click());
         waitForIdle();
 
-        assertFalse("Direct share targets were queried on a locked work profile user",
-                isQueryDirectShareCalledOnWorkProfile[0]);
-    }
-
-    @Test
-    public void testWorkTab_workUserNotRunning_workTargetsShown() {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
-        markWorkProfileUserAvailable();
-        List<ResolvedComponentInfo> personalResolvedComponentInfos =
-                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
-        List<ResolvedComponentInfo> workResolvedComponentInfos =
-                createResolvedComponentsForTest(3);
-        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
-        Intent sendIntent = createSendTextIntent();
-        sendIntent.setType(TEST_MIME_TYPE);
-        ChooserActivityOverrideData.getInstance().isWorkProfileUserRunning = false;
-
-        final ChooserActivity activity =
-                mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
-        final IChooserWrapper wrapper = (IChooserWrapper) activity;
-        waitForIdle();
-        onView(withIdFromRuntimeResource("contentPanel")).perform(swipeUp());
-        onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
-        waitForIdle();
-
-        assertEquals(3, wrapper.getWorkListAdapter().getCount());
-    }
-
-    @Test
-    public void testWorkTab_selectingWorkTabWithLockedWorkUser_directShareTargetsNotQueried() {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
-        markWorkProfileUserAvailable();
-        List<ResolvedComponentInfo> personalResolvedComponentInfos =
-                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
-        List<ResolvedComponentInfo> workResolvedComponentInfos =
-                createResolvedComponentsForTest(3);
-        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
-        ChooserActivityOverrideData.getInstance().isWorkProfileUserUnlocked = false;
-        boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false };
-        ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets =
-                chooserListAdapter -> {
-                    isQueryDirectShareCalledOnWorkProfile[0] =
-                            (chooserListAdapter.getUserHandle().getIdentifier() == 10);
-                    return null;
-                };
-        Intent sendIntent = createSendTextIntent();
-        sendIntent.setType(TEST_MIME_TYPE);
-
-        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
-        waitForIdle();
-        onView(withIdFromRuntimeResource("contentPanel"))
-                .perform(swipeUp());
-        onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
-        waitForIdle();
-
-        assertFalse("Direct share targets were queried on a locked work profile user",
-                isQueryDirectShareCalledOnWorkProfile[0]);
-    }
-
-    @Test
-    public void testWorkTab_workUserLocked_workTargetsShown() {
-        // enable the work tab feature flag
-        ResolverActivity.ENABLE_TABBED_VIEW = true;
-        markWorkProfileUserAvailable();
-        List<ResolvedComponentInfo> personalResolvedComponentInfos =
-                createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
-        List<ResolvedComponentInfo> workResolvedComponentInfos =
-                createResolvedComponentsForTest(3);
-        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
-        Intent sendIntent = createSendTextIntent();
-        sendIntent.setType(TEST_MIME_TYPE);
-        ChooserActivityOverrideData.getInstance().isWorkProfileUserUnlocked = false;
-
-        final ChooserActivity activity =
-                mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
-        final IChooserWrapper wrapper = (IChooserWrapper) activity;
-        waitForIdle();
-        onView(withIdFromRuntimeResource("contentPanel"))
-                .perform(swipeUp());
-        onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
-        waitForIdle();
-
-        assertEquals(3, wrapper.getWorkListAdapter().getCount());
+        verify(workProfileShortcutLoader, times(1)).queryShortcuts(any());
     }
 
     private Intent createChooserIntent(Intent intent, Intent[] initialIntents) {
@@ -3092,21 +2641,6 @@
         return shortcuts;
     }
 
-    private void assertCorrectShortcutToChooserTargetConversion(List<ShareShortcutInfo> shortcuts,
-            List<ChooserTarget> chooserTargets, int[] expectedOrder, float[] expectedScores) {
-        assertEquals(expectedOrder.length, chooserTargets.size());
-        for (int i = 0; i < chooserTargets.size(); i++) {
-            ChooserTarget ct = chooserTargets.get(i);
-            ShortcutInfo si = shortcuts.get(expectedOrder[i]).getShortcutInfo();
-            ComponentName cn = shortcuts.get(expectedOrder[i]).getTargetComponent();
-
-            assertEquals(si.getId(), ct.getIntentExtras().getString(Intent.EXTRA_SHORTCUT_ID));
-            assertEquals(si.getShortLabel(), ct.getTitle());
-            assertThat(Math.abs(expectedScores[i] - ct.getScore()) < 0.000001, is(true));
-            assertEquals(cn.flattenToString(), ct.getComponentName().flattenToString());
-        }
-    }
-
     private void markWorkProfileUserAvailable() {
         ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(10);
     }
@@ -3147,14 +2681,6 @@
                 .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
     }
 
-    private Matcher<View> withIdFromRuntimeResource(String id) {
-        return withId(getRuntimeResourceId(id, "id"));
-    }
-
-    private Matcher<View> withTextFromRuntimeResource(String id) {
-        return withText(getRuntimeResourceId(id, "string"));
-    }
-
     private static GridRecyclerSpanCountMatcher withGridColumnCount(int columnCount) {
         return new GridRecyclerSpanCountMatcher(Matchers.is(columnCount));
     }
@@ -3214,25 +2740,17 @@
                 .thenReturn(targetsPerRow);
     }
 
-    // ChooserWrapperActivity inherits from the framework ChooserActivity, so if the framework
-    // resources have been updated since the framework was last built/pushed, the inherited behavior
-    // (which is the focus of our testing) will still be implemented in terms of the old resource
-    // IDs; then when we try to assert those IDs in tests (e.g. `onView(withText(R.string.foo))`),
-    // the expected values won't match. The tests can instead call this method (with the same
-    // general semantics as Resources#getIdentifier() e.g. `getRuntimeResourceId("foo", "string")`)
-    // to refer to the resource by that name in the runtime chooser, regardless of whether the
-    // framework code on the device is up-to-date.
-    // TODO: is there a better way to do this? (Other than abandoning inheritance-based DI wrapper?)
-    private int getRuntimeResourceId(String name, String defType) {
-        int id = -1;
-        if (ChooserActivityOverrideData.getInstance().resources != null) {
-            id = ChooserActivityOverrideData.getInstance().resources.getIdentifier(
-                  name, defType, "android");
-        } else {
-            id = mActivityRule.getActivity().getResources().getIdentifier(name, defType, "android");
-        }
-        assertThat(id, greaterThan(0));
-
-        return id;
+    private SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>>
+            createShortcutLoaderFactory() {
+        SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+                new SparseArray<>();
+        ChooserActivityOverrideData.getInstance().shortcutLoaderFactory =
+                (userHandle, callback) -> {
+                    Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>> pair =
+                            new Pair<>(mock(ShortcutLoader.class), callback);
+                    shortcutLoaders.put(userHandle.getIdentifier(), pair);
+                    return pair.first;
+                };
+        return shortcutLoaders;
     }
 }
diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java
new file mode 100644
index 0000000..f1febed
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java
@@ -0,0 +1,467 @@
+/*
+ * 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 static android.testing.PollingCheck.waitFor;
+
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.action.ViewActions.swipeUp;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.isSelected;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import static com.android.intentresolver.ChooserWrapperActivity.sOverrides;
+import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.NO_BLOCKER;
+import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_ACCESS_BLOCKER;
+import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.PERSONAL_PROFILE_SHARE_BLOCKER;
+import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_ACCESS_BLOCKER;
+import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.ExpectedBlocker.WORK_PROFILE_SHARE_BLOCKER;
+import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab.PERSONAL;
+import static com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab.WORK;
+
+import static org.hamcrest.CoreMatchers.not;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.when;
+
+import android.companion.DeviceFilter;
+import android.content.Intent;
+import android.os.UserHandle;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.espresso.NoMatchingViewException;
+import androidx.test.rule.ActivityTestRule;
+
+import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
+import com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab;
+import com.android.internal.R;
+
+import junit.framework.AssertionFailedError;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+@DeviceFilter.MediumType
+@RunWith(Parameterized.class)
+public class UnbundledChooserActivityWorkProfileTest {
+
+    private static final UserHandle PERSONAL_USER_HANDLE = InstrumentationRegistry
+            .getInstrumentation().getTargetContext().getUser();
+    private static final UserHandle WORK_USER_HANDLE = UserHandle.of(10);
+
+    @Rule
+    public ActivityTestRule<ChooserWrapperActivity> mActivityRule =
+            new ActivityTestRule<>(ChooserWrapperActivity.class, false,
+                    false);
+    private final TestCase mTestCase;
+
+    public UnbundledChooserActivityWorkProfileTest(TestCase testCase) {
+        mTestCase = testCase;
+    }
+
+    @Before
+    public void cleanOverrideData() {
+        // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the
+        // permissions we require (which we'll read from the manifest at runtime).
+        InstrumentationRegistry
+                .getInstrumentation()
+                .getUiAutomation()
+                .adoptShellPermissionIdentity();
+
+        sOverrides.reset();
+    }
+
+    @Test
+    public void testBlocker() {
+        setUpPersonalAndWorkComponentInfos();
+        sOverrides.hasCrossProfileIntents = mTestCase.hasCrossProfileIntents();
+        sOverrides.myUserId = mTestCase.getMyUserHandle().getIdentifier();
+
+        launchActivity(mTestCase.getIsSendAction());
+        switchToTab(mTestCase.getTab());
+
+        switch (mTestCase.getExpectedBlocker()) {
+            case NO_BLOCKER:
+                assertNoBlockerDisplayed();
+                break;
+            case PERSONAL_PROFILE_SHARE_BLOCKER:
+                assertCantSharePersonalAppsBlockerDisplayed();
+                break;
+            case WORK_PROFILE_SHARE_BLOCKER:
+                assertCantShareWorkAppsBlockerDisplayed();
+                break;
+            case PERSONAL_PROFILE_ACCESS_BLOCKER:
+                assertCantAccessPersonalAppsBlockerDisplayed();
+                break;
+            case WORK_PROFILE_ACCESS_BLOCKER:
+                assertCantAccessWorkAppsBlockerDisplayed();
+                break;
+        }
+    }
+
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection tests() {
+        return Arrays.asList(
+                new TestCase(
+                        /* isSendAction= */ true,
+                        /* hasCrossProfileIntents= */ true,
+                        /* myUserHandle= */ WORK_USER_HANDLE,
+                        /* tab= */ WORK,
+                        /* expectedBlocker= */ NO_BLOCKER
+                ),
+                new TestCase(
+                        /* isSendAction= */ true,
+                        /* hasCrossProfileIntents= */ false,
+                        /* myUserHandle= */ WORK_USER_HANDLE,
+                        /* tab= */ WORK,
+                        /* expectedBlocker= */ NO_BLOCKER
+                ),
+                new TestCase(
+                        /* isSendAction= */ true,
+                        /* hasCrossProfileIntents= */ true,
+                        /* myUserHandle= */ PERSONAL_USER_HANDLE,
+                        /* tab= */ WORK,
+                        /* expectedBlocker= */ NO_BLOCKER
+                ),
+                new TestCase(
+                        /* isSendAction= */ true,
+                        /* hasCrossProfileIntents= */ false,
+                        /* myUserHandle= */ PERSONAL_USER_HANDLE,
+                        /* tab= */ WORK,
+                        /* expectedBlocker= */ WORK_PROFILE_SHARE_BLOCKER
+                ),
+                new TestCase(
+                        /* isSendAction= */ true,
+                        /* hasCrossProfileIntents= */ true,
+                        /* myUserHandle= */ WORK_USER_HANDLE,
+                        /* tab= */ PERSONAL,
+                        /* expectedBlocker= */ NO_BLOCKER
+                ),
+                new TestCase(
+                        /* isSendAction= */ true,
+                        /* hasCrossProfileIntents= */ false,
+                        /* myUserHandle= */ WORK_USER_HANDLE,
+                        /* tab= */ PERSONAL,
+                        /* expectedBlocker= */ PERSONAL_PROFILE_SHARE_BLOCKER
+                ),
+                new TestCase(
+                        /* isSendAction= */ true,
+                        /* hasCrossProfileIntents= */ true,
+                        /* myUserHandle= */ PERSONAL_USER_HANDLE,
+                        /* tab= */ PERSONAL,
+                        /* expectedBlocker= */ NO_BLOCKER
+                ),
+                new TestCase(
+                        /* isSendAction= */ true,
+                        /* hasCrossProfileIntents= */ false,
+                        /* myUserHandle= */ PERSONAL_USER_HANDLE,
+                        /* tab= */ PERSONAL,
+                        /* expectedBlocker= */ NO_BLOCKER
+                ),
+                new TestCase(
+                        /* isSendAction= */ false,
+                        /* hasCrossProfileIntents= */ true,
+                        /* myUserHandle= */ WORK_USER_HANDLE,
+                        /* tab= */ WORK,
+                        /* expectedBlocker= */ NO_BLOCKER
+                ),
+                new TestCase(
+                        /* isSendAction= */ false,
+                        /* hasCrossProfileIntents= */ false,
+                        /* myUserHandle= */ WORK_USER_HANDLE,
+                        /* tab= */ WORK,
+                        /* expectedBlocker= */ NO_BLOCKER
+                ),
+                new TestCase(
+                        /* isSendAction= */ false,
+                        /* hasCrossProfileIntents= */ true,
+                        /* myUserHandle= */ PERSONAL_USER_HANDLE,
+                        /* tab= */ WORK,
+                        /* expectedBlocker= */ NO_BLOCKER
+                ),
+                new TestCase(
+                        /* isSendAction= */ false,
+                        /* hasCrossProfileIntents= */ false,
+                        /* myUserHandle= */ PERSONAL_USER_HANDLE,
+                        /* tab= */ WORK,
+                        /* expectedBlocker= */ WORK_PROFILE_ACCESS_BLOCKER
+                ),
+                new TestCase(
+                        /* isSendAction= */ false,
+                        /* hasCrossProfileIntents= */ true,
+                        /* myUserHandle= */ WORK_USER_HANDLE,
+                        /* tab= */ PERSONAL,
+                        /* expectedBlocker= */ NO_BLOCKER
+                ),
+                new TestCase(
+                        /* isSendAction= */ false,
+                        /* hasCrossProfileIntents= */ false,
+                        /* myUserHandle= */ WORK_USER_HANDLE,
+                        /* tab= */ PERSONAL,
+                        /* expectedBlocker= */ PERSONAL_PROFILE_ACCESS_BLOCKER
+                ),
+                new TestCase(
+                        /* isSendAction= */ false,
+                        /* hasCrossProfileIntents= */ true,
+                        /* myUserHandle= */ PERSONAL_USER_HANDLE,
+                        /* tab= */ PERSONAL,
+                        /* expectedBlocker= */ NO_BLOCKER
+                ),
+                new TestCase(
+                        /* isSendAction= */ false,
+                        /* hasCrossProfileIntents= */ false,
+                        /* myUserHandle= */ PERSONAL_USER_HANDLE,
+                        /* tab= */ PERSONAL,
+                        /* expectedBlocker= */ NO_BLOCKER
+                )
+        );
+    }
+
+    private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile(
+            int numberOfResults, int userId) {
+        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+        for (int i = 0; i < numberOfResults; i++) {
+            infoList.add(
+                    ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId));
+        }
+        return infoList;
+    }
+
+    private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) {
+        List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+        for (int i = 0; i < numberOfResults; i++) {
+            infoList.add(ResolverDataProvider.createResolvedComponentInfo(i));
+        }
+        return infoList;
+    }
+
+    private void setUpPersonalAndWorkComponentInfos() {
+        markWorkProfileUserAvailable();
+        int workProfileTargets = 4;
+        List<ResolvedComponentInfo> personalResolvedComponentInfos =
+                createResolvedComponentsForTestWithOtherProfile(3,
+                        /* userId */ WORK_USER_HANDLE.getIdentifier());
+        List<ResolvedComponentInfo> workResolvedComponentInfos =
+                createResolvedComponentsForTest(workProfileTargets);
+        setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+    }
+
+    private void setupResolverControllers(
+            List<ResolvedComponentInfo> personalResolvedComponentInfos,
+            List<ResolvedComponentInfo> workResolvedComponentInfos) {
+        when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class)))
+                .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+        when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class))).thenReturn(workResolvedComponentInfos);
+        when(sOverrides.workResolverListController.getResolversForIntentAsUser(Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.anyBoolean(),
+                Mockito.isA(List.class),
+                eq(UserHandle.SYSTEM)))
+                .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+    }
+
+    private void waitForIdle() {
+        InstrumentationRegistry.getInstrumentation().waitForIdleSync();
+    }
+
+    private void markWorkProfileUserAvailable() {
+        ChooserWrapperActivity.sOverrides.workProfileUserHandle = WORK_USER_HANDLE;
+    }
+
+    private void assertCantAccessWorkAppsBlockerDisplayed() {
+        onView(withText(R.string.resolver_cross_profile_blocked))
+                .check(matches(isDisplayed()));
+        onView(withText(R.string.resolver_cant_access_work_apps_explanation))
+                .check(matches(isDisplayed()));
+    }
+
+    private void assertCantAccessPersonalAppsBlockerDisplayed() {
+        onView(withText(R.string.resolver_cross_profile_blocked))
+                .check(matches(isDisplayed()));
+        onView(withText(R.string.resolver_cant_access_personal_apps_explanation))
+                .check(matches(isDisplayed()));
+    }
+
+    private void assertCantShareWorkAppsBlockerDisplayed() {
+        onView(withText(R.string.resolver_cross_profile_blocked))
+                .check(matches(isDisplayed()));
+        onView(withText(R.string.resolver_cant_share_with_work_apps_explanation))
+                .check(matches(isDisplayed()));
+    }
+
+    private void assertCantSharePersonalAppsBlockerDisplayed() {
+        onView(withText(R.string.resolver_cross_profile_blocked))
+                .check(matches(isDisplayed()));
+        onView(withText(R.string.resolver_cant_share_with_personal_apps_explanation))
+                .check(matches(isDisplayed()));
+    }
+
+    private void assertNoBlockerDisplayed() {
+        try {
+            onView(withText(R.string.resolver_cross_profile_blocked))
+                    .check(matches(not(isDisplayed())));
+        } catch (NoMatchingViewException ignored) {
+        }
+    }
+
+    private void switchToTab(Tab tab) {
+        final int stringId = tab == Tab.WORK ? R.string.resolver_work_tab
+                : R.string.resolver_personal_tab;
+
+        waitFor(() -> {
+            onView(withText(stringId)).perform(click());
+            waitForIdle();
+
+            try {
+                onView(withText(stringId)).check(matches(isSelected()));
+                return true;
+            } catch (AssertionFailedError e) {
+                return false;
+            }
+        });
+
+        onView(withId(R.id.contentPanel))
+                .perform(swipeUp());
+        waitForIdle();
+    }
+
+    private Intent createTextIntent(boolean isSendAction) {
+        Intent sendIntent = new Intent();
+        if (isSendAction) {
+            sendIntent.setAction(Intent.ACTION_SEND);
+        }
+        sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending");
+        sendIntent.setType("text/plain");
+        return sendIntent;
+    }
+
+    private void launchActivity(boolean isSendAction) {
+        Intent sendIntent = createTextIntent(isSendAction);
+        mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test"));
+        waitForIdle();
+    }
+
+    public static class TestCase {
+        private final boolean mIsSendAction;
+        private final boolean mHasCrossProfileIntents;
+        private final UserHandle mMyUserHandle;
+        private final Tab mTab;
+        private final ExpectedBlocker mExpectedBlocker;
+
+        public enum ExpectedBlocker {
+            NO_BLOCKER,
+            PERSONAL_PROFILE_SHARE_BLOCKER,
+            WORK_PROFILE_SHARE_BLOCKER,
+            PERSONAL_PROFILE_ACCESS_BLOCKER,
+            WORK_PROFILE_ACCESS_BLOCKER
+        }
+
+        public enum Tab {
+            WORK,
+            PERSONAL
+        }
+
+        public TestCase(boolean isSendAction, boolean hasCrossProfileIntents,
+                UserHandle myUserHandle, Tab tab, ExpectedBlocker expectedBlocker) {
+            mIsSendAction = isSendAction;
+            mHasCrossProfileIntents = hasCrossProfileIntents;
+            mMyUserHandle = myUserHandle;
+            mTab = tab;
+            mExpectedBlocker = expectedBlocker;
+        }
+
+        public boolean getIsSendAction() {
+            return mIsSendAction;
+        }
+
+        public boolean hasCrossProfileIntents() {
+            return mHasCrossProfileIntents;
+        }
+
+        public UserHandle getMyUserHandle() {
+            return mMyUserHandle;
+        }
+
+        public Tab getTab() {
+            return mTab;
+        }
+
+        public ExpectedBlocker getExpectedBlocker() {
+            return mExpectedBlocker;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder result = new StringBuilder("test");
+
+            if (mTab == WORK) {
+                result.append("WorkTab_");
+            } else {
+                result.append("PersonalTab_");
+            }
+
+            if (mIsSendAction) {
+                result.append("sendAction_");
+            } else {
+                result.append("notSendAction_");
+            }
+
+            if (mHasCrossProfileIntents) {
+                result.append("hasCrossProfileIntents_");
+            } else {
+                result.append("doesNotHaveCrossProfileIntents_");
+            }
+
+            if (mMyUserHandle.equals(PERSONAL_USER_HANDLE)) {
+                result.append("myUserIsPersonal_");
+            } else {
+                result.append("myUserIsWork_");
+            }
+
+            if (mExpectedBlocker == ExpectedBlocker.NO_BLOCKER) {
+                result.append("thenNoBlocker");
+            } else if (mExpectedBlocker == PERSONAL_PROFILE_ACCESS_BLOCKER) {
+                result.append("thenAccessBlockerOnPersonalProfile");
+            } else if (mExpectedBlocker == PERSONAL_PROFILE_SHARE_BLOCKER) {
+                result.append("thenShareBlockerOnPersonalProfile");
+            } else if (mExpectedBlocker == WORK_PROFILE_ACCESS_BLOCKER) {
+                result.append("thenAccessBlockerOnWorkProfile");
+            } else if (mExpectedBlocker == WORK_PROFILE_SHARE_BLOCKER) {
+                result.append("thenShareBlockerOnWorkProfile");
+            }
+
+            return result.toString();
+        }
+    }
+}
diff --git a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt
new file mode 100644
index 0000000..7c2b07a
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt
@@ -0,0 +1,192 @@
+/*
+ * 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.chooser
+
+import android.app.prediction.AppTarget
+import android.app.prediction.AppTargetId
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.ResolveInfo
+import android.os.UserHandle
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.intentresolver.createChooserTarget
+import com.android.intentresolver.createShortcutInfo
+import com.android.intentresolver.mock
+import com.android.intentresolver.ResolverDataProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class TargetInfoTest {
+    private val context = InstrumentationRegistry.getInstrumentation().getContext()
+
+    @Test
+    fun testNewEmptyTargetInfo() {
+        val info = NotSelectableTargetInfo.newEmptyTargetInfo()
+        assertThat(info.isEmptyTargetInfo()).isTrue()
+        assertThat(info.isChooserTargetInfo()).isTrue()  // From legacy inheritance model.
+        assertThat(info.hasDisplayIcon()).isFalse()
+        assertThat(info.getDisplayIconHolder().getDisplayIcon()).isNull()
+    }
+
+    @Test
+    fun testNewPlaceholderTargetInfo() {
+        val info = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context)
+        assertThat(info.isPlaceHolderTargetInfo()).isTrue()
+        assertThat(info.isChooserTargetInfo()).isTrue()  // From legacy inheritance model.
+        assertThat(info.hasDisplayIcon()).isTrue()
+        // TODO: test infrastructure isn't set up to assert anything about the icon itself.
+    }
+
+    @Test
+    fun testNewSelectableTargetInfo() {
+        val resolvedIntent = Intent()
+        val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo(
+            resolvedIntent,
+            ResolverDataProvider.createResolveInfo(1, 0),
+            "label",
+            "extended info",
+            resolvedIntent,
+            /* resolveInfoPresentationGetter= */ null)
+        val chooserTarget = createChooserTarget(
+            "title", 0.3f, ResolverDataProvider.createComponentName(2), "test_shortcut_id")
+        val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(3), 3)
+        val appTarget = AppTarget(
+            AppTargetId("id"),
+            chooserTarget.componentName.packageName,
+            chooserTarget.componentName.className,
+            UserHandle.CURRENT)
+
+        val targetInfo = SelectableTargetInfo.newSelectableTargetInfo(
+            baseDisplayInfo,
+            mock(),
+            resolvedIntent,
+            chooserTarget,
+            0.1f,
+            shortcutInfo,
+            appTarget,
+            mock(),
+        )
+        assertThat(targetInfo.isSelectableTargetInfo).isTrue()
+        assertThat(targetInfo.isChooserTargetInfo).isTrue()  // From legacy inheritance model.
+        assertThat(targetInfo.displayResolveInfo).isSameInstanceAs(baseDisplayInfo)
+        assertThat(targetInfo.chooserTargetComponentName).isEqualTo(chooserTarget.componentName)
+        assertThat(targetInfo.directShareShortcutId).isEqualTo(shortcutInfo.id)
+        assertThat(targetInfo.directShareShortcutInfo).isSameInstanceAs(shortcutInfo)
+        assertThat(targetInfo.directShareAppTarget).isSameInstanceAs(appTarget)
+        assertThat(targetInfo.resolvedIntent).isSameInstanceAs(resolvedIntent)
+        // TODO: make more meaningful assertions about the behavior of a selectable target.
+    }
+
+    @Test
+    fun test_SelectableTargetInfo_componentName_no_source_info() {
+        val chooserTarget = createChooserTarget(
+            "title", 0.3f, ResolverDataProvider.createComponentName(1), "test_shortcut_id")
+        val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(2), 3)
+        val appTarget = AppTarget(
+            AppTargetId("id"),
+            chooserTarget.componentName.packageName,
+            chooserTarget.componentName.className,
+            UserHandle.CURRENT)
+        val pkgName = "org.package"
+        val className = "MainActivity"
+        val backupResolveInfo = ResolveInfo().apply {
+            activityInfo = ActivityInfo().apply {
+                packageName = pkgName
+                name = className
+            }
+        }
+
+        val targetInfo = SelectableTargetInfo.newSelectableTargetInfo(
+            null,
+            backupResolveInfo,
+            mock(),
+            chooserTarget,
+            0.1f,
+            shortcutInfo,
+            appTarget,
+            mock(),
+        )
+        assertThat(targetInfo.resolvedComponentName).isEqualTo(ComponentName(pkgName, className))
+    }
+
+    @Test
+    fun testNewDisplayResolveInfo() {
+        val intent = Intent(Intent.ACTION_SEND)
+        intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending")
+        intent.setType("text/plain")
+
+        val resolveInfo = ResolverDataProvider.createResolveInfo(3, 0)
+
+        val targetInfo = DisplayResolveInfo.newDisplayResolveInfo(
+            intent,
+            resolveInfo,
+            "label",
+            "extended info",
+            intent,
+            /* resolveInfoPresentationGetter= */ null)
+        assertThat(targetInfo.isDisplayResolveInfo()).isTrue()
+        assertThat(targetInfo.isMultiDisplayResolveInfo()).isFalse()
+        assertThat(targetInfo.isChooserTargetInfo()).isFalse()
+    }
+
+    @Test
+    fun testNewMultiDisplayResolveInfo() {
+        val intent = Intent(Intent.ACTION_SEND)
+        intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending")
+        intent.setType("text/plain")
+
+        val resolveInfo = ResolverDataProvider.createResolveInfo(3, 0)
+        val firstTargetInfo = DisplayResolveInfo.newDisplayResolveInfo(
+            intent,
+            resolveInfo,
+            "label 1",
+            "extended info 1",
+            intent,
+            /* resolveInfoPresentationGetter= */ null)
+        val secondTargetInfo = DisplayResolveInfo.newDisplayResolveInfo(
+            intent,
+            resolveInfo,
+            "label 2",
+            "extended info 2",
+            intent,
+            /* resolveInfoPresentationGetter= */ null)
+
+        val multiTargetInfo = MultiDisplayResolveInfo.newMultiDisplayResolveInfo(
+            listOf(firstTargetInfo, secondTargetInfo))
+
+        assertThat(multiTargetInfo.isMultiDisplayResolveInfo()).isTrue()
+        assertThat(multiTargetInfo.isDisplayResolveInfo()).isTrue()  // From legacy inheritance.
+        assertThat(multiTargetInfo.isChooserTargetInfo()).isFalse()
+
+        assertThat(multiTargetInfo.getExtendedInfo()).isNull()
+
+        assertThat(multiTargetInfo.getAllDisplayTargets())
+                .containsExactly(firstTargetInfo, secondTargetInfo)
+
+        assertThat(multiTargetInfo.hasSelected()).isFalse()
+        assertThat(multiTargetInfo.getSelectedTarget()).isNull()
+
+        multiTargetInfo.setSelected(1)
+
+        assertThat(multiTargetInfo.hasSelected()).isTrue()
+        assertThat(multiTargetInfo.getSelectedTarget()).isEqualTo(secondTargetInfo)
+
+        // TODO: consider exercising activity-start behavior.
+        // TODO: consider exercising DisplayResolveInfo base class behavior.
+    }
+}
diff --git a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java
new file mode 100644
index 0000000..448718c
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java
@@ -0,0 +1,107 @@
+/*
+ * 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.model;
+
+import static junit.framework.Assert.assertEquals;
+
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ResolveInfo;
+import android.os.Message;
+
+import androidx.test.InstrumentationRegistry;
+
+import com.android.intentresolver.ResolverActivity;
+
+import org.junit.Test;
+
+import java.util.List;
+
+public class AbstractResolverComparatorTest {
+
+    @Test
+    public void testPinned() {
+        ResolverActivity.ResolvedComponentInfo r1 = new ResolverActivity.ResolvedComponentInfo(
+                new ComponentName("package", "class"), new Intent(), new ResolveInfo()
+        );
+        r1.setPinned(true);
+
+        ResolverActivity.ResolvedComponentInfo r2 = new ResolverActivity.ResolvedComponentInfo(
+                new ComponentName("zackage", "zlass"), new Intent(), new ResolveInfo()
+        );
+
+        Context context = InstrumentationRegistry.getTargetContext();
+        AbstractResolverComparator comparator = getTestComparator(context);
+
+        assertEquals("Pinned ranks over unpinned", -1, comparator.compare(r1, r2));
+        assertEquals("Unpinned ranks under pinned", 1, comparator.compare(r2, r1));
+    }
+
+
+    @Test
+    public void testBothPinned() {
+        ResolveInfo pmInfo1 = new ResolveInfo();
+        pmInfo1.activityInfo = new ActivityInfo();
+        pmInfo1.activityInfo.packageName = "aaa";
+
+        ResolverActivity.ResolvedComponentInfo r1 = new ResolverActivity.ResolvedComponentInfo(
+                new ComponentName("package", "class"), new Intent(), pmInfo1);
+        r1.setPinned(true);
+
+        ResolveInfo pmInfo2 = new ResolveInfo();
+        pmInfo2.activityInfo = new ActivityInfo();
+        pmInfo2.activityInfo.packageName = "zzz";
+        ResolverActivity.ResolvedComponentInfo r2 = new ResolverActivity.ResolvedComponentInfo(
+                new ComponentName("zackage", "zlass"), new Intent(), pmInfo2);
+        r2.setPinned(true);
+
+        Context context = InstrumentationRegistry.getTargetContext();
+        AbstractResolverComparator comparator = getTestComparator(context);
+
+        assertEquals("Both pinned should rank alphabetically", -1, comparator.compare(r1, r2));
+    }
+
+    private AbstractResolverComparator getTestComparator(Context context) {
+        Intent intent = new Intent();
+
+        AbstractResolverComparator testComparator =
+                new AbstractResolverComparator(context, intent) {
+
+                    @Override
+                    int compare(ResolveInfo lhs, ResolveInfo rhs) {
+                        // Used for testing pinning, so we should never get here --- the overrides
+                        // should determine the result instead.
+                        return 1;
+                    }
+
+                    @Override
+                    void doCompute(List<ResolverActivity.ResolvedComponentInfo> targets) {}
+
+                    @Override
+                    public float getScore(ComponentName name) {
+                        return 0;
+                    }
+
+                    @Override
+                    void handleResultMessage(Message message) {}
+                };
+        return testComparator;
+    }
+
+}
diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
new file mode 100644
index 0000000..5756a0c
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.shortcuts
+
+import android.app.prediction.AppPredictor
+import android.content.ComponentName
+import android.content.Context
+import android.content.IntentFilter
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.ApplicationInfoFlags
+import android.content.pm.ShortcutManager
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.test.filters.SmallTest
+import com.android.intentresolver.any
+import com.android.intentresolver.chooser.DisplayResolveInfo
+import com.android.intentresolver.createAppTarget
+import com.android.intentresolver.createShareShortcutInfo
+import com.android.intentresolver.createShortcutInfo
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import org.junit.Assert.assertArrayEquals
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.util.concurrent.Executor
+import java.util.function.Consumer
+
+@SmallTest
+class ShortcutLoaderTest {
+    private val appInfo = ApplicationInfo().apply {
+        enabled = true
+        flags = 0
+    }
+    private val pm = mock<PackageManager> {
+        whenever(getApplicationInfo(any(), any<ApplicationInfoFlags>())).thenReturn(appInfo)
+    }
+    private val context = mock<Context> {
+        whenever(packageManager).thenReturn(pm)
+        whenever(createContextAsUser(any(), anyInt())).thenReturn(this)
+    }
+    private val executor = ImmediateExecutor()
+    private val intentFilter = mock<IntentFilter>()
+    private val appPredictor = mock<ShortcutLoader.AppPredictorProxy>()
+    private val callback = mock<Consumer<ShortcutLoader.Result>>()
+
+    @Test
+    fun test_app_predictor_result() {
+        val componentName = ComponentName("pkg", "Class")
+        val appTarget = mock<DisplayResolveInfo> {
+            whenever(resolvedComponentName).thenReturn(componentName)
+        }
+        val appTargets = arrayOf(appTarget)
+        val testSubject = ShortcutLoader(
+            context,
+            appPredictor,
+            UserHandle.of(0),
+            true,
+            intentFilter,
+            executor,
+            executor,
+            callback
+        )
+
+        testSubject.queryShortcuts(appTargets)
+
+        verify(appPredictor, times(1)).requestPredictionUpdate()
+        val appPredictorCallbackCaptor = ArgumentCaptor.forClass(AppPredictor.Callback::class.java)
+        verify(appPredictor, times(1))
+            .registerPredictionUpdates(any(), appPredictorCallbackCaptor.capture())
+
+        val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1)
+        val matchingAppTarget = createAppTarget(matchingShortcutInfo)
+        val shortcuts = listOf(
+            matchingAppTarget,
+            // mismatching shortcut
+            createAppTarget(
+                createShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+            )
+        )
+        appPredictorCallbackCaptor.value.onTargetsAvailable(shortcuts)
+
+        val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java)
+        verify(callback, times(1)).accept(resultCaptor.capture())
+
+        val result = resultCaptor.value
+        assertTrue("An app predictor result is expected", result.isFromAppPredictor)
+        assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets)
+        assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size)
+        assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget)
+        for (shortcut in result.shortcutsByApp[0].shortcuts) {
+            assertEquals(
+                "Wrong AppTarget in the cache",
+                matchingAppTarget,
+                result.directShareAppTargetCache[shortcut]
+            )
+            assertEquals(
+                "Wrong ShortcutInfo in the cache",
+                matchingShortcutInfo,
+                result.directShareShortcutInfoCache[shortcut]
+            )
+        }
+    }
+
+    @Test
+    fun test_shortcut_manager_result() {
+        val componentName = ComponentName("pkg", "Class")
+        val appTarget = mock<DisplayResolveInfo> {
+            whenever(resolvedComponentName).thenReturn(componentName)
+        }
+        val appTargets = arrayOf(appTarget)
+        val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1)
+        val shortcutManagerResult = listOf(
+            ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
+            // mismatching shortcut
+            createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+        )
+        val shortcutManager = mock<ShortcutManager> {
+            whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
+        }
+        whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
+        val testSubject = ShortcutLoader(
+            context,
+            null,
+            UserHandle.of(0),
+            true,
+            intentFilter,
+            executor,
+            executor,
+            callback
+        )
+
+        testSubject.queryShortcuts(appTargets)
+
+        val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java)
+        verify(callback, times(1)).accept(resultCaptor.capture())
+
+        val result = resultCaptor.value
+        assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor)
+        assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets)
+        assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size)
+        assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget)
+        for (shortcut in result.shortcutsByApp[0].shortcuts) {
+            assertTrue(
+                "AppTargets are not expected the cache of a ShortcutManager result",
+                result.directShareAppTargetCache.isEmpty()
+            )
+            assertEquals(
+                "Wrong ShortcutInfo in the cache",
+                matchingShortcutInfo,
+                result.directShareShortcutInfoCache[shortcut]
+            )
+        }
+    }
+
+    @Test
+    fun test_fallback_to_shortcut_manager() {
+        val componentName = ComponentName("pkg", "Class")
+        val appTarget = mock<DisplayResolveInfo> {
+            whenever(resolvedComponentName).thenReturn(componentName)
+        }
+        val appTargets = arrayOf(appTarget)
+        val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1)
+        val shortcutManagerResult = listOf(
+            ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
+            // mismatching shortcut
+            createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+        )
+        val shortcutManager = mock<ShortcutManager> {
+            whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
+        }
+        whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
+        val testSubject = ShortcutLoader(
+            context,
+            appPredictor,
+            UserHandle.of(0),
+            true,
+            intentFilter,
+            executor,
+            executor,
+            callback
+        )
+
+        testSubject.queryShortcuts(appTargets)
+
+        verify(appPredictor, times(1)).requestPredictionUpdate()
+        val appPredictorCallbackCaptor = ArgumentCaptor.forClass(AppPredictor.Callback::class.java)
+        verify(appPredictor, times(1))
+            .registerPredictionUpdates(any(), appPredictorCallbackCaptor.capture())
+        appPredictorCallbackCaptor.value.onTargetsAvailable(emptyList())
+
+        val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java)
+        verify(callback, times(1)).accept(resultCaptor.capture())
+
+        val result = resultCaptor.value
+        assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor)
+        assertArrayEquals("Wrong input app targets in the result", appTargets, result.appTargets)
+        assertEquals("Wrong shortcut count", 1, result.shortcutsByApp.size)
+        assertEquals("Wrong app target", appTarget, result.shortcutsByApp[0].appTarget)
+        for (shortcut in result.shortcutsByApp[0].shortcuts) {
+            assertTrue(
+                "AppTargets are not expected the cache of a ShortcutManager result",
+                result.directShareAppTargetCache.isEmpty()
+            )
+            assertEquals(
+                "Wrong ShortcutInfo in the cache",
+                matchingShortcutInfo,
+                result.directShareShortcutInfoCache[shortcut]
+            )
+        }
+    }
+
+    @Test
+    fun test_do_not_call_services_for_not_running_work_profile() {
+        testDisabledWorkProfileDoNotCallSystem(isUserRunning = false)
+    }
+
+    @Test
+    fun test_do_not_call_services_for_locked_work_profile() {
+        testDisabledWorkProfileDoNotCallSystem(isUserUnlocked = false)
+    }
+
+    @Test
+    fun test_do_not_call_services_if_quite_mode_is_enabled_for_work_profile() {
+        testDisabledWorkProfileDoNotCallSystem(isQuietModeEnabled = true)
+    }
+
+    @Test
+    fun test_call_services_for_not_running_main_profile() {
+        testAlwaysCallSystemForMainProfile(isUserRunning = false)
+    }
+
+    @Test
+    fun test_call_services_for_locked_main_profile() {
+        testAlwaysCallSystemForMainProfile(isUserUnlocked = false)
+    }
+
+    @Test
+    fun test_call_services_if_quite_mode_is_enabled_for_main_profile() {
+        testAlwaysCallSystemForMainProfile(isQuietModeEnabled = true)
+    }
+
+    private fun testDisabledWorkProfileDoNotCallSystem(
+        isUserRunning: Boolean = true,
+        isUserUnlocked: Boolean = true,
+        isQuietModeEnabled: Boolean = false
+    ) {
+        val userHandle = UserHandle.of(10)
+        val userManager = mock<UserManager> {
+            whenever(isUserRunning(userHandle)).thenReturn(isUserRunning)
+            whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked)
+            whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled)
+        }
+        whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager);
+        val appPredictor = mock<ShortcutLoader.AppPredictorProxy>()
+        val callback = mock<Consumer<ShortcutLoader.Result>>()
+        val testSubject = ShortcutLoader(
+            context,
+            appPredictor,
+            userHandle,
+            false,
+            intentFilter,
+            executor,
+            executor,
+            callback
+        )
+
+        testSubject.queryShortcuts(arrayOf<DisplayResolveInfo>(mock()))
+
+        verify(appPredictor, never()).requestPredictionUpdate()
+    }
+
+    private fun testAlwaysCallSystemForMainProfile(
+        isUserRunning: Boolean = true,
+        isUserUnlocked: Boolean = true,
+        isQuietModeEnabled: Boolean = false
+    ) {
+        val userHandle = UserHandle.of(10)
+        val userManager = mock<UserManager> {
+            whenever(isUserRunning(userHandle)).thenReturn(isUserRunning)
+            whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked)
+            whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled)
+        }
+        whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager);
+        val appPredictor = mock<ShortcutLoader.AppPredictorProxy>()
+        val callback = mock<Consumer<ShortcutLoader.Result>>()
+        val testSubject = ShortcutLoader(
+            context,
+            appPredictor,
+            userHandle,
+            true,
+            intentFilter,
+            executor,
+            executor,
+            callback
+        )
+
+        testSubject.queryShortcuts(arrayOf<DisplayResolveInfo>(mock()))
+
+        verify(appPredictor, times(1)).requestPredictionUpdate()
+    }
+}
+
+private class ImmediateExecutor : Executor {
+    override fun execute(r: Runnable) {
+        r.run()
+    }
+}
diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt
new file mode 100644
index 0000000..e0de005
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.shortcuts
+
+import android.app.prediction.AppTarget
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ShortcutInfo
+import android.content.pm.ShortcutManager.ShareShortcutInfo
+import android.service.chooser.ChooserTarget
+import com.android.intentresolver.createAppTarget
+import com.android.intentresolver.createShareShortcutInfo
+import org.junit.Assert.assertEquals
+import org.junit.Assert.assertNotNull
+import org.junit.Test
+
+private const val PACKAGE = "org.package"
+
+class ShortcutToChooserTargetConverterTest {
+    private val testSubject = ShortcutToChooserTargetConverter()
+    private val ranks = arrayOf(3 ,7, 1 ,3)
+    private val shortcuts = ranks
+        .foldIndexed(ArrayList<ShareShortcutInfo>(ranks.size)) { i, acc, rank ->
+            val id = i + 1
+            acc.add(
+                createShareShortcutInfo(
+                    id = "id-$i",
+                    componentName = ComponentName(PACKAGE, "Class$id"),
+                    rank,
+                )
+            )
+            acc
+        }
+
+    @Test
+    fun testConvertToChooserTarget_predictionService() {
+        val appTargets = shortcuts.map { createAppTarget(it.shortcutInfo) }
+        val expectedOrderAllShortcuts = intArrayOf(0, 1, 2, 3)
+        val expectedScoreAllShortcuts = floatArrayOf(1.0f, 0.99f, 0.98f, 0.97f)
+        val appTargetCache = HashMap<ChooserTarget, AppTarget>()
+        val shortcutInfoCache = HashMap<ChooserTarget, ShortcutInfo>()
+
+        var chooserTargets = testSubject.convertToChooserTarget(
+            shortcuts,
+            shortcuts,
+            appTargets,
+            appTargetCache,
+            shortcutInfoCache,
+        )
+
+        assertCorrectShortcutToChooserTargetConversion(
+            shortcuts,
+            chooserTargets,
+            expectedOrderAllShortcuts,
+            expectedScoreAllShortcuts,
+        )
+        assertAppTargetCache(chooserTargets, appTargetCache)
+        assertShortcutInfoCache(chooserTargets, shortcutInfoCache)
+
+        val subset = shortcuts.subList(1, shortcuts.size)
+        val expectedOrderSubset = intArrayOf(1, 2, 3)
+        val expectedScoreSubset = floatArrayOf(0.99f, 0.98f, 0.97f)
+        appTargetCache.clear()
+        shortcutInfoCache.clear()
+
+        chooserTargets = testSubject.convertToChooserTarget(
+            subset,
+            shortcuts,
+            appTargets,
+            appTargetCache,
+            shortcutInfoCache,
+        )
+
+        assertCorrectShortcutToChooserTargetConversion(
+            shortcuts,
+            chooserTargets,
+            expectedOrderSubset,
+            expectedScoreSubset,
+        )
+        assertAppTargetCache(chooserTargets, appTargetCache)
+        assertShortcutInfoCache(chooserTargets, shortcutInfoCache)
+    }
+
+    @Test
+    fun testConvertToChooserTarget_shortcutManager() {
+        val testSubject = ShortcutToChooserTargetConverter()
+        val expectedOrderAllShortcuts = intArrayOf(2, 0, 3, 1)
+        val expectedScoreAllShortcuts = floatArrayOf(1.0f, 0.99f, 0.99f, 0.98f)
+        val shortcutInfoCache = HashMap<ChooserTarget, ShortcutInfo>()
+
+        var chooserTargets = testSubject.convertToChooserTarget(
+            shortcuts,
+            shortcuts,
+            null,
+            null,
+            shortcutInfoCache,
+        )
+
+        assertCorrectShortcutToChooserTargetConversion(
+            shortcuts, chooserTargets,
+            expectedOrderAllShortcuts, expectedScoreAllShortcuts
+        )
+        assertShortcutInfoCache(chooserTargets, shortcutInfoCache)
+
+        val subset: MutableList<ShareShortcutInfo> = java.util.ArrayList()
+        subset.add(shortcuts[1])
+        subset.add(shortcuts[2])
+        subset.add(shortcuts[3])
+        val expectedOrderSubset = intArrayOf(2, 3, 1)
+        val expectedScoreSubset = floatArrayOf(1.0f, 0.99f, 0.98f)
+        shortcutInfoCache.clear()
+
+        chooserTargets = testSubject.convertToChooserTarget(
+            subset,
+            shortcuts,
+            null,
+            null,
+            shortcutInfoCache,
+        )
+
+        assertCorrectShortcutToChooserTargetConversion(
+            shortcuts, chooserTargets,
+            expectedOrderSubset, expectedScoreSubset
+        )
+        assertShortcutInfoCache(chooserTargets, shortcutInfoCache)
+    }
+
+    private fun assertCorrectShortcutToChooserTargetConversion(
+        shortcuts: List<ShareShortcutInfo>,
+        chooserTargets: List<ChooserTarget>,
+        expectedOrder: IntArray,
+        expectedScores: FloatArray,
+    ) {
+        assertEquals("Unexpected ChooserTarget count", expectedOrder.size, chooserTargets.size)
+        for (i in chooserTargets.indices) {
+            val ct = chooserTargets[i]
+            val si = shortcuts[expectedOrder[i]].shortcutInfo
+            val cn = shortcuts[expectedOrder[i]].targetComponent
+            assertEquals(si.id, ct.intentExtras.getString(Intent.EXTRA_SHORTCUT_ID))
+            assertEquals(si.label, ct.title)
+            assertEquals(expectedScores[i], ct.score)
+            assertEquals(cn, ct.componentName)
+        }
+    }
+
+    private fun assertAppTargetCache(
+        chooserTargets: List<ChooserTarget>, cache: Map<ChooserTarget, AppTarget>
+    ) {
+        for (ct in chooserTargets) {
+            val target = cache[ct]
+            assertNotNull("AppTarget is missing", target)
+        }
+    }
+
+    private fun assertShortcutInfoCache(
+        chooserTargets: List<ChooserTarget>, cache: Map<ChooserTarget, ShortcutInfo>
+    ) {
+        for (ct in chooserTargets) {
+            val si = cache[ct]
+            assertNotNull("AppTarget is missing", si)
+        }
+    }
+}