summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp13
-rw-r--r--java/res/layout/chooser_dialog.xml4
-rw-r--r--java/res/layout/chooser_grid.xml11
-rw-r--r--java/res/layout/chooser_grid_preview_file.xml2
-rw-r--r--java/res/layout/chooser_grid_preview_image.xml8
-rw-r--r--java/res/layout/chooser_grid_preview_text.xml2
-rw-r--r--java/res/layout/chooser_list_per_profile.xml5
-rw-r--r--java/res/layout/miniresolver.xml19
-rw-r--r--java/res/layout/resolver_different_item_header.xml3
-rw-r--r--java/res/layout/resolver_list.xml21
-rw-r--r--java/res/layout/resolver_list_with_default.xml19
-rw-r--r--java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java403
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java2539
-rw-r--r--java/src/com/android/intentresolver/ChooserActivityLogger.java174
-rw-r--r--java/src/com/android/intentresolver/ChooserActivityLoggerImpl.java84
-rw-r--r--java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java180
-rw-r--r--java/src/com/android/intentresolver/ChooserContentPreviewUi.java539
-rw-r--r--java/src/com/android/intentresolver/ChooserGridLayoutManager.java4
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java549
-rw-r--r--java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java143
-rw-r--r--java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java4
-rw-r--r--java/src/com/android/intentresolver/ChooserRequestParameters.java441
-rw-r--r--java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java64
-rw-r--r--java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java113
-rw-r--r--java/src/com/android/intentresolver/IntentForwarderActivity.java3
-rw-r--r--java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java154
-rw-r--r--java/src/com/android/intentresolver/NoCrossProfileEmptyStateProvider.java137
-rw-r--r--java/src/com/android/intentresolver/ResolverActivity.java264
-rw-r--r--java/src/com/android/intentresolver/ResolverListAdapter.java180
-rw-r--r--java/src/com/android/intentresolver/ResolverListController.java28
-rw-r--r--java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java115
-rw-r--r--java/src/com/android/intentresolver/ResolverViewPager.java11
-rw-r--r--java/src/com/android/intentresolver/ShortcutSelectionLogic.java193
-rw-r--r--java/src/com/android/intentresolver/SimpleIconFactory.java16
-rw-r--r--java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java114
-rw-r--r--java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java39
-rw-r--r--java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java136
-rw-r--r--java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java43
-rw-r--r--java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java55
-rw-r--r--java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java293
-rw-r--r--java/src/com/android/intentresolver/chooser/TargetInfo.java279
-rw-r--r--java/src/com/android/intentresolver/grid/DirectShareViewHolder.java197
-rw-r--r--java/src/com/android/intentresolver/grid/FooterViewHolder.java (renamed from java/src/com/android/intentresolver/ChooserFlags.java)21
-rw-r--r--java/src/com/android/intentresolver/grid/ItemGroupViewHolder.java76
-rw-r--r--java/src/com/android/intentresolver/grid/ItemViewHolder.java63
-rw-r--r--java/src/com/android/intentresolver/grid/SingleRowViewHolder.java73
-rw-r--r--java/src/com/android/intentresolver/grid/ViewHolderBase.java35
-rw-r--r--java/src/com/android/intentresolver/model/AbstractResolverComparator.java (renamed from java/src/com/android/intentresolver/AbstractResolverComparator.java)35
-rw-r--r--java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java (renamed from java/src/com/android/intentresolver/AppPredictionServiceResolverComparator.java)13
-rw-r--r--java/src/com/android/intentresolver/model/ResolverComparatorModel.java (renamed from java/src/com/android/intentresolver/ResolverComparatorModel.java)3
-rw-r--r--java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java (renamed from java/src/com/android/intentresolver/ResolverRankerServiceResolverComparator.java)26
-rw-r--r--java/src/com/android/intentresolver/shortcuts/AppPredictorFactory.kt67
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java426
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverter.java109
-rw-r--r--java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java1223
-rw-r--r--java/src/com/android/intentresolver/widget/RoundedRectImageView.java131
-rw-r--r--java/tests/Android.bp4
-rw-r--r--java/tests/AndroidManifest.xml4
-rw-r--r--java/tests/AndroidTest.xml4
-rw-r--r--java/tests/res/drawable/test320x240.pngbin0 -> 39533 bytes
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserActivityLoggerFake.java134
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java239
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java57
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt147
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java126
-rw-r--r--java/tests/src/com/android/intentresolver/IChooserWrapper.java3
-rw-r--r--java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt146
-rw-r--r--java/tests/src/com/android/intentresolver/ResolverActivityTest.java912
-rw-r--r--java/tests/src/com/android/intentresolver/ResolverDataProvider.java6
-rw-r--r--java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java227
-rw-r--r--java/tests/src/com/android/intentresolver/ResolverWrapperAdapter.java84
-rw-r--r--java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt289
-rw-r--r--java/tests/src/com/android/intentresolver/TestApplication.kt27
-rw-r--r--java/tests/src/com/android/intentresolver/TestHelpers.kt71
-rw-r--r--java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java1386
-rw-r--r--java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java455
-rw-r--r--java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt186
-rw-r--r--java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java107
-rw-r--r--java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt329
-rw-r--r--java/tests/src/com/android/intentresolver/shortcuts/ShortcutToChooserTargetConverterTest.kt177
80 files changed, 10523 insertions, 4499 deletions
diff --git a/Android.bp b/Android.bp
index 721b8fd4..047820e9 100644
--- a/Android.bp
+++ b/Android.bp
@@ -36,6 +36,7 @@ android_library {
min_sdk_version: "current",
srcs: [
"java/src/**/*.java",
+ "java/src/**/*.kt",
],
resource_dirs: [
"java/res",
@@ -45,7 +46,12 @@ android_library {
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",
+ "guava",
],
lint: {
@@ -67,10 +73,7 @@ android_app {
"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_dialog.xml b/java/res/layout/chooser_dialog.xml
index ff66bbb9..e31712c7 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 a95b0ebe..d863495d 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 6d2e76a0..c3392704 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"
diff --git a/java/res/layout/chooser_grid_preview_image.xml b/java/res/layout/chooser_grid_preview_image.xml
index 96054eb5..4d15bf75 100644
--- a/java/res/layout/chooser_grid_preview_image.xml
+++ b/java/res/layout/chooser_grid_preview_image.xml
@@ -32,7 +32,7 @@
android:paddingBottom="@dimen/chooser_view_spacing"
android:background="?android:attr/colorBackground">
- <view class="com.android.intentresolver.ChooserActivity$RoundedRectImageView"
+ <com.android.intentresolver.widget.RoundedRectImageView
android:id="@androidprv:id/content_preview_image_1_large"
android:layout_width="120dp"
android:layout_height="104dp"
@@ -41,7 +41,7 @@
android:gravity="center"
android:scaleType="centerCrop"/>
- <view class="com.android.intentresolver.ChooserActivity$RoundedRectImageView"
+ <com.android.intentresolver.widget.RoundedRectImageView
android:id="@androidprv:id/content_preview_image_2_large"
android:visibility="gone"
android:layout_width="120dp"
@@ -53,7 +53,7 @@
android:gravity="center"
android:scaleType="centerCrop"/>
- <view class="com.android.intentresolver.ChooserActivity$RoundedRectImageView"
+ <com.android.intentresolver.widget.RoundedRectImageView
android:id="@androidprv:id/content_preview_image_2_small"
android:visibility="gone"
android:layout_width="120dp"
@@ -65,7 +65,7 @@
android:gravity="center"
android:scaleType="centerCrop"/>
- <view class="com.android.intentresolver.ChooserActivity$RoundedRectImageView"
+ <com.android.intentresolver.widget.RoundedRectImageView
android:id="@androidprv:id/content_preview_image_3_small"
android:visibility="gone"
android:layout_width="120dp"
diff --git a/java/res/layout/chooser_grid_preview_text.xml b/java/res/layout/chooser_grid_preview_text.xml
index a9ed71b7..81fdbd08 100644
--- a/java/res/layout/chooser_grid_preview_text.xml
+++ b/java/res/layout/chooser_grid_preview_text.xml
@@ -75,7 +75,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 8d876cdf..1753e2f6 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/miniresolver.xml b/java/res/layout/miniresolver.xml
index ab65aa9b..7e31de57 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 4f801597..79ce6824 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 179c4073..44b14baf 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 341c58e7..192a5983 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/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
index 4f6c0bf1..8b0b10b0 100644
--- a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
@@ -17,26 +17,24 @@ package com.android.intentresolver;
import android.annotation.IntDef;
import android.annotation.Nullable;
+import android.annotation.NonNull;
+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 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
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;
- }
- };
- }
-
- 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;
+ mEmptyStateProvider = emptyStateProvider;
+ mQuietModeManager = quietModeManager;
}
- 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 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
@VisibleForTesting
public UserHandle getCurrentUserHandle() {
- return getActiveListAdapter().mResolverListController.getUserHandle();
+ return getActiveListAdapter().getUserHandle();
}
@Override
@@ -279,8 +236,6 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
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 +263,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
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 +271,18 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
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 +297,88 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* anyway.
*/
void showEmptyResolverListEmptyState(ResolverListAdapter listAdapter) {
- if (maybeShowNoCrossProfileIntentsEmptyState(listAdapter)) {
+ final EmptyState emptyState = mEmptyStateProvider.getEmptyState(listAdapter);
+
+ if (emptyState == null) {
return;
}
- if (maybeShowWorkProfileOffEmptyState(listAdapter)) {
- return;
+
+ 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());
+ });
}
- maybeShowNoAppsAvailableEmptyState(listAdapter);
+
+ showEmptyState(listAdapter, emptyState, clickListener);
}
- private boolean maybeShowNoCrossProfileIntentsEmptyState(ResolverListAdapter listAdapter) {
- if (!shouldShowNoCrossProfileIntentsEmptyState(listAdapter)) {
- return false;
- }
- 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);
+ /**
+ * Class to get user id of the current process
+ */
+ public static class MyUserIdProvider {
+ /**
+ * @return user id of the current process
+ */
+ public int getMyUserId() {
+ return UserHandle.myUserId();
}
- return true;
}
/**
- * Returns {@code true} if the work profile off empty state screen is shown.
+ * Utility class to check if there are cross profile intents, it is in a separate class so
+ * it could be mocked in tests
*/
- 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 CrossProfileIntentsChecker {
+
+ private final ContentResolver mContentResolver;
+
+ public CrossProfileIntentsChecker(@NonNull ContentResolver contentResolver) {
+ mContentResolver = contentResolver;
}
- }
- protected void showEmptyState(ResolverListAdapter activeListAdapter, String title,
- String subtitle) {
- showEmptyState(activeListAdapter, title, subtitle, /* buttonOnClick */ null);
+ /**
+ * 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 +386,9 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
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 +402,6 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
*/
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 +409,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
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 +417,6 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
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 +425,6 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
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)
@@ -599,6 +468,99 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
}
/**
+ * Returns an empty state to show for the current profile page (tab) if necessary.
+ * This could be used e.g. to show a blocker on a tab if device management policy doesn't
+ * allow to use it or there are no apps available.
+ */
+ public interface EmptyStateProvider {
+ /**
+ * When a non-null empty state is returned the corresponding profile page will show
+ * this empty state
+ * @param resolverListAdapter the current adapter
+ */
+ @Nullable
+ default EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ return null;
+ }
+ }
+
+ /**
+ * Empty state provider that combines multiple providers. Providers earlier in the list have
+ * priority, that is if there is a provider that returns non-null empty state then all further
+ * providers will be ignored.
+ */
+ public static class CompositeEmptyStateProvider implements EmptyStateProvider {
+
+ private final EmptyStateProvider[] mProviders;
+
+ public CompositeEmptyStateProvider(EmptyStateProvider... providers) {
+ mProviders = providers;
+ }
+
+ @Nullable
+ @Override
+ public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
+ for (EmptyStateProvider provider : mProviders) {
+ EmptyState emptyState = provider.getEmptyState(resolverListAdapter);
+ if (emptyState != null) {
+ return emptyState;
+ }
+ }
+ return null;
+ }
+ }
+
+ /**
+ * Describes how the blocked empty state should look like for a profile tab
+ */
+ public interface EmptyState {
+ /**
+ * Title that will be shown on the empty state
+ */
+ @Nullable
+ default String getTitle() { return null; }
+
+ /**
+ * Subtitle that will be shown underneath the title on the empty state
+ */
+ @Nullable
+ default String getSubtitle() { return null; }
+
+ /**
+ * If non-null then a button will be shown and this listener will be called
+ * when the button is clicked
+ */
+ @Nullable
+ default ClickListener getButtonClickListener() { return null; }
+
+ /**
+ * If true then default text ('No apps can perform this action') and style for the empty
+ * state will be applied, title and subtitle will be ignored.
+ */
+ default boolean useDefaultEmptyView() { return false; }
+
+ /**
+ * Returns true if for this empty state we should skip rebuilding of the apps list
+ * for this tab.
+ */
+ default boolean shouldSkipDataRebuild() { return false; }
+
+ /**
+ * Called when empty state is shown, could be used e.g. to track analytics events
+ */
+ default void onEmptyStateShown() {}
+
+ interface ClickListener {
+ void onClick(TabControl currentTab);
+ }
+
+ interface TabControl {
+ void showSpinner();
+ }
+ }
+
+
+ /**
* Listener for when the user switches on the work profile from the work tab.
*/
interface OnSwitchOnWorkSelectedListener {
@@ -611,14 +573,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
/**
* 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 +583,15 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
* 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 14d77427..fe1df879 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -16,12 +16,16 @@
package com.android.intentresolver;
-import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
+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 java.lang.annotation.RetentionPolicy.SOURCE;
+import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
@@ -32,13 +36,10 @@ 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,50 +51,38 @@ import android.content.IntentSender;
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.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.View.MeasureSpec;
@@ -102,36 +91,44 @@ 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 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.ResolverListAdapter.ViewHolder;
-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.SelectableTargetInfo.SelectableTargetInfoCommunicator;
import com.android.intentresolver.chooser.TargetInfo;
-
+import com.android.intentresolver.grid.DirectShareViewHolder;
+import com.android.intentresolver.grid.FooterViewHolder;
+import com.android.intentresolver.grid.ItemGroupViewHolder;
+import com.android.intentresolver.grid.ItemViewHolder;
+import com.android.intentresolver.grid.SingleRowViewHolder;
+import com.android.intentresolver.grid.ViewHolderBase;
+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.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;
@@ -139,7 +136,6 @@ 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 +144,19 @@ import java.util.HashMap;
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 +165,6 @@ public class ChooserActivity extends ResolverActivity implements
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,24 +179,21 @@ public class ChooserActivity extends ResolverActivity implements
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;
@@ -225,9 +211,6 @@ public class ChooserActivity extends ResolverActivity implements
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,
@@ -241,75 +224,65 @@ public class ChooserActivity extends ResolverActivity implements
* 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;
+ public 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` *primarily* 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." Unfortunately, for now we also have
+ * a vestigial design where ChooserActivity.onCreate() can invalidate a request, but it still
+ * has to call up to ResolverActivity.onCreate() before closing, and the base method delegates
+ * back down to other methods in ChooserActivity that aren't really relevant if we're closing
+ * (and so they'd normally want to assume it was a valid "creation," with non-null parameters).
+ * Any client null checks are workarounds for this condition that can be removed once that
+ * design is cleaned up. */
+ @Nullable
+ private ChooserRequestParameters mChooserRequest;
- private Bundle mReplacementExtras;
- private IntentSender mChosenComponentSender;
- private IntentSender mRefinementIntentSender;
- private RefinementResultReceiver mRefinementResultReceiver;
- private ChooserTarget[] mCallerChooserTargets;
- private ComponentName[] mFilteredComponentNames;
+ private boolean mShouldDisplayLandscape;
+ // statsd logger wrapper
+ protected ChooserActivityLogger mChooserActivityLogger;
- private Intent mReferrerFillInIntent;
+ @Nullable
+ private RefinementResultReceiver mRefinementResultReceiver;
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 {
- }
-
- // 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;
- private ContentPreviewCoordinator mPreviewCoord;
+ private final ExecutorService mBackgroundThreadPoolExecutor = Executors.newFixedThreadPool(5);
+
+ @Nullable
+ private ChooserContentPreviewCoordinator mPreviewCoordinator;
+
private int mScrollStatus = SCROLL_STATUS_IDLE;
@VisibleForTesting
@@ -321,210 +294,30 @@ public class ChooserActivity extends ResolverActivity implements
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 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);
+ private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>();
- 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);
- });
- }
+ public ChooserActivity() {}
- private void cancelLoads() {
- mHandler.removeMessages(IMAGE_LOAD_INTO_VIEW);
- mHandler.removeMessages(IMAGE_LOAD_TIMEOUT);
- }
+ private void setupPreDrawForSharedElementTransition(View v) {
+ v.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ v.getViewTreeObserver().removeOnPreDrawListener(this);
- 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;
+ if (!mRemoveSharedElements && isActivityTransitionRunning()) {
+ // Disable the window animations as it interferes with the transition animation.
+ getWindow().setWindowAnimations(0);
}
- mRemoveSharedElements = true;
mEnterTransitionAnimationDelegate.markImagePreviewReady();
+ return true;
}
- }
-
- 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);
- }
- }
- };
+ private void hideContentPreview() {
+ mRemoveSharedElements = true;
+ mEnterTransitionAnimationDelegate.markImagePreviewReady();
+ }
@Override
protected void onCreate(Bundle savedInstanceState) {
@@ -532,153 +325,49 @@ public class ChooserActivity extends ResolverActivity implements
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);
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,
+ this::hideContentPreview,
+ this::setupPreDrawForSharedElementTransition);
+
+ 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;
@@ -686,14 +375,15 @@ public class ChooserActivity extends ResolverActivity implements
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_SHARESHEET_MIMETYPE, mChooserRequest.getTargetType())
.addTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS, 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,14 +412,15 @@ public class ChooserActivity extends ResolverActivity implements
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
@@ -750,52 +441,51 @@ public class ChooserActivity extends ResolverActivity implements
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 +519,41 @@ public class ChooserActivity extends ResolverActivity implements
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 +568,10 @@ public class ChooserActivity extends ResolverActivity implements
return new ChooserMultiProfilePagerAdapter(
/* context */ this,
adapter,
- getPersonalProfileUserHandle(),
+ createEmptyStateProvider(/* workProfileUserHandle= */ null),
+ mQuietModeManager,
/* workProfileUserHandle= */ null,
- isSendAction(getTargetIntent()), mMaxTargetsPerRow);
+ mMaxTargetsPerRow);
}
private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles(
@@ -871,10 +597,11 @@ public class ChooserActivity extends ResolverActivity implements
/* context */ this,
personalAdapter,
workAdapter,
+ createEmptyStateProvider(/* workProfileUserHandle= */ getWorkProfileUserHandle()),
+ mQuietModeManager,
selectedProfile,
- getPersonalProfileUserHandle(),
getWorkProfileUserHandle(),
- isSendAction(getTargetIntent()), mMaxTargetsPerRow);
+ mMaxTargetsPerRow);
}
private int findSelectedProfile() {
@@ -897,13 +624,6 @@ public class ChooserActivity extends ResolverActivity implements
}
/**
- * 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)
@@ -1068,10 +788,55 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- 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.ActionButtonFactory buttonFactory =
+ new ChooserContentPreviewUi.ActionButtonFactory() {
+ @Override
+ public Button createCopyButton() {
+ return ChooserActivity.this.createCopyButton();
+ }
+
+ @Override
+ public Button createEditButton() {
+ return ChooserActivity.this.createEditButton(targetIntent);
+ }
+
+ @Override
+ public Button createNearbyButton() {
+ return ChooserActivity.this.createNearbyButton(targetIntent);
+ }
+ };
+
+ ViewGroup layout = ChooserContentPreviewUi.displayContentPreview(
+ previewType,
+ targetIntent,
+ getResources(),
+ getLayoutInflater(),
+ buttonFactory,
+ parent,
+ previewCoordinator,
+ getContentResolver(),
+ this::isImageType);
+
+ if (layout != null) {
+ adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
+ }
+ if (previewType != ChooserContentPreviewUi.CONTENT_PREVIEW_IMAGE) {
+ mEnterTransitionAnimationDelegate.markImagePreviewReady();
+ }
+
+ return layout;
}
@VisibleForTesting
@@ -1108,6 +873,19 @@ public class ChooserActivity extends ResolverActivity implements
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,8 +894,13 @@ public class ChooserActivity extends ResolverActivity implements
return null;
}
- final DisplayResolveInfo dri = new DisplayResolveInfo(
- originalIntent, ri, getString(com.android.internal.R.string.screenshot_edit), "", resolveIntent, null);
+ final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent,
+ ri,
+ getString(com.android.internal.R.string.screenshot_edit),
+ "",
+ resolveIntent,
+ null);
dri.setDisplayIcon(getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));
return dri;
}
@@ -1160,7 +943,7 @@ public class ChooserActivity extends ResolverActivity implements
icon = ri.loadIcon(getPackageManager());
}
- final DisplayResolveInfo dri = new DisplayResolveInfo(
+ final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
originalIntent, ri, name, "", resolveIntent, null);
dri.setDisplayIcon(icon);
return dri;
@@ -1192,7 +975,7 @@ public class ChooserActivity extends ResolverActivity implements
if (ti == null) return null;
final Button b = createActionButton(
- ti.getDisplayIcon(this),
+ ti.getDisplayIcon(),
ti.getDisplayLabel(),
(View unused) -> {
// Log share completion via nearby
@@ -1215,7 +998,7 @@ public class ChooserActivity extends ResolverActivity implements
if (ti == null) return null;
final Button b = createActionButton(
- ti.getDisplayIcon(this),
+ ti.getDisplayIcon(),
ti.getDisplayLabel(),
(View unused) -> {
// Log share completion via edit
@@ -1259,154 +1042,6 @@ public class ChooserActivity extends ResolverActivity implements
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,41 +1051,9 @@ public class ChooserActivity extends ResolverActivity implements
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);
+ @VisibleForTesting
+ protected boolean isImageType(String mimeType) {
+ return mimeType != null && mimeType.startsWith("image/");
}
private void logContentPreviewWarning(Uri uri) {
@@ -1461,130 +1064,6 @@ public class ChooserActivity extends ResolverActivity implements
+ "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 +1093,29 @@ public class ChooserActivity extends ResolverActivity implements
mRefinementResultReceiver.destroy();
mRefinementResultReceiver = null;
}
- mChooserHandler.removeAllMessages();
- if (mPreviewCoord != null) mPreviewCoord.cancelLoads();
+ mBackgroundThreadPoolExecutor.shutdownNow();
+
+ destroyProfileRecords();
+ }
- mChooserMultiProfilePagerAdapter.getActiveListAdapter().destroyAppPredictor();
- if (mChooserMultiProfilePagerAdapter.getInactiveListAdapter() != null) {
- mChooserMultiProfilePagerAdapter.getInactiveListAdapter().destroyAppPredictor();
+ 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 +1136,13 @@ public class ChooserActivity extends ResolverActivity implements
@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 +1153,17 @@ public class ChooserActivity extends ResolverActivity implements
@Override
public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) {
- if (mCallerChooserTargets != null && mCallerChooserTargets.length > 0) {
+ if (mChooserRequest == null) {
+ return;
+ }
+
+ if (mChooserRequest.getCallerChooserTargets().size() > 0) {
mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults(
/* origTarget */ null,
- Lists.newArrayList(mCallerChooserTargets),
+ mChooserRequest.getCallerChooserTargets(),
TARGET_TYPE_DEFAULT,
- /* directShareShortcutInfoCache */ null);
+ /* directShareShortcutInfoCache */ Collections.emptyMap(),
+ /* directShareAppTargetCache */ Collections.emptyMap());
}
}
@@ -1701,57 +1192,34 @@ public class ChooserActivity extends ResolverActivity implements
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 +1238,8 @@ public class ChooserActivity extends ResolverActivity implements
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 +1256,20 @@ public class ChooserActivity extends ResolverActivity implements
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;
}
}
@@ -1823,26 +1287,15 @@ public class ChooserActivity extends ResolverActivity implements
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;
- }
+ directTargetHashed = targetInfo.getHashedTargetIdForMetrics(this);
+ directTargetAlsoRanked = getRankedPosition(targetInfo);
+
+ numCallerProvided = mChooserRequest.getCallerChooserTargets().size();
getChooserActivityLogger().logShareTargetSelected(
SELECTION_TYPE_SERVICE,
targetInfo.getResolveInfo().activityInfo.processName,
value,
- selectableTargetInfo.isPinned()
+ targetInfo.isPinned()
);
break;
case ChooserListAdapter.TARGET_CALLER:
@@ -1900,16 +1353,16 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- 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 +1386,11 @@ public class ChooserActivity extends ResolverActivity implements
}
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 +1424,19 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- @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(int logCategory, 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);
+ final int apiLatency =
+ (int) (SystemClock.elapsedRealtime() - profileRecord.loadingStartTime);
getMetricsLogger().write(new LogMaker(logCategory).setSubtype(apiLatency));
}
void updateModelAndChooserCounts(TargetInfo info) {
- if (info != null && info instanceof MultiDisplayResolveInfo) {
+ if (info != null && info.isMultiDisplayResolveInfo()) {
info = ((MultiDisplayResolveInfo) info).getSelectedTarget();
}
if (info != null) {
@@ -2200,31 +1457,35 @@ public class ChooserActivity extends ResolverActivity implements
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(
- mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
- if (directShareAppPredictor == null) {
+ // Send DS target impression info to AppPredictor, only when user chooses app share.
+ if (targetInfo.isChooserTargetInfo()) {
return;
}
- // Send DS target impression info to AppPredictor, only when user chooses app share.
- if (targetInfo instanceof ChooserTargetInfo) {
+
+ AppPredictor directShareAppPredictor = getAppPredictor(
+ mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
+ if (directShareAppPredictor == null) {
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 +1493,18 @@ public class ChooserActivity extends ResolverActivity implements
}
private void sendClickToAppPredictor(TargetInfo targetInfo) {
- AppPredictor directShareAppPredictor = getAppPredictorForDirectShareIfEnabled(
- mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
- if (directShareAppPredictor == null) {
+ if (!targetInfo.isChooserTargetInfo()) {
return;
}
- if (!(targetInfo instanceof ChooserTargetInfo)) {
+
+ AppPredictor directShareAppPredictor = getAppPredictor(
+ mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
+ if (directShareAppPredictor == null) {
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 +1513,9 @@ public class ChooserActivity extends ResolverActivity implements
}
@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) {
@@ -2386,7 +1583,7 @@ public class ChooserActivity extends ResolverActivity implements
protected ChooserActivityLogger getChooserActivityLogger() {
if (mChooserActivityLogger == null) {
- mChooserActivityLogger = new ChooserActivityLoggerImpl();
+ mChooserActivityLogger = new ChooserActivityLogger();
}
return mChooserActivityLogger;
}
@@ -2405,56 +1602,139 @@ public class ChooserActivity extends ResolverActivity implements
@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(),
@@ -2489,23 +1769,6 @@ public class ChooserActivity extends ResolverActivity implements
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 +1795,8 @@ public class ChooserActivity extends ResolverActivity implements
}
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 +1902,7 @@ public class ChooserActivity extends ResolverActivity implements
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 =
@@ -2691,43 +1954,12 @@ public class ChooserActivity extends ResolverActivity implements
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());
- }
- }
-
@Override // ResolverListCommunicator
public void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
mChooserMultiProfilePagerAdapter.getActiveListAdapter().notifyDataSetChanged();
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 +1974,7 @@ public class ChooserActivity extends ResolverActivity implements
.setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage());
}
- if (chooserListAdapter.mDisplayList == null
- || chooserListAdapter.mDisplayList.isEmpty()) {
+ if (chooserListAdapter.getDisplayResolveInfoCount() == 0) {
chooserListAdapter.notifyDataSetChanged();
} else {
chooserListAdapter.updateAlphabeticalList();
@@ -2757,41 +1988,48 @@ public class ChooserActivity extends ResolverActivity implements
}
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);
- }
+ logDirectShareTargetReceived(
+ MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER,
+ userHandle);
- @VisibleForTesting
- protected boolean isQuietModeEnabled(UserHandle userHandle) {
- UserManager userManager = getSystemService(UserManager.class);
- return userManager.isQuietModeEnabled(userHandle);
+ sendVoiceChoicesIfNeeded();
+ getChooserActivityLogger().logSharesheetDirectLoadComplete();
}
private void setupScrollListener() {
@@ -2855,24 +2093,6 @@ public class ChooserActivity extends ResolverActivity implements
});
}
- @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 +2107,14 @@ public class ChooserActivity extends ResolverActivity implements
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 +2125,8 @@ public class ChooserActivity extends ResolverActivity implements
// 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);
}
}
@@ -2932,19 +2160,22 @@ public class ChooserActivity extends ResolverActivity implements
private void logActionShareWithPreview() {
Intent targetIntent = getTargetIntent();
- int previewType = findPreferredContentPreview(targetIntent, getContentResolver());
+ int previewType = ChooserContentPreviewUi.findPreferredContentPreview(
+ targetIntent, getContentResolver(), this::isImageType);
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 +2192,6 @@ public class ChooserActivity extends ResolverActivity implements
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.
@@ -3107,16 +2275,63 @@ public class ChooserActivity extends ResolverActivity implements
* handled by {@link ChooserListAdapter}
*/
@VisibleForTesting
- public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
- private ChooserListAdapter mChooserListAdapter;
- private final LayoutInflater mLayoutInflater;
+ public static final class ChooserGridAdapter extends
+ RecyclerView.Adapter<RecyclerView.ViewHolder> {
- private DirectShareViewHolder mDirectShareViewHolder;
- private int mChooserTargetWidth = 0;
- private boolean mShowAzLabelIfPoss;
- private boolean mLayoutRequested = false;
-
- private int mFooterHeight = 0;
+ /**
+ * 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.
+ */
+ 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;
@@ -3128,12 +2343,44 @@ public class ChooserActivity extends ResolverActivity implements
private static final int NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20;
- ChooserGridAdapter(ChooserListAdapter wrappedAdapter) {
+ 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;
+
+ ChooserGridAdapter(
+ Context context,
+ ChooserActivityDelegate chooserActivityDelegate,
+ ChooserListAdapter wrappedAdapter,
+ boolean shouldShowContentPreview,
+ int maxTargetsPerRow,
+ int numSheetExpansions) {
super();
+
+ mChooserActivityDelegate = chooserActivityDelegate;
+
mChooserListAdapter = wrappedAdapter;
- mLayoutInflater = LayoutInflater.from(ChooserActivity.this);
+ mLayoutInflater = LayoutInflater.from(context);
- mShowAzLabelIfPoss = getNumSheetExpansions() < NUM_EXPANSIONS_TO_HIDE_AZ_LABEL;
+ 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
@@ -3166,7 +2413,7 @@ public class ChooserActivity extends ResolverActivity implements
}
// Limit width to the maximum width of the chooser activity
- int maxWidth = getResources().getDimensionPixelSize(R.dimen.chooser_width);
+ int maxWidth = mChooserWidthPixels;
width = Math.min(maxWidth, width);
int newWidth = width / mMaxTargetsPerRow;
@@ -3178,22 +2425,6 @@ public class ChooserActivity extends ResolverActivity implements
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()
@@ -3214,11 +2445,11 @@ public class ChooserActivity extends ResolverActivity implements
public int getSystemRowCount() {
// For the tabbed case we show the sticky content preview above the tabs,
// please refer to shouldShowStickyContentPreview
- if (shouldShowTabs()) {
+ if (mChooserActivityDelegate.shouldShowTabs()) {
return 0;
}
- if (!isSendAction(getTargetIntent())) {
+ if (!mShouldShowContentPreview) {
return 0;
}
@@ -3230,7 +2461,7 @@ public class ChooserActivity extends ResolverActivity implements
}
public int getProfileRowCount() {
- if (shouldShowTabs()) {
+ if (mChooserActivityDelegate.shouldShowTabs()) {
return 0;
}
return mChooserListAdapter.getOtherProfile() == null ? 0 : 1;
@@ -3249,8 +2480,7 @@ public class ChooserActivity extends ResolverActivity implements
// 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()) {
+ if (mShouldShowContentPreview && !ActivityManager.isLowRamDeviceStatic()) {
return 1;
}
return 0;
@@ -3278,14 +2508,29 @@ public class ChooserActivity extends ResolverActivity implements
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case VIEW_TYPE_CONTENT_PREVIEW:
- return new ItemViewHolder(createContentPreviewView(parent), false, viewType);
+ return new ItemViewHolder(
+ mChooserActivityDelegate.buildContentPreview(parent),
+ viewType,
+ null,
+ null);
case VIEW_TYPE_PROFILE:
- return new ItemViewHolder(createProfileView(parent), false, viewType);
+ return new ItemViewHolder(
+ createProfileView(parent),
+ viewType,
+ null,
+ null);
case VIEW_TYPE_AZ_LABEL:
- return new ItemViewHolder(createAzLabelView(parent), false, viewType);
+ return new ItemViewHolder(
+ createAzLabelView(parent),
+ viewType,
+ null,
+ null);
case VIEW_TYPE_NORMAL:
return new ItemViewHolder(
- mChooserListAdapter.createView(parent), true, viewType);
+ mChooserListAdapter.createView(parent),
+ viewType,
+ mChooserActivityDelegate::onTargetSelected,
+ mChooserActivityDelegate::onTargetLongPressed);
case VIEW_TYPE_DIRECT_SHARE:
case VIEW_TYPE_CALLER_AND_RANK:
return createItemGroupViewHolder(viewType, parent);
@@ -3345,9 +2590,7 @@ public class ChooserActivity extends ResolverActivity implements
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();
+ mChooserActivityDelegate.updateProfileViewButton(profileRow);
return profileRow;
}
@@ -3369,17 +2612,13 @@ public class ChooserActivity extends ResolverActivity implements
v.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
- startSelected(holder.getItemIndex(column), false, true);
+ mChooserActivityDelegate.onTargetSelected(holder.getItemIndex(column));
}
});
// 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);
- }
+ mChooserActivityDelegate.onTargetLongPressed(holder.getItemIndex(column));
return true;
});
@@ -3440,7 +2679,7 @@ public class ChooserActivity extends ResolverActivity implements
mDirectShareViewHolder = new DirectShareViewHolder(parentGroup,
Lists.newArrayList(row1, row2), mMaxTargetsPerRow, viewType,
- mChooserMultiProfilePagerAdapter::getActiveListAdapter);
+ mChooserActivityDelegate::getValidTargetCount);
loadViewsIntoGroup(mDirectShareViewHolder);
return mDirectShareViewHolder;
@@ -3480,7 +2719,7 @@ public class ChooserActivity extends ResolverActivity implements
void bindItemViewHolder(int position, ItemViewHolder holder) {
View v = holder.itemView;
int listPosition = getListPosition(position);
- holder.mListPosition = listPosition;
+ holder.setListPosition(listPosition);
mChooserListAdapter.bindView(listPosition, v);
}
@@ -3495,7 +2734,7 @@ public class ChooserActivity extends ResolverActivity implements
end--;
}
- if (end == start && mChooserListAdapter.getItem(start) instanceof EmptyTargetInfo) {
+ 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) {
@@ -3506,9 +2745,7 @@ public class ChooserActivity extends ResolverActivity implements
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);
+ textView.setTranslationY(mChooserRowTextOptionTranslatePixelSize);
ValueAnimator translateAnim = ObjectAnimator.ofFloat(textView, "translationY",
0.0f);
translateAnim.setInterpolator(new DecelerateInterpolator(1.0f));
@@ -3538,7 +2775,7 @@ public class ChooserActivity extends ResolverActivity implements
position -= getSystemRowCount() + getProfileRowCount();
final int serviceCount = mChooserListAdapter.getServiceTargetCount();
- final int serviceRows = (int) Math.ceil((float) serviceCount / getMaxRankedTargets());
+ final int serviceRows = (int) Math.ceil((float) serviceCount / mMaxTargetsPerRow);
if (position < serviceRows) {
return position * mMaxTargetsPerRow;
}
@@ -3560,9 +2797,8 @@ public class ChooserActivity extends ResolverActivity implements
public void handleScroll(View v, int y, int oldy) {
boolean canExpandDirectShare = canExpandDirectShare();
if (mDirectShareViewHolder != null && canExpandDirectShare) {
- mDirectShareViewHolder.handleScroll(
- mChooserMultiProfilePagerAdapter.getActiveAdapterView(), y, oldy,
- mMaxTargetsPerRow);
+ mChooserActivityDelegate.handleScrollToExpandDirectShare(
+ mDirectShareViewHolder, y, oldy);
}
}
@@ -3587,273 +2823,7 @@ public class ChooserActivity extends ResolverActivity implements
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;
+ mChooserActivityDelegate.updateDirectShareExpansion(mDirectShareViewHolder);
}
}
@@ -3918,110 +2888,6 @@ public class ChooserActivity extends ResolverActivity implements
}
/**
- * 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.
*/
@@ -4071,11 +2937,13 @@ public class ChooserActivity extends ResolverActivity implements
*/
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 +2967,9 @@ public class ChooserActivity extends ResolverActivity implements
@Override
public void cancel() {
- mRootView.setAlpha(mFromAlpha);
+ if (mRootView != null) {
+ mRootView.setAlpha(mFromAlpha);
+ }
cleanup();
super.cancel();
}
@@ -4110,9 +2980,10 @@ public class ChooserActivity extends ResolverActivity implements
@Override
public void onAnimationEnd(Animation animation) {
- if (mActivity != null) {
- mActivity.finish();
- cleanup();
+ Activity activity = mActivity;
+ cleanup();
+ if (activity != null) {
+ activity.finish();
}
}
@@ -4128,14 +2999,34 @@ public class ChooserActivity extends ResolverActivity implements
@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 ProfileRecord(
+ @Nullable AppPredictor appPredictor,
+ @Nullable ShortcutLoader shortcutLoader) {
+ this.appPredictor = appPredictor;
+ this.shortcutLoader = shortcutLoader;
+ }
- private boolean shouldNearbyShareBeIncludedAsActionButton() {
- return !shouldNearbyShareBeFirstInRankedRow();
+ 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 1daae01a..811d5f3e 100644
--- a/java/src/com/android/intentresolver/ChooserActivityLogger.java
+++ b/java/src/com/android/intentresolver/ChooserActivityLogger.java
@@ -19,45 +19,116 @@ package com.android.intentresolver;
import android.content.Intent;
import android.provider.MediaStore;
+import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.InstanceIdSequence;
import com.android.internal.logging.UiEvent;
import com.android.internal.logging.UiEventLogger;
+import com.android.internal.logging.UiEventLoggerImpl;
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 {
+public class ChooserActivityLogger {
+ /**
+ * 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;
+
+ public ChooserActivityLogger() {
+ this(new UiEventLoggerImpl(), new DefaultFrameworkStatsLogger());
+ }
+
+ @VisibleForTesting
+ ChooserActivityLogger(UiEventLogger uiEventLogger, FrameworkStatsLogger frameworkLogger) {
+ mUiEventLogger = uiEventLogger;
+ mFrameworkStatsLogger = frameworkLogger;
+ }
+
/** 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 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. */
- void logShareTargetSelected(int targetType, String packageName, int positionPicked,
- boolean isPinned);
+ public void logShareTargetSelected(int targetType, String packageName, int positionPicked,
+ boolean isPinned) {
+ 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);
+ }
/** 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 +136,12 @@ public interface ChooserActivityLogger {
* 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 +149,14 @@ public interface ChooserActivityLogger {
/**
* 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 +165,26 @@ public interface ChooserActivityLogger {
* @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.
@@ -201,13 +285,13 @@ public interface ChooserActivityLogger {
/**
* 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 +302,7 @@ public interface ChooserActivityLogger {
* 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 +327,48 @@ public interface ChooserActivityLogger {
return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_DEFAULT;
}
}
+
+ 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 08a345bc..00000000
--- 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 00000000..fdc58170
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java
@@ -0,0 +1,180 @@
+/*
+ * 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.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.annotation.NonNull;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.Handler;
+import android.util.Size;
+import android.view.View;
+import android.view.animation.DecelerateInterpolator;
+
+import androidx.annotation.MainThread;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.widget.RoundedRectImageView;
+
+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.Callable;
+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,
+ Consumer<View> onSingleImageSuccessCallback) {
+ this.mBackgroundExecutor = MoreExecutors.listeningDecorator(backgroundExecutor);
+ this.mChooserActivity = chooserActivity;
+ this.mOnFailCallback = onFailCallback;
+ this.mOnSingleImageSuccessCallback = onSingleImageSuccessCallback;
+
+ this.mImageLoadTimeoutMillis =
+ chooserActivity.getResources().getInteger(R.integer.config_shortAnimTime);
+ }
+
+ @Override
+ public void loadUriIntoView(
+ final Callable<RoundedRectImageView> deferredImageViewProvider,
+ final Uri imageUri,
+ final int extraImageCount) {
+ final int size = mChooserActivity.getResources().getDimensionPixelSize(
+ R.dimen.chooser_preview_image_max_dimen);
+
+ 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 {
+ onLoadCompleted(
+ deferredImageViewProvider.call(),
+ loadedBitmap,
+ extraImageCount);
+ } catch (Exception e) { /* unimportant */ }
+ }
+
+ @Override
+ public void onFailure(Throwable t) {}
+ },
+ mHandler::post);
+ }
+
+ private static final int IMAGE_FADE_IN_MILLIS = 150;
+
+ private final ChooserActivity mChooserActivity;
+ private final ListeningExecutorService mBackgroundExecutor;
+ private final Runnable mOnFailCallback;
+ private final Consumer<View> mOnSingleImageSuccessCallback;
+ 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 RoundedRectImageView imageView,
+ @Nullable Bitmap loadedBitmap,
+ int extraImageCount) {
+ if (mChooserActivity.isFinishing()) {
+ return;
+ }
+
+ // TODO: legacy logic didn't handle a possible null view; handle the same as other
+ // single-image failures for now (i.e., this is also a factor in the "race" TODO below).
+ boolean thisLoadSucceeded = (imageView != null) && (loadedBitmap != null);
+ mAtLeastOneLoaded |= thisLoadSucceeded;
+
+ // TODO: this looks like a race condition. We may know that this specific image failed (i.e.
+ // it got a null Bitmap), but we'll only report that to the client (thereby failing out our
+ // pending loads) if we haven't yet succeeded in loading some other non-null Bitmap. But
+ // there could be other pending loads that would've returned non-null within the timeout
+ // window, except they end up (effectively) cancelled because this one single-image load
+ // "finished" (failed) faster. The outcome of that race may be fairly predictable (since we
+ // *might* imagine that the nulls would usually "load" faster?), but it's not guaranteed
+ // since the loads are queued in a thread pool (i.e., in parallel). One option for more
+ // deterministic behavior: don't signal the failure callback on a single-image load unless
+ // there are no other loads currently pending.
+ boolean wholeBatchFailed = !mAtLeastOneLoaded;
+
+ if (thisLoadSucceeded) {
+ onImageLoadedSuccessfully(loadedBitmap, imageView, extraImageCount);
+ } else if (imageView != null) {
+ imageView.setVisibility(View.GONE);
+ }
+
+ if (wholeBatchFailed) {
+ mOnFailCallback.run();
+ }
+ }
+
+ @MainThread
+ private void onImageLoadedSuccessfully(
+ @NonNull Bitmap image,
+ RoundedRectImageView imageView,
+ int extraImageCount) {
+ 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();
+
+ if (extraImageCount > 0) {
+ imageView.setExtraImageCount(extraImageCount);
+ }
+
+ mOnSingleImageSuccessCallback.accept(imageView);
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
new file mode 100644
index 00000000..22ff55db
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
@@ -0,0 +1,539 @@
+/*
+ * 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.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.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.ViewGroup.LayoutParams;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+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.concurrent.Callable;
+
+/**
+ * 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 {
+ /**
+ * 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 viewProvider A delegate that will be called exactly once upon completion of the
+ * load, from the UI thread, to provide the {@link RoundedRectImageView} that should be
+ * populated with the result (if the load was successful) or hidden (if the load failed). If
+ * this returns null, the load is discarded as a failure.
+ * @param imageUri The {@link Uri} of the image to load.
+ * @param extraImages The "extra image count" to set on the {@link RoundedRectImageView}
+ * if the image loads successfully.
+ *
+ * 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 loadUriIntoView(
+ Callable<RoundedRectImageView> viewProvider, Uri imageUri, int extraImages);
+ }
+
+ /**
+ * 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 ActionButtonFactory {
+ /** Create a button that copies the share content to the clipboard. */
+ Button createCopyButton();
+
+ /** Create a button that opens the share content in a system-default editor. */
+ Button createEditButton();
+
+ /** Create a "Share to Nearby" button. */
+ Button 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,
+ ActionButtonFactory buttonFactory,
+ ViewGroup parent,
+ ContentPreviewCoordinator previewCoord,
+ ContentResolver contentResolver,
+ ImageMimeTypeClassifier imageClassifier) {
+ ViewGroup layout = null;
+
+ switch (previewType) {
+ case CONTENT_PREVIEW_TEXT:
+ layout = displayTextContentPreview(
+ targetIntent,
+ resources,
+ layoutInflater,
+ buttonFactory,
+ parent,
+ previewCoord);
+ break;
+ case CONTENT_PREVIEW_IMAGE:
+ layout = displayImageContentPreview(
+ targetIntent,
+ resources,
+ layoutInflater,
+ buttonFactory,
+ parent,
+ previewCoord,
+ contentResolver,
+ imageClassifier);
+ break;
+ case CONTENT_PREVIEW_FILE:
+ layout = displayFileContentPreview(
+ targetIntent,
+ resources,
+ layoutInflater,
+ buttonFactory,
+ parent,
+ previewCoord,
+ contentResolver);
+ 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,
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ActionButtonFactory buttonFactory,
+ ViewGroup parent,
+ ContentPreviewCoordinator previewCoord) {
+ 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);
+ final int iconMargin = resources.getDimensionPixelSize(R.dimen.resolver_icon_margin);
+ addActionButton(actionRow, buttonFactory.createCopyButton(), iconMargin);
+ addActionButton(actionRow, buttonFactory.createNearbyButton(), iconMargin);
+
+ 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.loadUriIntoView(
+ () -> contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_thumbnail),
+ previewThumbnail,
+ 0);
+ }
+ }
+
+ return contentPreviewLayout;
+ }
+
+ private static ViewGroup displayImageContentPreview(
+ Intent targetIntent,
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ActionButtonFactory buttonFactory,
+ ViewGroup parent,
+ ContentPreviewCoordinator previewCoord,
+ ContentResolver contentResolver,
+ ImageMimeTypeClassifier imageClassifier) {
+ 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);
+ final int iconMargin = resources.getDimensionPixelSize(R.dimen.resolver_icon_margin);
+ //TODO: addActionButton(actionRow, buttonFactory.createCopyButton(), iconMargin);
+ addActionButton(actionRow, buttonFactory.createNearbyButton(), iconMargin);
+ addActionButton(actionRow, buttonFactory.createEditButton(), iconMargin);
+
+ 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);
+ previewCoord.loadUriIntoView(
+ () -> contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_image_1_large),
+ uri,
+ 0);
+ } else {
+ List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ List<Uri> imageUris = new ArrayList<>();
+ 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);
+ return contentPreviewLayout;
+ }
+
+ imagePreview.findViewById(com.android.internal.R.id.content_preview_image_1_large)
+ .setTransitionName(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
+ previewCoord.loadUriIntoView(
+ () -> contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_image_1_large),
+ imageUris.get(0),
+ 0);
+
+ if (imageUris.size() == 2) {
+ previewCoord.loadUriIntoView(
+ () -> contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_image_2_large),
+ imageUris.get(1),
+ 0);
+ } else if (imageUris.size() > 2) {
+ previewCoord.loadUriIntoView(
+ () -> contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_image_2_small),
+ imageUris.get(1),
+ 0);
+ previewCoord.loadUriIntoView(
+ () -> contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_image_3_small),
+ imageUris.get(2),
+ imageUris.size() - 3);
+ }
+ }
+
+ return contentPreviewLayout;
+ }
+
+ private static ViewGroup displayFileContentPreview(
+ Intent targetIntent,
+ Resources resources,
+ LayoutInflater layoutInflater,
+ ActionButtonFactory buttonFactory,
+ ViewGroup parent,
+ ContentPreviewCoordinator previewCoord,
+ ContentResolver contentResolver) {
+ 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);
+ final int iconMargin = resources.getDimensionPixelSize(R.dimen.resolver_icon_margin);
+ //TODO(b/120417119):
+ // addActionButton(actionRow, buttonFactory.createCopyButton(), iconMargin);
+ addActionButton(actionRow, buttonFactory.createNearbyButton(), iconMargin);
+
+ 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 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.loadUriIntoView(
+ () -> parent.findViewById(
+ 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);
+ }
+ }
+
+ private static void addActionButton(ViewGroup parent, Button b, int iconMargin) {
+ if (b == null) {
+ return;
+ }
+ final ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT
+ );
+ final int gap = iconMargin / 2;
+ lp.setMarginsRelative(gap, 0, gap, 0);
+ parent.addView(b, lp);
+ }
+
+ 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/ChooserGridLayoutManager.java b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
index 7c4b0c1f..5f373525 100644
--- a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
+++ b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
@@ -19,8 +19,8 @@ package com.android.intentresolver;
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 6d0c8337..59d1a6e3 100644
--- a/java/src/com/android/intentresolver/ChooserListAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -19,17 +19,22 @@ package com.android.intentresolver;
import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
+import android.annotation.Nullable;
import android.app.ActivityManager;
-import android.app.prediction.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.View;
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 class ChooserListAdapter extends ResolverListAdapter {
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 class ChooserListAdapter extends ResolverListAdapter {
}
};
- 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);
-
- mMaxShortcutTargetsPerApp =
- context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp);
- mChooserListCommunicator = chooserListCommunicator;
+ super(
+ context,
+ payloadIntents,
+ null,
+ rList,
+ filterLastUsed,
+ resolverListController,
+ userHandle,
+ targetIntent,
+ resolverListCommunicator,
+ false);
+
+ 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 @@ public class ChooserListAdapter extends ResolverListAdapter {
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 @@ public class ChooserListAdapter extends ResolverListAdapter {
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 @@ public class ChooserListAdapter extends ResolverListAdapter {
ri.noResourceId = true;
ri.icon = 0;
}
- mCallerTargets.add(new DisplayResolveInfo(ii, ri, ii, makePresentationGetter(ri)));
+ DisplayResolveInfo displayResolveInfo = DisplayResolveInfo.newDisplayResolveInfo(
+ ii, ri, ii, 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,41 +237,25 @@ public class ChooserListAdapter extends ResolverListAdapter {
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) {
@@ -266,30 +264,28 @@ public class ChooserListAdapter extends ResolverListAdapter {
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 +302,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
// 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 +321,47 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
}
- 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 +380,9 @@ public class ChooserListAdapter extends ResolverListAdapter {
@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 +397,8 @@ public class ChooserListAdapter extends ResolverListAdapter {
*/
public int getSelectableServiceTargetCount() {
int count = 0;
- for (ChooserTargetInfo info : mServiceTargets) {
- if (info instanceof SelectableTargetInfo) {
+ for (TargetInfo info : mServiceTargets) {
+ if (info.isSelectableTargetInfo()) {
count++;
}
}
@@ -426,9 +406,8 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
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;
@@ -436,19 +415,28 @@ public class ChooserListAdapter extends ResolverListAdapter {
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 +471,6 @@ public class ChooserListAdapter extends ResolverListAdapter {
return targetInfoForPosition(position, true);
}
-
/**
* Find target info for a given position.
* Since ChooserActivity displays several sections of content, determine which
@@ -533,8 +520,8 @@ public class ChooserListAdapter extends ResolverListAdapter {
protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {
// Checks if this info is already listed in callerTargets.
for (TargetInfo existingInfo : mCallerTargets) {
- if (mResolverListCommunicator
- .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) {
+ if (mResolverListCommunicator.resolveInfoMatch(
+ dri.getResolveInfo(), existingInfo.getResolveInfo())) {
return false;
}
}
@@ -544,10 +531,9 @@ public class ChooserListAdapter extends ResolverListAdapter {
/**
* 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 +541,36 @@ public class ChooserListAdapter extends ResolverListAdapter {
* 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 +598,14 @@ public class ChooserListAdapter extends ResolverListAdapter {
* 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 +627,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
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 +635,85 @@ public class ChooserListAdapter extends ResolverListAdapter {
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) {
+ return getChooserTargetIconDrawable(
+ mContext,
+ mTargetInfo.getChooserTargetIcon(),
+ mTargetInfo.getChooserTargetComponentName(),
+ mTargetInfo.getDirectShareShortcutInfo());
}
@Override
- protected void onPostExecute(Void arg) {
- if (mViewHolder != null) {
- mViewHolder.bindIcon(mTargetInfo);
+ protected void onPostExecute(@Nullable Drawable icon) {
+ if (icon != null && !mTargetInfo.hasDisplayIcon()) {
+ mTargetInfo.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 = 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 +723,4 @@ public class ChooserListAdapter extends ResolverListAdapter {
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 da78fc81..d0463fff 100644
--- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
@@ -16,27 +16,18 @@
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.internal.annotations.VisibleForTesting;
-import com.android.internal.widget.GridLayoutManager;
-import com.android.internal.widget.PagerAdapter;
-import com.android.internal.widget.RecyclerView;
/**
* A {@link PagerAdapter} which describes the work and personal profile share sheet screens.
@@ -46,37 +37,37 @@ public class ChooserMultiProfilePagerAdapter extends AbstractMultiProfilePagerAd
private static final int SINGLE_CELL_SPAN_SIZE = 1;
private final ChooserProfileDescriptor[] mItems;
- private final boolean mIsSendAction;
private int mBottomOffset;
private int mMaxTargetsPerRow;
ChooserMultiProfilePagerAdapter(Context context,
ChooserActivity.ChooserGridAdapter adapter,
- UserHandle personalProfileUserHandle,
+ EmptyStateProvider emptyStateProvider,
+ QuietModeManager quietModeManager,
UserHandle workProfileUserHandle,
- boolean isSendAction, int maxTargetsPerRow) {
- super(context, /* currentPage */ 0, personalProfileUserHandle, workProfileUserHandle);
+ int maxTargetsPerRow) {
+ super(context, /* currentPage */ 0, emptyStateProvider, quietModeManager,
+ workProfileUserHandle);
mItems = new ChooserProfileDescriptor[] {
createProfileDescriptor(adapter)
};
- mIsSendAction = isSendAction;
mMaxTargetsPerRow = maxTargetsPerRow;
}
ChooserMultiProfilePagerAdapter(Context context,
ChooserActivity.ChooserGridAdapter personalAdapter,
ChooserActivity.ChooserGridAdapter workAdapter,
+ EmptyStateProvider emptyStateProvider,
+ QuietModeManager quietModeManager,
@Profile int defaultProfile,
- UserHandle personalProfileUserHandle,
UserHandle workProfileUserHandle,
- boolean isSendAction, int maxTargetsPerRow) {
- super(context, /* currentPage */ defaultProfile, personalProfileUserHandle,
- workProfileUserHandle);
+ int maxTargetsPerRow) {
+ super(context, /* currentPage */ defaultProfile, emptyStateProvider,
+ quietModeManager, workProfileUserHandle);
mItems = new ChooserProfileDescriptor[] {
createProfileDescriptor(personalAdapter),
createProfileDescriptor(workAdapter)
};
- mIsSendAction = isSendAction;
mMaxTargetsPerRow = maxTargetsPerRow;
}
@@ -191,112 +182,6 @@ public class ChooserMultiProfilePagerAdapter extends AbstractMultiProfilePagerAd
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());
- }
- }
-
- @Override
- protected void showNoWorkToPersonalIntentsEmptyState(ResolverListAdapter activeListAdapter) {
- if (mIsSendAction) {
- showEmptyState(activeListAdapter,
- getCrossProfileBlockedTitle(),
- getCantShareWithPersonalMessage());
- } else {
- 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 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;
}
diff --git a/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
index 67571b44..250b6827 100644
--- a/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
+++ b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
@@ -22,8 +22,8 @@ import android.view.View;
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 00000000..81481bf1
--- /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 ae08ace2..2cfceeae 100644
--- a/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
+++ b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
@@ -20,9 +20,10 @@ package com.android.intentresolver;
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 @@ import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
* 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 {
-
- static final String WHICH_KEY = "which_key";
- static final String MULTI_DRI_KEY = "multi_dri_key";
-
- private MultiDisplayResolveInfo mMultiDisplayResolveInfo;
- private int mParentWhich;
-
- public ChooserStackedAppDialogFragment() {}
+public class ChooserStackedAppDialogFragment extends ChooserTargetActionsDialogFragment {
- 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 @@ public class ChooserStackedAppDialogFragment extends ChooserTargetActionsDialogF
@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 ffd173c7..f4d4a6d1 100644
--- a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
+++ b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
@@ -27,7 +27,6 @@ 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 +48,12 @@ import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
-import com.android.intentresolver.chooser.DisplayResolveInfo;
+import androidx.fragment.app.DialogFragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.recyclerview.widget.RecyclerView;
-import com.android.internal.widget.RecyclerView;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
-import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@@ -64,68 +64,61 @@ import java.util.stream.Collectors;
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";
+
+ private final List<DisplayResolveInfo> mTargetInfos;
+ private final UserHandle mUserHandle;
+ private final boolean mIsShortcutPinned;
- 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";
+ @Nullable
+ private final String mShortcutId;
- public ChooserTargetActionsDialogFragment() {}
+ @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 -> {
@@ -294,4 +287,24 @@ public class ChooserTargetActionsDialogFragment extends DialogFragment
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/IntentForwarderActivity.java b/java/src/com/android/intentresolver/IntentForwarderActivity.java
index 9b853c95..78240250 100644
--- a/java/src/com/android/intentresolver/IntentForwarderActivity.java
+++ b/java/src/com/android/intentresolver/IntentForwarderActivity.java
@@ -28,7 +28,6 @@ import android.app.Activity;
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.PackageManager;
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 @@ import java.util.concurrent.Executors;
* 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 00000000..5bf994d6
--- /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 00000000..420d26c5
--- /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 453a6e84..5a116b43 100644
--- a/java/src/com/android/intentresolver/ResolverActivity.java
+++ b/java/src/com/android/intentresolver/ResolverActivity.java
@@ -19,6 +19,9 @@ package com.android.intentresolver;
import static android.Manifest.permission.INTERACT_ACROSS_PROFILES;
import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL;
import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB_ACCESSIBILITY;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED;
@@ -26,6 +29,8 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK
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.PickOptionRequest.Option;
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.pm.UserInfo;
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,26 @@ import android.widget.TabWidget;
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.AbstractMultiProfilePagerAdapter.QuietModeManager;
+import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState;
import com.android.intentresolver.chooser.ChooserTargetInfo;
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;
@@ -116,10 +130,9 @@ import java.util.Set;
* 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 @@ public class ResolverActivity extends Activity implements
@VisibleForTesting
protected boolean mSupportsAlwaysUseOption;
protected ResolverDrawerLayout mResolverDrawerLayout;
- @UnsupportedAppUsage
protected PackageManager mPm;
protected int mLaunchedFromUid;
@@ -165,17 +177,12 @@ public class ResolverActivity extends Activity implements
/** 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 @@ public class ResolverActivity extends Activity implements
@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";
@@ -216,6 +225,9 @@ public class ResolverActivity extends Activity implements
private UserHandle mWorkProfileUserHandle;
+ @Nullable
+ private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
+
protected final LatencyTracker mLatencyTracker = getLatencyTracker();
private LatencyTracker getLatencyTracker() {
@@ -360,7 +372,6 @@ public class ResolverActivity extends Activity implements
* 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 +385,8 @@ public class ResolverActivity extends Activity implements
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());
@@ -474,6 +487,111 @@ public class ResolverActivity extends Activity implements
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 +602,21 @@ public class ResolverActivity extends Activity implements
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 +625,7 @@ public class ResolverActivity extends Activity implements
// 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 +658,15 @@ public class ResolverActivity extends Activity implements
(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() {
@@ -594,7 +719,7 @@ public class ResolverActivity extends Activity implements
}
protected boolean shouldShowTabs() {
- return hasWorkProfile() && ENABLE_TABBED_VIEW;
+ return hasWorkProfile();
}
protected void onProfileClick(View v) {
@@ -726,7 +851,6 @@ public class ResolverActivity extends Activity implements
}
}
- @Override // SelectableTargetInfoCommunicator ResolverListCommunicator
public Intent getTargetIntent() {
return mIntents.isEmpty() ? null : mIntents.get(0);
}
@@ -848,9 +972,9 @@ public class ResolverActivity extends Activity implements
}
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 +1499,7 @@ public class ResolverActivity extends Activity implements
.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 +1531,16 @@ public class ResolverActivity extends Activity implements
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 +1604,25 @@ public class ResolverActivity extends Activity implements
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.setDisplayIcon(drawable);
+ new ResolverListAdapter.ViewHolder(icon).bindIcon(otherProfileResolveInfo);
+ }
+ }
+ }.execute();
((TextView) findViewById(com.android.internal.R.id.open_cross_profile)).setText(
getResources().getString(
@@ -1521,31 +1661,29 @@ public class ResolverActivity extends Activity implements
|| 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 +1938,12 @@ public class ResolverActivity extends Activity implements
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 +2204,7 @@ public class ResolverActivity extends Activity implements
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 +2256,7 @@ public class ResolverActivity extends Activity implements
}
mWorkProfileHasBeenEnabled = true;
- mMultiProfilePagerAdapter.markWorkProfileEnabledBroadcastReceived();
+ mQuietModeManager.markWorkProfileEnabledBroadcastReceived();
} else {
// Must be an UNAVAILABLE broadcast, so we watch for the next availability
mWorkProfileHasBeenEnabled = false;
@@ -2135,13 +2272,11 @@ public class ResolverActivity extends Activity implements
};
}
- @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 +2325,6 @@ public class ResolverActivity extends Activity implements
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 +2381,9 @@ public class ResolverActivity extends Activity implements
}
- 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 898d8c8e..9f654594 100644
--- a/java/src/com/android/intentresolver/ResolverListAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverListAdapter.java
@@ -54,44 +54,61 @@ import android.widget.TextView;
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;
+
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,12 +120,22 @@ public class ResolverListAdapter extends BaseAdapter {
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();
}
+ public final DisplayResolveInfo getFirstDisplayResolveInfo() {
+ return mDisplayList.get(0);
+ }
+
+ public final ImmutableList<DisplayResolveInfo> getTargetsInCurrentDisplayList() {
+ return ImmutableList.copyOf(mDisplayList);
+ }
+
public void handlePackagesChanged() {
mResolverListCommunicator.onHandlePackagesChanged(this);
}
@@ -258,7 +285,7 @@ public class ResolverListAdapter extends BaseAdapter {
if (mBaseResolveList != null) {
List<ResolvedComponentInfo> currentResolveList = new ArrayList<>();
mResolverListController.addResolveListDedupe(currentResolveList,
- mResolverListCommunicator.getTargetIntent(),
+ mTargetIntent,
mBaseResolveList);
return currentResolveList;
} else {
@@ -334,7 +361,12 @@ public class ResolverListAdapter extends BaseAdapter {
if (otherProfileInfo != null) {
mOtherProfile = makeOtherProfileDisplayResolveInfo(
- mContext, otherProfileInfo, mPm, mResolverListCommunicator, mIconDpi);
+ mContext,
+ otherProfileInfo,
+ mPm,
+ mTargetIntent,
+ mResolverListCommunicator,
+ mIconDpi);
} else {
mOtherProfile = null;
try {
@@ -441,8 +473,13 @@ public class ResolverListAdapter extends BaseAdapter {
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,
+ makePresentationGetter(ri)));
}
}
@@ -490,10 +527,12 @@ public class ResolverListAdapter extends BaseAdapter {
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,
+ makePresentationGetter(add));
dri.setPinned(rci.isPinned());
if (rci.isPinned()) {
Log.i(TAG, "Pinned item: " + rci.name);
@@ -597,11 +636,15 @@ public class ResolverListAdapter extends BaseAdapter {
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);
}
@@ -636,26 +679,48 @@ public class ResolverListAdapter extends BaseAdapter {
if (info == null) {
holder.icon.setImageDrawable(
mContext.getDrawable(R.drawable.resolver_icon_placeholder));
+ 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.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);
+ }
}
+ }
- if (info instanceof DisplayResolveInfo
- && !((DisplayResolveInfo) info).hasDisplayIcon()) {
- new LoadIconTask((DisplayResolveInfo) info, holder).execute();
- } else {
- holder.bindIcon(info);
+ protected final void loadIcon(DisplayResolveInfo info) {
+ LoadIconTask task = mIconLoaders.get(info);
+ if (task == null) {
+ task = new LoadIconTask((DisplayResolveInfo) info);
+ mIconLoaders.put(info, task);
+ task.execute();
}
}
- protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
- return new LoadLabelTask(info, holder);
+ 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 +731,16 @@ public class ResolverListAdapter extends BaseAdapter {
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() {
@@ -721,9 +796,8 @@ public class ResolverListAdapter extends BaseAdapter {
}
}
- @VisibleForTesting
public UserHandle getUserHandle() {
- return mResolverListController.getUserHandle();
+ return mUserHandle;
}
protected List<ResolvedComponentInfo> getResolversForUser(UserHandle userHandle) {
@@ -779,6 +853,7 @@ public class ResolverListAdapter extends BaseAdapter {
Context context,
ResolvedComponentInfo resolvedComponentInfo,
PackageManager pm,
+ Intent targetIntent,
ResolverListCommunicator resolverListCommunicator,
int iconDpi) {
ResolveInfo resolveInfo = resolvedComponentInfo.getResolveInfoAt(0);
@@ -787,13 +862,12 @@ public class ResolverListAdapter extends BaseAdapter {
resolveInfo.activityInfo,
resolvedComponentInfo.getIntentAt(0));
Intent replacementIntent = resolverListCommunicator.getReplacementIntent(
- resolveInfo.activityInfo,
- resolverListCommunicator.getTargetIntent());
+ resolveInfo.activityInfo, targetIntent);
ResolveInfoPresentationGetter presentationGetter =
new ResolveInfoPresentationGetter(context, iconDpi, resolveInfo);
- return new DisplayResolveInfo(
+ return DisplayResolveInfo.newDisplayResolveInfo(
resolvedComponentInfo.getIntentAt(0),
resolveInfo,
resolveInfo.loadLabel(pm),
@@ -829,13 +903,12 @@ public class ResolverListAdapter extends BaseAdapter {
*/
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 +950,7 @@ public class ResolverListAdapter extends BaseAdapter {
}
public void bindIcon(TargetInfo info) {
- icon.setImageDrawable(info.getDisplayIcon(itemView.getContext()));
+ icon.setImageDrawable(info.getDisplayIcon());
if (info.isSuspended()) {
icon.setColorFilter(getSuspendedColorMatrix());
} else {
@@ -888,11 +961,9 @@ public class ResolverListAdapter extends BaseAdapter {
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
@@ -930,21 +1001,22 @@ public class ResolverListAdapter extends BaseAdapter {
@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
@@ -958,17 +1030,9 @@ public class ResolverListAdapter extends BaseAdapter {
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
notifyDataSetChanged();
}
}
-
- public void setViewHolder(ViewHolder holder) {
- mHolder = holder;
- mHolder.bindIcon(mDisplayResolveInfo);
- }
}
/**
diff --git a/java/src/com/android/intentresolver/ResolverListController.java b/java/src/com/android/intentresolver/ResolverListController.java
index ff616ce0..bfffe0d8 100644
--- a/java/src/com/android/intentresolver/ResolverListController.java
+++ b/java/src/com/android/intentresolver/ResolverListController.java
@@ -32,7 +32,8 @@ import android.os.UserHandle;
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 @@ public class ResolverListController {
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 @@ public class ResolverListController {
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 @@ public class ResolverListController {
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 @@ public class ResolverListController {
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 56d326c1..8cf65529 100644
--- a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
@@ -16,15 +16,7 @@
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;
@@ -32,8 +24,9 @@ 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;
/**
* A {@link PagerAdapter} which describes the work and personal profile intent resolver screens.
@@ -42,34 +35,33 @@ import com.android.internal.widget.PagerAdapter;
public class ResolverMultiProfilePagerAdapter extends AbstractMultiProfilePagerAdapter {
private final ResolverProfileDescriptor[] mItems;
- private final boolean mShouldShowNoCrossProfileIntentsEmptyState;
private boolean mUseLayoutWithDefault;
ResolverMultiProfilePagerAdapter(Context context,
ResolverListAdapter adapter,
- UserHandle personalProfileUserHandle,
+ EmptyStateProvider emptyStateProvider,
+ QuietModeManager quietModeManager,
UserHandle workProfileUserHandle) {
- super(context, /* currentPage */ 0, personalProfileUserHandle, workProfileUserHandle);
+ super(context, /* currentPage */ 0, emptyStateProvider, quietModeManager,
+ workProfileUserHandle);
mItems = new ResolverProfileDescriptor[] {
createProfileDescriptor(adapter)
};
- mShouldShowNoCrossProfileIntentsEmptyState = true;
}
ResolverMultiProfilePagerAdapter(Context context,
ResolverListAdapter personalAdapter,
ResolverListAdapter workAdapter,
+ EmptyStateProvider emptyStateProvider,
+ QuietModeManager quietModeManager,
@Profile int defaultProfile,
- UserHandle personalProfileUserHandle,
- UserHandle workProfileUserHandle,
- boolean shouldShowNoCrossProfileIntentsEmptyState) {
- super(context, /* currentPage */ defaultProfile, personalProfileUserHandle,
+ UserHandle workProfileUserHandle) {
+ super(context, /* currentPage */ defaultProfile, emptyStateProvider, quietModeManager,
workProfileUserHandle);
mItems = new ResolverProfileDescriptor[] {
createProfileDescriptor(personalAdapter),
createProfileDescriptor(workAdapter)
};
- mShouldShowNoCrossProfileIntentsEmptyState = shouldShowNoCrossProfileIntentsEmptyState;
}
private ResolverProfileDescriptor createProfileDescriptor(
@@ -169,93 +161,6 @@ public class ResolverMultiProfilePagerAdapter extends AbstractMultiProfilePagerA
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;
}
diff --git a/java/src/com/android/intentresolver/ResolverViewPager.java b/java/src/com/android/intentresolver/ResolverViewPager.java
index 1c234526..0804a2b8 100644
--- a/java/src/com/android/intentresolver/ResolverViewPager.java
+++ b/java/src/com/android/intentresolver/ResolverViewPager.java
@@ -21,7 +21,7 @@ import android.util.AttributeSet;
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 @@ public class ResolverViewPager extends ViewPager {
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 00000000..645b9391
--- /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 b05b4f68..ec5179ac 100644
--- a/java/src/com/android/intentresolver/SimpleIconFactory.java
+++ b/java/src/com/android/intentresolver/SimpleIconFactory.java
@@ -50,6 +50,8 @@ import android.util.AttributeSet;
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 @@ public class SimpleIconFactory {
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 @@ public class SimpleIconFactory {
*/
@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 @@ public class SimpleIconFactory {
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/WorkProfilePausedEmptyStateProvider.java b/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java
new file mode 100644
index 00000000..b7c89907
--- /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 1c763071..8b9bfb32 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();
-
- /**
- * 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;
+ @Override
+ public final boolean isChooserTargetInfo() {
+ return true;
+ }
- 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 e7ffe3c6..16dd28bc 100644
--- a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
+++ b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
@@ -20,68 +20,100 @@ import android.annotation.NonNull;
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 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 final boolean mIsSuspended;
private ResolveInfoPresentationGetter mResolveInfoPresentationGetter;
private boolean mPinned = false;
- public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, Intent pOrigIntent,
- ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
- this(originalIntent, pri, null /*mDisplayLabel*/, null /*mExtendedInfo*/, pOrigIntent,
+ /** Create a new {@code DisplayResolveInfo} instance. */
+ public static DisplayResolveInfo newDisplayResolveInfo(
+ Intent originalIntent,
+ ResolveInfo resolveInfo,
+ @NonNull Intent resolvedIntent,
+ @Nullable ResolveInfoPresentationGetter presentationGetter) {
+ return newDisplayResolveInfo(
+ originalIntent,
+ resolveInfo,
+ /* displayLabel=*/ null,
+ /* extendedInfo=*/ null,
+ resolvedIntent,
+ presentationGetter);
+ }
+
+ /** Create a new {@code DisplayResolveInfo} instance. */
+ public static DisplayResolveInfo newDisplayResolveInfo(
+ Intent originalIntent,
+ ResolveInfo resolveInfo,
+ CharSequence displayLabel,
+ CharSequence extendedInfo,
+ @NonNull Intent resolvedIntent,
+ @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
+ return new DisplayResolveInfo(
+ originalIntent,
+ resolveInfo,
+ displayLabel,
+ extendedInfo,
+ resolvedIntent,
resolveInfoPresentationGetter);
}
- public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel,
- CharSequence pInfo, @NonNull Intent resolvedIntent,
+ private DisplayResolveInfo(
+ Intent originalIntent,
+ ResolveInfo resolveInfo,
+ CharSequence displayLabel,
+ CharSequence extendedInfo,
+ @NonNull Intent resolvedIntent,
@Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
mSourceIntents.add(originalIntent);
- mResolveInfo = pri;
- mDisplayLabel = pLabel;
- mExtendedInfo = pInfo;
+ mResolveInfo = resolveInfo;
+ mDisplayLabel = displayLabel;
+ mExtendedInfo = extendedInfo;
mResolveInfoPresentationGetter = resolveInfoPresentationGetter;
+ 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,
+ private DisplayResolveInfo(
+ DisplayResolveInfo other,
+ Intent fillInIntent,
+ int flags,
ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
mSourceIntents.addAll(other.getAllSourceIntents());
mResolveInfo = other.mResolveInfo;
+ mIsSuspended = other.mIsSuspended;
mDisplayLabel = other.mDisplayLabel;
mDisplayIcon = other.mDisplayIcon;
mExtendedInfo = other.mExtendedInfo;
@@ -90,9 +122,10 @@ public class DisplayResolveInfo implements TargetInfo, Parcelable {
mResolveInfoPresentationGetter = resolveInfoPresentationGetter;
}
- 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;
@@ -100,6 +133,11 @@ public class DisplayResolveInfo implements TargetInfo, Parcelable {
mResolveInfoPresentationGetter = other.mResolveInfoPresentationGetter;
}
+ @Override
+ public final boolean isDisplayResolveInfo() {
+ return true;
+ }
+
public ResolveInfo getResolveInfo() {
return mResolveInfo;
}
@@ -124,7 +162,8 @@ public class DisplayResolveInfo implements TargetInfo, Parcelable {
mExtendedInfo = extendedInfo;
}
- public Drawable getDisplayIcon(Context context) {
+ @Override
+ public Drawable getDisplayIcon() {
return mDisplayIcon;
}
@@ -138,6 +177,11 @@ public class DisplayResolveInfo implements TargetInfo, Parcelable {
return mSourceIntents;
}
+ @Override
+ public ArrayList<DisplayResolveInfo> getAllDisplayTargets() {
+ return new ArrayList<>(Arrays.asList(this));
+ }
+
public void addAlternateSourceIntent(Intent alt) {
mSourceIntents.add(alt);
}
@@ -146,10 +190,6 @@ public class DisplayResolveInfo implements TargetInfo, Parcelable {
mDisplayIcon = icon;
}
- public boolean hasDisplayIcon() {
- return mDisplayIcon != null;
- }
-
public CharSequence getExtendedInfo() {
return mExtendedInfo;
}
@@ -172,14 +212,14 @@ public class DisplayResolveInfo implements TargetInfo, Parcelable {
@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 +236,4 @@ public class DisplayResolveInfo implements TargetInfo, Parcelable {
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 5133d997..29f00a35 100644
--- a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
+++ b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
@@ -23,6 +23,7 @@ import android.os.UserHandle;
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 @@ import java.util.ArrayList;
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 @@ public class MultiDisplayResolveInfo extends DisplayResolveInfo {
}
/**
- * 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 class MultiDisplayResolveInfo extends DisplayResolveInfo {
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 220870f2..3b4b89b1 100644
--- a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
@@ -18,12 +18,15 @@ package com.android.intentresolver.chooser;
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,51 @@ import java.util.List;
* 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;
+ }
+
+ @Override
+ public Drawable getDisplayIcon() {
+ return null;
+ }
+ };
+ }
+
+ /**
+ * 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 Drawable getDisplayIcon() {
+ AnimatedVectorDrawable avd = (AnimatedVectorDrawable)
+ context.getDrawable(R.drawable.chooser_direct_share_icon_placeholder);
+ avd.start(); // Start animation after generation.
+ return avd;
+ }
+
+ @Override
+ public boolean hasDisplayIcon() {
+ return true;
+ }
+ };
+ }
+
+ public final boolean isNotSelectableTargetInfo() {
+ return true;
+ }
public Intent getResolvedIntent() {
return null;
@@ -78,10 +125,6 @@ public abstract class NotSelectableTargetInfo implements ChooserTargetInfo {
return -0.1f;
}
- public ChooserTarget getChooserTarget() {
- return null;
- }
-
public boolean isSuspended() {
return false;
}
diff --git a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
index 1610d0fd..51a776db 100644
--- a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
@@ -18,31 +18,24 @@ 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.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,173 +44,145 @@ import java.util.List;
* 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 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;
+ @Nullable
+ private final AppTarget mAppTarget;
+ @Nullable
+ private final ShortcutInfo mShortcutInfo;
+
+ private final ComponentName mChooserTargetComponentName;
+ private final String mChooserTargetUnsanitizedTitle;
+ private final Icon mChooserTargetIcon;
+ private final Bundle mChooserTargetIntentExtras;
+
+ /**
+ * 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;
private final int mFillInFlags;
private final boolean mIsPinned;
private final float mModifiedScore;
- private boolean mIsSuspended = false;
- public SelectableTargetInfo(Context context, DisplayResolveInfo sourceInfo,
+ private Drawable mDisplayIcon;
+
+ /** Create a new {@link TargetInfo} instance representing a selectable target. */
+ 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 new SelectableTargetInfo(
+ sourceInfo,
+ backupResolveInfo,
+ resolvedIntent,
+ chooserTarget,
+ modifiedScore,
+ shortcutInfo,
+ appTarget,
+ referrerFillInIntent);
+ }
+
+ private SelectableTargetInfo(
+ @Nullable DisplayResolveInfo sourceInfo,
+ @Nullable ResolveInfo backupResolveInfo,
+ Intent resolvedIntent,
+ ChooserTarget chooserTarget,
+ float modifiedScore,
+ @Nullable ShortcutInfo shortcutInfo,
+ @Nullable AppTarget appTarget,
+ Intent referrerFillInIntent) {
mSourceInfo = sourceInfo;
- mChooserTarget = chooserTarget;
mModifiedScore = modifiedScore;
- mPm = mContext.getPackageManager();
- mSelectableTargetInfoCommunicator = selectableTargetInfoComunicator;
mShortcutInfo = shortcutInfo;
+ mAppTarget = appTarget;
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);
- }
+ mBackupResolveInfo = backupResolveInfo;
+ mResolvedIntent = resolvedIntent;
+ mReferrerFillInIntent = referrerFillInIntent;
mFillInIntent = null;
mFillInFlags = 0;
- mDisplayLabel = sanitizeDisplayLabel(chooserTarget.getTitle());
+ mChooserTargetComponentName = chooserTarget.getComponentName();
+ mChooserTargetUnsanitizedTitle = chooserTarget.getTitle().toString();
+ mChooserTargetIcon = chooserTarget.getIcon();
+ mChooserTargetIntentExtras = chooserTarget.getIntentExtras();
+
+ mDisplayLabel = sanitizeDisplayLabel(mChooserTargetUnsanitizedTitle);
}
- private SelectableTargetInfo(SelectableTargetInfo other,
- Intent fillInIntent, int flags) {
- mContext = other.mContext;
- mPm = other.mPm;
- mSelectableTargetInfoCommunicator = other.mSelectableTargetInfoCommunicator;
+ private SelectableTargetInfo(SelectableTargetInfo other, Intent fillInIntent, int flags) {
mSourceInfo = other.mSourceInfo;
mBackupResolveInfo = other.mBackupResolveInfo;
- mChooserTarget = other.mChooserTarget;
- mBadgeIcon = other.mBadgeIcon;
- mBadgeContentDescription = other.mBadgeContentDescription;
- synchronized (other) {
- mShortcutInfo = other.mShortcutInfo;
- mDisplayIcon = other.mDisplayIcon;
- }
+ mResolvedIntent = other.mResolvedIntent;
+ mShortcutInfo = other.mShortcutInfo;
+ mAppTarget = other.mAppTarget;
+ mDisplayIcon = other.mDisplayIcon;
mFillInIntent = fillInIntent;
mFillInFlags = flags;
mModifiedScore = other.mModifiedScore;
mIsPinned = other.mIsPinned;
+ mReferrerFillInIntent = other.mReferrerFillInIntent;
- mDisplayLabel = sanitizeDisplayLabel(mChooserTarget.getTitle());
+ mChooserTargetComponentName = other.mChooserTargetComponentName;
+ mChooserTargetUnsanitizedTitle = other.mChooserTargetUnsanitizedTitle;
+ mChooserTargetIcon = other.mChooserTargetIcon;
+ mChooserTargetIntentExtras = other.mChooserTargetIntentExtras;
+
+ mDisplayLabel = sanitizeDisplayLabel(mChooserTargetUnsanitizedTitle);
}
- private String sanitizeDisplayLabel(CharSequence label) {
- SpannableStringBuilder sb = new SpannableStringBuilder(label);
- sb.clearSpans();
- return sb.toString();
+ @Override
+ public boolean isSelectableTargetInfo() {
+ return true;
}
+ @Override
public boolean isSuspended() {
- return mIsSuspended;
+ return (mSourceInfo != null) && mSourceInfo.isSuspended();
}
+ @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
@@ -231,6 +196,16 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
return null;
}
+ @Override
+ public ComponentName getChooserTargetComponentName() {
+ return mChooserTargetComponentName;
+ }
+
+ @Nullable
+ public Icon getChooserTargetIcon() {
+ return mChooserTargetIcon;
+ }
+
private Intent getBaseIntentToSend() {
Intent result = getResolvedIntent();
if (result == null) {
@@ -240,7 +215,7 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
if (mFillInIntent != null) {
result.fillIn(mFillInIntent, mFillInFlags);
}
- result.fillIn(mSelectableTargetInfoCommunicator.getReferrerFillInIntent(), 0);
+ result.fillIn(mReferrerFillInIntent, 0);
}
return result;
}
@@ -256,8 +231,9 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
if (intent == null) {
return false;
}
- intent.setComponent(mChooserTarget.getComponentName());
- intent.putExtras(mChooserTarget.getIntentExtras());
+ 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
@@ -269,7 +245,7 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
// so we'll obey the caller's normal security checks.
final boolean ignoreTargetSecurity = mSourceInfo != null
&& mSourceInfo.getResolvedComponentName().getPackageName()
- .equals(mChooserTarget.getComponentName().getPackageName());
+ .equals(getChooserTargetComponentName().getPackageName());
activity.startActivityAsCaller(intent, options, ignoreTargetSecurity, userId);
return true;
}
@@ -296,12 +272,24 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
}
@Override
- public synchronized Drawable getDisplayIcon(Context context) {
+ public Drawable getDisplayIcon() {
return mDisplayIcon;
}
- public ChooserTarget getChooserTarget() {
- return mChooserTarget;
+ public void setDisplayIcon(Drawable icon) {
+ mDisplayIcon = icon;
+ }
+
+ @Override
+ @Nullable
+ public ShortcutInfo getDirectShareShortcutInfo() {
+ return mShortcutInfo;
+ }
+
+ @Override
+ @Nullable
+ public AppTarget getDirectShareAppTarget() {
+ return mAppTarget;
}
@Override
@@ -324,16 +312,21 @@ public final class SelectableTargetInfo implements ChooserTargetInfo {
return mIsPinned;
}
- /**
- * Necessary methods to communicate between {@link SelectableTargetInfo}
- * and {@link ResolverActivity} or {@link ChooserActivity}.
- */
- public interface SelectableTargetInfoCommunicator {
-
- ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo info);
-
- Intent getTargetIntent();
+ @Override
+ public HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context) {
+ final String plaintext =
+ getChooserTargetComponentName().getPackageName()
+ + mChooserTargetUnsanitizedTitle;
+ return HashedStringCache.getInstance().hashString(
+ context,
+ HASHED_STRING_CACHE_TAG,
+ plaintext,
+ mMaxHashSaltDays);
+ }
- Intent getReferrerFillInIntent();
+ private static String sanitizeDisplayLabel(CharSequence label) {
+ SpannableStringBuilder sb = new SpannableStringBuilder(label);
+ sb.clearSpans();
+ return sb.toString();
}
}
diff --git a/java/src/com/android/intentresolver/chooser/TargetInfo.java b/java/src/com/android/intentresolver/chooser/TargetInfo.java
index fabb26c2..0e100d4f 100644
--- a/java/src/com/android/intentresolver/chooser/TargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/TargetInfo.java
@@ -17,18 +17,26 @@
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.
@@ -46,13 +54,34 @@ public interface TargetInfo {
/**
* 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
@@ -107,11 +136,17 @@ public interface TargetInfo {
/**
* @return The drawable that should be used to represent this target including badge
- * @param context
*/
- Drawable getDisplayIcon(Context context);
+ @Nullable
+ Drawable getDisplayIcon();
/**
+ * @return true if display icon is available.
+ */
+ default boolean hasDisplayIcon() {
+ return getDisplayIcon() != null;
+ }
+ /**
* Clone this target with the given fill-in information.
*/
TargetInfo cloneFilledIn(Intent fillInIntent, int flags);
@@ -122,6 +157,28 @@ public interface TargetInfo {
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 +187,220 @@ public interface TargetInfo {
* @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/DirectShareViewHolder.java b/java/src/com/android/intentresolver/grid/DirectShareViewHolder.java
new file mode 100644
index 00000000..cfd54697
--- /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(ChooserActivity.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/ChooserFlags.java b/java/src/com/android/intentresolver/grid/FooterViewHolder.java
index 67f9046f..0c94e3ed 100644
--- a/java/src/com/android/intentresolver/ChooserFlags.java
+++ b/java/src/com/android/intentresolver/grid/FooterViewHolder.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2019 The Android Open Source Project
+ * 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.
@@ -14,20 +14,15 @@
* limitations under the License.
*/
-package com.android.intentresolver;
+package com.android.intentresolver.grid;
-import android.app.prediction.AppPredictionManager;
+import android.view.View;
/**
- * Common flags for {@link ChooserListAdapter} and {@link ChooserActivity}.
+ * A footer on the list, to support scrolling behavior below the navbar.
*/
-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;
+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 00000000..5470506b
--- /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 00000000..2ec56b1b
--- /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 00000000..a72da7aa
--- /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 00000000..78e9104a
--- /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
index 6f802876..271c6f98 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.Message;
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 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
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 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
}
}
- /**
- * 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 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
}
protected final void afterCompute() {
- final AfterCompute afterCompute = mAfterCompute;
+ final Runnable afterCompute = mAfterCompute;
if (afterCompute != null) {
- afterCompute.afterCompute();
+ afterCompute.run();
}
}
@@ -161,11 +155,6 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
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 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
* 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 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
* 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 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
/**
* 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 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
*
* @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 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
* 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
index 9b9fc1c0..c6bb2b85 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.Message;
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 @@ import java.util.concurrent.Executors;
* 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 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator
private ResolverRankerServiceResolverComparator mResolverRankerService;
private AppPredictionServiceComparatorModel mComparatorModel;
- AppPredictionServiceResolverComparator(
+ public AppPredictionServiceResolverComparator(
Context context,
Intent intent,
String referrerPackage,
@@ -166,17 +167,17 @@ class AppPredictionServiceResolverComparator extends AbstractResolverComparator
}
@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
index 79160c84..3616a853 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
index be3e6f18..4382f109 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.ResolverRankerService;
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 @@ import java.util.concurrent.TimeUnit;
/**
* 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 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
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 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
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 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
private class ResolverRankerServiceConnection implements ServiceConnection {
private final CountDownLatch mConnectSignal;
- public ResolverRankerServiceConnection(CountDownLatch connectSignal) {
+ ResolverRankerServiceConnection(CountDownLatch connectSignal) {
mConnectSignal = connectSignal;
}
@@ -424,8 +424,10 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
// 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 @@ class ResolverRankerServiceResolverComparator extends AbstractResolverComparator
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 00000000..82f40b91
--- /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 00000000..1cfa2c8d
--- /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 00000000..a37d6558
--- /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/ResolverDrawerLayout.java b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
new file mode 100644
index 00000000..29821e66
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ResolverDrawerLayout.java
@@ -0,0 +1,1223 @@
+/*
+ * 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 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 mCollapsibleHeight;
+ private int mUncollapsibleHeight;
+ 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;
+
+ 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);
+ 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 = mCollapsibleHeight;
+ mCollapsibleHeight = Math.min(mCollapsibleHeight, getMaxCollapsedHeight());
+
+ 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(mCollapsibleHeight + mUncollapsibleHeight, 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,
+ mCollapsibleHeight + mUncollapsibleHeight));
+ 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;
+ }
+
+ final int childCount = getChildCount();
+ for (int i = 0; i < childCount; i++) {
+ final View child = getChildAt(i);
+ final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+ if (!lp.ignoreOffset) {
+ child.offsetTopAndBottom((int) dy);
+ }
+ }
+ 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(mCollapsibleHeight + mUncollapsibleHeight, 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 < mCollapsibleHeight + mUncollapsibleHeight)
+ && isDismissable()) {
+ smoothScrollTo(mCollapsibleHeight + mUncollapsibleHeight, 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 < mCollapsibleHeight + mUncollapsibleHeight)
+ && ((mCollapseOffset < mCollapsibleHeight) || isDismissable())) {
+ info.addAction(AccessibilityAction.ACTION_SCROLL_UP);
+ info.setScrollable(true);
+ }
+ if (mCollapseOffset < mCollapsibleHeight) {
+ info.addAction(AccessibilityAction.ACTION_COLLAPSE);
+ }
+ if (mCollapseOffset < mCollapsibleHeight + mUncollapsibleHeight && 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();
+ }
+ }
+
+ final int oldCollapsibleHeight = mCollapsibleHeight;
+ mCollapsibleHeight = Math.max(0,
+ heightUsed - mAlwaysShowHeight - getMaxCollapsedHeight());
+ mUncollapsibleHeight = heightUsed - mCollapsibleHeight;
+
+ updateCollapseOffset(oldCollapsibleHeight, !isDragging());
+
+ if (getShowAtTop()) {
+ mTopOffset = 0;
+ } else {
+ mTopOffset = Math.max(0, heightSize - heightUsed) + (int) mCollapseOffset;
+ }
+
+ setMeasuredDimension(sourceWidth, heightSize);
+ }
+
+ /**
+ * @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;
+
+ 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;
+ }
+
+ int top = ypos + lp.topMargin;
+ if (lp.ignoreOffset) {
+ 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;
+ }
+
+ public static class LayoutParams extends MarginLayoutParams {
+ public boolean alwaysShow;
+ public boolean ignoreOffset;
+ public boolean hasNestedScrollIndicator;
+ public int maxHeight;
+
+ 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 00000000..cf7bd543
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/RoundedRectImageView.java
@@ -0,0 +1,131 @@
+/*
+ * 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;
+ }
+ }
+
+ @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/tests/Android.bp b/java/tests/Android.bp
index fdabc4e0..2913d128 100644
--- a/java/tests/Android.bp
+++ b/java/tests/Android.bp
@@ -7,7 +7,7 @@ android_test {
name: "IntentResolverUnitTests",
// Include all test java files.
- srcs: ["src/**/*.java"],
+ srcs: ["src/**/*.java", "src/**/*.kt"],
libs: [
"android.test.runner",
@@ -19,8 +19,8 @@ android_test {
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 bfe3a39f..306eccb9 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 f4e75c46..d1d77c10 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 00000000..9b5800da
--- /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 e4146cc5..00000000
--- 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 00000000..702e725a
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java
@@ -0,0 +1,239 @@
+/*
+ * 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 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.UiEventLogger;
+import com.android.internal.logging.UiEventLogger.UiEventEnum;
+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;
+
+ private ChooserActivityLogger mChooserLogger;
+
+ @Before
+ public void setUp() {
+ mChooserLogger = new ChooserActivityLogger(mUiEventLog, mFrameworkLog);
+ }
+
+ @After
+ public void tearDown() {
+ verifyNoMoreInteractions(mUiEventLog);
+ verifyNoMoreInteractions(mFrameworkLog);
+ }
+
+ @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 = ChooserActivity.SELECTION_TYPE_COPY;
+ final String packageName = "com.test.foo";
+ final int positionPicked = 123;
+ final boolean pinned = true;
+
+ mChooserLogger.logShareTargetSelected(targetType, packageName, positionPicked, pinned);
+
+ verify(mFrameworkLog).write(
+ eq(FrameworkStatsLog.RANKING_SELECTED),
+ eq(SharesheetTargetSelectedEvent.SHARESHEET_COPY_TARGET_SELECTED.getId()),
+ eq(packageName),
+ /* instanceId=*/ gt(0),
+ eq(positionPicked),
+ eq(pinned));
+ }
+
+ @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);
+
+ final int targetType = ChooserActivity.SELECTION_TYPE_COPY;
+ final String packageName = "com.test.foo";
+ final int positionPicked = 123;
+ final boolean pinned = true;
+
+ mChooserLogger.logShareTargetSelected(targetType, packageName, positionPicked, pinned);
+ chooserLogger2.logShareTargetSelected(targetType, packageName, positionPicked, pinned);
+
+ 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 = ChooserActivity.SELECTION_TYPE_COPY;
+ final String packageName = "com.test.foo";
+ final int positionPicked = 123;
+ final boolean pinned = true;
+
+ mChooserLogger.logShareTargetSelected(targetType, packageName, positionPicked, pinned);
+ 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());
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
index 080f1e41..5acdb42c 100644
--- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
+++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
@@ -16,21 +16,29 @@
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.intentresolver.shortcuts.ShortcutLoader;
import com.android.internal.logging.MetricsLogger;
-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 +57,8 @@ public class ChooserActivityOverrideData {
@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;
@@ -64,14 +73,14 @@ public class ChooserActivityOverrideData {
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;
@@ -81,22 +90,15 @@ public class ChooserActivityOverrideData {
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 +109,28 @@ public class ChooserActivityOverrideData {
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 00000000..6b34f8b9
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt
@@ -0,0 +1,147 @@
+/*
+ * 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.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 = */ mock(),
+ /* 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 0e9f010e..8c842786 100644
--- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
+++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
@@ -19,11 +19,13 @@ package com.android.intentresolver;
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,19 @@ import android.net.Uri;
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.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager;
import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;
-import com.android.intentresolver.ResolverListController;
import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.NotSelectableTargetInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.shortcuts.ShortcutLoader;
import com.android.internal.logging.MetricsLogger;
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 +66,34 @@ public class ChooserWrapperActivity
}
@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 +130,7 @@ public class ChooserWrapperActivity
@Override
protected TargetInfo getNearbySharingTarget(Intent originalIntent) {
- return new ChooserWrapperActivity.EmptyTargetInfo();
+ return NotSelectableTargetInfo.newEmptyTargetInfo();
}
@Override
@@ -139,6 +150,30 @@ public class ChooserWrapperActivity
}
@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)) {
@@ -221,7 +256,12 @@ public class ChooserWrapperActivity
public DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri,
CharSequence pLabel, CharSequence pInfo, Intent replacementIntent,
@Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
- return new DisplayResolveInfo(originalIntent, pri, pLabel, pInfo, replacementIntent,
+ return DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent,
+ pri,
+ pLabel,
+ pInfo,
+ replacementIntent,
resolveInfoPresentationGetter);
}
@@ -242,32 +282,18 @@ public class ChooserWrapperActivity
}
@Override
- protected void queryDirectShareTargets(ChooserListAdapter adapter,
- boolean skipAppPredictionService) {
- if (sOverrides.onQueryDirectShareTargets != null) {
- sOverrides.onQueryDirectShareTargets.apply(adapter);
- }
- 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);
+ protected ShortcutLoader createShortcutLoader(
+ Context context,
+ AppPredictor appPredictor,
+ UserHandle userHandle,
+ IntentFilter targetIntentFilter,
+ Consumer<ShortcutLoader.Result> callback) {
+ ShortcutLoader shortcutLoader =
+ sOverrides.shortcutLoaderFactory.invoke(userHandle, callback);
+ if (shortcutLoader != null) {
+ return shortcutLoader;
}
- return 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 f81cd023..0d44e147 100644
--- a/java/tests/src/com/android/intentresolver/IChooserWrapper.java
+++ b/java/tests/src/com/android/intentresolver/IChooserWrapper.java
@@ -25,6 +25,8 @@ 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
@@ -41,4 +43,5 @@ public interface IChooserWrapper {
@Nullable ResolveInfoPresentationGetter 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 00000000..159c6d6a
--- /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 00000000..07cbd6a4
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java
@@ -0,0 +1,912 @@
+/*
+ * 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.ResolverDataProvider.createPackageManagerMockedInfo;
+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.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ActivityScenario;
+import androidx.test.espresso.Espresso;
+import androidx.test.espresso.NoMatchingViewException;
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.rule.ActivityTestRule;
+import androidx.test.runner.AndroidJUnit4;
+
+import com.android.internal.R;
+import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
+import com.android.intentresolver.ResolverDataProvider.PackageManagerMockedInfo;
+import com.android.intentresolver.ResolverListAdapter.ActivityInfoPresentationGetter;
+import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;
+import com.android.intentresolver.widget.ResolverDrawerLayout;
+
+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 getActivityLabelAndSubLabel() throws Exception {
+ ActivityInfoPresentationGetter pg;
+ PackageManagerMockedInfo info;
+
+ info = createPackageManagerMockedInfo(false);
+ pg = new ActivityInfoPresentationGetter(
+ info.ctx, 0, info.activityInfo);
+ assertThat("Label should match app label", pg.getLabel().equals(
+ info.setAppLabel));
+ assertThat("Sublabel should match activity label if set",
+ pg.getSubLabel().equals(info.setActivityLabel));
+
+ info = createPackageManagerMockedInfo(true);
+ pg = new ActivityInfoPresentationGetter(
+ info.ctx, 0, info.activityInfo);
+ assertThat("With override permission label should match activity label if set",
+ pg.getLabel().equals(info.setActivityLabel));
+ assertThat("With override permission sublabel should be empty",
+ TextUtils.isEmpty(pg.getSubLabel()));
+ }
+
+ @Test
+ public void getResolveInfoLabelAndSubLabel() throws Exception {
+ ResolveInfoPresentationGetter pg;
+ PackageManagerMockedInfo info;
+
+ info = createPackageManagerMockedInfo(false);
+ pg = new ResolveInfoPresentationGetter(
+ info.ctx, 0, info.resolveInfo);
+ assertThat("Label should match app label", pg.getLabel().equals(
+ info.setAppLabel));
+ assertThat("Sublabel should match resolve info label if set",
+ pg.getSubLabel().equals(info.setResolveInfoLabel));
+
+ info = createPackageManagerMockedInfo(true);
+ pg = new ResolveInfoPresentationGetter(
+ info.ctx, 0, info.resolveInfo);
+ assertThat("With override permission label should match activity label if set",
+ pg.getLabel().equals(info.setActivityLabel));
+ assertThat("With override permission the sublabel should be the resolve info label",
+ pg.getSubLabel().equals(info.setResolveInfoLabel));
+ }
+
+ @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 33e7123f..01d07639 100644
--- a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
+++ b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
@@ -32,7 +32,7 @@ import android.test.mock.MockResources;
/**
* 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 @@ class ResolverDataProvider {
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;
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 00000000..239bffe0
--- /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 00000000..a53b41d1
--- /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 00000000..2c56e613
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ShortcutSelectionLogicTest.kt
@@ -0,0 +1,289 @@
+/*
+ * 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.pm.ShortcutInfo
+import android.service.chooser.ChooserTarget
+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 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 = */ mock(),
+ /* 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 = */ mock(),
+ /* 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 = */ mock(),
+ /* 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 = */ mock(),
+ /* 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 = */ mock(),
+ /* 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 = */ mock(),
+ /* 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 = */ null,
+ /* 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/TestApplication.kt b/java/tests/src/com/android/intentresolver/TestApplication.kt
new file mode 100644
index 00000000..849cfbab
--- /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 00000000..5b583fef
--- /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 b901fc1e..da72a749 100644
--- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
+++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
@@ -38,18 +38,16 @@ import static com.android.intentresolver.MatcherUtils.first;
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.eq;
import static org.mockito.Mockito.atLeastOnce;
@@ -83,21 +81,24 @@ import android.net.Uri;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.service.chooser.ChooserTarget;
+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 +118,7 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.function.Consumer;
import java.util.function.Function;
/**
@@ -130,7 +132,6 @@ import java.util.function.Function;
* 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 +253,31 @@ public class UnbundledChooserActivityTest {
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 +301,7 @@ public class UnbundledChooserActivityTest {
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 +321,8 @@ public class UnbundledChooserActivityTest {
.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 +342,8 @@ public class UnbundledChooserActivityTest {
.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 +363,9 @@ public class UnbundledChooserActivityTest {
.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 +387,11 @@ public class UnbundledChooserActivityTest {
.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 +414,9 @@ public class UnbundledChooserActivityTest {
.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 +425,7 @@ public class UnbundledChooserActivityTest {
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 +441,9 @@ public class UnbundledChooserActivityTest {
.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 +468,7 @@ public class UnbundledChooserActivityTest {
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 +601,8 @@ public class UnbundledChooserActivityTest {
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 +640,7 @@ public class UnbundledChooserActivityTest {
}
@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 +666,6 @@ public class UnbundledChooserActivityTest {
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 +675,6 @@ public class UnbundledChooserActivityTest {
@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 +712,6 @@ public class UnbundledChooserActivityTest {
@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 +760,8 @@ public class UnbundledChooserActivityTest {
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();
@@ -778,8 +790,8 @@ public class UnbundledChooserActivityTest {
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());
@@ -806,52 +818,11 @@ public class UnbundledChooserActivityTest {
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 +831,7 @@ public class UnbundledChooserActivityTest {
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 +848,17 @@ public class UnbundledChooserActivityTest {
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 +881,20 @@ public class UnbundledChooserActivityTest {
.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 +918,20 @@ public class UnbundledChooserActivityTest {
.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 +958,13 @@ public class UnbundledChooserActivityTest {
.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()));
}
@@ -1135,7 +1064,7 @@ public class UnbundledChooserActivityTest {
@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);
@@ -1192,10 +1121,11 @@ public class UnbundledChooserActivityTest {
.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 +1155,11 @@ public class UnbundledChooserActivityTest {
.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 +1188,11 @@ public class UnbundledChooserActivityTest {
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 +1228,11 @@ public class UnbundledChooserActivityTest {
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 +1279,9 @@ public class UnbundledChooserActivityTest {
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);
@@ -1453,41 +1299,55 @@ public class UnbundledChooserActivityTest {
// Set up resources
MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
- // Create direct share target
- List<ChooserTarget> serviceTargets = createDirectShareTargets(1, "");
- 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());
- // 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);
+ // send shortcuts
+ assertThat(
+ "Wrong number of app targets",
+ appTargets.getValue().length,
+ is(resolvedComponentInfos.size()));
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(1, "");
+ ShortcutLoader.Result result = new ShortcutLoader.Result(
+ true,
+ appTargets.getValue(),
+ new ShortcutLoader.ShortcutResultInfo[] {
+ new ShortcutLoader.ShortcutResultInfo(
+ appTargets.getValue()[0],
+ serviceTargets
+ )
+ },
+ new HashMap<>(),
+ new HashMap<>()
+ );
+ activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+ waitForIdle();
- 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 +1355,30 @@ public class UnbundledChooserActivityTest {
.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(),
+ // Currently we're seeing 4 invocations
+ // 1. ChooserActivity.logActionShareWithPreview()
+ // 2. ChooserActivity.onCreate()
+ // 3. ChooserActivity.logDirectShareTargetReceived()
+ // 4. ChooserActivity.startSelected -- which is the one we're after
+ verify(mockLogger, Mockito.times(4)).write(logMakerCaptor.capture());
+ LogMaker selectionLog = logMakerCaptor.getAllValues().get(3);
+ assertThat(
+ selectionLog.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",
+ String hashedName = (String) selectionLog.getTaggedData(
+ MetricsEvent.FIELD_HASHED_TARGET_NAME);
+ 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));
+ assertThat(
+ "The packages shouldn't match for app target and direct target",
+ selectionLog.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);
@@ -1530,41 +1396,57 @@ public class UnbundledChooserActivityTest {
// 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 +1454,20 @@ public class UnbundledChooserActivityTest {
.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(),
+ // Currently we're seeing 4 invocations
+ // 1. ChooserActivity.logActionShareWithPreview()
+ // 2. ChooserActivity.onCreate()
+ // 3. ChooserActivity.logDirectShareTargetReceived()
+ // 4. ChooserActivity.startSelected -- which is the one we're after
+ verify(mockLogger, Mockito.times(4)).write(logMakerCaptor.capture());
+ assertThat(logMakerCaptor.getAllValues().get(3).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));
+ .getAllValues().get(3).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(0));
}
- @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 +1475,7 @@ public class UnbundledChooserActivityTest {
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 +1490,68 @@ public class UnbundledChooserActivityTest {
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 +1559,7 @@ public class UnbundledChooserActivityTest {
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 +1574,65 @@ public class UnbundledChooserActivityTest {
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
@@ -1742,7 +1650,7 @@ public class UnbundledChooserActivityTest {
.getContext().getResources().getConfiguration()));
waitForIdle();
- onView(withIdFromRuntimeResource("resolver_list"))
+ onView(withId(com.android.internal.R.id.resolver_list))
.check(matches(withGridColumnCount(6)));
}
@@ -1760,8 +1668,7 @@ public class UnbundledChooserActivityTest {
}
private void testDirectTargetLoggingWithAppTargetNotRanked(
- int orientation, int appTargetsExpected
- ) throws InterruptedException {
+ int orientation, int appTargetsExpected) {
Configuration configuration =
new Configuration(InstrumentationRegistry.getInstrumentation().getContext()
.getResources().getConfiguration());
@@ -1799,9 +1706,8 @@ public class UnbundledChooserActivityTest {
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 +1721,9 @@ public class UnbundledChooserActivityTest {
/* 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)",
@@ -1850,8 +1753,6 @@ public class UnbundledChooserActivityTest {
@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 +1760,22 @@ public class UnbundledChooserActivityTest {
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 +1794,7 @@ public class UnbundledChooserActivityTest {
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 +1802,6 @@ public class UnbundledChooserActivityTest {
@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 +1815,14 @@ public class UnbundledChooserActivityTest {
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 +1838,10 @@ public class UnbundledChooserActivityTest {
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 +1854,6 @@ public class UnbundledChooserActivityTest {
@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 +1867,17 @@ public class UnbundledChooserActivityTest {
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 +1889,19 @@ public class UnbundledChooserActivityTest {
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 +1913,18 @@ public class UnbundledChooserActivityTest {
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 +1938,17 @@ public class UnbundledChooserActivityTest {
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 +1961,12 @@ public class UnbundledChooserActivityTest {
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 +1995,7 @@ public class UnbundledChooserActivityTest {
// 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 +2008,11 @@ public class UnbundledChooserActivityTest {
.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 +2027,59 @@ public class UnbundledChooserActivityTest {
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 +2087,11 @@ public class UnbundledChooserActivityTest {
.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);
+ Mockito.verify(logger, times(1))
+ .logShareTargetSelected(typeCaptor.capture(), any(), anyInt(), anyBoolean());
+ assertThat(typeCaptor.getValue(), is(ChooserActivity.SELECTION_TYPE_SERVICE));
}
@Test @Ignore
@@ -2290,44 +2123,7 @@ public class UnbundledChooserActivityTest {
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 +2147,14 @@ public class UnbundledChooserActivityTest {
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 +2168,16 @@ public class UnbundledChooserActivityTest {
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 +2193,7 @@ public class UnbundledChooserActivityTest {
return true;
};
- mActivityRule.launchActivity(sendIntent);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Test"));
waitForIdle();
assertThat(chosen[0], is(personalResolvedComponentInfos.get(1).getResolveInfoAt(0)));
@@ -2591,7 +2225,7 @@ public class UnbundledChooserActivityTest {
when(
ChooserActivityOverrideData
.getInstance().packageManager
- .resolveActivity(any(Intent.class), anyInt()))
+ .resolveActivity(any(Intent.class), any()))
.thenReturn(ri);
waitForIdle();
@@ -2605,8 +2239,6 @@ public class UnbundledChooserActivityTest {
@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 +2256,7 @@ public class UnbundledChooserActivityTest {
ChooserActivityOverrideData
.getInstance()
.packageManager
- .resolveActivity(any(Intent.class), anyInt()))
+ .resolveActivity(any(Intent.class), any()))
.thenReturn(createFakeResolveInfo());
waitForIdle();
@@ -2637,8 +2269,6 @@ public class UnbundledChooserActivityTest {
@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 +2287,22 @@ public class UnbundledChooserActivityTest {
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 +2319,17 @@ public class UnbundledChooserActivityTest {
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 +2354,7 @@ public class UnbundledChooserActivityTest {
ChooserActivityOverrideData
.getInstance()
.packageManager
- .resolveActivity(any(Intent.class), anyInt()))
+ .resolveActivity(any(Intent.class), any()))
.thenReturn(ri);
waitForIdle();
@@ -2740,150 +2368,35 @@ public class UnbundledChooserActivityTest {
}
@Test
- public void testWorkTab_selectingWorkTabWithPausedWorkProfile_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().isQuietModeEnabled = true;
- 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 paused work profile",
- isQueryDirectShareCalledOnWorkProfile[0]);
- }
-
- @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());
- 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;
+ 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);
- 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;
- };
+ 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 locked work profile user",
- isQueryDirectShareCalledOnWorkProfile[0]);
- }
+ verify(personalProfileShortcutLoader, times(1)).queryShortcuts(any());
- @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());
+ onView(withText(R.string.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 +2605,6 @@ public class UnbundledChooserActivityTest {
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 +2645,6 @@ public class UnbundledChooserActivityTest {
.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 +2704,17 @@ public class UnbundledChooserActivityTest {
.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 00000000..b7eecb3f
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java
@@ -0,0 +1,455 @@
+/*
+ * 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 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.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+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 com.android.intentresolver.ChooserWrapperActivity.sOverrides;
+
+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.internal.R;
+import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
+import com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab;
+
+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
+ ),
+// TODO(b/256869196) ChooserActivity goes into requestLayout loop
+// 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
+ ),
+// TODO(b/256869196) ChooserActivity goes into requestLayout loop
+// 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;
+
+ onView(withText(stringId)).perform(click());
+ waitForIdle();
+
+ 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 00000000..11837e08
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt
@@ -0,0 +1,186 @@
+/*
+ * 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.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 displayInfo: DisplayResolveInfo = mock()
+ 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 resolvedIntent = mock<Intent>()
+
+ val targetInfo = SelectableTargetInfo.newSelectableTargetInfo(
+ displayInfo,
+ mock(),
+ resolvedIntent,
+ chooserTarget,
+ 0.1f,
+ shortcutInfo,
+ appTarget,
+ mock(),
+ )
+ assertThat(targetInfo.isSelectableTargetInfo).isTrue()
+ assertThat(targetInfo.isChooserTargetInfo).isTrue() // From legacy inheritance model.
+ assertThat(targetInfo.displayResolveInfo).isSameInstanceAs(displayInfo)
+ 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 00000000..448718cd
--- /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 00000000..5756a0cd
--- /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 00000000..e0de005d
--- /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)
+ }
+ }
+}