summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
author Xin Li <delphij@google.com> 2023-04-18 16:34:38 -0700
committer Xin Li <delphij@google.com> 2023-04-18 16:34:38 -0700
commit4042e26988acfecda45dbcc4d01ac1be7813b42e (patch)
tree970a428d34bfaad9fd8e70ad12619586033d18a7 /java
parentd36ad81bb94eb3178d53a227c98692e559465ea2 (diff)
parent4bc99bb4b351fe2304b3c7e147248a28c507ae57 (diff)
Merge Android 13 QPR3 tm-qpr-dev-plus-aosp-without-vendor@9936994
Bug: 275386652 Merged-In: I392de610b3d3e044e23c83d29fd11061fbc7192d Change-Id: Ib64b6b991713c518faaab01935cad9e8a57e0d98
Diffstat (limited to 'java')
-rw-r--r--java/res/anim/slide_in_right.xml22
-rw-r--r--java/res/anim/slide_out_left.xml20
-rw-r--r--java/res/layout/chooser_dialog.xml1
-rw-r--r--java/res/layout/chooser_grid_preview_file.xml12
-rw-r--r--java/res/layout/chooser_grid_preview_image.xml48
-rw-r--r--java/res/layout/chooser_grid_preview_text.xml9
-rw-r--r--java/res/layout/chooser_image_preview_view.xml26
-rw-r--r--java/res/layout/chooser_image_preview_view_internals.xml (renamed from java/res/layout/image_preview_view.xml)13
-rw-r--r--java/res/layout/image_preview_image_item.xml24
-rw-r--r--java/res/layout/resolve_grid_item.xml1
-rw-r--r--java/res/layout/scrollable_image_preview_view.xml26
-rw-r--r--java/res/values/dimens.xml2
-rw-r--r--java/res/values/strings.xml16
-rw-r--r--java/res/values/styles.xml8
-rw-r--r--java/src-debug/com/android/intentresolver/flags/DebugFeatureFlagRepository.kt81
-rw-r--r--java/src-debug/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt30
-rw-r--r--java/src-release/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt24
-rw-r--r--java/src-release/com/android/intentresolver/flags/ReleaseFeatureFlagRepository.kt31
-rw-r--r--java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java42
-rw-r--r--java/src/com/android/intentresolver/AnnotatedUserHandles.java113
-rw-r--r--java/src/com/android/intentresolver/ChooserActionFactory.java515
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java661
-rw-r--r--java/src/com/android/intentresolver/ChooserActivityLogger.java63
-rw-r--r--java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java132
-rw-r--r--java/src/com/android/intentresolver/ChooserContentPreviewUi.java566
-rw-r--r--java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java83
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java3
-rw-r--r--java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java12
-rw-r--r--java/src/com/android/intentresolver/ChooserRefinementManager.java194
-rw-r--r--java/src/com/android/intentresolver/ChooserRequestParameters.java73
-rw-r--r--java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt58
-rw-r--r--java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java4
-rw-r--r--java/src/com/android/intentresolver/ImageLoader.kt26
-rw-r--r--java/src/com/android/intentresolver/ImagePreviewImageLoader.kt79
-rw-r--r--java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java6
-rw-r--r--java/src/com/android/intentresolver/ResolvedComponentInfo.java105
-rw-r--r--java/src/com/android/intentresolver/ResolverActivity.java1516
-rw-r--r--java/src/com/android/intentresolver/ResolverListAdapter.java33
-rw-r--r--java/src/com/android/intentresolver/ResolverListController.java77
-rw-r--r--java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java12
-rw-r--r--java/src/com/android/intentresolver/SecureSettings.kt29
-rw-r--r--java/src/com/android/intentresolver/WorkProfileAvailabilityManager.java166
-rw-r--r--java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java13
-rw-r--r--java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java60
-rw-r--r--java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java633
-rw-r--r--java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java44
-rw-r--r--java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java139
-rw-r--r--java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java70
-rw-r--r--java/src/com/android/intentresolver/chooser/TargetInfo.java42
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java310
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java35
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java130
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java236
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java179
-rw-r--r--java/src/com/android/intentresolver/contentpreview/IsHttpUri.kt27
-rw-r--r--java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt33
-rw-r--r--java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java138
-rw-r--r--java/src/com/android/intentresolver/flags/DeviceConfigProxy.kt33
-rw-r--r--java/src/com/android/intentresolver/flags/FeatureFlagRepository.kt25
-rw-r--r--java/src/com/android/intentresolver/flags/Flags.kt55
-rw-r--r--java/src/com/android/intentresolver/model/AbstractResolverComparator.java2
-rw-r--r--java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java2
-rw-r--r--java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java2
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java426
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt326
-rw-r--r--java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt163
-rw-r--r--java/src/com/android/intentresolver/widget/ImagePreviewView.kt173
-rw-r--r--java/src/com/android/intentresolver/widget/RecyclerViewExtensions.kt36
-rw-r--r--java/src/com/android/intentresolver/widget/ScrollableActionRow.kt22
-rw-r--r--java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt178
-rw-r--r--java/src/com/android/intentresolver/widget/ViewExtensions.kt39
-rw-r--r--java/tests/Android.bp5
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt154
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java28
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java29
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt71
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.kt61
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java54
-rw-r--r--java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt112
-rw-r--r--java/tests/src/com/android/intentresolver/FeatureFlagRule.kt56
-rw-r--r--java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt101
-rw-r--r--java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt3
-rw-r--r--java/tests/src/com/android/intentresolver/RequireFeatureFlags.kt23
-rw-r--r--java/tests/src/com/android/intentresolver/ResolverActivityTest.java61
-rw-r--r--java/tests/src/com/android/intentresolver/ResolverDataProvider.java52
-rw-r--r--java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java28
-rw-r--r--java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt31
-rw-r--r--java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt33
-rw-r--r--java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt38
-rw-r--r--java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java886
-rw-r--r--java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java21
-rw-r--r--java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt497
-rw-r--r--java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt212
-rw-r--r--java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt203
-rw-r--r--java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java12
-rw-r--r--java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt58
96 files changed, 7848 insertions, 3514 deletions
diff --git a/java/res/anim/slide_in_right.xml b/java/res/anim/slide_in_right.xml
new file mode 100644
index 00000000..3d3cd919
--- /dev/null
+++ b/java/res/anim/slide_in_right.xml
@@ -0,0 +1,22 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate android:fromXDelta="100%p" android:toXDelta="0"
+ android:duration="@android:integer/config_mediumAnimTime"/>
+ <alpha android:fromAlpha="0.0" android:toAlpha="1.0"
+ android:duration="@android:integer/config_mediumAnimTime" />
+</set> \ No newline at end of file
diff --git a/java/res/anim/slide_out_left.xml b/java/res/anim/slide_out_left.xml
new file mode 100644
index 00000000..b3471518
--- /dev/null
+++ b/java/res/anim/slide_out_left.xml
@@ -0,0 +1,20 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<set xmlns:android="http://schemas.android.com/apk/res/android">
+ <translate android:fromXDelta="0" android:toXDelta="-100%p"
+ android:duration="@android:integer/config_mediumAnimTime"/>
+</set> \ No newline at end of file
diff --git a/java/res/layout/chooser_dialog.xml b/java/res/layout/chooser_dialog.xml
index e31712c7..19ead35a 100644
--- a/java/res/layout/chooser_dialog.xml
+++ b/java/res/layout/chooser_dialog.xml
@@ -18,6 +18,7 @@
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/chooser_dialog_content"
android:background="@drawable/chooser_dialog_background"
android:orientation="vertical"
android:paddingBottom="8dp"
diff --git a/java/res/layout/chooser_grid_preview_file.xml b/java/res/layout/chooser_grid_preview_file.xml
index e98c3273..095e5d62 100644
--- a/java/res/layout/chooser_grid_preview_file.xml
+++ b/java/res/layout/chooser_grid_preview_file.xml
@@ -65,9 +65,19 @@
android:ellipsize="middle"
android:gravity="start|top"
android:paddingRight="24dp"
- android:singleLine="true"/>
+ android:singleLine="true"
+ android:textAppearance="@style/TextAppearance.ChooserDefault" />
</LinearLayout>
+ <TextView
+ android:id="@+id/reselection_action"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:text="@string/select_files"
+ android:gravity="center"
+ style="@style/ReselectionAction" />
+
<ViewStub
android:id="@+id/action_row_stub"
android:layout_width="match_parent"
diff --git a/java/res/layout/chooser_grid_preview_image.xml b/java/res/layout/chooser_grid_preview_image.xml
index 5c324140..792b7d4d 100644
--- a/java/res/layout/chooser_grid_preview_image.xml
+++ b/java/res/layout/chooser_grid_preview_image.xml
@@ -16,7 +16,6 @@
* limitations under the License.
*/
-->
-<!-- Layout Option: Supporting up to 3 images for preview -->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
@@ -25,13 +24,49 @@
android:orientation="vertical"
android:background="?android:attr/colorBackground">
- <com.android.intentresolver.widget.ImagePreviewView
- android:id="@androidprv:id/content_preview_image_area"
+ <CheckBox
+ android:id="@+id/include_text_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:paddingBottom="@dimen/chooser_view_spacing"
- android:background="?android:attr/colorBackground" />
+ android:layout_gravity="end"
+ android:layout_marginEnd="@dimen/chooser_edge_margin_normal"
+ android:visibility="gone" />
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:gravity="center_horizontal"
+ android:layout_marginBottom="@dimen/chooser_view_spacing">
+
+ <ViewStub
+ android:id="@+id/image_preview_stub"
+ android:inflatedId="@androidprv:id/content_preview_image_area"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+
+ <TextView
+ android:id="@androidprv:id/content_preview_text"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_weight="1"
+ android:layout_gravity="center_vertical"
+ android:paddingEnd="@dimen/chooser_edge_margin_normal"
+ android:maxLines="6"
+ android:ellipsize="end"
+ android:linksClickable="false"
+ android:visibility="gone"
+ android:textAppearance="@style/TextAppearance.ChooserDefault" />
+ </LinearLayout>
+
+ <TextView
+ android:id="@+id/reselection_action"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:text="@string/select_images"
+ android:gravity="center"
+ style="@style/ReselectionAction" />
<ViewStub
android:id="@+id/action_row_stub"
@@ -39,4 +74,3 @@
android:layout_height="wrap_content" />
</LinearLayout>
-
diff --git a/java/res/layout/chooser_grid_preview_text.xml b/java/res/layout/chooser_grid_preview_text.xml
index db7282e3..49a2edff 100644
--- a/java/res/layout/chooser_grid_preview_text.xml
+++ b/java/res/layout/chooser_grid_preview_text.xml
@@ -52,6 +52,15 @@
</RelativeLayout>
+ <TextView
+ android:id="@+id/reselection_action"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:text="@string/select_text"
+ android:gravity="center"
+ style="@style/ReselectionAction" />
+
<ViewStub
android:id="@+id/action_row_stub"
android:layout_width="match_parent"
diff --git a/java/res/layout/chooser_image_preview_view.xml b/java/res/layout/chooser_image_preview_view.xml
new file mode 100644
index 00000000..e81349c7
--- /dev/null
+++ b/java/res/layout/chooser_image_preview_view.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<com.android.intentresolver.widget.ChooserImagePreviewView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:paddingStart="@dimen/chooser_edge_margin_normal"
+ android:paddingEnd="@dimen/chooser_edge_margin_normal"
+ android:paddingBottom="@dimen/chooser_view_spacing"
+ android:background="?android:attr/colorBackground" />
diff --git a/java/res/layout/image_preview_view.xml b/java/res/layout/chooser_image_preview_view_internals.xml
index d2f94690..2b93edf8 100644
--- a/java/res/layout/image_preview_view.xml
+++ b/java/res/layout/chooser_image_preview_view_internals.xml
@@ -25,8 +25,9 @@
<com.android.intentresolver.widget.RoundedRectImageView
android:id="@androidprv:id/content_preview_image_1_large"
- android:layout_width="120dp"
- android:layout_height="104dp"
+ android:transitionName="screenshot_preview_image"
+ android:layout_width="@dimen/chooser_preview_image_width"
+ android:layout_height="@dimen/chooser_preview_image_height"
android:layout_alignParentTop="true"
android:adjustViewBounds="true"
android:gravity="center"
@@ -35,8 +36,8 @@
<com.android.intentresolver.widget.RoundedRectImageView
android:id="@androidprv:id/content_preview_image_2_large"
android:visibility="gone"
- android:layout_width="120dp"
- android:layout_height="104dp"
+ android:layout_width="@dimen/chooser_preview_image_width"
+ android:layout_height="@dimen/chooser_preview_image_height"
android:layout_alignParentTop="true"
android:layout_toRightOf="@androidprv:id/content_preview_image_1_large"
android:layout_marginLeft="10dp"
@@ -47,7 +48,7 @@
<com.android.intentresolver.widget.RoundedRectImageView
android:id="@androidprv:id/content_preview_image_2_small"
android:visibility="gone"
- android:layout_width="120dp"
+ android:layout_width="@dimen/chooser_preview_image_width"
android:layout_height="65dp"
android:layout_alignParentTop="true"
android:layout_toRightOf="@androidprv:id/content_preview_image_1_large"
@@ -59,7 +60,7 @@
<com.android.intentresolver.widget.RoundedRectImageView
android:id="@androidprv:id/content_preview_image_3_small"
android:visibility="gone"
- android:layout_width="120dp"
+ android:layout_width="@dimen/chooser_preview_image_width"
android:layout_height="65dp"
android:layout_below="@androidprv:id/content_preview_image_2_small"
android:layout_toRightOf="@androidprv:id/content_preview_image_1_large"
diff --git a/java/res/layout/image_preview_image_item.xml b/java/res/layout/image_preview_image_item.xml
new file mode 100644
index 00000000..c18cc279
--- /dev/null
+++ b/java/res/layout/image_preview_image_item.xml
@@ -0,0 +1,24 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<com.android.intentresolver.widget.RoundedRectImageView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/image"
+ android:layout_width="@dimen/chooser_preview_image_width"
+ android:layout_height="@dimen/chooser_preview_image_height"
+ android:layout_alignParentTop="true"
+ android:adjustViewBounds="false"
+ android:scaleType="centerCrop"/>
diff --git a/java/res/layout/resolve_grid_item.xml b/java/res/layout/resolve_grid_item.xml
index db6c7dd9..00ca9945 100644
--- a/java/res/layout/resolve_grid_item.xml
+++ b/java/res/layout/resolve_grid_item.xml
@@ -18,6 +18,7 @@
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:id="@androidprv:id/item"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content"
diff --git a/java/res/layout/scrollable_image_preview_view.xml b/java/res/layout/scrollable_image_preview_view.xml
new file mode 100644
index 00000000..c6c310e6
--- /dev/null
+++ b/java/res/layout/scrollable_image_preview_view.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<com.android.intentresolver.widget.ScrollableImagePreviewView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:paddingStart="@dimen/chooser_edge_margin_normal"
+ android:paddingEnd="@dimen/chooser_edge_margin_normal"
+ android:paddingBottom="@dimen/chooser_view_spacing"
+ android:background="?android:attr/colorBackground" />
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index 93cb4637..87eec7fb 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -25,6 +25,8 @@
<dimen name="chooser_edge_margin_normal">24dp</dimen>
<dimen name="chooser_preview_image_font_size">20sp</dimen>
<dimen name="chooser_preview_image_border">1dp</dimen>
+ <dimen name="chooser_preview_image_width">120dp</dimen>
+ <dimen name="chooser_preview_image_height">104dp</dimen>
<dimen name="chooser_preview_image_max_dimen">200dp</dimen>
<dimen name="chooser_preview_width">-1px</dimen>
<dimen name="chooser_header_scroll_elevation">4dp</dimen>
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index a536d3bc..24604ed3 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -101,4 +101,20 @@
<string name="miniresolver_use_personal_browser">Use personal browser</string>
<!-- Button option. Open the link in the work browser. [CHAR LIMIT=NONE] -->
<string name="miniresolver_use_work_browser">Use work browser</string>
+
+ <!-- Tittle for a button. Launches client-provided content reselection action. -->
+ <string name="select_files">Select Files</string>
+ <!-- Tittle for a button. Launches client-provided content reselection action. -->
+ <string name="select_images">Select Images</string>
+ <!-- Tittle for a button. Launches client-provided content reselection action. -->
+ <string name="select_text">Select Text</string>
+
+ <!-- Title for a button. Excludes a text from the shared content (a media and a text). -->
+ <string name="exclude_text">Exclude text</string>
+ <!-- Title for a button. Adds back a (previously excluded) text into the shared content. -->
+ <string name="include_text">Include text</string>
+ <!-- Title for a button. Excludes a web link from the shared content (a media and a text). -->
+ <string name="exclude_link">Exclude link</string>
+ <!-- Title for a button. Adds back a (previously excluded) web link into the shared content. -->
+ <string name="include_link">Include link</string>
</resources>
diff --git a/java/res/values/styles.xml b/java/res/values/styles.xml
index cbbf406d..ba6418a8 100644
--- a/java/res/values/styles.xml
+++ b/java/res/values/styles.xml
@@ -46,4 +46,12 @@
<item name="*android:iconfactoryIconSize">@dimen/chooser_icon_size</item>
<item name="*android:iconfactoryBadgeSize">@dimen/chooser_badge_size</item>
</style>
+
+ <style name="TextAppearance.ChooserDefault"
+ parent="@android:style/TextAppearance.DeviceDefault" />
+
+ <style name="ReselectionAction" parent="TextAppearance.ChooserDefault">
+ <item name="android:paddingTop">5dp</item>
+ <item name="android:paddingBottom">5dp</item>
+ </style>
</resources>
diff --git a/java/src-debug/com/android/intentresolver/flags/DebugFeatureFlagRepository.kt b/java/src-debug/com/android/intentresolver/flags/DebugFeatureFlagRepository.kt
new file mode 100644
index 00000000..5067c0ee
--- /dev/null
+++ b/java/src-debug/com/android/intentresolver/flags/DebugFeatureFlagRepository.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.flags
+
+import android.util.SparseBooleanArray
+import androidx.annotation.GuardedBy
+import com.android.systemui.flags.BooleanFlag
+import com.android.systemui.flags.FlagManager
+import com.android.systemui.flags.ReleasedFlag
+import com.android.systemui.flags.UnreleasedFlag
+import javax.annotation.concurrent.ThreadSafe
+
+@ThreadSafe
+internal class DebugFeatureFlagRepository(
+ private val flagManager: FlagManager,
+ private val deviceConfig: DeviceConfigProxy,
+) : FeatureFlagRepository {
+ @GuardedBy("self")
+ private val cache = hashMapOf<String, Boolean>()
+
+ override fun isEnabled(flag: UnreleasedFlag): Boolean = isFlagEnabled(flag)
+
+ override fun isEnabled(flag: ReleasedFlag): Boolean = isFlagEnabled(flag)
+
+ private fun isFlagEnabled(flag: BooleanFlag): Boolean {
+ synchronized(cache) {
+ cache[flag.name]?.let { return it }
+ }
+ val flagValue = readFlagValue(flag)
+ return synchronized(cache) {
+ // the first read saved in the cache wins
+ cache.getOrPut(flag.name) { flagValue }
+ }
+ }
+
+ private fun readFlagValue(flag: BooleanFlag): Boolean {
+ val localOverride = runCatching {
+ flagManager.isEnabled(flag.name)
+ }.getOrDefault(null)
+ val remoteOverride = deviceConfig.isEnabled(flag)
+
+ // Only check for teamfood if the default is false
+ // and there is no server override.
+ if (remoteOverride == null
+ && !flag.default
+ && localOverride == null
+ && !flag.isTeamfoodFlag
+ && flag.teamfood
+ ) {
+ return flagManager.isTeamfoodEnabled
+ }
+ return localOverride ?: remoteOverride ?: flag.default
+ }
+
+ companion object {
+ /** keep in sync with [com.android.systemui.flags.Flags] */
+ private const val TEAMFOOD_FLAG_NAME = "teamfood"
+
+ private val BooleanFlag.isTeamfoodFlag: Boolean
+ get() = name == TEAMFOOD_FLAG_NAME
+
+ private val FlagManager.isTeamfoodEnabled: Boolean
+ get() = runCatching {
+ isEnabled(TEAMFOOD_FLAG_NAME) ?: false
+ }.getOrDefault(false)
+ }
+}
diff --git a/java/src-debug/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt b/java/src-debug/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt
new file mode 100644
index 00000000..4ddb0447
--- /dev/null
+++ b/java/src-debug/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.flags
+
+import android.content.Context
+import android.os.Handler
+import android.os.Looper
+import com.android.systemui.flags.FlagManager
+
+class FeatureFlagRepositoryFactory {
+ fun create(context: Context): FeatureFlagRepository =
+ DebugFeatureFlagRepository(
+ FlagManager(context, Handler(Looper.getMainLooper())),
+ DeviceConfigProxy(),
+ )
+}
diff --git a/java/src-release/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt b/java/src-release/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt
new file mode 100644
index 00000000..6bf7579e
--- /dev/null
+++ b/java/src-release/com/android/intentresolver/flags/FeatureFlagRepositoryFactory.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.flags
+
+import android.content.Context
+
+class FeatureFlagRepositoryFactory {
+ fun create(context: Context): FeatureFlagRepository =
+ ReleaseFeatureFlagRepository(DeviceConfigProxy())
+}
diff --git a/java/src-release/com/android/intentresolver/flags/ReleaseFeatureFlagRepository.kt b/java/src-release/com/android/intentresolver/flags/ReleaseFeatureFlagRepository.kt
new file mode 100644
index 00000000..f9fa2c6a
--- /dev/null
+++ b/java/src-release/com/android/intentresolver/flags/ReleaseFeatureFlagRepository.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.flags
+
+import com.android.systemui.flags.ReleasedFlag
+import com.android.systemui.flags.UnreleasedFlag
+import javax.annotation.concurrent.ThreadSafe
+
+@ThreadSafe
+internal class ReleaseFeatureFlagRepository(
+ private val deviceConfig: DeviceConfigProxy,
+) : FeatureFlagRepository {
+ override fun isEnabled(flag: UnreleasedFlag): Boolean = flag.default
+
+ override fun isEnabled(flag: ReleasedFlag): Boolean =
+ deviceConfig.isEnabled(flag) ?: flag.default
+}
diff --git a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
index 17dbb8f2..e3f1b233 100644
--- a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
@@ -40,6 +40,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
+import java.util.function.Supplier;
/**
* Skeletal {@link PagerAdapter} implementation of a work or personal profile page for
@@ -61,22 +62,20 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
private Set<Integer> mLoadedPages;
private final EmptyStateProvider mEmptyStateProvider;
private final UserHandle mWorkProfileUserHandle;
- private final QuietModeManager mQuietModeManager;
+ private final Supplier<Boolean> mWorkProfileQuietModeChecker; // True when work is quiet.
- AbstractMultiProfilePagerAdapter(Context context, int currentPage,
+ AbstractMultiProfilePagerAdapter(
+ Context context,
+ int currentPage,
EmptyStateProvider emptyStateProvider,
- QuietModeManager quietModeManager,
+ Supplier<Boolean> workProfileQuietModeChecker,
UserHandle workProfileUserHandle) {
mContext = Objects.requireNonNull(context);
mCurrentPage = currentPage;
mLoadedPages = new HashSet<>();
mWorkProfileUserHandle = workProfileUserHandle;
mEmptyStateProvider = emptyStateProvider;
- mQuietModeManager = quietModeManager;
- }
-
- private boolean isQuietModeEnabled(UserHandle workProfileUserHandle) {
- return mQuietModeManager.isQuietModeEnabled(workProfileUserHandle);
+ mWorkProfileQuietModeChecker = workProfileQuietModeChecker;
}
void setOnProfileSelectedListener(OnProfileSelectedListener listener) {
@@ -433,7 +432,7 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
int count = listAdapter.getUnfilteredCount();
return (count == 0 && listAdapter.getPlaceholderCount() == 0)
|| (listAdapter.getUserHandle().equals(mWorkProfileUserHandle)
- && isQuietModeEnabled(mWorkProfileUserHandle));
+ && mWorkProfileQuietModeChecker.get());
}
protected static class ProfileDescriptor {
@@ -573,29 +572,4 @@ public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
*/
void onSwitchOnWorkSelected();
}
-
- /**
- * Describes an injector to be used for cross profile functionality. Overridable for testing.
- */
- public interface QuietModeManager {
- /**
- * Returns whether the given profile is in quiet mode or not.
- */
- boolean isQuietModeEnabled(UserHandle workProfileUserHandle);
-
- /**
- * 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/AnnotatedUserHandles.java b/java/src/com/android/intentresolver/AnnotatedUserHandles.java
new file mode 100644
index 00000000..b4365b84
--- /dev/null
+++ b/java/src/com/android/intentresolver/AnnotatedUserHandles.java
@@ -0,0 +1,113 @@
+/*
+ * 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.Activity;
+import android.app.ActivityManager;
+import android.os.UserHandle;
+import android.os.UserManager;
+
+/**
+ * Helper class to precompute the (immutable) designations of various user handles in the system
+ * that may contribute to the current Sharesheet session.
+ */
+public final class AnnotatedUserHandles {
+ /** The user id of the app that started the share activity. */
+ public final int userIdOfCallingApp;
+
+ /**
+ * The {@link UserHandle} that launched Sharesheet.
+ * TODO: I believe this would always be the handle corresponding to {@code userIdOfCallingApp}
+ * except possibly if the caller used {@link Activity#startActivityAsUser()} to launch
+ * Sharesheet as a different user than they themselves were running as. Verify and document.
+ */
+ public final UserHandle userHandleSharesheetLaunchedAs;
+
+ /**
+ * The {@link UserHandle} that owns the "personal tab" in a tabbed share UI (or the *only* 'tab'
+ * in a non-tabbed UI).
+ *
+ * This is never a work or clone user, but may either be the root user (0) or a "secondary"
+ * multi-user profile (i.e., one that's not root, work, nor clone). This is a "secondary"
+ * profile only when that user is the active "foreground" user.
+ *
+ * In the current implementation, we can assert that this is the root user (0) any time we
+ * display a tabbed UI (i.e., any time `workProfileUserHandle` is non-null), or any time that we
+ * have a clone profile. This note is only provided for informational purposes; clients should
+ * avoid making any reliances on that assumption.
+ */
+ public final UserHandle personalProfileUserHandle;
+
+ /**
+ * The {@link UserHandle} that owns the "work tab" in a tabbed share UI. This is (an arbitrary)
+ * one of the "managed" profiles associated with {@link personalProfileUserHandle}.
+ */
+ @Nullable
+ public final UserHandle workProfileUserHandle;
+
+ /**
+ * The {@link UserHandle} of the clone profile belonging to {@link personalProfileUserHandle}.
+ */
+ @Nullable
+ public final UserHandle cloneProfileUserHandle;
+
+ /**
+ * The "tab owner" user handle (i.e., either {@link personalProfileUserHandle} or
+ * {@link workProfileUserHandle}) that either matches or owns the profile of the
+ * {@link userHandleSharesheetLaunchedAs}.
+ *
+ * In the current implementation, we can assert that this is the same as
+ * `userHandleSharesheetLaunchedAs` except when the latter is the clone profile; then this is
+ * the "personal" profile owning that clone profile (which we currently know must belong to
+ * user 0, but clients should avoid making any reliances on that assumption).
+ */
+ public final UserHandle tabOwnerUserHandleForLaunch;
+
+ public AnnotatedUserHandles(Activity forShareActivity) {
+ userIdOfCallingApp = forShareActivity.getLaunchedFromUid();
+ if ((userIdOfCallingApp < 0) || UserHandle.isIsolated(userIdOfCallingApp)) {
+ throw new SecurityException("Can't start a resolver from uid " + userIdOfCallingApp);
+ }
+
+ // TODO: integrate logic for `ResolverActivity.EXTRA_CALLING_USER`.
+ userHandleSharesheetLaunchedAs = UserHandle.of(UserHandle.myUserId());
+
+ personalProfileUserHandle = UserHandle.of(ActivityManager.getCurrentUser());
+
+ UserManager userManager = forShareActivity.getSystemService(UserManager.class);
+ workProfileUserHandle = getWorkProfileForUser(userManager, personalProfileUserHandle);
+ cloneProfileUserHandle = getCloneProfileForUser(userManager, personalProfileUserHandle);
+
+ tabOwnerUserHandleForLaunch = (userHandleSharesheetLaunchedAs == workProfileUserHandle)
+ ? workProfileUserHandle : personalProfileUserHandle;
+ }
+
+ @Nullable
+ private static UserHandle getWorkProfileForUser(
+ UserManager userManager, UserHandle profileOwnerUserHandle) {
+ return userManager.getProfiles(profileOwnerUserHandle.getIdentifier()).stream()
+ .filter(info -> info.isManagedProfile()).findFirst()
+ .map(info -> info.getUserHandle()).orElse(null);
+ }
+
+ @Nullable
+ private static UserHandle getCloneProfileForUser(
+ UserManager userManager, UserHandle profileOwnerUserHandle) {
+ return null; // Not yet supported in framework.
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java
new file mode 100644
index 00000000..947155f3
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserActionFactory.java
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.ActivityOptions;
+import android.app.PendingIntent;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.service.chooser.ChooserAction;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.View;
+
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
+import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.flags.Flags;
+import com.android.intentresolver.widget.ActionRow;
+import com.android.internal.annotations.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Callable;
+import java.util.function.Consumer;
+
+/**
+ * Implementation of {@link ChooserContentPreviewUi.ActionFactory} specialized to the application
+ * requirements of Sharesheet / {@link ChooserActivity}.
+ */
+public final class ChooserActionFactory implements ChooserContentPreviewUi.ActionFactory {
+ /** Delegate interface to launch activities when the actions are selected. */
+ public interface ActionActivityStarter {
+ /**
+ * Request an activity launch for the provided target. Implementations may choose to exit
+ * the current activity when the target is launched.
+ */
+ void safelyStartActivityAsPersonalProfileUser(TargetInfo info);
+
+ /**
+ * Request an activity launch for the provided target, optionally employing the specified
+ * shared element transition. Implementations may choose to exit the current activity when
+ * the target is launched.
+ */
+ default void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+ TargetInfo info, View sharedElement, String sharedElementName) {
+ safelyStartActivityAsPersonalProfileUser(info);
+ }
+ }
+
+ private static final String TAG = "ChooserActions";
+
+ private static final int URI_PERMISSION_INTENT_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
+
+ private static final String CHIP_LABEL_METADATA_KEY = "android.service.chooser.chip_label";
+ private static final String CHIP_ICON_METADATA_KEY = "android.service.chooser.chip_icon";
+
+ private static final String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image";
+
+ private final Context mContext;
+ private final String mCopyButtonLabel;
+ private final Drawable mCopyButtonDrawable;
+ private final Runnable mOnCopyButtonClicked;
+ private final TargetInfo mEditSharingTarget;
+ private final Runnable mOnEditButtonClicked;
+ private final TargetInfo mNearbySharingTarget;
+ private final Runnable mOnNearbyButtonClicked;
+ private final ImmutableList<ChooserAction> mCustomActions;
+ private final Runnable mOnModifyShareClicked;
+ private final Consumer<Boolean> mExcludeSharedTextAction;
+ private final Consumer</* @Nullable */ Integer> mFinishCallback;
+ private final ChooserActivityLogger mLogger;
+
+ /**
+ * @param context
+ * @param chooserRequest data about the invocation of the current Sharesheet session.
+ * @param featureFlagRepository feature flags that may control the eligibility of some actions.
+ * @param integratedDeviceComponents info about other components that are available on this
+ * device to implement the supported action types.
+ * @param onUpdateSharedTextIsExcluded a delegate to be invoked when the "exclude shared text"
+ * setting is updated. The argument is whether the shared text is to be excluded.
+ * @param firstVisibleImageQuery a delegate that provides a reference to the first visible image
+ * View in the Sharesheet UI, if any, or null.
+ * @param activityStarter a delegate to launch activities when actions are selected.
+ * @param finishCallback a delegate to close the Sharesheet UI (e.g. because some action was
+ * completed).
+ */
+ public ChooserActionFactory(
+ Context context,
+ ChooserRequestParameters chooserRequest,
+ FeatureFlagRepository featureFlagRepository,
+ ChooserIntegratedDeviceComponents integratedDeviceComponents,
+ ChooserActivityLogger logger,
+ Consumer<Boolean> onUpdateSharedTextIsExcluded,
+ Callable</* @Nullable */ View> firstVisibleImageQuery,
+ ActionActivityStarter activityStarter,
+ Consumer</* @Nullable */ Integer> finishCallback) {
+ this(
+ context,
+ context.getString(com.android.internal.R.string.copy),
+ context.getDrawable(com.android.internal.R.drawable.ic_menu_copy_material),
+ makeOnCopyRunnable(
+ context,
+ chooserRequest.getTargetIntent(),
+ chooserRequest.getReferrerPackageName(),
+ finishCallback,
+ logger),
+ getEditSharingTarget(
+ context,
+ chooserRequest.getTargetIntent(),
+ integratedDeviceComponents),
+ makeOnEditRunnable(
+ getEditSharingTarget(
+ context,
+ chooserRequest.getTargetIntent(),
+ integratedDeviceComponents),
+ firstVisibleImageQuery,
+ activityStarter,
+ logger),
+ getNearbySharingTarget(
+ context,
+ chooserRequest.getTargetIntent(),
+ integratedDeviceComponents),
+ makeOnNearbyShareRunnable(
+ getNearbySharingTarget(
+ context,
+ chooserRequest.getTargetIntent(),
+ integratedDeviceComponents),
+ activityStarter,
+ finishCallback,
+ logger),
+ chooserRequest.getChooserActions(),
+ (featureFlagRepository.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)
+ ? createModifyShareRunnable(
+ chooserRequest.getModifyShareAction(),
+ finishCallback,
+ logger)
+ : null),
+ onUpdateSharedTextIsExcluded,
+ logger,
+ finishCallback);
+ }
+
+ @VisibleForTesting
+ ChooserActionFactory(
+ Context context,
+ String copyButtonLabel,
+ Drawable copyButtonDrawable,
+ Runnable onCopyButtonClicked,
+ TargetInfo editSharingTarget,
+ Runnable onEditButtonClicked,
+ TargetInfo nearbySharingTarget,
+ Runnable onNearbyButtonClicked,
+ List<ChooserAction> customActions,
+ @Nullable Runnable onModifyShareClicked,
+ Consumer<Boolean> onUpdateSharedTextIsExcluded,
+ ChooserActivityLogger logger,
+ Consumer</* @Nullable */ Integer> finishCallback) {
+ mContext = context;
+ mCopyButtonLabel = copyButtonLabel;
+ mCopyButtonDrawable = copyButtonDrawable;
+ mOnCopyButtonClicked = onCopyButtonClicked;
+ mEditSharingTarget = editSharingTarget;
+ mOnEditButtonClicked = onEditButtonClicked;
+ mNearbySharingTarget = nearbySharingTarget;
+ mOnNearbyButtonClicked = onNearbyButtonClicked;
+ mCustomActions = ImmutableList.copyOf(customActions);
+ mOnModifyShareClicked = onModifyShareClicked;
+ mExcludeSharedTextAction = onUpdateSharedTextIsExcluded;
+ mLogger = logger;
+ mFinishCallback = finishCallback;
+ }
+
+ /** Create an action that copies the share content to the clipboard. */
+ @Override
+ public ActionRow.Action createCopyButton() {
+ return new ActionRow.Action(
+ com.android.internal.R.id.chooser_copy_button,
+ mCopyButtonLabel,
+ mCopyButtonDrawable,
+ mOnCopyButtonClicked);
+ }
+
+ /** Create an action that opens the share content in a system-default editor. */
+ @Override
+ @Nullable
+ public ActionRow.Action createEditButton() {
+ if (mEditSharingTarget == null) {
+ return null;
+ }
+
+ return new ActionRow.Action(
+ com.android.internal.R.id.chooser_edit_button,
+ mEditSharingTarget.getDisplayLabel(),
+ mEditSharingTarget.getDisplayIconHolder().getDisplayIcon(),
+ mOnEditButtonClicked);
+ }
+
+ /** Create a "Share to Nearby" action. */
+ @Override
+ @Nullable
+ public ActionRow.Action createNearbyButton() {
+ if (mNearbySharingTarget == null) {
+ return null;
+ }
+
+ return new ActionRow.Action(
+ com.android.internal.R.id.chooser_nearby_button,
+ mNearbySharingTarget.getDisplayLabel(),
+ mNearbySharingTarget.getDisplayIconHolder().getDisplayIcon(),
+ mOnNearbyButtonClicked);
+ }
+
+ /** Create custom actions */
+ @Override
+ public List<ActionRow.Action> createCustomActions() {
+ List<ActionRow.Action> actions = new ArrayList<>();
+ for (int i = 0; i < mCustomActions.size(); i++) {
+ ActionRow.Action actionRow = createCustomAction(
+ mContext, mCustomActions.get(i), mFinishCallback, i, mLogger);
+ if (actionRow != null) {
+ actions.add(actionRow);
+ }
+ }
+ return actions;
+ }
+
+ /**
+ * Provides a share modification action, if any.
+ */
+ @Override
+ @Nullable
+ public Runnable getModifyShareAction() {
+ return mOnModifyShareClicked;
+ }
+
+ private static Runnable createModifyShareRunnable(
+ PendingIntent pendingIntent,
+ Consumer<Integer> finishCallback,
+ ChooserActivityLogger logger) {
+ if (pendingIntent == null) {
+ return null;
+ }
+
+ return () -> {
+ try {
+ pendingIntent.send();
+ } catch (PendingIntent.CanceledException e) {
+ Log.d(TAG, "Payload reselection action has been cancelled");
+ }
+ logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_MODIFY_SHARE);
+ finishCallback.accept(Activity.RESULT_OK);
+ };
+ }
+
+ /**
+ * <p>
+ * Creates an exclude-text action that can be called when the user changes shared text
+ * status in the Media + Text preview.
+ * </p>
+ * <p>
+ * <code>true</code> argument value indicates that the text should be excluded.
+ * </p>
+ */
+ @Override
+ public Consumer<Boolean> getExcludeSharedTextAction() {
+ return mExcludeSharedTextAction;
+ }
+
+ private static Runnable makeOnCopyRunnable(
+ Context context,
+ Intent targetIntent,
+ String referrerPackageName,
+ Consumer<Integer> finishCallback,
+ ChooserActivityLogger logger) {
+ return () -> {
+ if (targetIntent == null) {
+ finishCallback.accept(null);
+ return;
+ }
+
+ final String action = targetIntent.getAction();
+
+ ClipData clipData = null;
+ if (Intent.ACTION_SEND.equals(action)) {
+ String extraText = targetIntent.getStringExtra(Intent.EXTRA_TEXT);
+ Uri extraStream = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+
+ if (extraText != null) {
+ clipData = ClipData.newPlainText(null, extraText);
+ } else if (extraStream != null) {
+ clipData = ClipData.newUri(context.getContentResolver(), null, extraStream);
+ } else {
+ Log.w(TAG, "No data available to copy to clipboard");
+ return;
+ }
+ } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+ final ArrayList<Uri> streams = targetIntent.getParcelableArrayListExtra(
+ Intent.EXTRA_STREAM);
+ clipData = ClipData.newUri(context.getContentResolver(), null, streams.get(0));
+ for (int i = 1; i < streams.size(); i++) {
+ clipData.addItem(
+ context.getContentResolver(),
+ new ClipData.Item(streams.get(i)));
+ }
+ } else {
+ // expected to only be visible with ACTION_SEND or ACTION_SEND_MULTIPLE
+ // so warn about unexpected action
+ Log.w(TAG, "Action (" + action + ") not supported for copying to clipboard");
+ return;
+ }
+
+ ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(
+ Context.CLIPBOARD_SERVICE);
+ clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName);
+
+ logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_COPY);
+ finishCallback.accept(Activity.RESULT_OK);
+ };
+ }
+
+ private static TargetInfo getEditSharingTarget(
+ Context context,
+ Intent originalIntent,
+ ChooserIntegratedDeviceComponents integratedComponents) {
+ final ComponentName editorComponent = integratedComponents.getEditSharingComponent();
+
+ final Intent resolveIntent = new Intent(originalIntent);
+ // Retain only URI permission grant flags if present. Other flags may prevent the scene
+ // transition animation from running (i.e FLAG_ACTIVITY_NO_ANIMATION,
+ // FLAG_ACTIVITY_NEW_TASK, FLAG_ACTIVITY_NEW_DOCUMENT) but also not needed.
+ resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS);
+ resolveIntent.setComponent(editorComponent);
+ 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 = context.getContentResolver().getType(uri);
+ resolveIntent.setDataAndType(uri, mimeType);
+ }
+ }
+ } else {
+ Log.e(TAG, originalAction + " is not supported.");
+ return null;
+ }
+ final ResolveInfo ri = context.getPackageManager().resolveActivity(
+ resolveIntent, PackageManager.GET_META_DATA);
+ if (ri == null || ri.activityInfo == null) {
+ Log.e(TAG, "Device-specified editor (" + editorComponent + ") not available");
+ return null;
+ }
+
+ final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent,
+ ri,
+ context.getString(com.android.internal.R.string.screenshot_edit),
+ "",
+ resolveIntent,
+ null);
+ dri.getDisplayIconHolder().setDisplayIcon(
+ context.getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));
+ return dri;
+ }
+
+ private static Runnable makeOnEditRunnable(
+ TargetInfo editSharingTarget,
+ Callable</* @Nullable */ View> firstVisibleImageQuery,
+ ActionActivityStarter activityStarter,
+ ChooserActivityLogger logger) {
+ return () -> {
+ // Log share completion via edit.
+ logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_EDIT);
+
+ View firstImageView = null;
+ try {
+ firstImageView = firstVisibleImageQuery.call();
+ } catch (Exception e) { /* ignore */ }
+ // Action bar is user-independent; always start as primary.
+ if (firstImageView == null) {
+ activityStarter.safelyStartActivityAsPersonalProfileUser(editSharingTarget);
+ } else {
+ activityStarter.safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+ editSharingTarget, firstImageView, IMAGE_EDITOR_SHARED_ELEMENT);
+ }
+ };
+ }
+
+ private static TargetInfo getNearbySharingTarget(
+ Context context,
+ Intent originalIntent,
+ ChooserIntegratedDeviceComponents integratedComponents) {
+ final ComponentName cn = integratedComponents.getNearbySharingComponent();
+ if (cn == null) {
+ return null;
+ }
+
+ final Intent resolveIntent = new Intent(originalIntent);
+ resolveIntent.setComponent(cn);
+ final ResolveInfo ri = context.getPackageManager().resolveActivity(
+ resolveIntent, PackageManager.GET_META_DATA);
+ if (ri == null || ri.activityInfo == null) {
+ Log.e(TAG, "Device-specified nearby sharing component (" + cn
+ + ") not available");
+ return null;
+ }
+
+ // Allow the nearby sharing component to provide a more appropriate icon and label
+ // for the chip.
+ CharSequence name = null;
+ Drawable icon = null;
+ final Bundle metaData = ri.activityInfo.metaData;
+ if (metaData != null) {
+ try {
+ final Resources pkgRes = context.getPackageManager().getResourcesForActivity(cn);
+ final int nameResId = metaData.getInt(CHIP_LABEL_METADATA_KEY);
+ name = pkgRes.getString(nameResId);
+ final int resId = metaData.getInt(CHIP_ICON_METADATA_KEY);
+ icon = pkgRes.getDrawable(resId);
+ } catch (NameNotFoundException | Resources.NotFoundException ex) { /* ignore */ }
+ }
+ if (TextUtils.isEmpty(name)) {
+ name = ri.loadLabel(context.getPackageManager());
+ }
+ if (icon == null) {
+ icon = ri.loadIcon(context.getPackageManager());
+ }
+
+ final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent, ri, name, "", resolveIntent, null);
+ dri.getDisplayIconHolder().setDisplayIcon(icon);
+ return dri;
+ }
+
+ private static Runnable makeOnNearbyShareRunnable(
+ TargetInfo nearbyShareTarget,
+ ActionActivityStarter activityStarter,
+ Consumer<Integer> finishCallback,
+ ChooserActivityLogger logger) {
+ return () -> {
+ logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_NEARBY);
+ // Action bar is user-independent; always start as primary.
+ activityStarter.safelyStartActivityAsPersonalProfileUser(nearbyShareTarget);
+ };
+ }
+
+ @Nullable
+ private static ActionRow.Action createCustomAction(
+ Context context,
+ ChooserAction action,
+ Consumer<Integer> finishCallback,
+ int position,
+ ChooserActivityLogger logger) {
+ Drawable icon = action.getIcon().loadDrawable(context);
+ if (icon == null && TextUtils.isEmpty(action.getLabel())) {
+ return null;
+ }
+ return new ActionRow.Action(
+ action.getLabel(),
+ icon,
+ () -> {
+ try {
+ action.getAction().send(
+ null,
+ 0,
+ null,
+ null,
+ null,
+ null,
+ ActivityOptions.makeCustomAnimation(
+ context,
+ R.anim.slide_in_right,
+ R.anim.slide_out_left)
+ .toBundle());
+ } catch (PendingIntent.CanceledException e) {
+ Log.d(TAG, "Custom action, " + action.getLabel() + ", has been cancelled");
+ }
+ logger.logCustomActionSelected(position);
+ finishCallback.accept(Activity.RESULT_OK);
+ }
+ );
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index ceab62b2..ae5be26d 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -36,44 +36,30 @@ import android.app.prediction.AppPredictor;
import android.app.prediction.AppTarget;
import android.app.prediction.AppTargetEvent;
import android.app.prediction.AppTargetId;
-import android.content.ClipData;
-import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
-import android.content.IntentSender.SendIntentException;
import android.content.SharedPreferences;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
-import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.content.res.Configuration;
-import android.content.res.Resources;
import android.database.Cursor;
-import android.graphics.Bitmap;
import android.graphics.Insets;
-import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
-import android.os.Handler;
-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.Settings;
import android.service.chooser.ChooserTarget;
-import android.text.TextUtils;
import android.util.Log;
-import android.util.Size;
import android.util.Slog;
import android.util.SparseArray;
import android.view.View;
@@ -97,6 +83,10 @@ import com.android.intentresolver.NoCrossProfileEmptyStateProvider.DevicePolicyB
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
+import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.flags.FeatureFlagRepositoryFactory;
+import com.android.intentresolver.flags.Flags;
import com.android.intentresolver.grid.ChooserGridAdapter;
import com.android.intentresolver.grid.DirectShareViewHolder;
import com.android.intentresolver.model.AbstractResolverComparator;
@@ -104,16 +94,13 @@ import com.android.intentresolver.model.AppPredictionServiceResolverComparator;
import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
import com.android.intentresolver.shortcuts.AppPredictorFactory;
import com.android.intentresolver.shortcuts.ShortcutLoader;
-import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.ResolverDrawerLayout;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.content.PackageMonitor;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
-import com.android.internal.util.FrameworkStatsLog;
import java.io.File;
-import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.text.Collator;
@@ -205,6 +192,8 @@ public class ChooserActivity extends ResolverActivity implements
| Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
+ private ChooserIntegratedDeviceComponents mIntegratedDeviceComponents;
+
/* TODO: this is `nullable` because we have to defer the assignment til onCreate(). We make the
* only assignment there, and expect it to be ready by the time we ever use it --
* someday if we move all the usage to a component with a narrower lifecycle (something that
@@ -214,13 +203,15 @@ public class ChooserActivity extends ResolverActivity implements
@Nullable
private ChooserRequestParameters mChooserRequest;
+ private ChooserRefinementManager mRefinementManager;
+
+ private FeatureFlagRepository mFeatureFlagRepository;
+ private ChooserContentPreviewUi mChooserContentPreviewUi;
+
private boolean mShouldDisplayLandscape;
// statsd logger wrapper
protected ChooserActivityLogger mChooserActivityLogger;
- @Nullable
- private RefinementResultReceiver mRefinementResultReceiver;
-
private long mChooserShownTime;
protected boolean mIsSuccessfullySelected;
@@ -240,9 +231,6 @@ public class ChooserActivity extends ResolverActivity implements
private final ExecutorService mBackgroundThreadPoolExecutor = Executors.newFixedThreadPool(5);
- @Nullable
- private ChooserContentPreviewCoordinator mPreviewCoordinator;
-
private int mScrollStatus = SCROLL_STATUS_IDLE;
@VisibleForTesting
@@ -254,6 +242,8 @@ public class ChooserActivity extends ResolverActivity implements
private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>();
+ private boolean mExcludeSharedText = false;
+
public ChooserActivity() {}
@Override
@@ -263,9 +253,16 @@ public class ChooserActivity extends ResolverActivity implements
getChooserActivityLogger().logSharesheetTriggered();
+ mFeatureFlagRepository = createFeatureFlagRepository();
+ mIntegratedDeviceComponents = getIntegratedDeviceComponents();
+
try {
mChooserRequest = new ChooserRequestParameters(
- getIntent(), getReferrer(), getNearbySharingComponent());
+ getIntent(),
+ getReferrerPackageName(),
+ getReferrer(),
+ mIntegratedDeviceComponents,
+ mFeatureFlagRepository);
} catch (IllegalArgumentException e) {
Log.e(TAG, "Caller provided invalid Chooser request parameters", e);
finish();
@@ -273,6 +270,29 @@ public class ChooserActivity extends ResolverActivity implements
return;
}
+ mRefinementManager = new ChooserRefinementManager(
+ this,
+ mChooserRequest.getRefinementIntentSender(),
+ (validatedRefinedTarget) -> {
+ maybeRemoveSharedText(validatedRefinedTarget);
+ if (super.onTargetSelected(validatedRefinedTarget, false)) {
+ finish();
+ }
+ },
+ () -> {
+ mRefinementManager.destroy();
+ finish();
+ });
+
+ mChooserContentPreviewUi = new ChooserContentPreviewUi(
+ mChooserRequest.getTargetIntent(),
+ getContentResolver(),
+ this::isImageType,
+ createPreviewImageLoader(),
+ createChooserActionFactory(),
+ mEnterTransitionAnimationDelegate,
+ mFeatureFlagRepository);
+
setAdditionalTargets(mChooserRequest.getAdditionalTargets());
setSafeForwardingMode(true);
@@ -291,11 +311,6 @@ public class ChooserActivity extends ResolverActivity implements
mChooserRequest.getTargetIntentFilter()),
mChooserRequest.getTargetIntentFilter());
- mPreviewCoordinator = new ChooserContentPreviewCoordinator(
- mBackgroundThreadPoolExecutor,
- this,
- () -> mEnterTransitionAnimationDelegate.markImagePreviewReady(false));
-
super.onCreate(
savedInstanceState,
mChooserRequest.getTargetIntent(),
@@ -341,26 +356,35 @@ public class ChooserActivity extends ResolverActivity implements
}
getChooserActivityLogger().logShareStarted(
- FrameworkStatsLog.SHARESHEET_STARTED,
getReferrerPackageName(),
mChooserRequest.getTargetType(),
mChooserRequest.getCallerChooserTargets().size(),
(mChooserRequest.getInitialIntents() == null)
? 0 : mChooserRequest.getInitialIntents().length,
isWorkProfile(),
- ChooserContentPreviewUi.findPreferredContentPreview(
- getTargetIntent(), getContentResolver(), this::isImageType),
- mChooserRequest.getTargetAction()
+ mChooserContentPreviewUi.getPreferredContentPreview(),
+ mChooserRequest.getTargetAction(),
+ mChooserRequest.getChooserActions().size(),
+ mChooserRequest.getModifyShareAction() != null
);
mEnterTransitionAnimationDelegate.postponeTransition();
}
+ @VisibleForTesting
+ protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() {
+ return ChooserIntegratedDeviceComponents.get(this, new SecureSettings());
+ }
+
@Override
protected int appliedThemeResId() {
return R.style.Theme_DeviceDefault_Chooser;
}
+ protected FeatureFlagRepository createFeatureFlagRepository() {
+ return new FeatureFlagRepositoryFactory().create(getApplicationContext());
+ }
+
private void createProfileRecords(
AppPredictorFactory factory, IntentFilter targetIntentFilter) {
UserHandle mainUserHandle = getPersonalProfileUserHandle();
@@ -489,7 +513,7 @@ public class ChooserActivity extends ResolverActivity implements
/* context */ this,
adapter,
createEmptyStateProvider(/* workProfileUserHandle= */ null),
- mQuietModeManager,
+ /* workProfileQuietModeChecker= */ () -> false,
/* workProfileUserHandle= */ null,
mMaxTargetsPerRow);
}
@@ -518,7 +542,7 @@ public class ChooserActivity extends ResolverActivity implements
personalAdapter,
workAdapter,
createEmptyStateProvider(/* workProfileUserHandle= */ getWorkProfileUserHandle()),
- mQuietModeManager,
+ () -> mWorkProfileAvailability.isQuietModeEnabled(),
selectedProfile,
getWorkProfileUserHandle(),
mMaxTargetsPerRow);
@@ -539,8 +563,7 @@ public class ChooserActivity extends ResolverActivity implements
|| mChooserMultiProfilePagerAdapter
.getCurrentRootAdapter().getSystemRowCount() != 0) {
getChooserActivityLogger().logActionShareWithPreview(
- ChooserContentPreviewUi.findPreferredContentPreview(
- getTargetIntent(), getContentResolver(), this::isImageType));
+ mChooserContentPreviewUi.getPreferredContentPreview());
}
return postRebuildListInternal(rebuildCompleted);
}
@@ -591,51 +614,6 @@ public class ChooserActivity extends ResolverActivity implements
updateProfileViewButton();
}
- private void onCopyButtonClicked() {
- Intent targetIntent = getTargetIntent();
- if (targetIntent == null) {
- finish();
- } else {
- final String action = targetIntent.getAction();
-
- ClipData clipData = null;
- if (Intent.ACTION_SEND.equals(action)) {
- String extraText = targetIntent.getStringExtra(Intent.EXTRA_TEXT);
- Uri extraStream = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
-
- if (extraText != null) {
- clipData = ClipData.newPlainText(null, extraText);
- } else if (extraStream != null) {
- clipData = ClipData.newUri(getContentResolver(), null, extraStream);
- } else {
- Log.w(TAG, "No data available to copy to clipboard");
- return;
- }
- } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
- final ArrayList<Uri> streams = targetIntent.getParcelableArrayListExtra(
- Intent.EXTRA_STREAM);
- clipData = ClipData.newUri(getContentResolver(), null, streams.get(0));
- for (int i = 1; i < streams.size(); i++) {
- clipData.addItem(getContentResolver(), new ClipData.Item(streams.get(i)));
- }
- } else {
- // expected to only be visible with ACTION_SEND or ACTION_SEND_MULTIPLE
- // so warn about unexpected action
- Log.w(TAG, "Action (" + action + ") not supported for copying to clipboard");
- return;
- }
-
- ClipboardManager clipboardManager = (ClipboardManager) getSystemService(
- Context.CLIPBOARD_SERVICE);
- clipboardManager.setPrimaryClipAsPackage(clipData, getReferrerPackageName());
-
- getChooserActivityLogger().logActionSelected(ChooserActivityLogger.SELECTION_TYPE_COPY);
-
- setResult(RESULT_OK);
- finish();
- }
- }
-
@Override
protected void onResume() {
super.onResume();
@@ -707,226 +685,19 @@ public class ChooserActivity extends ResolverActivity implements
* @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 = ChooserContentPreviewUi.findPreferredContentPreview(
- targetIntent, getContentResolver(), this::isImageType);
-
- ChooserContentPreviewUi.ActionFactory actionFactory =
- new ChooserContentPreviewUi.ActionFactory() {
- @Override
- public ActionRow.Action createCopyButton() {
- return ChooserActivity.this.createCopyAction();
- }
-
- @Nullable
- @Override
- public ActionRow.Action createEditButton() {
- return ChooserActivity.this.createEditAction(targetIntent);
- }
-
- @Nullable
- @Override
- public ActionRow.Action createNearbyButton() {
- return ChooserActivity.this.createNearbyAction(targetIntent);
- }
- };
-
- ViewGroup layout = ChooserContentPreviewUi.displayContentPreview(
- previewType,
- targetIntent,
+ protected ViewGroup createContentPreviewView(ViewGroup parent) {
+ ViewGroup layout = mChooserContentPreviewUi.displayContentPreview(
getResources(),
getLayoutInflater(),
- actionFactory,
- R.layout.chooser_action_row,
- parent,
- previewCoordinator,
- mEnterTransitionAnimationDelegate::markImagePreviewReady,
- getContentResolver(),
- this::isImageType);
+ parent);
if (layout != null) {
adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
}
- if (previewType != ChooserContentPreviewUi.CONTENT_PREVIEW_IMAGE) {
- mEnterTransitionAnimationDelegate.markImagePreviewReady(false);
- }
return layout;
}
- @VisibleForTesting
- protected ComponentName getNearbySharingComponent() {
- String nearbyComponent = Settings.Secure.getString(
- getContentResolver(),
- Settings.Secure.NEARBY_SHARING_COMPONENT);
- if (TextUtils.isEmpty(nearbyComponent)) {
- nearbyComponent = getString(R.string.config_defaultNearbySharingComponent);
- }
- if (TextUtils.isEmpty(nearbyComponent)) {
- return null;
- }
- return ComponentName.unflattenFromString(nearbyComponent);
- }
-
- @VisibleForTesting
- protected @Nullable ComponentName getEditSharingComponent() {
- String editorPackage = getApplicationContext().getString(R.string.config_systemImageEditor);
- if (editorPackage == null || TextUtils.isEmpty(editorPackage)) {
- return null;
- }
- return ComponentName.unflattenFromString(editorPackage);
- }
-
- @VisibleForTesting
- protected TargetInfo getEditSharingTarget(Intent originalIntent) {
- final ComponentName cn = getEditSharingComponent();
-
- final Intent resolveIntent = new Intent(originalIntent);
- // Retain only URI permission grant flags if present. Other flags may prevent the scene
- // transition animation from running (i.e FLAG_ACTIVITY_NO_ANIMATION,
- // FLAG_ACTIVITY_NEW_TASK, FLAG_ACTIVITY_NEW_DOCUMENT) but also not needed.
- resolveIntent.setFlags(originalIntent.getFlags() & URI_PERMISSION_INTENT_FLAGS);
- resolveIntent.setComponent(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) {
- Log.e(TAG, "Device-specified image edit component (" + cn
- + ") not available");
- return null;
- }
-
- final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
- originalIntent,
- ri,
- getString(com.android.internal.R.string.screenshot_edit),
- "",
- resolveIntent,
- null);
- dri.getDisplayIconHolder().setDisplayIcon(
- getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));
- return dri;
- }
-
- @VisibleForTesting
- protected TargetInfo getNearbySharingTarget(Intent originalIntent) {
- final ComponentName cn = getNearbySharingComponent();
- if (cn == null) return null;
-
- final Intent resolveIntent = new Intent(originalIntent);
- resolveIntent.setComponent(cn);
- final ResolveInfo ri = getPackageManager().resolveActivity(
- resolveIntent, PackageManager.GET_META_DATA);
- if (ri == null || ri.activityInfo == null) {
- Log.e(TAG, "Device-specified nearby sharing component (" + cn
- + ") not available");
- return null;
- }
-
- // Allow the nearby sharing component to provide a more appropriate icon and label
- // for the chip.
- CharSequence name = null;
- Drawable icon = null;
- final Bundle metaData = ri.activityInfo.metaData;
- if (metaData != null) {
- try {
- final Resources pkgRes = getPackageManager().getResourcesForActivity(cn);
- final int nameResId = metaData.getInt(CHIP_LABEL_METADATA_KEY);
- name = pkgRes.getString(nameResId);
- final int resId = metaData.getInt(CHIP_ICON_METADATA_KEY);
- icon = pkgRes.getDrawable(resId);
- } catch (Resources.NotFoundException ex) {
- } catch (NameNotFoundException ex) {
- }
- }
- if (TextUtils.isEmpty(name)) {
- name = ri.loadLabel(getPackageManager());
- }
- if (icon == null) {
- icon = ri.loadIcon(getPackageManager());
- }
-
- final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo(
- originalIntent, ri, name, "", resolveIntent, null);
- dri.getDisplayIconHolder().setDisplayIcon(icon);
- return dri;
- }
-
- private ActionRow.Action createCopyAction() {
- return new ActionRow.Action(
- com.android.internal.R.id.chooser_copy_button,
- getString(com.android.internal.R.string.copy),
- getDrawable(com.android.internal.R.drawable.ic_menu_copy_material),
- this::onCopyButtonClicked);
- }
-
- @Nullable
- private ActionRow.Action createNearbyAction(Intent originalIntent) {
- final TargetInfo ti = getNearbySharingTarget(originalIntent);
- if (ti == null) {
- return null;
- }
-
- return new ActionRow.Action(
- com.android.internal.R.id.chooser_nearby_button,
- ti.getDisplayLabel(),
- ti.getDisplayIconHolder().getDisplayIcon(),
- () -> {
- getChooserActivityLogger().logActionSelected(
- ChooserActivityLogger.SELECTION_TYPE_NEARBY);
- // Action bar is user-independent, always start as primary
- safelyStartActivityAsUser(ti, getPersonalProfileUserHandle());
- finish();
- });
- }
-
- @Nullable
- private ActionRow.Action createEditAction(Intent originalIntent) {
- final TargetInfo ti = getEditSharingTarget(originalIntent);
- if (ti == null) {
- return null;
- }
-
- return new ActionRow.Action(
- com.android.internal.R.id.chooser_edit_button,
- ti.getDisplayLabel(),
- ti.getDisplayIconHolder().getDisplayIcon(),
- () -> {
- // Log share completion via edit
- getChooserActivityLogger().logActionSelected(
- ChooserActivityLogger.SELECTION_TYPE_EDIT);
- View firstImgView = getFirstVisibleImgPreviewView();
- // Action bar is user-independent, always start as primary
- if (firstImgView == null) {
- safelyStartActivityAsUser(ti, getPersonalProfileUserHandle());
- finish();
- } else {
- ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
- this, firstImgView, IMAGE_EDITOR_SHARED_ELEMENT);
- safelyStartActivityAsUser(
- ti, getPersonalProfileUserHandle(), options.toBundle());
- startFinishAnimation();
- }
- }
- );
- }
-
@Nullable
private View getFirstVisibleImgPreviewView() {
View firstImage = findViewById(com.android.internal.R.id.content_preview_image_1_large);
@@ -972,9 +743,9 @@ public class ChooserActivity extends ResolverActivity implements
mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET);
}
- if (mRefinementResultReceiver != null) {
- mRefinementResultReceiver.destroy();
- mRefinementResultReceiver = null;
+ if (mRefinementManager != null) { // TODO: null-checked in case of early-destroy, or skip?
+ mRefinementManager.destroy();
+ mRefinementManager = null;
}
mBackgroundThreadPoolExecutor.shutdownNow();
@@ -1098,34 +869,11 @@ public class ChooserActivity extends ResolverActivity implements
@Override
protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) {
- if (mChooserRequest.getRefinementIntentSender() != null) {
- final Intent fillIn = new Intent();
- final List<Intent> sourceIntents = target.getAllSourceIntents();
- if (!sourceIntents.isEmpty()) {
- fillIn.putExtra(Intent.EXTRA_INTENT, sourceIntents.get(0));
- if (sourceIntents.size() > 1) {
- final Intent[] alts = new Intent[sourceIntents.size() - 1];
- for (int i = 1, N = sourceIntents.size(); i < N; i++) {
- alts[i - 1] = sourceIntents.get(i);
- }
- fillIn.putExtra(Intent.EXTRA_ALTERNATE_INTENTS, alts);
- }
- if (mRefinementResultReceiver != null) {
- mRefinementResultReceiver.destroy();
- }
- mRefinementResultReceiver = new RefinementResultReceiver(this, target, null);
- fillIn.putExtra(Intent.EXTRA_RESULT_RECEIVER,
- mRefinementResultReceiver);
- try {
- mChooserRequest.getRefinementIntentSender().sendIntent(
- this, 0, fillIn, null, null);
- return false;
- } catch (SendIntentException e) {
- Log.e(TAG, "Refinement IntentSender failed to send", e);
- }
- }
+ if (mRefinementManager.maybeHandleSelection(target)) {
+ return false;
}
updateModelAndChooserCounts(target);
+ maybeRemoveSharedText(target);
return super.onTargetSelected(target, alwaysCheck);
}
@@ -1237,45 +985,6 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- private IntentFilter getTargetIntentFilter() {
- return getTargetIntentFilter(getTargetIntent());
- }
-
- private 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;
- }
- }
-
private void logDirectShareTargetReceived(UserHandle forUser) {
ProfileRecord profileRecord = getProfileRecord(forUser);
if (profileRecord == null) {
@@ -1314,6 +1023,27 @@ public class ChooserActivity extends ResolverActivity implements
mIsSuccessfullySelected = true;
}
+ private void maybeRemoveSharedText(@androidx.annotation.NonNull TargetInfo targetInfo) {
+ Intent targetIntent = targetInfo.getTargetIntent();
+ if (targetIntent == null) {
+ return;
+ }
+ Intent originalTargetIntent = new Intent(mChooserRequest.getTargetIntent());
+ // Our TargetInfo implementations add associated component to the intent, let's do the same
+ // for the sake of the comparison below.
+ if (targetIntent.getComponent() != null) {
+ originalTargetIntent.setComponent(targetIntent.getComponent());
+ }
+ // Use filterEquals as a way to check that the primary intent is in use (and not an
+ // alternative one). For example, an app is sharing an image and a link with mime type
+ // "image/png" and provides an alternative intent to share only the link with mime type
+ // "text/uri". Should there be a target that accepts only the latter, the alternative intent
+ // will be used and we don't want to exclude the link from it.
+ if (mExcludeSharedText && originalTargetIntent.filterEquals(targetIntent)) {
+ targetIntent.removeExtra(Intent.EXTRA_TEXT);
+ }
+ }
+
private void sendImpressionToAppPredictor(TargetInfo targetInfo, ChooserListAdapter adapter) {
// Send DS target impression info to AppPredictor, only when user chooses app share.
if (targetInfo.isChooserTargetInfo()) {
@@ -1369,46 +1099,6 @@ public class ChooserActivity extends ResolverActivity implements
return (record == null) ? null : record.appPredictor;
}
- void onRefinementResult(TargetInfo selectedTarget, Intent matchingIntent) {
- if (mRefinementResultReceiver != null) {
- mRefinementResultReceiver.destroy();
- mRefinementResultReceiver = null;
- }
- if (selectedTarget == null) {
- Log.e(TAG, "Refinement result intent did not match any known targets; canceling");
- } else if (!checkTargetSourceIntent(selectedTarget, matchingIntent)) {
- Log.e(TAG, "onRefinementResult: Selected target " + selectedTarget
- + " cannot match refined source intent " + matchingIntent);
- } else {
- TargetInfo clonedTarget = selectedTarget.cloneFilledIn(matchingIntent, 0);
- if (super.onTargetSelected(clonedTarget, false)) {
- updateModelAndChooserCounts(clonedTarget);
- finish();
- return;
- }
- }
- onRefinementCanceled();
- }
-
- void onRefinementCanceled() {
- if (mRefinementResultReceiver != null) {
- mRefinementResultReceiver.destroy();
- mRefinementResultReceiver = null;
- }
- finish();
- }
-
- boolean checkTargetSourceIntent(TargetInfo target, Intent matchingIntent) {
- final List<Intent> targetIntents = target.getAllSourceIntents();
- for (int i = 0, N = targetIntents.size(); i < N; i++) {
- final Intent targetIntent = targetIntents.get(i);
- if (targetIntent.filterEquals(matchingIntent)) {
- return true;
- }
- }
- return false;
- }
-
/**
* Sort intents alphabetically based on display label.
*/
@@ -1433,14 +1123,19 @@ public class ChooserActivity extends ResolverActivity implements
}
public class ChooserListController extends ResolverListController {
- public ChooserListController(Context context,
+ public ChooserListController(
+ Context context,
PackageManager pm,
Intent targetIntent,
String referrerPackageName,
int launchedFromUid,
- UserHandle userId,
AbstractResolverComparator resolverComparator) {
- super(context, pm, targetIntent, referrerPackageName, launchedFromUid, userId,
+ super(
+ context,
+ pm,
+ targetIntent,
+ referrerPackageName,
+ launchedFromUid,
resolverComparator);
}
@@ -1485,7 +1180,7 @@ public class ChooserActivity extends ResolverActivity implements
@Override
public View buildContentPreview(ViewGroup parent) {
- return createContentPreviewView(parent, mPreviewCoordinator);
+ return createContentPreviewView(parent);
}
@Override
@@ -1500,9 +1195,9 @@ public class ChooserActivity extends ResolverActivity implements
.getActiveListAdapter()
.targetInfoForPosition(
selectedPosition, /* filtered= */ true);
- // ItemViewHolder contents should always be "display resolve info"
- // targets, but check just to make sure.
- if (longPressedTargetInfo.isDisplayResolveInfo()) {
+ // Only a direct share target or an app target is expected
+ if (longPressedTargetInfo.isDisplayResolveInfo()
+ || longPressedTargetInfo.isSelectableTargetInfo()) {
showTargetDetails(longPressedTargetInfo);
}
}
@@ -1576,8 +1271,9 @@ public class ChooserActivity extends ResolverActivity implements
maxTargetsPerRow);
}
+ @Override
@VisibleForTesting
- protected ResolverListController createListController(UserHandle userHandle) {
+ protected ChooserListController createListController(UserHandle userHandle) {
AppPredictor appPredictor = getAppPredictor(userHandle);
AbstractResolverComparator resolverComparator;
if (appPredictor != null) {
@@ -1594,23 +1290,55 @@ public class ChooserActivity extends ResolverActivity implements
mPm,
getTargetIntent(),
getReferrerPackageName(),
- mLaunchedFromUid,
- userHandle,
+ getAnnotatedUserHandles().userIdOfCallingApp,
resolverComparator);
}
@VisibleForTesting
- protected Bitmap loadThumbnail(Uri uri, Size size) {
- if (uri == null || size == null) {
- return null;
+ protected ImageLoader createPreviewImageLoader() {
+ final int cacheSize;
+ if (mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW)) {
+ float chooserWidth = getResources().getDimension(R.dimen.chooser_width);
+ float imageWidth = getResources().getDimension(R.dimen.chooser_preview_image_width);
+ cacheSize = (int) (Math.ceil(chooserWidth / imageWidth) + 2);
+ } else {
+ cacheSize = 3;
}
+ return new ImagePreviewImageLoader(this, getLifecycle(), cacheSize);
+ }
- try {
- return getContentResolver().loadThumbnail(uri, size, null);
- } catch (IOException | NullPointerException | SecurityException ex) {
- getChooserActivityLogger().logContentPreviewWarning(uri);
- }
- return null;
+ private ChooserActionFactory createChooserActionFactory() {
+ return new ChooserActionFactory(
+ this,
+ mChooserRequest,
+ mFeatureFlagRepository,
+ mIntegratedDeviceComponents,
+ getChooserActivityLogger(),
+ (isExcluded) -> mExcludeSharedText = isExcluded,
+ this::getFirstVisibleImgPreviewView,
+ new ChooserActionFactory.ActionActivityStarter() {
+ @Override
+ public void safelyStartActivityAsPersonalProfileUser(TargetInfo targetInfo) {
+ safelyStartActivityAsUser(targetInfo, getPersonalProfileUserHandle());
+ finish();
+ }
+
+ @Override
+ public void safelyStartActivityAsPersonalProfileUserWithSharedElementTransition(
+ TargetInfo targetInfo, View sharedElement, String sharedElementName) {
+ ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(
+ ChooserActivity.this, sharedElement, sharedElementName);
+ safelyStartActivityAsUser(
+ targetInfo, getPersonalProfileUserHandle(), options.toBundle());
+ startFinishAnimation();
+ }
+ },
+ (status) -> {
+ if (status != null) {
+ setResult(status);
+ }
+ finish();
+ });
}
private void handleScroll(View view, int x, int y, int oldx, int oldy) {
@@ -1845,21 +1573,20 @@ public class ChooserActivity extends ResolverActivity implements
}
@MainThread
- private void onShortcutsLoaded(
- UserHandle userHandle, ShortcutLoader.Result shortcutsResult) {
+ private void onShortcutsLoaded(UserHandle userHandle, ShortcutLoader.Result result) {
if (DEBUG) {
Log.d(TAG, "onShortcutsLoaded for user: " + userHandle);
}
- mDirectShareShortcutInfoCache.putAll(shortcutsResult.directShareShortcutInfoCache);
- mDirectShareAppTargetCache.putAll(shortcutsResult.directShareAppTargetCache);
+ mDirectShareShortcutInfoCache.putAll(result.getDirectShareShortcutInfoCache());
+ mDirectShareAppTargetCache.putAll(result.getDirectShareAppTargetCache());
ChooserListAdapter adapter =
mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle);
if (adapter != null) {
- for (ShortcutLoader.ShortcutResultInfo resultInfo : shortcutsResult.shortcutsByApp) {
+ for (ShortcutLoader.ShortcutResultInfo resultInfo : result.getShortcutsByApp()) {
adapter.addServiceResults(
- resultInfo.appTarget,
- resultInfo.shortcuts,
- shortcutsResult.isFromAppPredictor
+ resultInfo.getAppTarget(),
+ resultInfo.getShortcuts(),
+ result.isFromAppPredictor()
? TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
: TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER,
mDirectShareShortcutInfoCache,
@@ -1946,12 +1673,24 @@ public class ChooserActivity extends ResolverActivity implements
private boolean shouldShowStickyContentPreviewNoOrientationCheck() {
return shouldShowTabs()
- && mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+ && (mMultiProfilePagerAdapter.getListAdapterForUserHandle(
UserHandle.of(UserHandle.myUserId())).getCount() > 0
+ || shouldShowContentPreviewWhenEmpty())
&& shouldShowContentPreview();
}
/**
+ * This method could be used to override the default behavior when we hide the preview area
+ * when the current tab doesn't have any items.
+ *
+ * @return true if we want to show the content preview area even if the tab for the current
+ * user is empty
+ */
+ protected boolean shouldShowContentPreviewWhenEmpty() {
+ return false;
+ }
+
+ /**
* @return true if we want to show the content preview area
*/
protected boolean shouldShowContentPreview() {
@@ -1964,10 +1703,10 @@ public class ChooserActivity extends ResolverActivity implements
// We don't show it in landscape as otherwise there is no room for scrolling.
// If the sticky content preview will be shown at some point with orientation change,
// then always preload it to avoid subsequent resizing of the share sheet.
- ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
+ ViewGroup contentPreviewContainer =
+ findViewById(com.android.internal.R.id.content_preview_container);
if (contentPreviewContainer.getChildCount() == 0) {
- ViewGroup contentPreviewView =
- createContentPreviewView(contentPreviewContainer, mPreviewCoordinator);
+ ViewGroup contentPreviewView = createContentPreviewView(contentPreviewContainer);
contentPreviewContainer.addView(contentPreviewView);
}
}
@@ -2101,66 +1840,6 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- static class ChooserTargetRankingInfo {
- public final List<AppTarget> scores;
- public final UserHandle userHandle;
-
- ChooserTargetRankingInfo(List<AppTarget> chooserTargetScores,
- UserHandle userHandle) {
- this.scores = chooserTargetScores;
- this.userHandle = userHandle;
- }
- }
-
- static class RefinementResultReceiver extends ResultReceiver {
- private ChooserActivity mChooserActivity;
- private TargetInfo mSelectedTarget;
-
- public RefinementResultReceiver(ChooserActivity host, TargetInfo target,
- Handler handler) {
- super(handler);
- mChooserActivity = host;
- mSelectedTarget = target;
- }
-
- @Override
- protected void onReceiveResult(int resultCode, Bundle resultData) {
- if (mChooserActivity == null) {
- Log.e(TAG, "Destroyed RefinementResultReceiver received a result");
- return;
- }
- if (resultData == null) {
- Log.e(TAG, "RefinementResultReceiver received null resultData");
- return;
- }
-
- switch (resultCode) {
- case RESULT_CANCELED:
- mChooserActivity.onRefinementCanceled();
- break;
- case RESULT_OK:
- Parcelable intentParcelable = resultData.getParcelable(Intent.EXTRA_INTENT);
- if (intentParcelable instanceof Intent) {
- mChooserActivity.onRefinementResult(mSelectedTarget,
- (Intent) intentParcelable);
- } else {
- Log.e(TAG, "RefinementResultReceiver received RESULT_OK but no Intent"
- + " in resultData with key Intent.EXTRA_INTENT");
- }
- break;
- default:
- Log.w(TAG, "Unknown result code " + resultCode
- + " sent to RefinementResultReceiver");
- break;
- }
- }
-
- public void destroy() {
- mChooserActivity = null;
- mSelectedTarget = null;
- }
- }
-
/**
* Used in combination with the scene transition when launching the image editor
*/
diff --git a/java/src/com/android/intentresolver/ChooserActivityLogger.java b/java/src/com/android/intentresolver/ChooserActivityLogger.java
index 9109bf93..1f606f26 100644
--- a/java/src/com/android/intentresolver/ChooserActivityLogger.java
+++ b/java/src/com/android/intentresolver/ChooserActivityLogger.java
@@ -24,6 +24,7 @@ import android.provider.MediaStore;
import android.util.HashedStringCache;
import android.util.Log;
+import com.android.intentresolver.contentpreview.ContentPreviewType;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.InstanceId;
import com.android.internal.logging.InstanceIdSequence;
@@ -48,6 +49,8 @@ public class ChooserActivityLogger {
public static final int SELECTION_TYPE_COPY = 4;
public static final int SELECTION_TYPE_NEARBY = 5;
public static final int SELECTION_TYPE_EDIT = 6;
+ public static final int SELECTION_TYPE_MODIFY_SHARE = 7;
+ public static final int SELECTION_TYPE_CUSTOM_ACTION = 8;
/**
* This shim is provided only for testing. In production, clients will only ever use a
@@ -66,7 +69,9 @@ public class ChooserActivityLogger {
int numAppProvidedAppTargets,
boolean isWorkProfile,
int previewType,
- int intentType);
+ int intentType,
+ int numCustomActions,
+ boolean modifyShareActionProvided);
/** Overload to use for logging {@code FrameworkStatsLog.RANKING_SELECTED}. */
void write(
@@ -114,9 +119,16 @@ public class ChooserActivityLogger {
}
/** Logs a UiEventReported event for the system sharesheet completing initial start-up. */
- public void logShareStarted(int eventId, String packageName, String mimeType,
- int appProvidedDirect, int appProvidedApp, boolean isWorkprofile, int previewType,
- String intent) {
+ public void logShareStarted(
+ String packageName,
+ String mimeType,
+ int appProvidedDirect,
+ int appProvidedApp,
+ boolean isWorkprofile,
+ int previewType,
+ String intent,
+ int customActionCount,
+ boolean modifyShareActionProvided) {
mFrameworkStatsLogger.write(FrameworkStatsLog.SHARESHEET_STARTED,
/* event_id = 1 */ SharesheetStartedEvent.SHARE_STARTED.getId(),
/* package_name = 2 */ packageName,
@@ -126,7 +138,24 @@ public class ChooserActivityLogger {
/* num_app_provided_app_targets = 6 */ appProvidedApp,
/* is_workprofile = 7 */ isWorkprofile,
/* previewType = 8 */ typeFromPreviewInt(previewType),
- /* intentType = 9 */ typeFromIntentString(intent));
+ /* intentType = 9 */ typeFromIntentString(intent),
+ /* num_provided_custom_actions = 10 */ customActionCount,
+ /* modify_share_action_provided = 11 */ modifyShareActionProvided);
+ }
+
+ /**
+ * Log that a custom action has been tapped by the user.
+ *
+ * @param positionPicked index of the custom action within the list of custom actions.
+ */
+ public void logCustomActionSelected(int positionPicked) {
+ mFrameworkStatsLogger.write(FrameworkStatsLog.RANKING_SELECTED,
+ /* event_id = 1 */
+ SharesheetTargetSelectedEvent.SHARESHEET_CUSTOM_ACTION_SELECTED.getId(),
+ /* package_name = 2 */ null,
+ /* instance_id = 3 */ getInstanceId().getId(),
+ /* position_picked = 4 */ positionPicked,
+ /* is_pinned = 5 */ false);
}
/**
@@ -328,7 +357,11 @@ public class ChooserActivityLogger {
@UiEvent(doc = "User selected the nearby target.")
SHARESHEET_NEARBY_TARGET_SELECTED(626),
@UiEvent(doc = "User selected the edit target.")
- SHARESHEET_EDIT_TARGET_SELECTED(669);
+ SHARESHEET_EDIT_TARGET_SELECTED(669),
+ @UiEvent(doc = "User selected the modify share target.")
+ SHARESHEET_MODIFY_SHARE_SELECTED(1316),
+ @UiEvent(doc = "User selected a custom action.")
+ SHARESHEET_CUSTOM_ACTION_SELECTED(1317);
private final int mId;
SharesheetTargetSelectedEvent(int id) {
@@ -352,6 +385,10 @@ public class ChooserActivityLogger {
return SHARESHEET_NEARBY_TARGET_SELECTED;
case SELECTION_TYPE_EDIT:
return SHARESHEET_EDIT_TARGET_SELECTED;
+ case SELECTION_TYPE_MODIFY_SHARE:
+ return SHARESHEET_MODIFY_SHARE_SELECTED;
+ case SELECTION_TYPE_CUSTOM_ACTION:
+ return SHARESHEET_CUSTOM_ACTION_SELECTED;
default:
return INVALID;
}
@@ -396,11 +433,11 @@ public class ChooserActivityLogger {
*/
private static int typeFromPreviewInt(int previewType) {
switch(previewType) {
- case ChooserContentPreviewUi.CONTENT_PREVIEW_IMAGE:
+ case ContentPreviewType.CONTENT_PREVIEW_IMAGE:
return FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_IMAGE;
- case ChooserContentPreviewUi.CONTENT_PREVIEW_FILE:
+ case ContentPreviewType.CONTENT_PREVIEW_FILE:
return FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_FILE;
- case ChooserContentPreviewUi.CONTENT_PREVIEW_TEXT:
+ case ContentPreviewType.CONTENT_PREVIEW_TEXT:
default:
return FrameworkStatsLog
.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_TYPE_UNKNOWN;
@@ -463,7 +500,9 @@ public class ChooserActivityLogger {
int numAppProvidedAppTargets,
boolean isWorkProfile,
int previewType,
- int intentType) {
+ int intentType,
+ int numCustomActions,
+ boolean modifyShareActionProvided) {
FrameworkStatsLog.write(
frameworkEventId,
/* event_id = 1 */ appEventId,
@@ -474,7 +513,9 @@ public class ChooserActivityLogger {
/* num_app_provided_app_targets */ numAppProvidedAppTargets,
/* is_workprofile */ isWorkProfile,
/* previewType = 8 */ previewType,
- /* intentType = 9 */ intentType);
+ /* intentType = 9 */ intentType,
+ /* num_provided_custom_actions = 10 */ numCustomActions,
+ /* modify_share_action_provided = 11 */ modifyShareActionProvided);
}
@Override
diff --git a/java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java b/java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java
deleted file mode 100644
index 0b8dbe35..00000000
--- a/java/src/com/android/intentresolver/ChooserContentPreviewCoordinator.java
+++ /dev/null
@@ -1,132 +0,0 @@
-/*
- * Copyright (C) 2008 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver;
-
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.os.Handler;
-import android.util.Size;
-
-import androidx.annotation.MainThread;
-import androidx.annotation.Nullable;
-
-import com.google.common.util.concurrent.FutureCallback;
-import com.google.common.util.concurrent.Futures;
-import com.google.common.util.concurrent.ListenableFuture;
-import com.google.common.util.concurrent.ListeningExecutorService;
-import com.google.common.util.concurrent.MoreExecutors;
-
-import java.util.concurrent.ExecutorService;
-import java.util.function.Consumer;
-
-/**
- * Delegate to manage deferred resource loads for content preview assets, while
- * implementing Chooser's application logic for determining timeout/success/failure conditions.
- */
-public class ChooserContentPreviewCoordinator implements
- ChooserContentPreviewUi.ContentPreviewCoordinator {
- public ChooserContentPreviewCoordinator(
- ExecutorService backgroundExecutor,
- ChooserActivity chooserActivity,
- Runnable onFailCallback) {
- this.mBackgroundExecutor = MoreExecutors.listeningDecorator(backgroundExecutor);
- this.mChooserActivity = chooserActivity;
- this.mOnFailCallback = onFailCallback;
-
- this.mImageLoadTimeoutMillis =
- chooserActivity.getResources().getInteger(R.integer.config_shortAnimTime);
- }
-
- @Override
- public void loadImage(final Uri imageUri, final Consumer<Bitmap> callback) {
- final int size = mChooserActivity.getResources().getDimensionPixelSize(
- R.dimen.chooser_preview_image_max_dimen);
-
- // TODO: apparently this timeout is only used for not holding shared element transition
- // animation for too long. If so, we already have a better place for it
- // EnterTransitionAnimationDelegate.
- mHandler.postDelayed(this::onWatchdogTimeout, mImageLoadTimeoutMillis);
-
- ListenableFuture<Bitmap> bitmapFuture = mBackgroundExecutor.submit(
- () -> mChooserActivity.loadThumbnail(imageUri, new Size(size, size)));
-
- Futures.addCallback(
- bitmapFuture,
- new FutureCallback<Bitmap>() {
- @Override
- public void onSuccess(Bitmap loadedBitmap) {
- try {
- callback.accept(loadedBitmap);
- onLoadCompleted(loadedBitmap);
- } catch (Exception e) { /* unimportant */ }
- }
-
- @Override
- public void onFailure(Throwable t) {
- callback.accept(null);
- }
- },
- mHandler::post);
- }
-
- private final ChooserActivity mChooserActivity;
- private final ListeningExecutorService mBackgroundExecutor;
- private final Runnable mOnFailCallback;
- private final int mImageLoadTimeoutMillis;
-
- // TODO: this uses a `Handler` because there doesn't seem to be a straightforward way to get a
- // `ScheduledExecutorService` that posts to the UI thread unless we use Dagger. Eventually we'll
- // use Dagger and can inject this as a `@UiThread ScheduledExecutorService`.
- private final Handler mHandler = new Handler();
-
- private boolean mAtLeastOneLoaded = false;
-
- @MainThread
- private void onWatchdogTimeout() {
- if (mChooserActivity.isFinishing()) {
- return;
- }
-
- // If at least one image loads within the timeout period, allow other loads to continue.
- if (!mAtLeastOneLoaded) {
- mOnFailCallback.run();
- }
- }
-
- @MainThread
- private void onLoadCompleted(@Nullable Bitmap loadedBitmap) {
- if (mChooserActivity.isFinishing()) {
- return;
- }
-
- // TODO: the following logic can be described as "invoke the fail callback when the first
- // image loading has failed". Historically, before we had switched from a single-threaded
- // pool to a multi-threaded pool, we first loaded the transition element's image (the image
- // preview is the only case when those callbacks matter) and aborting the animation on it's
- // failure was reasonable. With the multi-thread pool, the first result may belong to any
- // image and thus we can falsely abort the animation.
- // Now, when we track the transition view state directly and after the timeout logic will
- // be moved into ChooserActivity$EnterTransitionAnimationDelegate, we can just get rid of
- // the fail callback and the following logic altogether.
- mAtLeastOneLoaded |= loadedBitmap != null;
- boolean wholeBatchFailed = !mAtLeastOneLoaded;
-
- if (wholeBatchFailed) {
- mOnFailCallback.run();
- }
- }
-}
diff --git a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
deleted file mode 100644
index ff88e5e1..00000000
--- a/java/src/com/android/intentresolver/ChooserContentPreviewUi.java
+++ /dev/null
@@ -1,566 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver;
-
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import android.animation.ObjectAnimator;
-import android.animation.ValueAnimator;
-import android.annotation.IntDef;
-import android.content.ClipData;
-import android.content.ContentResolver;
-import android.content.Intent;
-import android.content.res.Resources;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.net.Uri;
-import android.provider.DocumentsContract;
-import android.provider.Downloads;
-import android.provider.OpenableColumns;
-import android.text.TextUtils;
-import android.util.Log;
-import android.util.PluralsMessageFormatter;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.view.ViewStub;
-import android.view.animation.DecelerateInterpolator;
-import android.widget.ImageView;
-import android.widget.TextView;
-
-import androidx.annotation.LayoutRes;
-import androidx.annotation.Nullable;
-
-import com.android.intentresolver.widget.ActionRow;
-import com.android.intentresolver.widget.ImagePreviewView;
-import com.android.intentresolver.widget.RoundedRectImageView;
-import com.android.internal.annotations.VisibleForTesting;
-
-import java.lang.annotation.Retention;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Consumer;
-
-/**
- * Collection of helpers for building the content preview UI displayed in {@link ChooserActivity}.
- *
- * TODO: this "namespace" was pulled out of {@link ChooserActivity} as a bucket of static methods
- * to show that they're one-shot procedures with no dependencies back to {@link ChooserActivity}
- * state other than the delegates that are explicitly provided. There may be more appropriate
- * abstractions (e.g., maybe this can be a "widget" added directly to the view hierarchy to show the
- * appropriate preview), or it may at least be safe (and more convenient) to adopt a more "object
- * oriented" design where the static specifiers are removed and some of the dependencies are cached
- * as ivars when this "class" is initialized.
- */
-public final class ChooserContentPreviewUi {
- private static final int IMAGE_FADE_IN_MILLIS = 150;
-
- /**
- * Delegate to handle background resource loads that are dependencies of content previews.
- */
- public interface ContentPreviewCoordinator {
- /**
- * Request that an image be loaded in the background and set into a view.
- *
- * @param imageUri The {@link Uri} of the image to load.
- *
- * TODO: it looks like clients are probably capable of passing the view directly, but the
- * deferred computation here is a closer match to the legacy model for now.
- */
- void loadImage(Uri imageUri, Consumer<Bitmap> callback);
- }
-
- /**
- * Delegate to build the default system action buttons to display in the preview layout, if/when
- * they're determined to be appropriate for the particular preview we display.
- * TODO: clarify why action buttons are part of preview logic.
- */
- public interface ActionFactory {
- /** Create an action that copies the share content to the clipboard. */
- ActionRow.Action createCopyButton();
-
- /** Create an action that opens the share content in a system-default editor. */
- @Nullable
- ActionRow.Action createEditButton();
-
- /** Create an "Share to Nearby" action. */
- @Nullable
- ActionRow.Action createNearbyButton();
- }
-
- /**
- * Testing shim to specify whether a given mime type is considered to be an "image."
- *
- * TODO: move away from {@link ChooserActivityOverrideData} as a model to configure our tests,
- * then migrate {@link ChooserActivity#isImageType(String)} into this class.
- */
- public interface ImageMimeTypeClassifier {
- /** @return whether the specified {@code mimeType} is classified as an "image" type. */
- boolean isImageType(String mimeType);
- }
-
- @Retention(SOURCE)
- @IntDef({CONTENT_PREVIEW_FILE, CONTENT_PREVIEW_IMAGE, CONTENT_PREVIEW_TEXT})
- private @interface ContentPreviewType {
- }
-
- // Starting at 1 since 0 is considered "undefined" for some of the database transformations
- // of tron logs.
- @VisibleForTesting
- public static final int CONTENT_PREVIEW_IMAGE = 1;
- @VisibleForTesting
- public static final int CONTENT_PREVIEW_FILE = 2;
- @VisibleForTesting
- public static final int CONTENT_PREVIEW_TEXT = 3;
-
- private static final String TAG = "ChooserPreview";
-
- private static final String PLURALS_COUNT = "count";
- private static final String PLURALS_FILE_NAME = "file_name";
-
- /** Determine the most appropriate type of preview to show for the provided {@link Intent}. */
- @ContentPreviewType
- public static int findPreferredContentPreview(
- Intent targetIntent,
- ContentResolver resolver,
- ImageMimeTypeClassifier imageClassifier) {
- /* In {@link android.content.Intent#getType}, the app may specify a very general mime type
- * that broadly covers all data being shared, such as {@literal *}/* when sending an image
- * and text. We therefore should inspect each item for the preferred type, in order: IMAGE,
- * FILE, TEXT. */
- String action = targetIntent.getAction();
- if (Intent.ACTION_SEND.equals(action)) {
- Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
- return findPreferredContentPreview(uri, resolver, imageClassifier);
- } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
- List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
- if (uris == null || uris.isEmpty()) {
- return CONTENT_PREVIEW_TEXT;
- }
-
- for (Uri uri : uris) {
- // Defaulting to file preview when there are mixed image/file types is
- // preferable, as it shows the user the correct number of items being shared
- int uriPreviewType = findPreferredContentPreview(uri, resolver, imageClassifier);
- if (uriPreviewType == CONTENT_PREVIEW_FILE) {
- return CONTENT_PREVIEW_FILE;
- }
- }
-
- return CONTENT_PREVIEW_IMAGE;
- }
-
- return CONTENT_PREVIEW_TEXT;
- }
-
- /**
- * Display a content preview of the specified {@code previewType} to preview the content of the
- * specified {@code intent}.
- */
- public static ViewGroup displayContentPreview(
- @ContentPreviewType int previewType,
- Intent targetIntent,
- Resources resources,
- LayoutInflater layoutInflater,
- ActionFactory actionFactory,
- @LayoutRes int actionRowLayout,
- ViewGroup parent,
- ContentPreviewCoordinator previewCoord,
- Consumer<Boolean> onTransitionTargetReady,
- ContentResolver contentResolver,
- ImageMimeTypeClassifier imageClassifier) {
- ViewGroup layout = null;
-
- switch (previewType) {
- case CONTENT_PREVIEW_TEXT:
- layout = displayTextContentPreview(
- targetIntent,
- layoutInflater,
- createTextPreviewActions(actionFactory),
- parent,
- previewCoord,
- actionRowLayout);
- break;
- case CONTENT_PREVIEW_IMAGE:
- layout = displayImageContentPreview(
- targetIntent,
- layoutInflater,
- createImagePreviewActions(actionFactory),
- parent,
- previewCoord,
- onTransitionTargetReady,
- contentResolver,
- imageClassifier,
- actionRowLayout);
- break;
- case CONTENT_PREVIEW_FILE:
- layout = displayFileContentPreview(
- targetIntent,
- resources,
- layoutInflater,
- createFilePreviewActions(actionFactory),
- parent,
- previewCoord,
- contentResolver,
- actionRowLayout);
- break;
- default:
- Log.e(TAG, "Unexpected content preview type: " + previewType);
- }
-
- return layout;
- }
-
- private static Cursor queryResolver(ContentResolver resolver, Uri uri) {
- return resolver.query(uri, null, null, null, null);
- }
-
- @ContentPreviewType
- private static int findPreferredContentPreview(
- Uri uri, ContentResolver resolver, ImageMimeTypeClassifier imageClassifier) {
- if (uri == null) {
- return CONTENT_PREVIEW_TEXT;
- }
-
- String mimeType = resolver.getType(uri);
- return imageClassifier.isImageType(mimeType) ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE;
- }
-
- private static ViewGroup displayTextContentPreview(
- Intent targetIntent,
- LayoutInflater layoutInflater,
- List<ActionRow.Action> actions,
- ViewGroup parent,
- ContentPreviewCoordinator previewCoord,
- @LayoutRes int actionRowLayout) {
- ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
- R.layout.chooser_grid_preview_text, parent, false);
-
- final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
- if (actionRow != null) {
- actionRow.setActions(actions);
- }
-
- CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
- if (sharingText == null) {
- contentPreviewLayout
- .findViewById(com.android.internal.R.id.content_preview_text_layout)
- .setVisibility(View.GONE);
- } else {
- TextView textView = contentPreviewLayout.findViewById(
- com.android.internal.R.id.content_preview_text);
- textView.setText(sharingText);
- }
-
- String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE);
- if (TextUtils.isEmpty(previewTitle)) {
- contentPreviewLayout
- .findViewById(com.android.internal.R.id.content_preview_title_layout)
- .setVisibility(View.GONE);
- } else {
- TextView previewTitleView = contentPreviewLayout.findViewById(
- com.android.internal.R.id.content_preview_title);
- previewTitleView.setText(previewTitle);
-
- ClipData previewData = targetIntent.getClipData();
- Uri previewThumbnail = null;
- if (previewData != null) {
- if (previewData.getItemCount() > 0) {
- ClipData.Item previewDataItem = previewData.getItemAt(0);
- previewThumbnail = previewDataItem.getUri();
- }
- }
-
- ImageView previewThumbnailView = contentPreviewLayout.findViewById(
- com.android.internal.R.id.content_preview_thumbnail);
- if (previewThumbnail == null) {
- previewThumbnailView.setVisibility(View.GONE);
- } else {
- previewCoord.loadImage(
- previewThumbnail,
- (bitmap) -> updateViewWithImage(
- contentPreviewLayout.findViewById(
- com.android.internal.R.id.content_preview_thumbnail),
- bitmap));
- }
- }
-
- return contentPreviewLayout;
- }
-
- private static List<ActionRow.Action> createTextPreviewActions(ActionFactory actionFactory) {
- ArrayList<ActionRow.Action> actions = new ArrayList<>(2);
- actions.add(actionFactory.createCopyButton());
- ActionRow.Action nearbyAction = actionFactory.createNearbyButton();
- if (nearbyAction != null) {
- actions.add(nearbyAction);
- }
- return actions;
- }
-
- private static ViewGroup displayImageContentPreview(
- Intent targetIntent,
- LayoutInflater layoutInflater,
- List<ActionRow.Action> actions,
- ViewGroup parent,
- ContentPreviewCoordinator previewCoord,
- Consumer<Boolean> onTransitionTargetReady,
- ContentResolver contentResolver,
- ImageMimeTypeClassifier imageClassifier,
- @LayoutRes int actionRowLayout) {
- ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
- R.layout.chooser_grid_preview_image, parent, false);
- ImagePreviewView imagePreview = contentPreviewLayout.findViewById(
- com.android.internal.R.id.content_preview_image_area);
-
- final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
- if (actionRow != null) {
- actionRow.setActions(actions);
- }
-
- final ImagePreviewImageLoader imageLoader = new ImagePreviewImageLoader(previewCoord);
- final ArrayList<Uri> imageUris = new ArrayList<>();
- String action = targetIntent.getAction();
- if (Intent.ACTION_SEND.equals(action)) {
- // TODO: why don't we use image classifier in this case as well?
- Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
- imageUris.add(uri);
- } else {
- List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
- for (Uri uri : uris) {
- if (imageClassifier.isImageType(contentResolver.getType(uri))) {
- imageUris.add(uri);
- }
- }
- }
-
- if (imageUris.size() == 0) {
- Log.i(TAG, "Attempted to display image preview area with zero"
- + " available images detected in EXTRA_STREAM list");
- imagePreview.setVisibility(View.GONE);
- onTransitionTargetReady.accept(false);
- return contentPreviewLayout;
- }
-
- imagePreview.setSharedElementTransitionTarget(
- ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME,
- onTransitionTargetReady);
- imagePreview.setImages(imageUris, imageLoader);
-
- return contentPreviewLayout;
- }
-
- private static List<ActionRow.Action> createImagePreviewActions(
- ActionFactory buttonFactory) {
- ArrayList<ActionRow.Action> actions = new ArrayList<>(2);
- //TODO: add copy action;
- ActionRow.Action action = buttonFactory.createNearbyButton();
- if (action != null) {
- actions.add(action);
- }
- action = buttonFactory.createEditButton();
- if (action != null) {
- actions.add(action);
- }
- return actions;
- }
-
- private static ViewGroup displayFileContentPreview(
- Intent targetIntent,
- Resources resources,
- LayoutInflater layoutInflater,
- List<ActionRow.Action> actions,
- ViewGroup parent,
- ContentPreviewCoordinator previewCoord,
- ContentResolver contentResolver,
- @LayoutRes int actionRowLayout) {
- ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
- R.layout.chooser_grid_preview_file, parent, false);
-
- final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
- if (actionRow != null) {
- actionRow.setActions(actions);
- }
-
- String action = targetIntent.getAction();
- if (Intent.ACTION_SEND.equals(action)) {
- Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
- loadFileUriIntoView(uri, contentPreviewLayout, previewCoord, contentResolver);
- } else {
- List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
- int uriCount = uris.size();
-
- if (uriCount == 0) {
- contentPreviewLayout.setVisibility(View.GONE);
- Log.i(TAG,
- "Appears to be no uris available in EXTRA_STREAM, removing "
- + "preview area");
- return contentPreviewLayout;
- } else if (uriCount == 1) {
- loadFileUriIntoView(
- uris.get(0), contentPreviewLayout, previewCoord, contentResolver);
- } else {
- FileInfo fileInfo = extractFileInfo(uris.get(0), contentResolver);
- int remUriCount = uriCount - 1;
- Map<String, Object> arguments = new HashMap<>();
- arguments.put(PLURALS_COUNT, remUriCount);
- arguments.put(PLURALS_FILE_NAME, fileInfo.name);
- String fileName =
- PluralsMessageFormatter.format(resources, arguments, R.string.file_count);
-
- TextView fileNameView = contentPreviewLayout.findViewById(
- com.android.internal.R.id.content_preview_filename);
- fileNameView.setText(fileName);
-
- View thumbnailView = contentPreviewLayout.findViewById(
- com.android.internal.R.id.content_preview_file_thumbnail);
- thumbnailView.setVisibility(View.GONE);
-
- ImageView fileIconView = contentPreviewLayout.findViewById(
- com.android.internal.R.id.content_preview_file_icon);
- fileIconView.setVisibility(View.VISIBLE);
- fileIconView.setImageResource(R.drawable.ic_file_copy);
- }
- }
-
- return contentPreviewLayout;
- }
-
- private static List<ActionRow.Action> createFilePreviewActions(ActionFactory actionFactory) {
- List<ActionRow.Action> actions = new ArrayList<>(1);
- //TODO(b/120417119):
- // add action buttonFactory.createCopyButton()
- ActionRow.Action action = actionFactory.createNearbyButton();
- if (action != null) {
- actions.add(action);
- }
- return actions;
- }
-
- private static ActionRow inflateActionRow(ViewGroup parent, @LayoutRes int actionRowLayout) {
- final ViewStub stub = parent.findViewById(com.android.intentresolver.R.id.action_row_stub);
- if (stub != null) {
- stub.setLayoutResource(actionRowLayout);
- stub.inflate();
- }
- return parent.findViewById(com.android.internal.R.id.chooser_action_row);
- }
-
- private static void logContentPreviewWarning(Uri uri) {
- // The ContentResolver already logs the exception. Log something more informative.
- Log.w(TAG, "Could not load (" + uri.toString() + ") thumbnail/name for preview. If "
- + "desired, consider using Intent#createChooser to launch the ChooserActivity, "
- + "and set your Intent's clipData and flags in accordance with that method's "
- + "documentation");
- }
-
- private static void loadFileUriIntoView(
- final Uri uri,
- final View parent,
- final ContentPreviewCoordinator previewCoord,
- final ContentResolver contentResolver) {
- FileInfo fileInfo = extractFileInfo(uri, contentResolver);
-
- TextView fileNameView = parent.findViewById(
- com.android.internal.R.id.content_preview_filename);
- fileNameView.setText(fileInfo.name);
-
- if (fileInfo.hasThumbnail) {
- previewCoord.loadImage(
- uri,
- (bitmap) -> updateViewWithImage(
- parent.findViewById(
- com.android.internal.R.id.content_preview_file_thumbnail),
- bitmap));
- } else {
- View thumbnailView = parent.findViewById(
- com.android.internal.R.id.content_preview_file_thumbnail);
- thumbnailView.setVisibility(View.GONE);
-
- ImageView fileIconView = parent.findViewById(
- com.android.internal.R.id.content_preview_file_icon);
- fileIconView.setVisibility(View.VISIBLE);
- fileIconView.setImageResource(R.drawable.chooser_file_generic);
- }
- }
-
- private static void updateViewWithImage(RoundedRectImageView imageView, Bitmap image) {
- if (image == null) {
- imageView.setVisibility(View.GONE);
- return;
- }
- imageView.setVisibility(View.VISIBLE);
- imageView.setAlpha(0.0f);
- imageView.setImageBitmap(image);
-
- ValueAnimator fadeAnim = ObjectAnimator.ofFloat(imageView, "alpha", 0.0f, 1.0f);
- fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
- fadeAnim.setDuration(IMAGE_FADE_IN_MILLIS);
- fadeAnim.start();
- }
-
- private static FileInfo extractFileInfo(Uri uri, ContentResolver resolver) {
- String fileName = null;
- boolean hasThumbnail = false;
-
- try (Cursor cursor = queryResolver(resolver, uri)) {
- if (cursor != null && cursor.getCount() > 0) {
- int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
- int titleIndex = cursor.getColumnIndex(Downloads.Impl.COLUMN_TITLE);
- int flagsIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS);
-
- cursor.moveToFirst();
- if (nameIndex != -1) {
- fileName = cursor.getString(nameIndex);
- } else if (titleIndex != -1) {
- fileName = cursor.getString(titleIndex);
- }
-
- if (flagsIndex != -1) {
- hasThumbnail = (cursor.getInt(flagsIndex)
- & DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
- }
- }
- } catch (SecurityException | NullPointerException e) {
- logContentPreviewWarning(uri);
- }
-
- if (TextUtils.isEmpty(fileName)) {
- fileName = uri.getPath();
- int index = fileName.lastIndexOf('/');
- if (index != -1) {
- fileName = fileName.substring(index + 1);
- }
- }
-
- return new FileInfo(fileName, hasThumbnail);
- }
-
- private static class FileInfo {
- public final String name;
- public final boolean hasThumbnail;
-
- FileInfo(String name, boolean hasThumbnail) {
- this.name = name;
- this.hasThumbnail = hasThumbnail;
- }
- }
-
- private ChooserContentPreviewUi() {}
-}
diff --git a/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
new file mode 100644
index 00000000..5fbf03a0
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserIntegratedDeviceComponents.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.provider.Settings;
+import android.text.TextUtils;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * Helper to look up the components available on this device to handle assorted built-in actions
+ * like "Edit" that may be displayed for certain content/preview types. The components are queried
+ * when this record is instantiated, and are then immutable for a given instance.
+ *
+ * Because this describes the app's external execution environment, test methods may prefer to
+ * provide explicit values to override the default lookup logic.
+ */
+public class ChooserIntegratedDeviceComponents {
+ @Nullable
+ private final ComponentName mEditSharingComponent;
+
+ @Nullable
+ private final ComponentName mNearbySharingComponent;
+
+ /** Look up the integrated components available on this device. */
+ public static ChooserIntegratedDeviceComponents get(
+ Context context,
+ SecureSettings secureSettings) {
+ return new ChooserIntegratedDeviceComponents(
+ getEditSharingComponent(context),
+ getNearbySharingComponent(context, secureSettings));
+ }
+
+ @VisibleForTesting
+ ChooserIntegratedDeviceComponents(
+ ComponentName editSharingComponent, ComponentName nearbySharingComponent) {
+ mEditSharingComponent = editSharingComponent;
+ mNearbySharingComponent = nearbySharingComponent;
+ }
+
+ public ComponentName getEditSharingComponent() {
+ return mEditSharingComponent;
+ }
+
+ public ComponentName getNearbySharingComponent() {
+ return mNearbySharingComponent;
+ }
+
+ private static ComponentName getEditSharingComponent(Context context) {
+ String editorComponent = context.getApplicationContext().getString(
+ R.string.config_systemImageEditor);
+ return TextUtils.isEmpty(editorComponent)
+ ? null : ComponentName.unflattenFromString(editorComponent);
+ }
+
+ private static ComponentName getNearbySharingComponent(Context context,
+ SecureSettings secureSettings) {
+ String nearbyComponent = secureSettings.getString(
+ context.getContentResolver(), Settings.Secure.NEARBY_SHARING_COMPONENT);
+ if (TextUtils.isEmpty(nearbyComponent)) {
+ nearbyComponent = context.getString(R.string.config_defaultNearbySharingComponent);
+ }
+ return TextUtils.isEmpty(nearbyComponent)
+ ? null : ComponentName.unflattenFromString(nearbyComponent);
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java
index 699190f9..f0651360 100644
--- a/java/src/com/android/intentresolver/ChooserListAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -49,7 +49,6 @@ import android.widget.TextView;
import androidx.annotation.WorkerThread;
-import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
import com.android.intentresolver.chooser.NotSelectableTargetInfo;
@@ -264,7 +263,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
}
holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
- holder.bindIcon(info);
+ holder.bindIcon(info, /*animate =*/ true);
if (info.isSelectableTargetInfo()) {
// direct share targets should append the application name for a better readout
DisplayResolveInfo rInfo = info.getDisplayResolveInfo();
diff --git a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
index 39d1fab0..3e2ea473 100644
--- a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
@@ -48,7 +48,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
Context context,
ChooserGridAdapter adapter,
EmptyStateProvider emptyStateProvider,
- QuietModeManager quietModeManager,
+ Supplier<Boolean> workProfileQuietModeChecker,
UserHandle workProfileUserHandle,
int maxTargetsPerRow) {
this(
@@ -56,7 +56,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
new ChooserProfileAdapterBinder(maxTargetsPerRow),
ImmutableList.of(adapter),
emptyStateProvider,
- quietModeManager,
+ workProfileQuietModeChecker,
/* defaultProfile= */ 0,
workProfileUserHandle,
new BottomPaddingOverrideSupplier(context));
@@ -67,7 +67,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
ChooserGridAdapter personalAdapter,
ChooserGridAdapter workAdapter,
EmptyStateProvider emptyStateProvider,
- QuietModeManager quietModeManager,
+ Supplier<Boolean> workProfileQuietModeChecker,
@Profile int defaultProfile,
UserHandle workProfileUserHandle,
int maxTargetsPerRow) {
@@ -76,7 +76,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
new ChooserProfileAdapterBinder(maxTargetsPerRow),
ImmutableList.of(personalAdapter, workAdapter),
emptyStateProvider,
- quietModeManager,
+ workProfileQuietModeChecker,
defaultProfile,
workProfileUserHandle,
new BottomPaddingOverrideSupplier(context));
@@ -87,7 +87,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
ChooserProfileAdapterBinder adapterBinder,
ImmutableList<ChooserGridAdapter> gridAdapters,
EmptyStateProvider emptyStateProvider,
- QuietModeManager quietModeManager,
+ Supplier<Boolean> workProfileQuietModeChecker,
@Profile int defaultProfile,
UserHandle workProfileUserHandle,
BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {
@@ -97,7 +97,7 @@ public class ChooserMultiProfilePagerAdapter extends GenericMultiProfilePagerAda
adapterBinder,
gridAdapters,
emptyStateProvider,
- quietModeManager,
+ workProfileQuietModeChecker,
defaultProfile,
workProfileUserHandle,
() -> makeProfileView(context),
diff --git a/java/src/com/android/intentresolver/ChooserRefinementManager.java b/java/src/com/android/intentresolver/ChooserRefinementManager.java
new file mode 100644
index 00000000..3ddc1c7c
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserRefinementManager.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.IntentSender.SendIntentException;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.ResultReceiver;
+import android.util.Log;
+
+import com.android.intentresolver.chooser.TargetInfo;
+
+import java.util.List;
+import java.util.function.Consumer;
+
+/**
+ * Helper class to manage Sharesheet's "refinement" flow, where callers supply a "refinement
+ * activity" that will be invoked when a target is selected, allowing the calling app to add
+ * additional extras and other refinements (subject to {@link Intent#filterEquals()}), e.g., to
+ * convert the format of the payload, or lazy-download some data that was deferred in the original
+ * call).
+ */
+public final class ChooserRefinementManager {
+ private static final String TAG = "ChooserRefinement";
+
+ @Nullable
+ private final IntentSender mRefinementIntentSender;
+
+ private final Context mContext;
+ private final Consumer<TargetInfo> mOnSelectionRefined;
+ private final Runnable mOnRefinementCancelled;
+
+ @Nullable
+ private RefinementResultReceiver mRefinementResultReceiver;
+
+ public ChooserRefinementManager(
+ Context context,
+ @Nullable IntentSender refinementIntentSender,
+ Consumer<TargetInfo> onSelectionRefined,
+ Runnable onRefinementCancelled) {
+ mContext = context;
+ mRefinementIntentSender = refinementIntentSender;
+ mOnSelectionRefined = onSelectionRefined;
+ mOnRefinementCancelled = onRefinementCancelled;
+ }
+
+ /**
+ * Delegate the user's {@code selectedTarget} to the refinement flow, if possible.
+ * @return true if the selection should wait for a now-started refinement flow, or false if it
+ * can proceed by the default (non-refinement) logic.
+ */
+ public boolean maybeHandleSelection(TargetInfo selectedTarget) {
+ if (mRefinementIntentSender == null) {
+ return false;
+ }
+ if (selectedTarget.getAllSourceIntents().isEmpty()) {
+ return false;
+ }
+
+ destroy(); // Terminate any prior sessions.
+ mRefinementResultReceiver = new RefinementResultReceiver(
+ refinedIntent -> {
+ destroy();
+ TargetInfo refinedTarget =
+ selectedTarget.tryToCloneWithAppliedRefinement(refinedIntent);
+ if (refinedTarget != null) {
+ mOnSelectionRefined.accept(refinedTarget);
+ } else {
+ Log.e(TAG, "Failed to apply refinement to any matching source intent");
+ mOnRefinementCancelled.run();
+ }
+ },
+ mOnRefinementCancelled,
+ mContext.getMainThreadHandler());
+
+ Intent refinementRequest = makeRefinementRequest(mRefinementResultReceiver, selectedTarget);
+ try {
+ mRefinementIntentSender.sendIntent(mContext, 0, refinementRequest, null, null);
+ return true;
+ } catch (SendIntentException e) {
+ Log.e(TAG, "Refinement IntentSender failed to send", e);
+ }
+ return false;
+ }
+
+ /** Clean up any ongoing refinement session. */
+ public void destroy() {
+ if (mRefinementResultReceiver != null) {
+ mRefinementResultReceiver.destroy();
+ mRefinementResultReceiver = null;
+ }
+ }
+
+ private static Intent makeRefinementRequest(
+ RefinementResultReceiver resultReceiver, TargetInfo originalTarget) {
+ final Intent fillIn = new Intent();
+ final List<Intent> sourceIntents = originalTarget.getAllSourceIntents();
+ fillIn.putExtra(Intent.EXTRA_INTENT, sourceIntents.get(0));
+ final int sourceIntentCount = sourceIntents.size();
+ if (sourceIntentCount > 1) {
+ fillIn.putExtra(
+ Intent.EXTRA_ALTERNATE_INTENTS,
+ sourceIntents
+ .subList(1, sourceIntentCount)
+ .toArray(new Intent[sourceIntentCount - 1]));
+ }
+ fillIn.putExtra(Intent.EXTRA_RESULT_RECEIVER, resultReceiver.copyForSending());
+ return fillIn;
+ }
+
+ private static class RefinementResultReceiver extends ResultReceiver {
+ private final Consumer<Intent> mOnSelectionRefined;
+ private final Runnable mOnRefinementCancelled;
+
+ private boolean mDestroyed;
+
+ RefinementResultReceiver(
+ Consumer<Intent> onSelectionRefined,
+ Runnable onRefinementCancelled,
+ Handler handler) {
+ super(handler);
+ mOnSelectionRefined = onSelectionRefined;
+ mOnRefinementCancelled = onRefinementCancelled;
+ }
+
+ public void destroy() {
+ mDestroyed = true;
+ }
+
+ @Override
+ protected void onReceiveResult(int resultCode, Bundle resultData) {
+ if (mDestroyed) {
+ Log.e(TAG, "Destroyed RefinementResultReceiver received a result");
+ return;
+ }
+ if (resultData == null) {
+ Log.e(TAG, "RefinementResultReceiver received null resultData");
+ // TODO: treat as cancellation?
+ return;
+ }
+
+ switch (resultCode) {
+ case Activity.RESULT_CANCELED:
+ mOnRefinementCancelled.run();
+ break;
+ case Activity.RESULT_OK:
+ Parcelable intentParcelable = resultData.getParcelable(Intent.EXTRA_INTENT);
+ if (intentParcelable instanceof Intent) {
+ mOnSelectionRefined.accept((Intent) intentParcelable);
+ } else {
+ Log.e(TAG, "No valid Intent.EXTRA_INTENT in 'OK' refinement result data");
+ }
+ break;
+ default:
+ Log.w(TAG, "Received unknown refinement result " + resultCode);
+ break;
+ }
+ }
+
+ /**
+ * Apps can't load this class directly, so we need a regular ResultReceiver copy for
+ * sending. Obtain this by parceling and unparceling (one weird trick).
+ */
+ ResultReceiver copyForSending() {
+ Parcel parcel = Parcel.obtain();
+ writeToParcel(parcel, 0);
+ parcel.setDataPosition(0);
+ ResultReceiver receiverForSending = ResultReceiver.CREATOR.createFromParcel(parcel);
+ parcel.recycle();
+ return receiverForSending;
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java
index 81481bf1..3d99e475 100644
--- a/java/src/com/android/intentresolver/ChooserRequestParameters.java
+++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java
@@ -18,6 +18,7 @@ package com.android.intentresolver;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Intent;
import android.content.IntentFilter;
@@ -26,11 +27,15 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.PatternMatcher;
+import android.service.chooser.ChooserAction;
import android.service.chooser.ChooserTarget;
import android.text.TextUtils;
import android.util.Log;
import android.util.Pair;
+import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.flags.Flags;
+
import com.google.common.collect.ImmutableList;
import java.net.URISyntaxException;
@@ -66,10 +71,14 @@ public class ChooserRequestParameters {
Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK;
private final Intent mTarget;
+ private final ChooserIntegratedDeviceComponents mIntegratedDeviceComponents;
+ private final String mReferrerPackageName;
private final Pair<CharSequence, Integer> mTitleSpec;
private final Intent mReferrerFillInIntent;
private final ImmutableList<ComponentName> mFilteredComponentNames;
private final ImmutableList<ChooserTarget> mCallerChooserTargets;
+ private final @NonNull ImmutableList<ChooserAction> mChooserActions;
+ private final PendingIntent mModifyShareAction;
private final boolean mRetainInOnStop;
@Nullable
@@ -95,12 +104,18 @@ public class ChooserRequestParameters {
public ChooserRequestParameters(
final Intent clientIntent,
+ String referrerPackageName,
final Uri referrer,
- @Nullable final ComponentName nearbySharingComponent) {
+ ChooserIntegratedDeviceComponents integratedDeviceComponents,
+ FeatureFlagRepository featureFlags) {
final Intent requestedTarget = parseTargetIntentExtra(
clientIntent.getParcelableExtra(Intent.EXTRA_INTENT));
mTarget = intentWithModifiedLaunchFlags(requestedTarget);
+ mIntegratedDeviceComponents = integratedDeviceComponents;
+
+ mReferrerPackageName = referrerPackageName;
+
mAdditionalTargets = intentsWithModifiedLaunchFlagsFromExtraIfPresent(
clientIntent, Intent.EXTRA_ALTERNATE_INTENTS);
@@ -120,7 +135,8 @@ public class ChooserRequestParameters {
mRefinementIntentSender = clientIntent.getParcelableExtra(
Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER);
- mFilteredComponentNames = getFilteredComponentNames(clientIntent, nearbySharingComponent);
+ mFilteredComponentNames = getFilteredComponentNames(
+ clientIntent, mIntegratedDeviceComponents.getNearbySharingComponent());
mCallerChooserTargets = parseCallerTargetsFromClientIntent(clientIntent);
@@ -130,6 +146,13 @@ public class ChooserRequestParameters {
mSharedText = mTarget.getStringExtra(Intent.EXTRA_TEXT);
mTargetIntentFilter = getTargetIntentFilter(mTarget);
+
+ mChooserActions = featureFlags.isEnabled(Flags.SHARESHEET_CUSTOM_ACTIONS)
+ ? getChooserActions(clientIntent)
+ : ImmutableList.of();
+ mModifyShareAction = featureFlags.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)
+ ? getModifyShareAction(clientIntent)
+ : null;
}
public Intent getTargetIntent() {
@@ -150,6 +173,10 @@ public class ChooserRequestParameters {
return getTargetIntent().getType();
}
+ public String getReferrerPackageName() {
+ return mReferrerPackageName;
+ }
+
@Nullable
public CharSequence getTitle() {
return mTitleSpec.first;
@@ -171,8 +198,18 @@ public class ChooserRequestParameters {
return mCallerChooserTargets;
}
+ @NonNull
+ public ImmutableList<ChooserAction> getChooserActions() {
+ return mChooserActions;
+ }
+
+ @Nullable
+ public PendingIntent getModifyShareAction() {
+ return mModifyShareAction;
+ }
+
/**
- * Whether the {@link ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP} behavior was requested.
+ * Whether the {@link ChooserActivity#EXTRA_PRIVATE_RETAIN_IN_ON_STOP} behavior was requested.
*/
public boolean shouldRetainInOnStop() {
return mRetainInOnStop;
@@ -221,6 +258,10 @@ public class ChooserRequestParameters {
return mTargetIntentFilter;
}
+ public ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() {
+ return mIntegratedDeviceComponents;
+ }
+
private static boolean isSendAction(@Nullable String action) {
return (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action));
}
@@ -300,6 +341,32 @@ public class ChooserRequestParameters {
.collect(toImmutableList());
}
+ @NonNull
+ private static ImmutableList<ChooserAction> getChooserActions(Intent intent) {
+ return streamParcelableArrayExtra(
+ intent,
+ Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS,
+ ChooserAction.class,
+ true,
+ true)
+ .collect(toImmutableList());
+ }
+
+ @Nullable
+ private static PendingIntent getModifyShareAction(Intent intent) {
+ try {
+ return intent.getParcelableExtra(
+ Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION,
+ PendingIntent.class);
+ } catch (Throwable t) {
+ Log.w(
+ TAG,
+ "Unable to retrieve Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION argument",
+ t);
+ return null;
+ }
+ }
+
private static <T> Collector<T, ?, ImmutableList<T>> toImmutableList() {
return Collectors.collectingAndThen(Collectors.toList(), ImmutableList::copyOf);
}
diff --git a/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt b/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt
index a0bf61b6..b1178aa5 100644
--- a/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt
+++ b/java/src/com/android/intentresolver/EnterTransitionAnimationDelegate.kt
@@ -15,23 +15,31 @@
*/
package com.android.intentresolver
-import android.app.Activity
import android.app.SharedElementCallback
import android.view.View
-import com.android.intentresolver.widget.ResolverDrawerLayout
+import androidx.activity.ComponentActivity
+import androidx.lifecycle.lifecycleScope
+import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback
+import com.android.internal.annotations.VisibleForTesting
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
import java.util.function.Supplier
/**
* A helper class to track app's readiness for the scene transition animation.
* The app is ready when both the image is laid out and the drawer offset is calculated.
*/
-internal class EnterTransitionAnimationDelegate(
- private val activity: Activity,
- private val resolverDrawerLayoutSupplier: Supplier<ResolverDrawerLayout?>
-) : View.OnLayoutChangeListener {
- private var removeSharedElements = false
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+class EnterTransitionAnimationDelegate(
+ private val activity: ComponentActivity,
+ private val transitionTargetSupplier: Supplier<View?>,
+) : View.OnLayoutChangeListener, TransitionElementStatusCallback {
+
+ private val transitionElements = HashSet<String>()
private var previewReady = false
private var offsetCalculated = false
+ private var timeoutJob: Job? = null
init {
activity.setEnterSharedElementCallback(
@@ -46,12 +54,27 @@ internal class EnterTransitionAnimationDelegate(
})
}
- fun postponeTransition() = activity.postponeEnterTransition()
-
- fun markImagePreviewReady(runTransitionAnimation: Boolean) {
- if (!runTransitionAnimation) {
- removeSharedElements = true
+ fun postponeTransition() {
+ activity.postponeEnterTransition()
+ timeoutJob = activity.lifecycleScope.launch {
+ delay(activity.resources.getInteger(R.integer.config_shortAnimTime).toLong())
+ onTimeout()
}
+ }
+
+ private fun onTimeout() {
+ // We only mark the preview readiness and not the offset readiness
+ // (see [#markOffsetCalculated()]) as this is what legacy logic, effectively, did. We might
+ // want to review that aspect separately.
+ onAllTransitionElementsReady()
+ }
+
+ override fun onTransitionElementReady(name: String) {
+ transitionElements.add(name)
+ }
+
+ override fun onAllTransitionElementsReady() {
+ timeoutJob?.cancel()
if (!previewReady) {
previewReady = true
maybeStartListenForLayout()
@@ -69,15 +92,12 @@ internal class EnterTransitionAnimationDelegate(
names: MutableList<String>,
sharedElements: MutableMap<String, View>
) {
- if (removeSharedElements) {
- names.remove(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)
- sharedElements.remove(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME)
- }
- removeSharedElements = false
+ names.removeAll { !transitionElements.contains(it) }
+ sharedElements.entries.removeAll { !transitionElements.contains(it.key) }
}
private fun maybeStartListenForLayout() {
- val drawer = resolverDrawerLayoutSupplier.get()
+ val drawer = transitionTargetSupplier.get()
if (previewReady && offsetCalculated && drawer != null) {
if (drawer.isInLayout) {
startPostponedEnterTransition()
@@ -98,7 +118,7 @@ internal class EnterTransitionAnimationDelegate(
}
private fun startPostponedEnterTransition() {
- if (!removeSharedElements && activity.isActivityTransitionRunning) {
+ if (transitionElements.isNotEmpty() && activity.isActivityTransitionRunning) {
// Disable the window animations as it interferes with the transition animation.
activity.window.setWindowAnimations(0)
}
diff --git a/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java
index 9bbdf7c7..7613f35f 100644
--- a/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/GenericMultiProfilePagerAdapter.java
@@ -81,7 +81,7 @@ class GenericMultiProfilePagerAdapter<
AdapterBinder<PageViewT, SinglePageAdapterT> adapterBinder,
ImmutableList<SinglePageAdapterT> adapters,
EmptyStateProvider emptyStateProvider,
- QuietModeManager quietModeManager,
+ Supplier<Boolean> workProfileQuietModeChecker,
@Profile int defaultProfile,
UserHandle workProfileUserHandle,
Supplier<ViewGroup> pageViewInflater,
@@ -90,7 +90,7 @@ class GenericMultiProfilePagerAdapter<
context,
/* currentPage= */ defaultProfile,
emptyStateProvider,
- quietModeManager,
+ workProfileQuietModeChecker,
workProfileUserHandle);
mListAdapterExtractor = listAdapterExtractor;
diff --git a/java/src/com/android/intentresolver/ImageLoader.kt b/java/src/com/android/intentresolver/ImageLoader.kt
new file mode 100644
index 00000000..0ed8b122
--- /dev/null
+++ b/java/src/com/android/intentresolver/ImageLoader.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.graphics.Bitmap
+import android.net.Uri
+import java.util.function.Consumer
+
+interface ImageLoader : suspend (Uri) -> Bitmap? {
+ fun loadImage(uri: Uri, callback: Consumer<Bitmap?>)
+ fun prePopulate(uris: List<Uri>)
+}
diff --git a/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt
index e68eb66a..7b6651a2 100644
--- a/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt
+++ b/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt
@@ -16,23 +16,72 @@
package com.android.intentresolver
+import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
-import kotlinx.coroutines.suspendCancellableCoroutine
-
-// TODO: convert ChooserContentPreviewCoordinator to Kotlin and merge this class into it.
-internal class ImagePreviewImageLoader(
- private val previewCoordinator: ChooserContentPreviewUi.ContentPreviewCoordinator
-) : suspend (Uri) -> Bitmap? {
-
- override suspend fun invoke(uri: Uri): Bitmap? =
- suspendCancellableCoroutine { continuation ->
- val callback = java.util.function.Consumer<Bitmap?> { bitmap ->
- try {
- continuation.resumeWith(Result.success(bitmap))
- } catch (ignored: Exception) {
- }
+import android.util.Size
+import androidx.annotation.GuardedBy
+import androidx.annotation.VisibleForTesting
+import androidx.collection.LruCache
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.coroutineScope
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import java.util.function.Consumer
+
+@VisibleForTesting
+class ImagePreviewImageLoader @JvmOverloads constructor(
+ private val context: Context,
+ private val lifecycle: Lifecycle,
+ cacheSize: Int,
+ private val dispatcher: CoroutineDispatcher = Dispatchers.IO
+) : ImageLoader {
+
+ private val thumbnailSize: Size =
+ context.resources.getDimensionPixelSize(R.dimen.chooser_preview_image_max_dimen).let {
+ Size(it, it)
+ }
+
+ @GuardedBy("self")
+ private val cache = LruCache<Uri, CompletableDeferred<Bitmap?>>(cacheSize)
+
+ override suspend fun invoke(uri: Uri): Bitmap? = loadImageAsync(uri)
+
+ override fun loadImage(uri: Uri, callback: Consumer<Bitmap?>) {
+ lifecycle.coroutineScope.launch {
+ val image = loadImageAsync(uri)
+ if (isActive) {
+ callback.accept(image)
}
- previewCoordinator.loadImage(uri, callback)
}
+ }
+
+ override fun prePopulate(uris: List<Uri>) {
+ uris.asSequence().take(cache.maxSize()).forEach { uri ->
+ lifecycle.coroutineScope.launch {
+ loadImageAsync(uri)
+ }
+ }
+ }
+
+ private suspend fun loadImageAsync(uri: Uri): Bitmap? {
+ return synchronized(cache) {
+ cache.get(uri) ?: CompletableDeferred<Bitmap?>().also { result ->
+ cache.put(uri, result)
+ lifecycle.coroutineScope.launch(dispatcher) {
+ result.loadBitmap(uri)
+ }
+ }
+ }.await()
+ }
+
+ private fun CompletableDeferred<Bitmap?>.loadBitmap(uri: Uri) {
+ val bitmap = runCatching {
+ context.contentResolver.loadThumbnail(uri, thumbnailSize, null)
+ }.getOrNull()
+ complete(bitmap)
+ }
}
diff --git a/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java b/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java
index 5bf994d6..c1373f4b 100644
--- a/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java
+++ b/java/src/com/android/intentresolver/NoAppsAvailableEmptyStateProvider.java
@@ -101,9 +101,9 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider {
if (mWorkProfileUserHandle == null) {
return false;
}
- List<ResolverActivity.ResolvedComponentInfo> resolversForIntent =
+ List<ResolvedComponentInfo> resolversForIntent =
adapter.getResolversForUser(UserHandle.of(mMyUserIdProvider.getMyUserId()));
- for (ResolverActivity.ResolvedComponentInfo info : resolversForIntent) {
+ for (ResolvedComponentInfo info : resolversForIntent) {
ResolveInfo resolveInfo = info.getResolveInfoAt(0);
if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) {
return true;
@@ -151,4 +151,4 @@ public class NoAppsAvailableEmptyStateProvider implements EmptyStateProvider {
.write();
}
}
-} \ No newline at end of file
+}
diff --git a/java/src/com/android/intentresolver/ResolvedComponentInfo.java b/java/src/com/android/intentresolver/ResolvedComponentInfo.java
new file mode 100644
index 00000000..ecb72cbf
--- /dev/null
+++ b/java/src/com/android/intentresolver/ResolvedComponentInfo.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Record type to store all resolutions that are deduped to a single target component, along with
+ * other metadata about the component (which applies to all of the resolutions in the record).
+ * This record is assembled when we're first processing resolutions, and then later it's used to
+ * derive the {@link TargetInfo} record(s) that specify how the resolutions will be presented as
+ * targets in the UI.
+ */
+public final class ResolvedComponentInfo {
+ public final ComponentName name;
+ private final List<Intent> mIntents = new ArrayList<>();
+ private final List<ResolveInfo> mResolveInfos = new ArrayList<>();
+ private boolean mPinned;
+
+ /**
+ * @param name the name of the component that owns all the resolutions added to this record.
+ * @param intent an initial {@link Intent} to add to this record
+ * @param info the {@link ResolveInfo} associated with the given {@code intent}.
+ */
+ public ResolvedComponentInfo(ComponentName name, Intent intent, ResolveInfo info) {
+ this.name = name;
+ add(intent, info);
+ }
+
+ /**
+ * Add an {@link Intent} and associated {@link ResolveInfo} as resolutions for this component.
+ */
+ public void add(Intent intent, ResolveInfo info) {
+ mIntents.add(intent);
+ mResolveInfos.add(info);
+ }
+
+ /** @return the number of {@link Intent}/{@link ResolveInfo} pairs added to this record. */
+ public int getCount() {
+ return mIntents.size();
+ }
+
+ /** @return the {@link Intent} at the specified {@code index}, if any, or else null. */
+ public Intent getIntentAt(int index) {
+ return (index >= 0) ? mIntents.get(index) : null;
+ }
+
+ /** @return the {@link ResolveInfo} at the specified {@code index}, if any, or else null. */
+ public ResolveInfo getResolveInfoAt(int index) {
+ return (index >= 0) ? mResolveInfos.get(index) : null;
+ }
+
+ /**
+ * @return the index of the provided {@link Intent} among those that have been added to this
+ * {@link ResolvedComponentInfo}, or -1 if it has't been added.
+ */
+ public int findIntent(Intent intent) {
+ return mIntents.indexOf(intent);
+ }
+
+ /**
+ * @return the index of the provided {@link ResolveInfo} among those that have been added to
+ * this {@link ResolvedComponentInfo}, or -1 if it has't been added.
+ */
+ public int findResolveInfo(ResolveInfo info) {
+ return mResolveInfos.indexOf(info);
+ }
+
+ /**
+ * @return whether this component was pinned by a call to {@link #setPinned()}.
+ * TODO: consolidate sources of pinning data and/or document how this differs from other places
+ * we make a "pinning" determination.
+ */
+ public boolean isPinned() {
+ return mPinned;
+ }
+
+ /**
+ * Set whether this component will be considered pinned in future calls to {@link #isPinned()}.
+ * TODO: consolidate sources of pinning data and/or document how this differs from other places
+ * we make a "pinning" determination.
+ */
+ public void setPinned(boolean pinned) {
+ mPinned = pinned;
+ }
+}
diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java
index 5573e18a..d224299e 100644
--- a/java/src/com/android/intentresolver/ResolverActivity.java
+++ b/java/src/com/android/intentresolver/ResolverActivity.java
@@ -44,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.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
@@ -61,7 +60,6 @@ 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;
@@ -105,7 +103,6 @@ import com.android.intentresolver.AbstractMultiProfilePagerAdapter.EmptyStatePro
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.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
@@ -163,7 +160,6 @@ public class ResolverActivity extends FragmentActivity implements
protected boolean mSupportsAlwaysUseOption;
protected ResolverDrawerLayout mResolverDrawerLayout;
protected PackageManager mPm;
- protected int mLaunchedFromUid;
private static final String TAG = "ResolverActivity";
private static final boolean DEBUG = false;
@@ -192,7 +188,7 @@ public class ResolverActivity extends FragmentActivity implements
@VisibleForTesting
protected AbstractMultiProfilePagerAdapter mMultiProfilePagerAdapter;
- protected QuietModeManager mQuietModeManager;
+ protected WorkProfileAvailabilityManager mWorkProfileAvailability;
// Intent extra for connected audio devices
public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device";
@@ -202,7 +198,7 @@ public class ResolverActivity extends FragmentActivity implements
* <p>Can only be used if there is a work profile.
* <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}.
*/
- static final String EXTRA_SELECTED_PROFILE =
+ protected static final String EXTRA_SELECTED_PROFILE =
"com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE";
/**
@@ -217,15 +213,20 @@ public class ResolverActivity extends FragmentActivity implements
static final String EXTRA_CALLING_USER =
"com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER";
- static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL;
- static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK;
+ protected static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL;
+ protected static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK;
- private BroadcastReceiver mWorkProfileStateReceiver;
private UserHandle mHeaderCreatorUser;
- private Supplier<UserHandle> mLazyWorkProfileUserHandle = () -> {
- final UserHandle result = fetchWorkProfileUserProfile();
- mLazyWorkProfileUserHandle = () -> result;
+ // User handle annotations are lazy-initialized to ensure that they're computed exactly once
+ // (even though they can't be computed prior to activity creation).
+ // TODO: use a less ad-hoc pattern for lazy initialization (by switching to Dagger or
+ // introducing a common `LazySingletonSupplier` API, etc), and/or migrate all dependents to a
+ // new component whose lifecycle is limited to the "created" Activity (so that we can just hold
+ // the annotations as a `final` ivar, which is a better way to show immutability).
+ private Supplier<AnnotatedUserHandles> mLazyAnnotatedUserHandles = () -> {
+ final AnnotatedUserHandles result = new AnnotatedUserHandles(this);
+ mLazyAnnotatedUserHandles = () -> result;
return result;
};
@@ -234,22 +235,6 @@ public class ResolverActivity extends FragmentActivity implements
protected final LatencyTracker mLatencyTracker = getLatencyTracker();
- private LatencyTracker getLatencyTracker() {
- return LatencyTracker.getInstance(this);
- }
-
- /**
- * Get the string resource to be used as a label for the link to the resolver activity for an
- * action.
- *
- * @param action The action to resolve
- *
- * @return The string resource to be used as a label
- */
- public static @StringRes int getLabelRes(String action) {
- return ActionTitle.forAction(action).labelRes;
- }
-
private enum ActionTitle {
VIEW(Intent.ACTION_VIEW,
com.android.internal.R.string.whichViewApplication,
@@ -333,27 +318,6 @@ public class ResolverActivity extends FragmentActivity implements
};
}
- private Intent makeMyIntent() {
- Intent intent = new Intent(getIntent());
- intent.setComponent(null);
- // The resolver activity is set to be hidden from recent tasks.
- // we don't want this attribute to be propagated to the next activity
- // being launched. Note that if the original Intent also had this
- // flag set, we are now losing it. That should be a very rare case
- // and we can live with this.
- intent.setFlags(intent.getFlags()&~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
- return intent;
- }
-
- /**
- * Call {@link Activity#onCreate} without initializing anything further. This should
- * only be used when the activity is about to be immediately finished to avoid wasting
- * initializing steps and leaking resources.
- */
- protected void super_onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- }
-
@Override
protected void onCreate(Bundle savedInstanceState) {
// Use a specialized prompt when we're handling the 'Home' app startActivity()
@@ -389,18 +353,15 @@ public class ResolverActivity extends FragmentActivity 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());
- mLaunchedFromUid = getLaunchedFromUid();
- if (mLaunchedFromUid < 0 || UserHandle.isIsolated(mLaunchedFromUid)) {
- // Gulp!
- finish();
- return;
- }
+ // Force computation of user handle annotations in order to validate the caller ID. (See the
+ // associated TODO comment to explain why this is structured as a lazy computation.)
+ AnnotatedUserHandles unusedReferenceToHandles = mLazyAnnotatedUserHandles.get();
+
+ mWorkProfileAvailability = createWorkProfileAvailabilityManager();
mPm = getPackageManager();
@@ -490,48 +451,6 @@ public class ResolverActivity extends FragmentActivity 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());
@@ -549,7 +468,8 @@ public class ResolverActivity extends FragmentActivity implements
/* defaultSubtitleResource= */
R.string.resolver_cant_access_personal_apps_explanation,
/* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL,
- /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_RESOLVER);
+ /* devicePolicyEventCategory= */
+ ResolverActivity.METRICS_CATEGORY_RESOLVER);
final AbstractMultiProfilePagerAdapter.EmptyState noPersonalToWorkEmptyState =
new DevicePolicyBlockerEmptyState(/* context= */ this,
@@ -559,24 +479,605 @@ public class ResolverActivity extends FragmentActivity implements
/* defaultSubtitleResource= */
R.string.resolver_cant_access_work_apps_explanation,
/* devicePolicyEventId= */ RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK,
- /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_RESOLVER);
+ /* devicePolicyEventCategory= */
+ ResolverActivity.METRICS_CATEGORY_RESOLVER);
return new NoCrossProfileEmptyStateProvider(getPersonalProfileUserHandle(),
noWorkToPersonalEmptyState, noPersonalToWorkEmptyState,
createCrossProfileIntentsChecker(), createMyUserIdProvider());
}
- protected EmptyStateProvider createEmptyStateProvider(
+ protected int appliedThemeResId() {
+ return R.style.Theme_DeviceDefault_Resolver;
+ }
+
+ /**
+ * Numerous layouts are supported, each with optional ViewGroups.
+ * Make sure the inset gets added to the correct View, using
+ * a footer for Lists so it can properly scroll under the navbar.
+ */
+ protected boolean shouldAddFooterView() {
+ if (useLayoutWithDefault()) return true;
+
+ View buttonBar = findViewById(com.android.internal.R.id.button_bar);
+ if (buttonBar == null || buttonBar.getVisibility() == View.GONE) return true;
+
+ return false;
+ }
+
+ protected void applyFooterView(int height) {
+ if (mFooterSpacer == null) {
+ mFooterSpacer = new Space(getApplicationContext());
+ } else {
+ ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
+ .getActiveAdapterView().removeFooterView(mFooterSpacer);
+ }
+ mFooterSpacer.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT,
+ mSystemWindowInsets.bottom));
+ ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
+ .getActiveAdapterView().addFooterView(mFooterSpacer);
+ }
+
+ protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
+ mSystemWindowInsets = insets.getSystemWindowInsets();
+
+ mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top,
+ mSystemWindowInsets.right, 0);
+
+ resetButtonBar();
+
+ if (shouldUseMiniResolver()) {
+ View buttonContainer = findViewById(com.android.internal.R.id.button_bar_container);
+ buttonContainer.setPadding(0, 0, 0, mSystemWindowInsets.bottom
+ + getResources().getDimensionPixelOffset(R.dimen.resolver_button_bar_spacing));
+ }
+
+ // Need extra padding so the list can fully scroll up
+ if (shouldAddFooterView()) {
+ applyFooterView(mSystemWindowInsets.bottom);
+ }
+
+ return insets.consumeSystemWindowInsets();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
+ if (mIsIntentPicker && shouldShowTabs() && !useLayoutWithDefault()
+ && !shouldUseMiniResolver()) {
+ updateIntentPickerPaddings();
+ }
+
+ if (mSystemWindowInsets != null) {
+ mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top,
+ mSystemWindowInsets.right, 0);
+ }
+ }
+
+ public int getLayoutResource() {
+ return R.layout.resolver_list;
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+
+ final Window window = this.getWindow();
+ final WindowManager.LayoutParams attrs = window.getAttributes();
+ attrs.privateFlags &= ~SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+ window.setAttributes(attrs);
+
+ if (mRegistered) {
+ mPersonalPackageMonitor.unregister();
+ if (mWorkPackageMonitor != null) {
+ mWorkPackageMonitor.unregister();
+ }
+ mRegistered = false;
+ }
+ final Intent intent = getIntent();
+ if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction()
+ && !mResolvingHome && !mRetainInOnStop) {
+ // This resolver is in the unusual situation where it has been
+ // launched at the top of a new task. We don't let it be added
+ // to the recent tasks shown to the user, and we need to make sure
+ // that each time we are launched we get the correct launching
+ // uid (not re-using the same resolver from an old launching uid),
+ // so we will now finish ourself since being no longer visible,
+ // the user probably can't get back to us.
+ if (!isChangingConfigurations()) {
+ finish();
+ }
+ }
+ // TODO: should we clean up the work-profile manager before we potentially finish() above?
+ mWorkProfileAvailability.unregisterWorkProfileStateReceiver(this);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (!isChangingConfigurations() && mPickOptionRequest != null) {
+ mPickOptionRequest.cancel();
+ }
+ if (mMultiProfilePagerAdapter != null
+ && mMultiProfilePagerAdapter.getActiveListAdapter() != null) {
+ mMultiProfilePagerAdapter.getActiveListAdapter().onDestroy();
+ }
+ }
+
+ public void onButtonClick(View v) {
+ final int id = v.getId();
+ ListView listView = (ListView) mMultiProfilePagerAdapter.getActiveAdapterView();
+ ResolverListAdapter currentListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();
+ int which = currentListAdapter.hasFilteredItem()
+ ? currentListAdapter.getFilteredPosition()
+ : listView.getCheckedItemPosition();
+ boolean hasIndexBeenFiltered = !currentListAdapter.hasFilteredItem();
+ startSelected(which, id == com.android.internal.R.id.button_always, hasIndexBeenFiltered);
+ }
+
+ public void startSelected(int which, boolean always, boolean hasIndexBeenFiltered) {
+ if (isFinishing()) {
+ return;
+ }
+ ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .resolveInfoForPosition(which, hasIndexBeenFiltered);
+ if (mResolvingHome && hasManagedProfile() && !supportsManagedProfiles(ri)) {
+ Toast.makeText(this,
+ getWorkProfileNotSupportedMsg(
+ ri.activityInfo.loadLabel(getPackageManager()).toString()),
+ Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .targetInfoForPosition(which, hasIndexBeenFiltered);
+ if (target == null) {
+ return;
+ }
+ if (onTargetSelected(target, always)) {
+ if (always && mSupportsAlwaysUseOption) {
+ MetricsLogger.action(
+ this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_ALWAYS);
+ } else if (mSupportsAlwaysUseOption) {
+ MetricsLogger.action(
+ this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_JUST_ONCE);
+ } else {
+ MetricsLogger.action(
+ this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_TAP);
+ }
+ MetricsLogger.action(this,
+ mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()
+ ? MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_APP_FEATURED
+ : MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_NONE_FEATURED);
+ finish();
+ }
+ }
+
+ /**
+ * Replace me in subclasses!
+ */
+ @Override // ResolverListCommunicator
+ public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
+ return defIntent;
+ }
+
+ protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildCompleted) {
+ final ItemClickListener listener = new ItemClickListener();
+ setupAdapterListView((ListView) mMultiProfilePagerAdapter.getActiveAdapterView(), listener);
+ if (shouldShowTabs() && mIsIntentPicker) {
+ final ResolverDrawerLayout rdl = findViewById(com.android.internal.R.id.contentPanel);
+ if (rdl != null) {
+ rdl.setMaxCollapsedHeight(getResources()
+ .getDimensionPixelSize(useLayoutWithDefault()
+ ? R.dimen.resolver_max_collapsed_height_with_default_with_tabs
+ : R.dimen.resolver_max_collapsed_height_with_tabs));
+ }
+ }
+ }
+
+ protected boolean onTargetSelected(TargetInfo target, boolean always) {
+ final ResolveInfo ri = target.getResolveInfo();
+ final Intent intent = target != null ? target.getResolvedIntent() : null;
+
+ if (intent != null && (mSupportsAlwaysUseOption
+ || mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem())
+ && mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredResolveList() != null) {
+ // Build a reasonable intent filter, based on what matched.
+ IntentFilter filter = new IntentFilter();
+ Intent filterIntent;
+
+ if (intent.getSelector() != null) {
+ filterIntent = intent.getSelector();
+ } else {
+ filterIntent = intent;
+ }
+
+ String action = filterIntent.getAction();
+ if (action != null) {
+ filter.addAction(action);
+ }
+ Set<String> categories = filterIntent.getCategories();
+ if (categories != null) {
+ for (String cat : categories) {
+ filter.addCategory(cat);
+ }
+ }
+ filter.addCategory(Intent.CATEGORY_DEFAULT);
+
+ int cat = ri.match & IntentFilter.MATCH_CATEGORY_MASK;
+ Uri data = filterIntent.getData();
+ if (cat == IntentFilter.MATCH_CATEGORY_TYPE) {
+ String mimeType = filterIntent.resolveType(this);
+ if (mimeType != null) {
+ try {
+ filter.addDataType(mimeType);
+ } catch (IntentFilter.MalformedMimeTypeException e) {
+ Log.w("ResolverActivity", e);
+ filter = null;
+ }
+ }
+ }
+ if (data != null && data.getScheme() != null) {
+ // We need the data specification if there was no type,
+ // OR if the scheme is not one of our magical "file:"
+ // or "content:" schemes (see IntentFilter for the reason).
+ if (cat != IntentFilter.MATCH_CATEGORY_TYPE
+ || (!"file".equals(data.getScheme())
+ && !"content".equals(data.getScheme()))) {
+ filter.addDataScheme(data.getScheme());
+
+ // Look through the resolved filter to determine which part
+ // of it matched the original Intent.
+ Iterator<PatternMatcher> pIt = ri.filter.schemeSpecificPartsIterator();
+ if (pIt != null) {
+ String ssp = data.getSchemeSpecificPart();
+ while (ssp != null && pIt.hasNext()) {
+ PatternMatcher p = pIt.next();
+ if (p.match(ssp)) {
+ filter.addDataSchemeSpecificPart(p.getPath(), p.getType());
+ break;
+ }
+ }
+ }
+ Iterator<IntentFilter.AuthorityEntry> aIt = ri.filter.authoritiesIterator();
+ if (aIt != null) {
+ while (aIt.hasNext()) {
+ IntentFilter.AuthorityEntry a = aIt.next();
+ if (a.match(data) >= 0) {
+ int port = a.getPort();
+ filter.addDataAuthority(a.getHost(),
+ port >= 0 ? Integer.toString(port) : null);
+ break;
+ }
+ }
+ }
+ pIt = ri.filter.pathsIterator();
+ if (pIt != null) {
+ String path = data.getPath();
+ while (path != null && pIt.hasNext()) {
+ PatternMatcher p = pIt.next();
+ if (p.match(path)) {
+ filter.addDataPath(p.getPath(), p.getType());
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (filter != null) {
+ final int N = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getUnfilteredResolveList().size();
+ ComponentName[] set;
+ // If we don't add back in the component for forwarding the intent to a managed
+ // profile, the preferred activity may not be updated correctly (as the set of
+ // components we tell it we knew about will have changed).
+ final boolean needToAddBackProfileForwardingComponent =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null;
+ if (!needToAddBackProfileForwardingComponent) {
+ set = new ComponentName[N];
+ } else {
+ set = new ComponentName[N + 1];
+ }
+
+ int bestMatch = 0;
+ for (int i=0; i<N; i++) {
+ ResolveInfo r = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getUnfilteredResolveList().get(i).getResolveInfoAt(0);
+ set[i] = new ComponentName(r.activityInfo.packageName,
+ r.activityInfo.name);
+ if (r.match > bestMatch) bestMatch = r.match;
+ }
+
+ if (needToAddBackProfileForwardingComponent) {
+ set[N] = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getOtherProfile().getResolvedComponentName();
+ final int otherProfileMatch = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getOtherProfile().getResolveInfo().match;
+ if (otherProfileMatch > bestMatch) bestMatch = otherProfileMatch;
+ }
+
+ if (always) {
+ final int userId = getUserId();
+ final PackageManager pm = getPackageManager();
+
+ // Set the preferred Activity
+ pm.addUniquePreferredActivity(filter, bestMatch, set, intent.getComponent());
+
+ if (ri.handleAllWebDataURI) {
+ // Set default Browser if needed
+ final String packageName = pm.getDefaultBrowserPackageNameAsUser(userId);
+ if (TextUtils.isEmpty(packageName)) {
+ pm.setDefaultBrowserPackageNameAsUser(ri.activityInfo.packageName, userId);
+ }
+ }
+ } else {
+ try {
+ mMultiProfilePagerAdapter.getActiveListAdapter()
+ .mResolverListController.setLastChosen(intent, filter, bestMatch);
+ } catch (RemoteException re) {
+ Log.d(TAG, "Error calling setLastChosenActivity\n" + re);
+ }
+ }
+ }
+ }
+
+ if (target != null) {
+ safelyStartActivity(target);
+
+ // Rely on the ActivityManager to pop up a dialog regarding app suspension
+ // and return false
+ if (target.isSuspended()) {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public void onActivityStarted(TargetInfo cti) {
+ // Do nothing
+ }
+
+ @Override // ResolverListCommunicator
+ public boolean shouldGetActivityMetadata() {
+ return false;
+ }
+
+ public boolean shouldAutoLaunchSingleChoice(TargetInfo target) {
+ return !target.isSuspended();
+ }
+
+ // TODO: this method takes an unused `UserHandle` because the override in `ChooserActivity` uses
+ // that data to set up other components as dependencies of the controller. In reality, these
+ // methods don't require polymorphism, because they're only invoked from within their respective
+ // concrete class; `ResolverActivity` will never call this method expecting to get a
+ // `ChooserListController` (subclass) result, because `ResolverActivity` only invokes this
+ // method as part of handling `createMultiProfilePagerAdapter()`, which is itself overridden in
+ // `ChooserActivity`. A future refactoring could better express the coupling between the adapter
+ // and controller types; in the meantime, structuring as an override (with matching signatures)
+ // shows that these methods are *structurally* related, and helps to prevent any regressions in
+ // the future if resolver *were* to make any (non-overridden) calls to a version that used a
+ // different signature (and thus didn't return the subclass type).
+ @VisibleForTesting
+ protected ResolverListController createListController(UserHandle unused) {
+ return new ResolverListController(
+ this,
+ mPm,
+ getTargetIntent(),
+ getReferrerPackageName(),
+ getAnnotatedUserHandles().userIdOfCallingApp);
+ }
+
+ /**
+ * Finishing procedures to be performed after the list has been rebuilt.
+ * </p>Subclasses must call postRebuildListInternal at the end of postRebuildList.
+ * @param rebuildCompleted
+ * @return <code>true</code> if the activity is finishing and creation should halt.
+ */
+ protected boolean postRebuildList(boolean rebuildCompleted) {
+ return postRebuildListInternal(rebuildCompleted);
+ }
+
+ void onHorizontalSwipeStateChanged(int state) {}
+
+ /**
+ * Callback called when user changes the profile tab.
+ * <p>This method is intended to be overridden by subclasses.
+ */
+ protected void onProfileTabSelected() { }
+
+ /**
+ * Add a label to signify that the user can pick a different app.
+ * @param adapter The adapter used to provide data to item views.
+ */
+ public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) {
+ final boolean useHeader = adapter.hasFilteredItem();
+ if (useHeader) {
+ FrameLayout stub = findViewById(com.android.internal.R.id.stub);
+ stub.setVisibility(View.VISIBLE);
+ TextView textView = (TextView) LayoutInflater.from(this).inflate(
+ R.layout.resolver_different_item_header, null, false);
+ if (shouldShowTabs()) {
+ textView.setGravity(Gravity.CENTER);
+ }
+ stub.addView(textView);
+ }
+ }
+
+ protected void resetButtonBar() {
+ if (!mSupportsAlwaysUseOption) {
+ return;
+ }
+ final ViewGroup buttonLayout = findViewById(com.android.internal.R.id.button_bar);
+ if (buttonLayout == null) {
+ Log.e(TAG, "Layout unexpectedly does not have a button bar");
+ return;
+ }
+ ResolverListAdapter activeListAdapter =
+ mMultiProfilePagerAdapter.getActiveListAdapter();
+ View buttonBarDivider = findViewById(com.android.internal.R.id.resolver_button_bar_divider);
+ if (!useLayoutWithDefault()) {
+ int inset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0;
+ buttonLayout.setPadding(buttonLayout.getPaddingLeft(), buttonLayout.getPaddingTop(),
+ buttonLayout.getPaddingRight(), getResources().getDimensionPixelSize(
+ R.dimen.resolver_button_bar_spacing) + inset);
+ }
+ if (activeListAdapter.isTabLoaded()
+ && mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)
+ && !useLayoutWithDefault()) {
+ buttonLayout.setVisibility(View.INVISIBLE);
+ if (buttonBarDivider != null) {
+ buttonBarDivider.setVisibility(View.INVISIBLE);
+ }
+ setButtonBarIgnoreOffset(/* ignoreOffset */ false);
+ return;
+ }
+ if (buttonBarDivider != null) {
+ buttonBarDivider.setVisibility(View.VISIBLE);
+ }
+ buttonLayout.setVisibility(View.VISIBLE);
+ setButtonBarIgnoreOffset(/* ignoreOffset */ true);
+
+ mOnceButton = (Button) buttonLayout.findViewById(com.android.internal.R.id.button_once);
+ mAlwaysButton = (Button) buttonLayout.findViewById(com.android.internal.R.id.button_always);
+
+ resetAlwaysOrOnceButtonBar();
+ }
+
+ protected String getMetricsCategory() {
+ return METRICS_CATEGORY_RESOLVER;
+ }
+
+ @Override // ResolverListCommunicator
+ public void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
+ if (listAdapter == mMultiProfilePagerAdapter.getActiveListAdapter()) {
+ if (listAdapter.getUserHandle().equals(getWorkProfileUserHandle())
+ && mWorkProfileAvailability.isWaitingToEnableWorkProfile()) {
+ // We have just turned on the work profile and entered the pass code to start it,
+ // now we are waiting to receive the ACTION_USER_UNLOCKED broadcast. There is no
+ // point in reloading the list now, since the work profile user is still
+ // turning on.
+ return;
+ }
+ boolean listRebuilt = mMultiProfilePagerAdapter.rebuildActiveTab(true);
+ if (listRebuilt) {
+ ResolverListAdapter activeListAdapter =
+ mMultiProfilePagerAdapter.getActiveListAdapter();
+ activeListAdapter.notifyDataSetChanged();
+ if (activeListAdapter.getCount() == 0 && !inactiveListAdapterHasItems()) {
+ // We no longer have any items... just finish the activity.
+ finish();
+ }
+ }
+ } else {
+ mMultiProfilePagerAdapter.clearInactiveProfileCache();
+ }
+ }
+
+ protected void maybeLogProfileChange() {}
+
+ // @NonFinalForTesting
+ @VisibleForTesting
+ protected MyUserIdProvider createMyUserIdProvider() {
+ return new MyUserIdProvider();
+ }
+
+ // @NonFinalForTesting
+ @VisibleForTesting
+ protected CrossProfileIntentsChecker createCrossProfileIntentsChecker() {
+ return new CrossProfileIntentsChecker(getContentResolver());
+ }
+
+ // @NonFinalForTesting
+ @VisibleForTesting
+ protected WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() {
+ final UserHandle workUser = getWorkProfileUserHandle();
+
+ return new WorkProfileAvailabilityManager(
+ getSystemService(UserManager.class),
+ workUser,
+ () -> {
+ if (mMultiProfilePagerAdapter.getCurrentUserHandle().equals(workUser)) {
+ mMultiProfilePagerAdapter.rebuildActiveTab(true);
+ } else {
+ mMultiProfilePagerAdapter.clearInactiveProfileCache();
+ }
+ });
+ }
+
+ // TODO: have tests override `getAnnotatedUserHandles()`, and make this method `final`.
+ // @NonFinalForTesting
+ @Nullable
+ protected UserHandle getWorkProfileUserHandle() {
+ return getAnnotatedUserHandles().workProfileUserHandle;
+ }
+
+ // @NonFinalForTesting
+ @VisibleForTesting
+ public void safelyStartActivity(TargetInfo cti) {
+ // We're dispatching intents that might be coming from legacy apps, so
+ // don't kill ourselves.
+ StrictMode.disableDeathOnFileUriExposure();
+ try {
+ UserHandle currentUserHandle = mMultiProfilePagerAdapter.getCurrentUserHandle();
+ safelyStartActivityInternal(cti, currentUserHandle, null);
+ } finally {
+ StrictMode.enableDeathOnFileUriExposure();
+ }
+ }
+
+ // @NonFinalForTesting
+ @VisibleForTesting
+ protected ResolverListAdapter createResolverListAdapter(Context context,
+ List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList,
+ boolean filterLastUsed, UserHandle userHandle) {
+ Intent startIntent = getIntent();
+ boolean isAudioCaptureDevice =
+ startIntent.getBooleanExtra(EXTRA_IS_AUDIO_CAPTURE_DEVICE, false);
+ return new ResolverListAdapter(
+ context,
+ payloadIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ createListController(userHandle),
+ userHandle,
+ getTargetIntent(),
+ this,
+ isAudioCaptureDevice);
+ }
+
+ private LatencyTracker getLatencyTracker() {
+ return LatencyTracker.getInstance(this);
+ }
+
+ /**
+ * Get the string resource to be used as a label for the link to the resolver activity for an
+ * action.
+ *
+ * @param action The action to resolve
+ *
+ * @return The string resource to be used as a label
+ */
+ public static @StringRes int getLabelRes(String action) {
+ return ActionTitle.forAction(action).labelRes;
+ }
+
+ protected final EmptyStateProvider createEmptyStateProvider(
@Nullable UserHandle workProfileUserHandle) {
final EmptyStateProvider blockerEmptyStateProvider = createBlockerEmptyStateProvider();
final EmptyStateProvider workProfileOffEmptyStateProvider =
new WorkProfilePausedEmptyStateProvider(this, workProfileUserHandle,
- mQuietModeManager,
+ mWorkProfileAvailability,
/* onSwitchOnWorkSelectedListener= */
- () -> { if (mOnSwitchOnWorkSelectedListener != null) {
- mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected();
- }},
+ () -> {
+ if (mOnSwitchOnWorkSelectedListener != null) {
+ mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected();
+ }
+ },
getMetricsCategory());
final EmptyStateProvider noAppsEmptyStateProvider = new NoAppsAvailableEmptyStateProvider(
@@ -595,9 +1096,32 @@ public class ResolverActivity extends FragmentActivity implements
);
}
- private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForOneProfile(
- Intent[] initialIntents,
- List<ResolveInfo> rList, boolean filterLastUsed) {
+ private Intent makeMyIntent() {
+ Intent intent = new Intent(getIntent());
+ intent.setComponent(null);
+ // The resolver activity is set to be hidden from recent tasks.
+ // we don't want this attribute to be propagated to the next activity
+ // being launched. Note that if the original Intent also had this
+ // flag set, we are now losing it. That should be a very rare case
+ // and we can live with this.
+ intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+ return intent;
+ }
+
+ /**
+ * Call {@link Activity#onCreate} without initializing anything further. This should
+ * only be used when the activity is about to be immediately finished to avoid wasting
+ * initializing steps and leaking resources.
+ */
+ protected final void super_onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ }
+
+ private ResolverMultiProfilePagerAdapter
+ createResolverMultiProfilePagerAdapterForOneProfile(
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed) {
ResolverListAdapter adapter = createResolverListAdapter(
/* context */ this,
/* payloadIntents */ mIntents,
@@ -605,12 +1129,11 @@ public class ResolverActivity extends FragmentActivity implements
rList,
filterLastUsed,
/* userHandle */ UserHandle.of(UserHandle.myUserId()));
- QuietModeManager quietModeManager = createQuietModeManager();
return new ResolverMultiProfilePagerAdapter(
/* context */ this,
adapter,
createEmptyStateProvider(/* workProfileUserHandle= */ null),
- quietModeManager,
+ /* workProfileQuietModeChecker= */ () -> false,
/* workProfileUserHandle= */ null);
}
@@ -661,28 +1184,23 @@ public class ResolverActivity extends FragmentActivity implements
(filterLastUsed && UserHandle.myUserId()
== workProfileUserHandle.getIdentifier()),
/* userHandle */ workProfileUserHandle);
- QuietModeManager quietModeManager = createQuietModeManager();
return new ResolverMultiProfilePagerAdapter(
/* context */ this,
personalAdapter,
workAdapter,
createEmptyStateProvider(getWorkProfileUserHandle()),
- quietModeManager,
+ () -> mWorkProfileAvailability.isQuietModeEnabled(),
selectedProfile,
getWorkProfileUserHandle());
}
- protected int appliedThemeResId() {
- return R.style.Theme_DeviceDefault_Resolver;
- }
-
/**
* Returns {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK} if the {@link
* #EXTRA_SELECTED_PROFILE} extra was supplied, or {@code -1} if no extra was supplied.
* @throws IllegalArgumentException if the value passed to the {@link #EXTRA_SELECTED_PROFILE}
* extra is not {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}
*/
- int getSelectedProfileExtra() {
+ final int getSelectedProfileExtra() {
int selectedProfile = -1;
if (getIntent().hasExtra(EXTRA_SELECTED_PROFILE)) {
selectedProfile = getIntent().getIntExtra(EXTRA_SELECTED_PROFILE, /* defValue = */ -1);
@@ -695,43 +1213,27 @@ public class ResolverActivity extends FragmentActivity implements
return selectedProfile;
}
- protected @Profile int getCurrentProfile() {
+ protected final @Profile int getCurrentProfile() {
return (UserHandle.myUserId() == UserHandle.USER_SYSTEM ? PROFILE_PERSONAL : PROFILE_WORK);
}
- protected UserHandle getPersonalProfileUserHandle() {
- return UserHandle.of(ActivityManager.getCurrentUser());
+ protected final AnnotatedUserHandles getAnnotatedUserHandles() {
+ return mLazyAnnotatedUserHandles.get();
}
- @Nullable
- protected UserHandle getWorkProfileUserHandle() {
- return mLazyWorkProfileUserHandle.get();
- }
-
- @Nullable
- private UserHandle fetchWorkProfileUserProfile() {
- UserManager userManager = getSystemService(UserManager.class);
- if (userManager == null) {
- return null;
- }
- UserHandle result = null;
- for (final UserInfo userInfo : userManager.getProfiles(ActivityManager.getCurrentUser())) {
- if (userInfo.isManagedProfile()) {
- result = userInfo.getUserHandle();
- }
- }
- return result;
+ protected final UserHandle getPersonalProfileUserHandle() {
+ return getAnnotatedUserHandles().personalProfileUserHandle;
}
private boolean hasWorkProfile() {
return getWorkProfileUserHandle() != null;
}
- protected boolean shouldShowTabs() {
+ protected final boolean shouldShowTabs() {
return hasWorkProfile();
}
- protected void onProfileClick(View v) {
+ protected final void onProfileClick(View v) {
final DisplayResolveInfo dri =
mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile();
if (dri == null) {
@@ -745,70 +1247,6 @@ public class ResolverActivity extends FragmentActivity implements
finish();
}
- /**
- * Numerous layouts are supported, each with optional ViewGroups.
- * Make sure the inset gets added to the correct View, using
- * a footer for Lists so it can properly scroll under the navbar.
- */
- protected boolean shouldAddFooterView() {
- if (useLayoutWithDefault()) return true;
-
- View buttonBar = findViewById(com.android.internal.R.id.button_bar);
- if (buttonBar == null || buttonBar.getVisibility() == View.GONE) return true;
-
- return false;
- }
-
- protected void applyFooterView(int height) {
- if (mFooterSpacer == null) {
- mFooterSpacer = new Space(getApplicationContext());
- } else {
- ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
- .getActiveAdapterView().removeFooterView(mFooterSpacer);
- }
- mFooterSpacer.setLayoutParams(new AbsListView.LayoutParams(LayoutParams.MATCH_PARENT,
- mSystemWindowInsets.bottom));
- ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
- .getActiveAdapterView().addFooterView(mFooterSpacer);
- }
-
- protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
- mSystemWindowInsets = insets.getSystemWindowInsets();
-
- mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top,
- mSystemWindowInsets.right, 0);
-
- resetButtonBar();
-
- if (shouldUseMiniResolver()) {
- View buttonContainer = findViewById(com.android.internal.R.id.button_bar_container);
- buttonContainer.setPadding(0, 0, 0, mSystemWindowInsets.bottom
- + getResources().getDimensionPixelOffset(R.dimen.resolver_button_bar_spacing));
- }
-
- // Need extra padding so the list can fully scroll up
- if (shouldAddFooterView()) {
- applyFooterView(mSystemWindowInsets.bottom);
- }
-
- return insets.consumeSystemWindowInsets();
- }
-
- @Override
- public void onConfigurationChanged(Configuration newConfig) {
- super.onConfigurationChanged(newConfig);
- mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
- if (mIsIntentPicker && shouldShowTabs() && !useLayoutWithDefault()
- && !shouldUseMiniResolver()) {
- updateIntentPickerPaddings();
- }
-
- if (mSystemWindowInsets != null) {
- mResolverDrawerLayout.setPadding(mSystemWindowInsets.left, mSystemWindowInsets.top,
- mSystemWindowInsets.right, 0);
- }
- }
-
private void updateIntentPickerPaddings() {
View titleCont = findViewById(com.android.internal.R.id.title_container);
titleCont.setPadding(
@@ -824,8 +1262,20 @@ public class ResolverActivity extends FragmentActivity implements
getResources().getDimensionPixelSize(R.dimen.resolver_button_bar_spacing));
}
+ private void maybeLogCrossProfileTargetLaunch(TargetInfo cti, UserHandle currentUserHandle) {
+ if (!hasWorkProfile() || currentUserHandle.equals(getUser())) {
+ return;
+ }
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_CROSS_PROFILE_TARGET_OPENED)
+ .setBoolean(currentUserHandle.equals(getPersonalProfileUserHandle()))
+ .setStrings(getMetricsCategory(),
+ cti.isInDirectShareMetricsCategory() ? "direct_share" : "other_target")
+ .write();
+ }
+
@Override // ResolverListCommunicator
- public void sendVoiceChoicesIfNeeded() {
+ public final void sendVoiceChoicesIfNeeded() {
if (!isVoiceInteraction()) {
// Clearly not needed.
return;
@@ -833,7 +1283,7 @@ public class ResolverActivity extends FragmentActivity implements
int count = mMultiProfilePagerAdapter.getActiveListAdapter().getCount();
final Option[] options = new Option[count];
- for (int i = 0, N = options.length; i < N; i++) {
+ for (int i = 0; i < options.length; i++) {
TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter().getItem(i);
if (target == null) {
// If this occurs, a new set of targets is being loaded. Let that complete,
@@ -848,7 +1298,7 @@ public class ResolverActivity extends FragmentActivity implements
getVoiceInteractor().submitRequest(mPickOptionRequest);
}
- Option optionForChooserTarget(TargetInfo target, int index) {
+ final Option optionForChooserTarget(TargetInfo target, int index) {
return new Option(target.getDisplayLabel(), index);
}
@@ -860,11 +1310,11 @@ public class ResolverActivity extends FragmentActivity implements
}
}
- public Intent getTargetIntent() {
+ public final Intent getTargetIntent() {
return mIntents.isEmpty() ? null : mIntents.get(0);
}
- protected String getReferrerPackageName() {
+ protected final String getReferrerPackageName() {
final Uri referrer = getReferrer();
if (referrer != null && "android-app".equals(referrer.getScheme())) {
return referrer.getHost();
@@ -872,12 +1322,8 @@ public class ResolverActivity extends FragmentActivity implements
return null;
}
- public int getLayoutResource() {
- return R.layout.resolver_list;
- }
-
@Override // ResolverListCommunicator
- public void updateProfileViewButton() {
+ public final void updateProfileViewButton() {
if (mProfileView == null) {
return;
}
@@ -897,8 +1343,8 @@ public class ResolverActivity extends FragmentActivity implements
}
private void setProfileSwitchMessage(int contentUserHint) {
- if (contentUserHint != UserHandle.USER_CURRENT &&
- contentUserHint != UserHandle.myUserId()) {
+ if ((contentUserHint != UserHandle.USER_CURRENT)
+ && (contentUserHint != UserHandle.myUserId())) {
UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
UserInfo originUserInfo = userManager.getUserInfo(contentUserHint);
boolean originIsManaged = originUserInfo != null ? originUserInfo.isManagedProfile()
@@ -936,11 +1382,11 @@ public class ResolverActivity extends FragmentActivity implements
* more detailed onCreate methods, so that it will be set correctly in the case where
* there is only one intent to resolve and it is thus started immediately.</p>
*/
- public void setSafeForwardingMode(boolean safeForwarding) {
+ public final void setSafeForwardingMode(boolean safeForwarding) {
mSafeForwardingMode = safeForwarding;
}
- protected CharSequence getTitleForAction(Intent intent, int defaultTitleRes) {
+ protected final CharSequence getTitleForAction(Intent intent, int defaultTitleRes) {
final ActionTitle title = mResolvingHome
? ActionTitle.HOME
: ActionTitle.forAction(intent.getAction());
@@ -959,14 +1405,14 @@ public class ResolverActivity extends FragmentActivity implements
}
}
- void dismiss() {
+ final void dismiss() {
if (!isFinishing()) {
finish();
}
}
@Override
- protected void onRestart() {
+ protected final void onRestart() {
super.onRestart();
if (!mRegistered) {
mPersonalPackageMonitor.register(this, getMainLooper(),
@@ -981,9 +1427,9 @@ public class ResolverActivity extends FragmentActivity implements
}
mRegistered = true;
}
- if (shouldShowTabs() && mQuietModeManager.isWaitingToEnableWorkProfile()) {
- if (mQuietModeManager.isQuietModeEnabled(getWorkProfileUserHandle())) {
- mQuietModeManager.markWorkProfileEnabledBroadcastReceived();
+ if (shouldShowTabs() && mWorkProfileAvailability.isWaitingToEnableWorkProfile()) {
+ if (mWorkProfileAvailability.isQuietModeEnabled()) {
+ mWorkProfileAvailability.markWorkProfileEnabledBroadcastReceived();
}
}
mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
@@ -991,84 +1437,17 @@ public class ResolverActivity extends FragmentActivity implements
}
@Override
- protected void onStart() {
+ protected final void onStart() {
super.onStart();
this.getWindow().addSystemFlags(SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS);
if (shouldShowTabs()) {
- mWorkProfileStateReceiver = createWorkProfileStateReceiver();
- registerWorkProfileStateReceiver();
-
- mWorkProfileHasBeenEnabled = isWorkProfileEnabled();
- }
- }
-
- private boolean isWorkProfileEnabled() {
- UserHandle workUserHandle = getWorkProfileUserHandle();
- UserManager userManager = getSystemService(UserManager.class);
-
- return !userManager.isQuietModeEnabled(workUserHandle)
- && userManager.isUserUnlocked(workUserHandle);
- }
-
- private void registerWorkProfileStateReceiver() {
- IntentFilter filter = new IntentFilter();
- filter.addAction(Intent.ACTION_USER_UNLOCKED);
- filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
- filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
- registerReceiverAsUser(mWorkProfileStateReceiver, UserHandle.ALL, filter, null, null);
- }
-
- @Override
- protected void onStop() {
- super.onStop();
-
- final Window window = this.getWindow();
- final WindowManager.LayoutParams attrs = window.getAttributes();
- attrs.privateFlags &= ~SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
- window.setAttributes(attrs);
-
- if (mRegistered) {
- mPersonalPackageMonitor.unregister();
- if (mWorkPackageMonitor != null) {
- mWorkPackageMonitor.unregister();
- }
- mRegistered = false;
- }
- final Intent intent = getIntent();
- if ((intent.getFlags() & FLAG_ACTIVITY_NEW_TASK) != 0 && !isVoiceInteraction()
- && !mResolvingHome && !mRetainInOnStop) {
- // This resolver is in the unusual situation where it has been
- // launched at the top of a new task. We don't let it be added
- // to the recent tasks shown to the user, and we need to make sure
- // that each time we are launched we get the correct launching
- // uid (not re-using the same resolver from an old launching uid),
- // so we will now finish ourself since being no longer visible,
- // the user probably can't get back to us.
- if (!isChangingConfigurations()) {
- finish();
- }
- }
- if (mWorkPackageMonitor != null) {
- unregisterReceiver(mWorkProfileStateReceiver);
- mWorkPackageMonitor = null;
+ mWorkProfileAvailability.registerWorkProfileStateReceiver(this);
}
}
@Override
- protected void onDestroy() {
- super.onDestroy();
- if (!isChangingConfigurations() && mPickOptionRequest != null) {
- mPickOptionRequest.cancel();
- }
- if (mMultiProfilePagerAdapter != null
- && mMultiProfilePagerAdapter.getActiveListAdapter() != null) {
- mMultiProfilePagerAdapter.getActiveListAdapter().onDestroy();
- }
- }
-
- @Override
- protected void onSaveInstanceState(Bundle outState) {
+ protected final void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
if (viewPager != null) {
@@ -1077,7 +1456,7 @@ public class ResolverActivity extends FragmentActivity implements
}
@Override
- protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ protected final void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
resetButtonBar();
ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
@@ -1161,55 +1540,6 @@ public class ResolverActivity extends FragmentActivity implements
mAlwaysButton.setEnabled(enabled);
}
- public void onButtonClick(View v) {
- final int id = v.getId();
- ListView listView = (ListView) mMultiProfilePagerAdapter.getActiveAdapterView();
- ResolverListAdapter currentListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();
- int which = currentListAdapter.hasFilteredItem()
- ? currentListAdapter.getFilteredPosition()
- : listView.getCheckedItemPosition();
- boolean hasIndexBeenFiltered = !currentListAdapter.hasFilteredItem();
- startSelected(which, id == com.android.internal.R.id.button_always, hasIndexBeenFiltered);
- }
-
- public void startSelected(int which, boolean always, boolean hasIndexBeenFiltered) {
- if (isFinishing()) {
- return;
- }
- ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter()
- .resolveInfoForPosition(which, hasIndexBeenFiltered);
- if (mResolvingHome && hasManagedProfile() && !supportsManagedProfiles(ri)) {
- Toast.makeText(this,
- getWorkProfileNotSupportedMsg(
- ri.activityInfo.loadLabel(getPackageManager()).toString()),
- Toast.LENGTH_LONG).show();
- return;
- }
-
- TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter()
- .targetInfoForPosition(which, hasIndexBeenFiltered);
- if (target == null) {
- return;
- }
- if (onTargetSelected(target, always)) {
- if (always && mSupportsAlwaysUseOption) {
- MetricsLogger.action(
- this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_ALWAYS);
- } else if (mSupportsAlwaysUseOption) {
- MetricsLogger.action(
- this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_JUST_ONCE);
- } else {
- MetricsLogger.action(
- this, MetricsProto.MetricsEvent.ACTION_APP_DISAMBIG_TAP);
- }
- MetricsLogger.action(this,
- mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()
- ? MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_APP_FEATURED
- : MetricsProto.MetricsEvent.ACTION_HIDE_APP_DISAMBIG_NONE_FEATURED);
- finish();
- }
- }
-
private String getWorkProfileNotSupportedMsg(String launcherName) {
return getSystemService(DevicePolicyManager.class).getResources().getString(
RESOLVER_WORK_PROFILE_NOT_SUPPORTED,
@@ -1219,14 +1549,6 @@ public class ResolverActivity extends FragmentActivity implements
launcherName);
}
- /**
- * Replace me in subclasses!
- */
- @Override // ResolverListCommunicator
- public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
- return defIntent;
- }
-
@Override // ResolverListCommunicator
public final void onPostListReady(ResolverListAdapter listAdapter, boolean doPostProcessing,
boolean rebuildCompleted) {
@@ -1254,204 +1576,17 @@ public class ResolverActivity extends FragmentActivity implements
}
}
- protected void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildCompleted) {
- final ItemClickListener listener = new ItemClickListener();
- setupAdapterListView((ListView) mMultiProfilePagerAdapter.getActiveAdapterView(), listener);
- if (shouldShowTabs() && mIsIntentPicker) {
- final ResolverDrawerLayout rdl = findViewById(com.android.internal.R.id.contentPanel);
- if (rdl != null) {
- rdl.setMaxCollapsedHeight(getResources()
- .getDimensionPixelSize(useLayoutWithDefault()
- ? R.dimen.resolver_max_collapsed_height_with_default_with_tabs
- : R.dimen.resolver_max_collapsed_height_with_tabs));
- }
- }
- }
-
- protected boolean onTargetSelected(TargetInfo target, boolean always) {
- final ResolveInfo ri = target.getResolveInfo();
- final Intent intent = target != null ? target.getResolvedIntent() : null;
-
- if (intent != null && (mSupportsAlwaysUseOption
- || mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem())
- && mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredResolveList() != null) {
- // Build a reasonable intent filter, based on what matched.
- IntentFilter filter = new IntentFilter();
- Intent filterIntent;
-
- if (intent.getSelector() != null) {
- filterIntent = intent.getSelector();
- } else {
- filterIntent = intent;
- }
-
- String action = filterIntent.getAction();
- if (action != null) {
- filter.addAction(action);
- }
- Set<String> categories = filterIntent.getCategories();
- if (categories != null) {
- for (String cat : categories) {
- filter.addCategory(cat);
- }
- }
- filter.addCategory(Intent.CATEGORY_DEFAULT);
-
- int cat = ri.match & IntentFilter.MATCH_CATEGORY_MASK;
- Uri data = filterIntent.getData();
- if (cat == IntentFilter.MATCH_CATEGORY_TYPE) {
- String mimeType = filterIntent.resolveType(this);
- if (mimeType != null) {
- try {
- filter.addDataType(mimeType);
- } catch (IntentFilter.MalformedMimeTypeException e) {
- Log.w("ResolverActivity", e);
- filter = null;
- }
- }
- }
- if (data != null && data.getScheme() != null) {
- // We need the data specification if there was no type,
- // OR if the scheme is not one of our magical "file:"
- // or "content:" schemes (see IntentFilter for the reason).
- if (cat != IntentFilter.MATCH_CATEGORY_TYPE
- || (!"file".equals(data.getScheme())
- && !"content".equals(data.getScheme()))) {
- filter.addDataScheme(data.getScheme());
-
- // Look through the resolved filter to determine which part
- // of it matched the original Intent.
- Iterator<PatternMatcher> pIt = ri.filter.schemeSpecificPartsIterator();
- if (pIt != null) {
- String ssp = data.getSchemeSpecificPart();
- while (ssp != null && pIt.hasNext()) {
- PatternMatcher p = pIt.next();
- if (p.match(ssp)) {
- filter.addDataSchemeSpecificPart(p.getPath(), p.getType());
- break;
- }
- }
- }
- Iterator<IntentFilter.AuthorityEntry> aIt = ri.filter.authoritiesIterator();
- if (aIt != null) {
- while (aIt.hasNext()) {
- IntentFilter.AuthorityEntry a = aIt.next();
- if (a.match(data) >= 0) {
- int port = a.getPort();
- filter.addDataAuthority(a.getHost(),
- port >= 0 ? Integer.toString(port) : null);
- break;
- }
- }
- }
- pIt = ri.filter.pathsIterator();
- if (pIt != null) {
- String path = data.getPath();
- while (path != null && pIt.hasNext()) {
- PatternMatcher p = pIt.next();
- if (p.match(path)) {
- filter.addDataPath(p.getPath(), p.getType());
- break;
- }
- }
- }
- }
- }
-
- if (filter != null) {
- final int N = mMultiProfilePagerAdapter.getActiveListAdapter()
- .getUnfilteredResolveList().size();
- ComponentName[] set;
- // If we don't add back in the component for forwarding the intent to a managed
- // profile, the preferred activity may not be updated correctly (as the set of
- // components we tell it we knew about will have changed).
- final boolean needToAddBackProfileForwardingComponent =
- mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null;
- if (!needToAddBackProfileForwardingComponent) {
- set = new ComponentName[N];
- } else {
- set = new ComponentName[N + 1];
- }
-
- int bestMatch = 0;
- for (int i=0; i<N; i++) {
- ResolveInfo r = mMultiProfilePagerAdapter.getActiveListAdapter()
- .getUnfilteredResolveList().get(i).getResolveInfoAt(0);
- set[i] = new ComponentName(r.activityInfo.packageName,
- r.activityInfo.name);
- if (r.match > bestMatch) bestMatch = r.match;
- }
-
- if (needToAddBackProfileForwardingComponent) {
- set[N] = mMultiProfilePagerAdapter.getActiveListAdapter()
- .getOtherProfile().getResolvedComponentName();
- final int otherProfileMatch = mMultiProfilePagerAdapter.getActiveListAdapter()
- .getOtherProfile().getResolveInfo().match;
- if (otherProfileMatch > bestMatch) bestMatch = otherProfileMatch;
- }
-
- if (always) {
- final int userId = getUserId();
- final PackageManager pm = getPackageManager();
-
- // Set the preferred Activity
- pm.addUniquePreferredActivity(filter, bestMatch, set, intent.getComponent());
-
- if (ri.handleAllWebDataURI) {
- // Set default Browser if needed
- final String packageName = pm.getDefaultBrowserPackageNameAsUser(userId);
- if (TextUtils.isEmpty(packageName)) {
- pm.setDefaultBrowserPackageNameAsUser(ri.activityInfo.packageName, userId);
- }
- }
- } else {
- try {
- mMultiProfilePagerAdapter.getActiveListAdapter()
- .mResolverListController.setLastChosen(intent, filter, bestMatch);
- } catch (RemoteException re) {
- Log.d(TAG, "Error calling setLastChosenActivity\n" + re);
- }
- }
- }
- }
-
- if (target != null) {
- safelyStartActivity(target);
-
- // Rely on the ActivityManager to pop up a dialog regarding app suspension
- // and return false
- if (target.isSuspended()) {
- return false;
- }
- }
-
- return true;
- }
-
- @VisibleForTesting
- public void safelyStartActivity(TargetInfo cti) {
- // We're dispatching intents that might be coming from legacy apps, so
- // don't kill ourselves.
- StrictMode.disableDeathOnFileUriExposure();
- try {
- UserHandle currentUserHandle = mMultiProfilePagerAdapter.getCurrentUserHandle();
- safelyStartActivityInternal(cti, currentUserHandle, null);
- } finally {
- StrictMode.enableDeathOnFileUriExposure();
- }
- }
-
/**
* Start activity as a fixed user handle.
* @param cti TargetInfo to be launched.
* @param user User to launch this activity as.
*/
- @VisibleForTesting
- public void safelyStartActivityAsUser(TargetInfo cti, UserHandle user) {
+ @VisibleForTesting(visibility = VisibleForTesting.Visibility.PROTECTED)
+ public final void safelyStartActivityAsUser(TargetInfo cti, UserHandle user) {
safelyStartActivityAsUser(cti, user, null);
}
- protected void safelyStartActivityAsUser(
+ protected final void safelyStartActivityAsUser(
TargetInfo cti, UserHandle user, @Nullable Bundle options) {
// We're dispatching intents that might be coming from legacy apps, so
// don't kill ourselves.
@@ -1494,76 +1629,20 @@ public class ResolverActivity extends FragmentActivity implements
maybeLogCrossProfileTargetLaunch(cti, user);
}
} catch (RuntimeException e) {
- Slog.wtf(TAG, "Unable to launch as uid " + mLaunchedFromUid
+ Slog.wtf(TAG,
+ "Unable to launch as uid " + getAnnotatedUserHandles().userIdOfCallingApp
+ " package " + getLaunchedFromPackage() + ", while running in "
+ ActivityThread.currentProcessName(), e);
}
}
- private void maybeLogCrossProfileTargetLaunch(TargetInfo cti, UserHandle currentUserHandle) {
- if (!hasWorkProfile() || currentUserHandle.equals(getUser())) {
- return;
- }
- DevicePolicyEventLogger
- .createEvent(DevicePolicyEnums.RESOLVER_CROSS_PROFILE_TARGET_OPENED)
- .setBoolean(currentUserHandle.equals(getPersonalProfileUserHandle()))
- .setStrings(getMetricsCategory(),
- cti.isInDirectShareMetricsCategory() ? "direct_share" : "other_target")
- .write();
- }
-
-
- public void onActivityStarted(TargetInfo cti) {
- // Do nothing
- }
-
- @Override // ResolverListCommunicator
- public boolean shouldGetActivityMetadata() {
- return false;
- }
-
- public boolean shouldAutoLaunchSingleChoice(TargetInfo target) {
- return !target.isSuspended();
- }
-
- void showTargetDetails(ResolveInfo ri) {
+ final void showTargetDetails(ResolveInfo ri) {
Intent in = new Intent().setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
.setData(Uri.fromParts("package", ri.activityInfo.packageName, null))
.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
startActivityAsUser(in, mMultiProfilePagerAdapter.getCurrentUserHandle());
}
- @VisibleForTesting
- protected ResolverListAdapter createResolverListAdapter(Context context,
- List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList,
- boolean filterLastUsed, UserHandle userHandle) {
- Intent startIntent = getIntent();
- boolean isAudioCaptureDevice =
- startIntent.getBooleanExtra(EXTRA_IS_AUDIO_CAPTURE_DEVICE, false);
- return new ResolverListAdapter(
- context,
- payloadIntents,
- initialIntents,
- rList,
- filterLastUsed,
- createListController(userHandle),
- userHandle,
- getTargetIntent(),
- this,
- isAudioCaptureDevice);
- }
-
- @VisibleForTesting
- protected ResolverListController createListController(UserHandle userHandle) {
- return new ResolverListController(
- this,
- mPm,
- getTargetIntent(),
- getReferrerPackageName(),
- mLaunchedFromUid,
- userHandle);
- }
-
/**
* Sets up the content view.
* @return <code>true</code> if the activity is finishing and creation should halt.
@@ -1650,8 +1729,7 @@ public class ResolverActivity extends FragmentActivity implements
findViewById(com.android.internal.R.id.button_open).setOnClickListener(v -> {
Intent intent = otherProfileResolveInfo.getResolvedIntent();
- safelyStartActivityAsUser(otherProfileResolveInfo,
- inactiveAdapter.mResolverListController.getUserHandle());
+ safelyStartActivityAsUser(otherProfileResolveInfo, inactiveAdapter.getUserHandle());
finish();
});
}
@@ -1700,16 +1778,6 @@ public class ResolverActivity extends FragmentActivity implements
/**
* Finishing procedures to be performed after the list has been rebuilt.
- * </p>Subclasses must call postRebuildListInternal at the end of postRebuildList.
- * @param rebuildCompleted
- * @return <code>true</code> if the activity is finishing and creation should halt.
- */
- protected boolean postRebuildList(boolean rebuildCompleted) {
- return postRebuildListInternal(rebuildCompleted);
- }
-
- /**
- * Finishing procedures to be performed after the list has been rebuilt.
* @param rebuildCompleted
* @return <code>true</code> if the activity is finishing and creation should halt.
*/
@@ -1965,8 +2033,6 @@ public class ResolverActivity extends FragmentActivity implements
RESOLVER_WORK_TAB, () -> getString(R.string.resolver_work_tab));
}
- void onHorizontalSwipeStateChanged(int state) {}
-
private void maybeHideDivider() {
if (!mIsIntentPicker) {
return;
@@ -1978,12 +2044,6 @@ public class ResolverActivity extends FragmentActivity implements
divider.setVisibility(View.GONE);
}
- /**
- * Callback called when user changes the profile tab.
- * <p>This method is intended to be overridden by subclasses.
- */
- protected void onProfileTabSelected() { }
-
private void resetCheckedItem() {
if (!mIsIntentPicker) {
return;
@@ -2030,20 +2090,17 @@ public class ResolverActivity extends FragmentActivity implements
}
/**
- * Add a label to signify that the user can pick a different app.
- * @param adapter The adapter used to provide data to item views.
+ * Updates the button bar container {@code ignoreOffset} layout param.
+ * <p>Setting this to {@code true} means that the button bar will be glued to the bottom of
+ * the screen.
*/
- public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) {
- final boolean useHeader = adapter.hasFilteredItem();
- if (useHeader) {
- FrameLayout stub = findViewById(com.android.internal.R.id.stub);
- stub.setVisibility(View.VISIBLE);
- TextView textView = (TextView) LayoutInflater.from(this).inflate(
- R.layout.resolver_different_item_header, null, false);
- if (shouldShowTabs()) {
- textView.setGravity(Gravity.CENTER);
- }
- stub.addView(textView);
+ private void setButtonBarIgnoreOffset(boolean ignoreOffset) {
+ View buttonBarContainer = findViewById(com.android.internal.R.id.button_bar_container);
+ if (buttonBarContainer != null) {
+ ResolverDrawerLayout.LayoutParams layoutParams =
+ (ResolverDrawerLayout.LayoutParams) buttonBarContainer.getLayoutParams();
+ layoutParams.ignoreOffset = ignoreOffset;
+ buttonBarContainer.setLayoutParams(layoutParams);
}
}
@@ -2091,61 +2148,6 @@ public class ResolverActivity extends FragmentActivity implements
mHeaderCreatorUser = listAdapter.getUserHandle();
}
- protected void resetButtonBar() {
- if (!mSupportsAlwaysUseOption) {
- return;
- }
- final ViewGroup buttonLayout = findViewById(com.android.internal.R.id.button_bar);
- if (buttonLayout == null) {
- Log.e(TAG, "Layout unexpectedly does not have a button bar");
- return;
- }
- ResolverListAdapter activeListAdapter =
- mMultiProfilePagerAdapter.getActiveListAdapter();
- View buttonBarDivider = findViewById(com.android.internal.R.id.resolver_button_bar_divider);
- if (!useLayoutWithDefault()) {
- int inset = mSystemWindowInsets != null ? mSystemWindowInsets.bottom : 0;
- buttonLayout.setPadding(buttonLayout.getPaddingLeft(), buttonLayout.getPaddingTop(),
- buttonLayout.getPaddingRight(), getResources().getDimensionPixelSize(
- R.dimen.resolver_button_bar_spacing) + inset);
- }
- if (activeListAdapter.isTabLoaded()
- && mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)
- && !useLayoutWithDefault()) {
- buttonLayout.setVisibility(View.INVISIBLE);
- if (buttonBarDivider != null) {
- buttonBarDivider.setVisibility(View.INVISIBLE);
- }
- setButtonBarIgnoreOffset(/* ignoreOffset */ false);
- return;
- }
- if (buttonBarDivider != null) {
- buttonBarDivider.setVisibility(View.VISIBLE);
- }
- buttonLayout.setVisibility(View.VISIBLE);
- setButtonBarIgnoreOffset(/* ignoreOffset */ true);
-
- mOnceButton = (Button) buttonLayout.findViewById(com.android.internal.R.id.button_once);
- mAlwaysButton = (Button) buttonLayout.findViewById(com.android.internal.R.id.button_always);
-
- resetAlwaysOrOnceButtonBar();
- }
-
- /**
- * Updates the button bar container {@code ignoreOffset} layout param.
- * <p>Setting this to {@code true} means that the button bar will be glued to the bottom of
- * the screen.
- */
- private void setButtonBarIgnoreOffset(boolean ignoreOffset) {
- View buttonBarContainer = findViewById(com.android.internal.R.id.button_bar_container);
- if (buttonBarContainer != null) {
- ResolverDrawerLayout.LayoutParams layoutParams =
- (ResolverDrawerLayout.LayoutParams) buttonBarContainer.getLayoutParams();
- layoutParams.ignoreOffset = ignoreOffset;
- buttonBarContainer.setLayoutParams(layoutParams);
- }
- }
-
private void resetAlwaysOrOnceButtonBar() {
// Disable both buttons initially
setAlwaysButtonEnabled(false, ListView.INVALID_POSITION, false);
@@ -2171,7 +2173,7 @@ public class ResolverActivity extends FragmentActivity implements
}
@Override // ResolverListCommunicator
- public boolean useLayoutWithDefault() {
+ public final boolean useLayoutWithDefault() {
// We only use the default app layout when the profile of the active user has a
// filtered item. We always show the same default app even in the inactive user profile.
boolean currentUserAdapterHasFilteredItem;
@@ -2190,7 +2192,7 @@ public class ResolverActivity extends FragmentActivity implements
* If {@code retainInOnStop} is set to true, we will not finish ourselves when onStop gets
* called and we are launched in a new task.
*/
- protected void setRetainInOnStop(boolean retainInOnStop) {
+ protected final void setRetainInOnStop(boolean retainInOnStop) {
mRetainInOnStop = retainInOnStop;
}
@@ -2198,43 +2200,13 @@ public class ResolverActivity extends FragmentActivity implements
* Check a simple match for the component of two ResolveInfos.
*/
@Override // ResolverListCommunicator
- public boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs) {
+ public final boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs) {
return lhs == null ? rhs == null
: lhs.activityInfo == null ? rhs.activityInfo == null
: Objects.equals(lhs.activityInfo.name, rhs.activityInfo.name)
&& Objects.equals(lhs.activityInfo.packageName, rhs.activityInfo.packageName);
}
- protected String getMetricsCategory() {
- return METRICS_CATEGORY_RESOLVER;
- }
-
- @Override // ResolverListCommunicator
- public void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
- if (listAdapter == mMultiProfilePagerAdapter.getActiveListAdapter()) {
- if (listAdapter.getUserHandle().equals(getWorkProfileUserHandle())
- && 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
- // turning on.
- return;
- }
- boolean listRebuilt = mMultiProfilePagerAdapter.rebuildActiveTab(true);
- if (listRebuilt) {
- ResolverListAdapter activeListAdapter =
- mMultiProfilePagerAdapter.getActiveListAdapter();
- activeListAdapter.notifyDataSetChanged();
- if (activeListAdapter.getCount() == 0 && !inactiveListAdapterHasItems()) {
- // We no longer have any items... just finish the activity.
- finish();
- }
- }
- } else {
- mMultiProfilePagerAdapter.clearInactiveProfileCache();
- }
- }
-
private boolean inactiveListAdapterHasItems() {
if (!shouldShowTabs()) {
return false;
@@ -2242,101 +2214,7 @@ public class ResolverActivity extends FragmentActivity implements
return mMultiProfilePagerAdapter.getInactiveListAdapter().getCount() > 0;
}
- private BroadcastReceiver createWorkProfileStateReceiver() {
- return new BroadcastReceiver() {
- @Override
- public void onReceive(Context context, Intent intent) {
- String action = intent.getAction();
- if (!TextUtils.equals(action, Intent.ACTION_USER_UNLOCKED)
- && !TextUtils.equals(action, Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
- && !TextUtils.equals(action, Intent.ACTION_MANAGED_PROFILE_AVAILABLE)) {
- return;
- }
-
- int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
-
- if (userId != getWorkProfileUserHandle().getIdentifier()) {
- return;
- }
-
- if (isWorkProfileEnabled()) {
- if (mWorkProfileHasBeenEnabled) {
- return;
- }
-
- mWorkProfileHasBeenEnabled = true;
- mQuietModeManager.markWorkProfileEnabledBroadcastReceived();
- } else {
- // Must be an UNAVAILABLE broadcast, so we watch for the next availability
- mWorkProfileHasBeenEnabled = false;
- }
-
- if (mMultiProfilePagerAdapter.getCurrentUserHandle()
- .equals(getWorkProfileUserHandle())) {
- mMultiProfilePagerAdapter.rebuildActiveTab(true);
- } else {
- mMultiProfilePagerAdapter.clearInactiveProfileCache();
- }
- }
- };
- }
-
- 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;
-
- public ResolvedComponentInfo(ComponentName name, Intent intent, ResolveInfo info) {
- this.name = name;
- add(intent, info);
- }
-
- public void add(Intent intent, ResolveInfo info) {
- mIntents.add(intent);
- mResolveInfos.add(info);
- }
-
- public int getCount() {
- return mIntents.size();
- }
-
- public Intent getIntentAt(int index) {
- return index >= 0 ? mIntents.get(index) : null;
- }
-
- public ResolveInfo getResolveInfoAt(int index) {
- return index >= 0 ? mResolveInfos.get(index) : null;
- }
-
- public int findIntent(Intent intent) {
- for (int i = 0, N = mIntents.size(); i < N; i++) {
- if (intent.equals(mIntents.get(i))) {
- return i;
- }
- }
- return -1;
- }
-
- public int findResolveInfo(ResolveInfo info) {
- for (int i = 0, N = mResolveInfos.size(); i < N; i++) {
- if (info.equals(mResolveInfos.get(i))) {
- return i;
- }
- }
- return -1;
- }
-
- public boolean isPinned() {
- return mPinned;
- }
-
- public void setPinned(boolean pinned) {
- mPinned = pinned;
- }
- }
-
- class ItemClickListener implements AdapterView.OnItemClickListener,
+ final class ItemClickListener implements AdapterView.OnItemClickListener,
AdapterView.OnItemLongClickListener {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
@@ -2397,7 +2275,7 @@ public class ResolverActivity extends FragmentActivity implements
&& match <= IntentFilter.MATCH_CATEGORY_PATH;
}
- static class PickTargetOptionRequest extends PickOptionRequest {
+ static final class PickTargetOptionRequest extends PickOptionRequest {
public PickTargetOptionRequest(@Nullable Prompt prompt, Option[] options,
@Nullable Bundle extras) {
super(prompt, options, extras);
@@ -2433,6 +2311,4 @@ public class ResolverActivity extends FragmentActivity implements
}
}
}
-
- protected void maybeLogProfileChange() {}
}
diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java
index eecb914c..eac275cc 100644
--- a/java/src/com/android/intentresolver/ResolverListAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverListAdapter.java
@@ -18,6 +18,7 @@ package com.android.intentresolver;
import static android.content.Context.ACTIVITY_SERVICE;
+import android.animation.ObjectAnimator;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
@@ -42,12 +43,12 @@ import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.view.animation.DecelerateInterpolator;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.ImageView;
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;
@@ -287,11 +288,7 @@ public class ResolverListAdapter extends BaseAdapter {
mBaseResolveList);
return currentResolveList;
} else {
- return mResolverListController.getResolversForIntent(
- /* shouldGetResolvedFilter= */ true,
- mResolverListCommunicator.shouldGetActivityMetadata(),
- mResolverListCommunicator.shouldGetOnlyDefaultActivities(),
- mIntents);
+ return getResolversForUser(mUserHandle);
}
}
@@ -802,10 +799,12 @@ public class ResolverListAdapter extends BaseAdapter {
}
protected List<ResolvedComponentInfo> getResolversForUser(UserHandle userHandle) {
- return mResolverListController.getResolversForIntentAsUser(true,
+ return mResolverListController.getResolversForIntentAsUser(
+ /* shouldGetResolvedFilter= */ true,
mResolverListCommunicator.shouldGetActivityMetadata(),
mResolverListCommunicator.shouldGetOnlyDefaultActivities(),
- mIntents, userHandle);
+ mIntents,
+ userHandle);
}
protected List<Intent> getIntents() {
@@ -914,6 +913,7 @@ public class ResolverListAdapter extends BaseAdapter {
*/
@VisibleForTesting
public static class ViewHolder {
+ private static final long IMAGE_FADE_IN_MILLIS = 150;
public View itemView;
public Drawable defaultItemViewBackground;
@@ -952,7 +952,22 @@ public class ResolverListAdapter extends BaseAdapter {
}
public void bindIcon(TargetInfo info) {
- icon.setImageDrawable(info.getDisplayIconHolder().getDisplayIcon());
+ bindIcon(info, false);
+ }
+
+ /**
+ * Bind view holder to a TargetInfo, run icon reveal animation, if required.
+ */
+ public void bindIcon(TargetInfo info, boolean animate) {
+ Drawable displayIcon = info.getDisplayIconHolder().getDisplayIcon();
+ boolean runAnimation = animate && (icon.getDrawable() == null) && (displayIcon != null);
+ icon.setImageDrawable(displayIcon);
+ if (runAnimation) {
+ ObjectAnimator animator = ObjectAnimator.ofFloat(icon, "alpha", 0.0f, 1.0f);
+ animator.setInterpolator(new DecelerateInterpolator(1.0f));
+ animator.setDuration(IMAGE_FADE_IN_MILLIS);
+ animator.start();
+ }
if (info.isSuspended()) {
icon.setColorFilter(getSuspendedColorMatrix());
} else {
diff --git a/java/src/com/android/intentresolver/ResolverListController.java b/java/src/com/android/intentresolver/ResolverListController.java
index bfffe0d8..b4544c43 100644
--- a/java/src/com/android/intentresolver/ResolverListController.java
+++ b/java/src/com/android/intentresolver/ResolverListController.java
@@ -58,7 +58,6 @@ public class ResolverListController {
private static final String TAG = "ResolverListController";
private static final boolean DEBUG = false;
- private final UserHandle mUserHandle;
private AbstractResolverComparator mResolverComparator;
private boolean isComputed = false;
@@ -68,9 +67,8 @@ public class ResolverListController {
PackageManager pm,
Intent targetIntent,
String referrerPackage,
- int launchedFromUid,
- UserHandle userHandle) {
- this(context, pm, targetIntent, referrerPackage, launchedFromUid, userHandle,
+ int launchedFromUid) {
+ this(context, pm, targetIntent, referrerPackage, launchedFromUid,
new ResolverRankerServiceResolverComparator(
context, targetIntent, referrerPackage, null, null));
}
@@ -81,14 +79,12 @@ public class ResolverListController {
Intent targetIntent,
String referrerPackage,
int launchedFromUid,
- UserHandle userHandle,
AbstractResolverComparator resolverComparator) {
mContext = context;
mpm = pm;
mLaunchedFromUid = launchedFromUid;
mTargetIntent = targetIntent;
mReferrerPackage = referrerPackage;
- mUserHandle = userHandle;
mResolverComparator = resolverComparator;
}
@@ -108,17 +104,11 @@ public class ResolverListController {
filter, match, intent.getComponent());
}
- @VisibleForTesting
- public List<ResolverActivity.ResolvedComponentInfo> getResolversForIntent(
- boolean shouldGetResolvedFilter,
- boolean shouldGetActivityMetadata,
- boolean shouldGetOnlyDefaultActivities,
- List<Intent> intents) {
- return getResolversForIntentAsUser(shouldGetResolvedFilter, shouldGetActivityMetadata,
- shouldGetOnlyDefaultActivities, intents, mUserHandle);
- }
-
- public List<ResolverActivity.ResolvedComponentInfo> getResolversForIntentAsUser(
+ /**
+ * Get data about all the ways the user with the specified handle can resolve any of the
+ * provided {@code intents}.
+ */
+ public List<ResolvedComponentInfo> getResolversForIntentAsUser(
boolean shouldGetResolvedFilter,
boolean shouldGetActivityMetadata,
boolean shouldGetOnlyDefaultActivities,
@@ -132,11 +122,9 @@ public class ResolverListController {
return getResolversForIntentAsUserInternal(intents, userHandle, baseFlags);
}
- private List<ResolverActivity.ResolvedComponentInfo> getResolversForIntentAsUserInternal(
- List<Intent> intents,
- UserHandle userHandle,
- int baseFlags) {
- List<ResolverActivity.ResolvedComponentInfo> resolvedComponents = null;
+ private List<ResolvedComponentInfo> getResolversForIntentAsUserInternal(
+ List<Intent> intents, UserHandle userHandle, int baseFlags) {
+ List<ResolvedComponentInfo> resolvedComponents = null;
for (int i = 0, N = intents.size(); i < N; i++) {
Intent intent = intents.get(i);
int flags = baseFlags;
@@ -160,14 +148,8 @@ public class ResolverListController {
}
@VisibleForTesting
- public UserHandle getUserHandle() {
- return mUserHandle;
- }
-
- @VisibleForTesting
- public void addResolveListDedupe(List<ResolverActivity.ResolvedComponentInfo> into,
- Intent intent,
- List<ResolveInfo> from) {
+ public void addResolveListDedupe(
+ List<ResolvedComponentInfo> into, Intent intent, List<ResolveInfo> from) {
final int fromCount = from.size();
final int intoCount = into.size();
for (int i = 0; i < fromCount; i++) {
@@ -175,7 +157,7 @@ public class ResolverListController {
boolean found = false;
// Only loop to the end of into as it was before we started; no dupes in from.
for (int j = 0; j < intoCount; j++) {
- final ResolverActivity.ResolvedComponentInfo rci = into.get(j);
+ final ResolvedComponentInfo rci = into.get(j);
if (isSameResolvedComponent(newInfo, rci)) {
found = true;
rci.add(intent, newInfo);
@@ -185,8 +167,7 @@ public class ResolverListController {
if (!found) {
final ComponentName name = new ComponentName(
newInfo.activityInfo.packageName, newInfo.activityInfo.name);
- final ResolverActivity.ResolvedComponentInfo rci =
- new ResolverActivity.ResolvedComponentInfo(name, intent, newInfo);
+ final ResolvedComponentInfo rci = new ResolvedComponentInfo(name, intent, newInfo);
rci.setPinned(isComponentPinned(name));
into.add(rci);
}
@@ -206,10 +187,9 @@ public class ResolverListController {
// To preserve the inputList, optionally will return the original list if any modification has
// been made.
@VisibleForTesting
- public ArrayList<ResolverActivity.ResolvedComponentInfo> filterIneligibleActivities(
- List<ResolverActivity.ResolvedComponentInfo> inputList,
- boolean returnCopyOfOriginalListIfModified) {
- ArrayList<ResolverActivity.ResolvedComponentInfo> listToReturn = null;
+ public ArrayList<ResolvedComponentInfo> filterIneligibleActivities(
+ List<ResolvedComponentInfo> inputList, boolean returnCopyOfOriginalListIfModified) {
+ ArrayList<ResolvedComponentInfo> listToReturn = null;
for (int i = inputList.size()-1; i >= 0; i--) {
ActivityInfo ai = inputList.get(i)
.getResolveInfoAt(0).activityInfo;
@@ -235,13 +215,12 @@ public class ResolverListController {
// To preserve the inputList, optionally will return the original list if any modification has
// been made.
@VisibleForTesting
- public ArrayList<ResolverActivity.ResolvedComponentInfo> filterLowPriority(
- List<ResolverActivity.ResolvedComponentInfo> inputList,
- boolean returnCopyOfOriginalListIfModified) {
- ArrayList<ResolverActivity.ResolvedComponentInfo> listToReturn = null;
+ public ArrayList<ResolvedComponentInfo> filterLowPriority(
+ List<ResolvedComponentInfo> inputList, boolean returnCopyOfOriginalListIfModified) {
+ ArrayList<ResolvedComponentInfo> listToReturn = null;
// Only display the first matches that are either of equal
// priority or have asked to be default options.
- ResolverActivity.ResolvedComponentInfo rci0 = inputList.get(0);
+ ResolvedComponentInfo rci0 = inputList.get(0);
ResolveInfo r0 = rci0.getResolveInfoAt(0);
int N = inputList.size();
for (int i = 1; i < N; i++) {
@@ -266,8 +245,7 @@ public class ResolverListController {
return listToReturn;
}
- private void compute(List<ResolverActivity.ResolvedComponentInfo> inputList)
- throws InterruptedException {
+ private void compute(List<ResolvedComponentInfo> inputList) throws InterruptedException {
if (mResolverComparator == null) {
Log.d(TAG, "Comparator has already been destroyed; skipped.");
return;
@@ -281,7 +259,7 @@ public class ResolverListController {
@VisibleForTesting
@WorkerThread
- public void sort(List<ResolverActivity.ResolvedComponentInfo> inputList) {
+ public void sort(List<ResolvedComponentInfo> inputList) {
try {
long beforeRank = System.currentTimeMillis();
if (!isComputed) {
@@ -300,7 +278,7 @@ public class ResolverListController {
@VisibleForTesting
@WorkerThread
- public void topK(List<ResolverActivity.ResolvedComponentInfo> inputList, int k) {
+ public void topK(List<ResolvedComponentInfo> inputList, int k) {
if (inputList == null || inputList.isEmpty() || k <= 0) {
return;
}
@@ -317,7 +295,7 @@ public class ResolverListController {
}
// Top of this heap has lowest rank.
- PriorityQueue<ResolverActivity.ResolvedComponentInfo> minHeap = new PriorityQueue<>(k,
+ PriorityQueue<ResolvedComponentInfo> minHeap = new PriorityQueue<>(k,
(o1, o2) -> -mResolverComparator.compare(o1, o2));
final int size = inputList.size();
// Use this pointer to keep track of the position of next element
@@ -325,7 +303,7 @@ public class ResolverListController {
int pointer = size - 1;
minHeap.addAll(inputList.subList(size - k, size));
for (int i = size - k - 1; i >= 0; --i) {
- ResolverActivity.ResolvedComponentInfo ci = inputList.get(i);
+ ResolvedComponentInfo ci = inputList.get(i);
if (-mResolverComparator.compare(ci, minHeap.peek()) > 0) {
// When ranked higher than top of heap, remove top of heap,
// update input list with it, add this new element to heap.
@@ -354,8 +332,7 @@ public class ResolverListController {
}
}
- private static boolean isSameResolvedComponent(ResolveInfo a,
- ResolverActivity.ResolvedComponentInfo b) {
+ private static boolean isSameResolvedComponent(ResolveInfo a, ResolvedComponentInfo b) {
final ActivityInfo ai = a.activityInfo;
return ai.packageName.equals(b.name.getPackageName())
&& ai.name.equals(b.name.getClassName());
diff --git a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
index 65de9409..48e3b62d 100644
--- a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
+++ b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
@@ -43,13 +43,13 @@ public class ResolverMultiProfilePagerAdapter extends
Context context,
ResolverListAdapter adapter,
EmptyStateProvider emptyStateProvider,
- QuietModeManager quietModeManager,
+ Supplier<Boolean> workProfileQuietModeChecker,
UserHandle workProfileUserHandle) {
this(
context,
ImmutableList.of(adapter),
emptyStateProvider,
- quietModeManager,
+ workProfileQuietModeChecker,
/* defaultProfile= */ 0,
workProfileUserHandle,
new BottomPaddingOverrideSupplier());
@@ -59,14 +59,14 @@ public class ResolverMultiProfilePagerAdapter extends
ResolverListAdapter personalAdapter,
ResolverListAdapter workAdapter,
EmptyStateProvider emptyStateProvider,
- QuietModeManager quietModeManager,
+ Supplier<Boolean> workProfileQuietModeChecker,
@Profile int defaultProfile,
UserHandle workProfileUserHandle) {
this(
context,
ImmutableList.of(personalAdapter, workAdapter),
emptyStateProvider,
- quietModeManager,
+ workProfileQuietModeChecker,
defaultProfile,
workProfileUserHandle,
new BottomPaddingOverrideSupplier());
@@ -76,7 +76,7 @@ public class ResolverMultiProfilePagerAdapter extends
Context context,
ImmutableList<ResolverListAdapter> listAdapters,
EmptyStateProvider emptyStateProvider,
- QuietModeManager quietModeManager,
+ Supplier<Boolean> workProfileQuietModeChecker,
@Profile int defaultProfile,
UserHandle workProfileUserHandle,
BottomPaddingOverrideSupplier bottomPaddingOverrideSupplier) {
@@ -86,7 +86,7 @@ public class ResolverMultiProfilePagerAdapter extends
(listView, bindAdapter) -> listView.setAdapter(bindAdapter),
listAdapters,
emptyStateProvider,
- quietModeManager,
+ workProfileQuietModeChecker,
defaultProfile,
workProfileUserHandle,
() -> (ViewGroup) LayoutInflater.from(context).inflate(
diff --git a/java/src/com/android/intentresolver/SecureSettings.kt b/java/src/com/android/intentresolver/SecureSettings.kt
new file mode 100644
index 00000000..a4853fd8
--- /dev/null
+++ b/java/src/com/android/intentresolver/SecureSettings.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.content.ContentResolver
+import android.provider.Settings
+
+/**
+ * A proxy class for secure settings, for easier testing.
+ */
+open class SecureSettings {
+ open fun getString(resolver: ContentResolver, name: String): String? {
+ return Settings.Secure.getString(resolver, name)
+ }
+}
diff --git a/java/src/com/android/intentresolver/WorkProfileAvailabilityManager.java b/java/src/com/android/intentresolver/WorkProfileAvailabilityManager.java
new file mode 100644
index 00000000..6e51520b
--- /dev/null
+++ b/java/src/com/android/intentresolver/WorkProfileAvailabilityManager.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.AsyncTask;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.text.TextUtils;
+
+import androidx.annotation.VisibleForTesting;
+
+/** Monitor for runtime conditions that may disable work profile display. */
+public class WorkProfileAvailabilityManager {
+ private final UserManager mUserManager;
+ private final UserHandle mWorkProfileUserHandle;
+ private final Runnable mOnWorkProfileStateUpdated;
+
+ private BroadcastReceiver mWorkProfileStateReceiver;
+
+ private boolean mIsWaitingToEnableWorkProfile;
+ private boolean mWorkProfileHasBeenEnabled;
+
+ public WorkProfileAvailabilityManager(
+ UserManager userManager,
+ UserHandle workProfileUserHandle,
+ Runnable onWorkProfileStateUpdated) {
+ mUserManager = userManager;
+ mWorkProfileUserHandle = workProfileUserHandle;
+ mWorkProfileHasBeenEnabled = isWorkProfileEnabled();
+ mOnWorkProfileStateUpdated = onWorkProfileStateUpdated;
+ }
+
+ /**
+ * Register a {@link BroadcastReceiver}, if we haven't already, to be notified about work
+ * profile availability changes.
+ *
+ * TODO: this takes the context for testing, because we don't have a context on hand when we
+ * set up this component's default "override" in {@link ChooserActivityOverrideData#reset()}.
+ * The use of these overrides in our testing design is questionable and can hopefully be
+ * improved someday; then this context should be injected in our constructor & held as `final`.
+ *
+ * TODO: consider injecting an optional `Lifecycle` so that this component can automatically
+ * manage its own registration/unregistration. (This would be optional because registration of
+ * the receiver is conditional on having `shouldShowTabs()` in our session.)
+ */
+ public void registerWorkProfileStateReceiver(Context context) {
+ if (mWorkProfileStateReceiver != null) {
+ return;
+ }
+ mWorkProfileStateReceiver = createWorkProfileStateReceiver();
+
+ IntentFilter filter = new IntentFilter();
+ filter.addAction(Intent.ACTION_USER_UNLOCKED);
+ filter.addAction(Intent.ACTION_MANAGED_PROFILE_AVAILABLE);
+ filter.addAction(Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE);
+ context.registerReceiverAsUser(
+ mWorkProfileStateReceiver, UserHandle.ALL, filter, null, null);
+ }
+
+ /**
+ * Unregister any {@link BroadcastReceiver} currently waiting for a work-enabled broadcast.
+ *
+ * TODO: this takes the context for testing, because we don't have a context on hand when we
+ * set up this component's default "override" in {@link ChooserActivityOverrideData#reset()}.
+ * The use of these overrides in our testing design is questionable and can hopefully be
+ * improved someday; then this context should be injected in our constructor & held as `final`.
+ */
+ public void unregisterWorkProfileStateReceiver(Context context) {
+ if (mWorkProfileStateReceiver == null) {
+ return;
+ }
+ context.unregisterReceiver(mWorkProfileStateReceiver);
+ mWorkProfileStateReceiver = null;
+ }
+
+ public boolean isQuietModeEnabled() {
+ return mUserManager.isQuietModeEnabled(mWorkProfileUserHandle);
+ }
+
+ // TODO: why do clients only care about the result of `isQuietModeEnabled()`, even though
+ // internally (in `isWorkProfileEnabled()`) we also check this 'unlocked' condition?
+ @VisibleForTesting
+ public boolean isWorkProfileUserUnlocked() {
+ return mUserManager.isUserUnlocked(mWorkProfileUserHandle);
+ }
+
+ /**
+ * Request that quiet mode be enabled (or disabled) for the work profile.
+ * TODO: this is only used to disable quiet mode; should that be hard-coded?
+ */
+ public void requestQuietModeEnabled(boolean enabled) {
+ AsyncTask.THREAD_POOL_EXECUTOR.execute(
+ () -> mUserManager.requestQuietModeEnabled(enabled, mWorkProfileUserHandle));
+ mIsWaitingToEnableWorkProfile = true;
+ }
+
+ /**
+ * Stop waiting for a work-enabled broadcast.
+ * TODO: this seems strangely low-level to include as part of the public API. Maybe some
+ * responsibilities need to be pulled over from the client?
+ */
+ public void markWorkProfileEnabledBroadcastReceived() {
+ mIsWaitingToEnableWorkProfile = false;
+ }
+
+ public boolean isWaitingToEnableWorkProfile() {
+ return mIsWaitingToEnableWorkProfile;
+ }
+
+ private boolean isWorkProfileEnabled() {
+ return (mWorkProfileUserHandle != null)
+ && !isQuietModeEnabled()
+ && isWorkProfileUserUnlocked();
+ }
+
+ private BroadcastReceiver createWorkProfileStateReceiver() {
+ return new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ String action = intent.getAction();
+ if (!TextUtils.equals(action, Intent.ACTION_USER_UNLOCKED)
+ && !TextUtils.equals(action, Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE)
+ && !TextUtils.equals(action, Intent.ACTION_MANAGED_PROFILE_AVAILABLE)) {
+ return;
+ }
+
+ if (intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1)
+ != mWorkProfileUserHandle.getIdentifier()) {
+ return;
+ }
+
+ if (isWorkProfileEnabled()) {
+ if (mWorkProfileHasBeenEnabled) {
+ return;
+ }
+ mWorkProfileHasBeenEnabled = true;
+ mIsWaitingToEnableWorkProfile = false;
+ } else {
+ // Must be an UNAVAILABLE broadcast, so we watch for the next availability.
+ // TODO: confirm the above reasoning (& handling of "UNAVAILABLE" in general).
+ mWorkProfileHasBeenEnabled = false;
+ }
+
+ mOnWorkProfileStateUpdated.run();
+ }
+ };
+ }
+}
diff --git a/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java b/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java
index b7c89907..0333039b 100644
--- a/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java
+++ b/java/src/com/android/intentresolver/WorkProfilePausedEmptyStateProvider.java
@@ -26,11 +26,10 @@ 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;
+import com.android.internal.R;
/**
* Chooser/ResolverActivity empty state provider that returns empty state which is shown when
@@ -39,19 +38,19 @@ import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeMana
public class WorkProfilePausedEmptyStateProvider implements EmptyStateProvider {
private final UserHandle mWorkProfileUserHandle;
- private final QuietModeManager mQuietModeManager;
+ private final WorkProfileAvailabilityManager mWorkProfileAvailability;
private final String mMetricsCategory;
private final OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
private final Context mContext;
public WorkProfilePausedEmptyStateProvider(@NonNull Context context,
@Nullable UserHandle workProfileUserHandle,
- @NonNull QuietModeManager quietModeManager,
+ @NonNull WorkProfileAvailabilityManager workProfileAvailability,
@Nullable OnSwitchOnWorkSelectedListener onSwitchOnWorkSelectedListener,
@NonNull String metricsCategory) {
mContext = context;
mWorkProfileUserHandle = workProfileUserHandle;
- mQuietModeManager = quietModeManager;
+ mWorkProfileAvailability = workProfileAvailability;
mMetricsCategory = metricsCategory;
mOnSwitchOnWorkSelectedListener = onSwitchOnWorkSelectedListener;
}
@@ -60,7 +59,7 @@ public class WorkProfilePausedEmptyStateProvider implements EmptyStateProvider {
@Override
public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) {
if (!resolverListAdapter.getUserHandle().equals(mWorkProfileUserHandle)
- || !mQuietModeManager.isQuietModeEnabled(mWorkProfileUserHandle)
+ || !mWorkProfileAvailability.isQuietModeEnabled()
|| resolverListAdapter.getCount() == 0) {
return null;
}
@@ -74,7 +73,7 @@ public class WorkProfilePausedEmptyStateProvider implements EmptyStateProvider {
if (mOnSwitchOnWorkSelectedListener != null) {
mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected();
}
- mQuietModeManager.requestQuietModeEnabled(false, mWorkProfileUserHandle);
+ mWorkProfileAvailability.requestQuietModeEnabled(false);
}, mMetricsCategory);
}
diff --git a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
index db5ae0b4..29be6dc6 100644
--- a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
+++ b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
@@ -27,7 +27,6 @@ import android.content.pm.ResolveInfo;
import android.os.Bundle;
import android.os.UserHandle;
-import com.android.intentresolver.ResolverActivity;
import com.android.intentresolver.TargetPresentationGetter;
import java.util.ArrayList;
@@ -97,25 +96,22 @@ public class DisplayResolveInfo implements TargetInfo {
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);
- intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name));
- mResolvedIntent = intent;
+ mResolvedIntent = createResolvedIntent(resolvedIntent, ai);
}
private DisplayResolveInfo(
DisplayResolveInfo other,
- Intent fillInIntent,
- int flags,
+ @Nullable Intent baseIntentToSend,
TargetPresentationGetter presentationGetter) {
mSourceIntents.addAll(other.getAllSourceIntents());
mResolveInfo = other.mResolveInfo;
mIsSuspended = other.mIsSuspended;
mDisplayLabel = other.mDisplayLabel;
mExtendedInfo = other.mExtendedInfo;
- mResolvedIntent = new Intent(other.mResolvedIntent);
- mResolvedIntent.fillIn(fillInIntent, flags);
+
+ mResolvedIntent = createResolvedIntent(
+ baseIntentToSend == null ? other.mResolvedIntent : baseIntentToSend,
+ mResolveInfo.activityInfo);
mPresentationGetter = presentationGetter;
mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());
@@ -133,6 +129,14 @@ public class DisplayResolveInfo implements TargetInfo {
mDisplayIconHolder.setDisplayIcon(other.mDisplayIconHolder.getDisplayIcon());
}
+ private static Intent createResolvedIntent(Intent resolvedIntent, ActivityInfo ai) {
+ final Intent result = new Intent(resolvedIntent);
+ result.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT
+ | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
+ result.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name));
+ return result;
+ }
+
@Override
public final boolean isDisplayResolveInfo() {
return true;
@@ -168,8 +172,21 @@ public class DisplayResolveInfo implements TargetInfo {
}
@Override
- public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
- return new DisplayResolveInfo(this, fillInIntent, flags, mPresentationGetter);
+ @Nullable
+ public DisplayResolveInfo tryToCloneWithAppliedRefinement(Intent proposedRefinement) {
+ Intent matchingBase =
+ getAllSourceIntents()
+ .stream()
+ .filter(i -> i.filterEquals(proposedRefinement))
+ .findFirst()
+ .orElse(null);
+ if (matchingBase == null) {
+ return null;
+ }
+
+ Intent merged = new Intent(matchingBase);
+ merged.fillIn(proposedRefinement, 0);
+ return new DisplayResolveInfo(this, merged, mPresentationGetter);
}
@Override
@@ -201,13 +218,7 @@ public class DisplayResolveInfo implements TargetInfo {
}
@Override
- public boolean start(Activity activity, Bundle options) {
- activity.startActivity(mResolvedIntent, options);
- return true;
- }
-
- @Override
- public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
+ public boolean startAsCaller(Activity activity, Bundle options, int userId) {
TargetInfo.prepareIntentForCrossProfileLaunch(mResolvedIntent, userId);
activity.startActivityAsCaller(mResolvedIntent, options, false, userId);
return true;
@@ -216,10 +227,21 @@ public class DisplayResolveInfo implements TargetInfo {
@Override
public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
TargetInfo.prepareIntentForCrossProfileLaunch(mResolvedIntent, user.getIdentifier());
+ // TODO: is this equivalent to `startActivityAsCaller` with `ignoreTargetSecurity=true`? If
+ // so, we can consolidate on the one API method to show that this flag is the only
+ // distinction between `startAsCaller` and `startAsUser`. We can even bake that flag into
+ // the `TargetActivityStarter` upfront since it just reflects our "safe forwarding mode" --
+ // which is constant for the duration of our lifecycle, leaving clients no other
+ // responsibilities in this logic.
activity.startActivityAsUser(mResolvedIntent, options, user);
return false;
}
+ @Override
+ public Intent getTargetIntent() {
+ return mResolvedIntent;
+ }
+
public boolean isSuspended() {
return mIsSuspended;
}
diff --git a/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java b/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java
new file mode 100644
index 00000000..2d9683e1
--- /dev/null
+++ b/java/src/com/android/intentresolver/chooser/ImmutableTargetInfo.java
@@ -0,0 +1,633 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.chooser;
+
+import android.annotation.NonNull;
+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.os.Bundle;
+import android.os.UserHandle;
+import android.util.HashedStringCache;
+
+import androidx.annotation.VisibleForTesting;
+
+import com.google.common.collect.ImmutableList;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * An implementation of {@link TargetInfo} with immutable data. Any modifications must be made by
+ * creating a new instance (e.g., via {@link ImmutableTargetInfo#toBuilder()}).
+ */
+public final class ImmutableTargetInfo implements TargetInfo {
+ private static final String TAG = "TargetInfo";
+
+ /** Delegate interface to implement {@link TargetInfo#getHashedTargetIdForMetrics()}. */
+ public interface TargetHashProvider {
+ /** Request a hash for the specified {@code target}. */
+ HashedStringCache.HashResult getHashedTargetIdForMetrics(
+ TargetInfo target, Context context);
+ }
+
+ /** Delegate interface to request that the target be launched by a particular API. */
+ public interface TargetActivityStarter {
+ /**
+ * Request that the delegate use the {@link Activity#startAsCaller()} API to launch the
+ * specified {@code target}.
+ *
+ * @return true if the target was launched successfully.
+ */
+ boolean startAsCaller(TargetInfo target, Activity activity, Bundle options, int userId);
+
+ /**
+ * Request that the delegate use the {@link Activity#startAsUser()} API to launch the
+ * specified {@code target}.
+ *
+ * @return true if the target was launched successfully.
+ */
+ boolean startAsUser(TargetInfo target, Activity activity, Bundle options, UserHandle user);
+ }
+
+ enum LegacyTargetType {
+ NOT_LEGACY_TARGET,
+ EMPTY_TARGET_INFO,
+ PLACEHOLDER_TARGET_INFO,
+ SELECTABLE_TARGET_INFO,
+ DISPLAY_RESOLVE_INFO,
+ MULTI_DISPLAY_RESOLVE_INFO
+ };
+
+ /** Builder API to construct {@code ImmutableTargetInfo} instances. */
+ public static class Builder {
+ @Nullable
+ private ComponentName mResolvedComponentName;
+
+ @Nullable
+ private Intent mResolvedIntent;
+
+ @Nullable
+ private Intent mBaseIntentToSend;
+
+ @Nullable
+ private Intent mTargetIntent;
+
+ @Nullable
+ private ComponentName mChooserTargetComponentName;
+
+ @Nullable
+ private ShortcutInfo mDirectShareShortcutInfo;
+
+ @Nullable
+ private AppTarget mDirectShareAppTarget;
+
+ @Nullable
+ private DisplayResolveInfo mDisplayResolveInfo;
+
+ @Nullable
+ private TargetHashProvider mHashProvider;
+
+ @Nullable
+ private Intent mReferrerFillInIntent;
+
+ @Nullable
+ private TargetActivityStarter mActivityStarter;
+
+ @Nullable
+ private ResolveInfo mResolveInfo;
+
+ @Nullable
+ private CharSequence mDisplayLabel;
+
+ @Nullable
+ private CharSequence mExtendedInfo;
+
+ @Nullable
+ private IconHolder mDisplayIconHolder;
+
+ private boolean mIsSuspended;
+ private boolean mIsPinned;
+ private float mModifiedScore = -0.1f;
+ private LegacyTargetType mLegacyType = LegacyTargetType.NOT_LEGACY_TARGET;
+
+ private ImmutableList<Intent> mAlternateSourceIntents = ImmutableList.of();
+ private ImmutableList<DisplayResolveInfo> mAllDisplayTargets = ImmutableList.of();
+
+ /**
+ * Configure an {@link Intent} to be built in to the output target as the resolution for the
+ * requested target data.
+ */
+ public Builder setResolvedIntent(Intent resolvedIntent) {
+ mResolvedIntent = resolvedIntent;
+ return this;
+ }
+
+ /**
+ * Configure an {@link Intent} to be built in to the output target as the "base intent to
+ * send," which may be a refinement of any of our source targets. This is private because
+ * it's only used internally by {@link #tryToCloneWithAppliedRefinement()}; if it's ever
+ * expanded, the builder should probably be responsible for enforcing the refinement check.
+ */
+ private Builder setBaseIntentToSend(Intent baseIntent) {
+ mBaseIntentToSend = baseIntent;
+ return this;
+ }
+
+ /**
+ * Configure an {@link Intent} to be built in to the output as the "target intent."
+ */
+ public Builder setTargetIntent(Intent targetIntent) {
+ mTargetIntent = targetIntent;
+ return this;
+ }
+
+ /**
+ * Configure a fill-in intent provided by the referrer to be used in populating the launch
+ * intent if the output target is ever selected.
+ *
+ * @see android.content.Intent#fillIn(Intent, int)
+ */
+ public Builder setReferrerFillInIntent(@Nullable Intent referrerFillInIntent) {
+ mReferrerFillInIntent = referrerFillInIntent;
+ return this;
+ }
+
+ /**
+ * Configure a {@link ComponentName} to be built in to the output target, as the real
+ * component we were able to resolve on this device given the available target data.
+ */
+ public Builder setResolvedComponentName(@Nullable ComponentName resolvedComponentName) {
+ mResolvedComponentName = resolvedComponentName;
+ return this;
+ }
+
+ /**
+ * Configure a {@link ComponentName} to be built in to the output target, as the component
+ * supposedly associated with a {@link ChooserTarget} from which the builder data is being
+ * derived.
+ */
+ public Builder setChooserTargetComponentName(@Nullable ComponentName componentName) {
+ mChooserTargetComponentName = componentName;
+ return this;
+ }
+
+ /** Configure the {@link TargetActivityStarter} to be built in to the output target. */
+ public Builder setActivityStarter(TargetActivityStarter activityStarter) {
+ mActivityStarter = activityStarter;
+ return this;
+ }
+
+ /** Configure the {@link ResolveInfo} to be built in to the output target. */
+ public Builder setResolveInfo(ResolveInfo resolveInfo) {
+ mResolveInfo = resolveInfo;
+ return this;
+ }
+
+ /** Configure the display label to be built in to the output target. */
+ public Builder setDisplayLabel(CharSequence displayLabel) {
+ mDisplayLabel = displayLabel;
+ return this;
+ }
+
+ /** Configure the extended info to be built in to the output target. */
+ public Builder setExtendedInfo(CharSequence extendedInfo) {
+ mExtendedInfo = extendedInfo;
+ return this;
+ }
+
+ /** Configure the {@link IconHolder} to be built in to the output target. */
+ public Builder setDisplayIconHolder(IconHolder displayIconHolder) {
+ mDisplayIconHolder = displayIconHolder;
+ return this;
+ }
+
+ /** Configure the list of alternate source intents we could resolve for this target. */
+ public Builder setAlternateSourceIntents(List<Intent> sourceIntents) {
+ mAlternateSourceIntents = immutableCopyOrEmpty(sourceIntents);
+ return this;
+ }
+
+ /**
+ * Configure the full list of source intents we could resolve for this target. This is
+ * effectively the same as calling {@link #setResolvedIntent()} with the first element of
+ * the list, and {@link #setAlternateSourceIntents()} with the remainder (or clearing those
+ * fields on the builder if there are no corresponding elements in the list).
+ */
+ public Builder setAllSourceIntents(List<Intent> sourceIntents) {
+ if ((sourceIntents == null) || sourceIntents.isEmpty()) {
+ setResolvedIntent(null);
+ setAlternateSourceIntents(null);
+ return this;
+ }
+
+ setResolvedIntent(sourceIntents.get(0));
+ setAlternateSourceIntents(sourceIntents.subList(1, sourceIntents.size()));
+ return this;
+ }
+
+ /** Configure the list of display targets to be built in to the output target. */
+ public Builder setAllDisplayTargets(List<DisplayResolveInfo> targets) {
+ mAllDisplayTargets = immutableCopyOrEmpty(targets);
+ return this;
+ }
+
+ /** Configure the is-suspended status to be built in to the output target. */
+ public Builder setIsSuspended(boolean isSuspended) {
+ mIsSuspended = isSuspended;
+ return this;
+ }
+
+ /** Configure the is-pinned status to be built in to the output target. */
+ public Builder setIsPinned(boolean isPinned) {
+ mIsPinned = isPinned;
+ return this;
+ }
+
+ /** Configure the modified score to be built in to the output target. */
+ public Builder setModifiedScore(float modifiedScore) {
+ mModifiedScore = modifiedScore;
+ return this;
+ }
+
+ /** Configure the {@link ShortcutInfo} to be built in to the output target. */
+ public Builder setDirectShareShortcutInfo(@Nullable ShortcutInfo shortcutInfo) {
+ mDirectShareShortcutInfo = shortcutInfo;
+ return this;
+ }
+
+ /** Configure the {@link AppTarget} to be built in to the output target. */
+ public Builder setDirectShareAppTarget(@Nullable AppTarget appTarget) {
+ mDirectShareAppTarget = appTarget;
+ return this;
+ }
+
+ /** Configure the {@link DisplayResolveInfo} to be built in to the output target. */
+ public Builder setDisplayResolveInfo(@Nullable DisplayResolveInfo displayResolveInfo) {
+ mDisplayResolveInfo = displayResolveInfo;
+ return this;
+ }
+
+ /** Configure the {@link TargetHashProvider} to be built in to the output target. */
+ public Builder setHashProvider(@Nullable TargetHashProvider hashProvider) {
+ mHashProvider = hashProvider;
+ return this;
+ }
+
+ Builder setLegacyType(@NonNull LegacyTargetType legacyType) {
+ mLegacyType = legacyType;
+ return this;
+ }
+
+ /** Construct an {@code ImmutableTargetInfo} with the current builder data. */
+ public ImmutableTargetInfo build() {
+ List<Intent> sourceIntents = new ArrayList<>();
+ if (mResolvedIntent != null) {
+ sourceIntents.add(mResolvedIntent);
+ }
+ if (mAlternateSourceIntents != null) {
+ sourceIntents.addAll(mAlternateSourceIntents);
+ }
+
+ Intent baseIntentToSend = mBaseIntentToSend;
+ if ((baseIntentToSend == null) && !sourceIntents.isEmpty()) {
+ baseIntentToSend = sourceIntents.get(0);
+ }
+ if (baseIntentToSend != null) {
+ baseIntentToSend = new Intent(baseIntentToSend);
+ if (mReferrerFillInIntent != null) {
+ baseIntentToSend.fillIn(mReferrerFillInIntent, 0);
+ }
+ }
+
+ return new ImmutableTargetInfo(
+ baseIntentToSend,
+ ImmutableList.copyOf(sourceIntents),
+ mTargetIntent,
+ mReferrerFillInIntent,
+ mResolvedComponentName,
+ mChooserTargetComponentName,
+ mActivityStarter,
+ mResolveInfo,
+ mDisplayLabel,
+ mExtendedInfo,
+ mDisplayIconHolder,
+ mAllDisplayTargets,
+ mIsSuspended,
+ mIsPinned,
+ mModifiedScore,
+ mDirectShareShortcutInfo,
+ mDirectShareAppTarget,
+ mDisplayResolveInfo,
+ mHashProvider,
+ mLegacyType);
+ }
+ }
+
+ @Nullable
+ private final Intent mReferrerFillInIntent;
+
+ @Nullable
+ private final ComponentName mResolvedComponentName;
+
+ @Nullable
+ private final ComponentName mChooserTargetComponentName;
+
+ @Nullable
+ private final ShortcutInfo mDirectShareShortcutInfo;
+
+ @Nullable
+ private final AppTarget mDirectShareAppTarget;
+
+ @Nullable
+ private final DisplayResolveInfo mDisplayResolveInfo;
+
+ @Nullable
+ private final TargetHashProvider mHashProvider;
+
+ private final Intent mBaseIntentToSend;
+ private final ImmutableList<Intent> mSourceIntents;
+ private final Intent mTargetIntent;
+ private final TargetActivityStarter mActivityStarter;
+ private final ResolveInfo mResolveInfo;
+ private final CharSequence mDisplayLabel;
+ private final CharSequence mExtendedInfo;
+ private final IconHolder mDisplayIconHolder;
+ private final ImmutableList<DisplayResolveInfo> mAllDisplayTargets;
+ private final boolean mIsSuspended;
+ private final boolean mIsPinned;
+ private final float mModifiedScore;
+ private final LegacyTargetType mLegacyType;
+
+ /** Construct a {@link Builder}. */
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ /** Construct a {@link Builder} pre-initialized to match this target. */
+ public Builder toBuilder() {
+ return newBuilder()
+ .setBaseIntentToSend(getBaseIntentToSend())
+ .setResolvedIntent(getResolvedIntent())
+ .setTargetIntent(getTargetIntent())
+ .setReferrerFillInIntent(getReferrerFillInIntent())
+ .setResolvedComponentName(getResolvedComponentName())
+ .setChooserTargetComponentName(getChooserTargetComponentName())
+ .setActivityStarter(mActivityStarter)
+ .setResolveInfo(getResolveInfo())
+ .setDisplayLabel(getDisplayLabel())
+ .setExtendedInfo(getExtendedInfo())
+ .setDisplayIconHolder(getDisplayIconHolder())
+ .setAllSourceIntents(getAllSourceIntents())
+ .setAllDisplayTargets(getAllDisplayTargets())
+ .setIsSuspended(isSuspended())
+ .setIsPinned(isPinned())
+ .setModifiedScore(getModifiedScore())
+ .setDirectShareShortcutInfo(getDirectShareShortcutInfo())
+ .setDirectShareAppTarget(getDirectShareAppTarget())
+ .setDisplayResolveInfo(getDisplayResolveInfo())
+ .setHashProvider(getHashProvider())
+ .setLegacyType(mLegacyType);
+ }
+
+ @VisibleForTesting
+ Intent getBaseIntentToSend() {
+ return mBaseIntentToSend;
+ }
+
+ @Override
+ @Nullable
+ public ImmutableTargetInfo tryToCloneWithAppliedRefinement(Intent proposedRefinement) {
+ Intent matchingBase =
+ getAllSourceIntents()
+ .stream()
+ .filter(i -> i.filterEquals(proposedRefinement))
+ .findFirst()
+ .orElse(null);
+ if (matchingBase == null) {
+ return null;
+ }
+
+ Intent merged = new Intent(matchingBase);
+ merged.fillIn(proposedRefinement, 0);
+ return toBuilder().setBaseIntentToSend(merged).build();
+ }
+
+ @Override
+ public Intent getResolvedIntent() {
+ return (mSourceIntents.isEmpty() ? null : mSourceIntents.get(0));
+ }
+
+ @Override
+ public Intent getTargetIntent() {
+ return mTargetIntent;
+ }
+
+ @Nullable
+ public Intent getReferrerFillInIntent() {
+ return mReferrerFillInIntent;
+ }
+
+ @Override
+ @Nullable
+ public ComponentName getResolvedComponentName() {
+ return mResolvedComponentName;
+ }
+
+ @Override
+ @Nullable
+ public ComponentName getChooserTargetComponentName() {
+ return mChooserTargetComponentName;
+ }
+
+ @Override
+ public boolean startAsCaller(Activity activity, Bundle options, int userId) {
+ // TODO: make sure that the component name is set in all cases
+ return mActivityStarter.startAsCaller(this, activity, options, userId);
+ }
+
+ @Override
+ public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
+ // TODO: make sure that the component name is set in all cases
+ return mActivityStarter.startAsUser(this, activity, options, user);
+ }
+
+ @Override
+ public ResolveInfo getResolveInfo() {
+ return mResolveInfo;
+ }
+
+ @Override
+ public CharSequence getDisplayLabel() {
+ return mDisplayLabel;
+ }
+
+ @Override
+ public CharSequence getExtendedInfo() {
+ return mExtendedInfo;
+ }
+
+ @Override
+ public IconHolder getDisplayIconHolder() {
+ return mDisplayIconHolder;
+ }
+
+ @Override
+ public List<Intent> getAllSourceIntents() {
+ return mSourceIntents;
+ }
+
+ @Override
+ public ArrayList<DisplayResolveInfo> getAllDisplayTargets() {
+ ArrayList<DisplayResolveInfo> targets = new ArrayList<>();
+ targets.addAll(mAllDisplayTargets);
+ return targets;
+ }
+
+ @Override
+ public boolean isSuspended() {
+ return mIsSuspended;
+ }
+
+ @Override
+ public boolean isPinned() {
+ return mIsPinned;
+ }
+
+ @Override
+ public float getModifiedScore() {
+ return mModifiedScore;
+ }
+
+ @Override
+ @Nullable
+ public ShortcutInfo getDirectShareShortcutInfo() {
+ return mDirectShareShortcutInfo;
+ }
+
+ @Override
+ @Nullable
+ public AppTarget getDirectShareAppTarget() {
+ return mDirectShareAppTarget;
+ }
+
+ @Override
+ @Nullable
+ public DisplayResolveInfo getDisplayResolveInfo() {
+ return mDisplayResolveInfo;
+ }
+
+ @Override
+ public HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context) {
+ return (mHashProvider == null)
+ ? null : mHashProvider.getHashedTargetIdForMetrics(this, context);
+ }
+
+ @VisibleForTesting
+ @Nullable
+ TargetHashProvider getHashProvider() {
+ return mHashProvider;
+ }
+
+ @Override
+ public boolean isEmptyTargetInfo() {
+ return mLegacyType == LegacyTargetType.EMPTY_TARGET_INFO;
+ }
+
+ @Override
+ public boolean isPlaceHolderTargetInfo() {
+ return mLegacyType == LegacyTargetType.PLACEHOLDER_TARGET_INFO;
+ }
+
+ @Override
+ public boolean isNotSelectableTargetInfo() {
+ return isEmptyTargetInfo() || isPlaceHolderTargetInfo();
+ }
+
+ @Override
+ public boolean isSelectableTargetInfo() {
+ return mLegacyType == LegacyTargetType.SELECTABLE_TARGET_INFO;
+ }
+
+ @Override
+ public boolean isChooserTargetInfo() {
+ return isNotSelectableTargetInfo() || isSelectableTargetInfo();
+ }
+
+ @Override
+ public boolean isMultiDisplayResolveInfo() {
+ return mLegacyType == LegacyTargetType.MULTI_DISPLAY_RESOLVE_INFO;
+ }
+
+ @Override
+ public boolean isDisplayResolveInfo() {
+ return (mLegacyType == LegacyTargetType.DISPLAY_RESOLVE_INFO)
+ || isMultiDisplayResolveInfo();
+ }
+
+ private ImmutableTargetInfo(
+ Intent baseIntentToSend,
+ ImmutableList<Intent> sourceIntents,
+ Intent targetIntent,
+ @Nullable Intent referrerFillInIntent,
+ @Nullable ComponentName resolvedComponentName,
+ @Nullable ComponentName chooserTargetComponentName,
+ TargetActivityStarter activityStarter,
+ ResolveInfo resolveInfo,
+ CharSequence displayLabel,
+ CharSequence extendedInfo,
+ IconHolder iconHolder,
+ ImmutableList<DisplayResolveInfo> allDisplayTargets,
+ boolean isSuspended,
+ boolean isPinned,
+ float modifiedScore,
+ @Nullable ShortcutInfo directShareShortcutInfo,
+ @Nullable AppTarget directShareAppTarget,
+ @Nullable DisplayResolveInfo displayResolveInfo,
+ @Nullable TargetHashProvider hashProvider,
+ LegacyTargetType legacyType) {
+ mBaseIntentToSend = baseIntentToSend;
+ mSourceIntents = sourceIntents;
+ mTargetIntent = targetIntent;
+ mReferrerFillInIntent = referrerFillInIntent;
+ mResolvedComponentName = resolvedComponentName;
+ mChooserTargetComponentName = chooserTargetComponentName;
+ mActivityStarter = activityStarter;
+ mResolveInfo = resolveInfo;
+ mDisplayLabel = displayLabel;
+ mExtendedInfo = extendedInfo;
+ mDisplayIconHolder = iconHolder;
+ mAllDisplayTargets = allDisplayTargets;
+ mIsSuspended = isSuspended;
+ mIsPinned = isPinned;
+ mModifiedScore = modifiedScore;
+ mDirectShareShortcutInfo = directShareShortcutInfo;
+ mDirectShareAppTarget = directShareAppTarget;
+ mDisplayResolveInfo = displayResolveInfo;
+ mHashProvider = hashProvider;
+ mLegacyType = legacyType;
+ }
+
+ private static <E> ImmutableList<E> immutableCopyOrEmpty(@Nullable List<E> source) {
+ return (source == null) ? ImmutableList.of() : ImmutableList.copyOf(source);
+ }
+}
diff --git a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
index 29f00a35..b97e6b45 100644
--- a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
+++ b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
@@ -17,12 +17,14 @@
package com.android.intentresolver.chooser;
import android.app.Activity;
+import android.content.Intent;
import android.os.Bundle;
import android.os.UserHandle;
-import com.android.intentresolver.ResolverActivity;
+import androidx.annotation.Nullable;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
/**
@@ -30,7 +32,7 @@ import java.util.List;
*/
public class MultiDisplayResolveInfo extends DisplayResolveInfo {
- ArrayList<DisplayResolveInfo> mTargetInfos = new ArrayList<>();
+ final ArrayList<DisplayResolveInfo> mTargetInfos;
// Index of selected target
private int mSelected = -1;
@@ -66,8 +68,9 @@ public class MultiDisplayResolveInfo extends DisplayResolveInfo {
/**
* 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.
+ * TODO: provide as a generic {@code List<DisplayResolveInfo>} once
+ * {@link com.android.intentresolver.ChooserActivity} stops requiring the signature to match
+ * that of the other "lists" it builds up.
*/
@Override
public ArrayList<DisplayResolveInfo> getAllDisplayTargets() {
@@ -93,12 +96,27 @@ public class MultiDisplayResolveInfo extends DisplayResolveInfo {
}
@Override
- public boolean start(Activity activity, Bundle options) {
- return mTargetInfos.get(mSelected).start(activity, options);
+ @Nullable
+ public MultiDisplayResolveInfo tryToCloneWithAppliedRefinement(Intent proposedRefinement) {
+ final int size = mTargetInfos.size();
+ ArrayList<DisplayResolveInfo> targetInfos = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ DisplayResolveInfo target = mTargetInfos.get(i);
+ DisplayResolveInfo targetClone = (i == mSelected)
+ ? target.tryToCloneWithAppliedRefinement(proposedRefinement)
+ : new DisplayResolveInfo(target);
+ if (targetClone == null) {
+ return null;
+ }
+ targetInfos.add(targetClone);
+ }
+ MultiDisplayResolveInfo clone = new MultiDisplayResolveInfo(targetInfos);
+ clone.mSelected = mSelected;
+ return clone;
}
@Override
- public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
+ public boolean startAsCaller(Activity activity, Bundle options, int userId) {
return mTargetInfos.get(mSelected).startAsCaller(activity, options, userId);
}
@@ -106,4 +124,16 @@ public class MultiDisplayResolveInfo extends DisplayResolveInfo {
public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
return mTargetInfos.get(mSelected).startAsUser(activity, options, user);
}
+
+ @Override
+ public Intent getTargetIntent() {
+ return mTargetInfos.get(mSelected).getTargetIntent();
+ }
+
+ @Override
+ public List<Intent> getAllSourceIntents() {
+ return hasSelected()
+ ? mTargetInfos.get(mSelected).getAllSourceIntents()
+ : Collections.emptyList();
+ }
}
diff --git a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
index d6333374..6444e13b 100644
--- a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
@@ -16,34 +16,30 @@
package com.android.intentresolver.chooser;
+import android.annotation.Nullable;
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 com.android.intentresolver.R;
-import com.android.intentresolver.ResolverActivity;
-import java.util.List;
+import java.util.function.Supplier;
/**
* 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 extends ChooserTargetInfo {
+public final class NotSelectableTargetInfo {
/** Create a non-selectable {@link TargetInfo} with no content. */
public static TargetInfo newEmptyTargetInfo() {
- return new NotSelectableTargetInfo() {
- @Override
- public boolean isEmptyTargetInfo() {
- return true;
- }
- };
+ return ImmutableTargetInfo.newBuilder()
+ .setLegacyType(ImmutableTargetInfo.LegacyTargetType.EMPTY_TARGET_INFO)
+ .setDisplayIconHolder(makeReadOnlyIconHolder(() -> null))
+ .setActivityStarter(makeNoOpActivityStarter())
+ .build();
}
/**
@@ -51,102 +47,51 @@ public abstract class NotSelectableTargetInfo extends ChooserTargetInfo {
* unless/until it can be replaced by the result of a pending asynchronous load.
*/
public static TargetInfo newPlaceHolderTargetInfo(Context context) {
- return new NotSelectableTargetInfo() {
- @Override
- public boolean isPlaceHolderTargetInfo() {
- return true;
- }
-
- @Override
- public IconHolder getDisplayIconHolder() {
- return new IconHolder() {
- @Override
- public Drawable getDisplayIcon() {
- AnimatedVectorDrawable avd = (AnimatedVectorDrawable)
- context.getDrawable(
- R.drawable.chooser_direct_share_icon_placeholder);
- avd.start(); // Start animation after generation.
- return avd;
- }
-
- @Override
- public void setDisplayIcon(Drawable icon) {}
- };
- }
-
- @Override
- public boolean hasDisplayIcon() {
- return true;
- }
- };
- }
-
- public final boolean isNotSelectableTargetInfo() {
- return true;
- }
-
- public Intent getResolvedIntent() {
- return null;
- }
-
- public ComponentName getResolvedComponentName() {
- return null;
- }
-
- public boolean start(Activity activity, Bundle options) {
- return false;
- }
-
- public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
- return false;
- }
-
- public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
- return false;
- }
-
- public ResolveInfo getResolveInfo() {
- return null;
- }
-
- public CharSequence getDisplayLabel() {
- return null;
- }
-
- public CharSequence getExtendedInfo() {
- return null;
- }
-
- public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
- return null;
+ return ImmutableTargetInfo.newBuilder()
+ .setLegacyType(ImmutableTargetInfo.LegacyTargetType.PLACEHOLDER_TARGET_INFO)
+ .setDisplayIconHolder(
+ makeReadOnlyIconHolder(() -> makeStartedPlaceholderDrawable(context)))
+ .setActivityStarter(makeNoOpActivityStarter())
+ .build();
}
- public List<Intent> getAllSourceIntents() {
- return null;
+ private static Drawable makeStartedPlaceholderDrawable(Context context) {
+ AnimatedVectorDrawable avd = (AnimatedVectorDrawable) context.getDrawable(
+ R.drawable.chooser_direct_share_icon_placeholder);
+ avd.start(); // Start animation after generation.
+ return avd;
}
- public float getModifiedScore() {
- return -0.1f;
- }
-
- public boolean isSuspended() {
- return false;
- }
+ private static ImmutableTargetInfo.IconHolder makeReadOnlyIconHolder(
+ Supplier</* @Nullable */ Drawable> iconProvider) {
+ return new ImmutableTargetInfo.IconHolder() {
+ @Override
+ @Nullable
+ public Drawable getDisplayIcon() {
+ return iconProvider.get();
+ }
- public boolean isPinned() {
- return false;
+ @Override
+ public void setDisplayIcon(Drawable icon) {}
+ };
}
- @Override
- public IconHolder getDisplayIconHolder() {
- return new IconHolder() {
+ private static ImmutableTargetInfo.TargetActivityStarter makeNoOpActivityStarter() {
+ return new ImmutableTargetInfo.TargetActivityStarter() {
@Override
- public Drawable getDisplayIcon() {
- return null;
+ public boolean startAsCaller(
+ TargetInfo target, Activity activity, Bundle options, int userId) {
+ return false;
}
@Override
- public void setDisplayIcon(Drawable icon) {}
+ public boolean startAsUser(
+ TargetInfo target, Activity activity, Bundle options, UserHandle user) {
+ return false;
+ }
};
}
+
+ // TODO: merge all the APIs up to a single `TargetInfo` class.
+ private NotSelectableTargetInfo() {}
}
diff --git a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
index 3ab50175..1fbe2da7 100644
--- a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
@@ -33,7 +33,6 @@ import android.text.SpannableStringBuilder;
import android.util.HashedStringCache;
import android.util.Log;
-import com.android.intentresolver.ResolverActivity;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import java.util.ArrayList;
@@ -79,7 +78,6 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
private final CharSequence mChooserTargetUnsanitizedTitle;
private final Icon mChooserTargetIcon;
private final Bundle mChooserTargetIntentExtras;
- private final int mFillInFlags;
private final boolean mIsPinned;
private final float mModifiedScore;
private final boolean mIsSuspended;
@@ -92,12 +90,6 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
private final TargetActivityStarter mActivityStarter;
/**
- * 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}.
*/
@@ -160,6 +152,7 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
sourceInfo,
backupResolveInfo,
resolvedIntent,
+ null,
chooserTargetComponentName,
chooserTargetUnsanitizedTitle,
chooserTargetIcon,
@@ -167,15 +160,14 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
modifiedScore,
shortcutInfo,
appTarget,
- referrerFillInIntent,
- /* fillInIntent = */ null,
- /* fillInFlags = */ 0);
+ referrerFillInIntent);
}
private SelectableTargetInfo(
@Nullable DisplayResolveInfo sourceInfo,
@Nullable ResolveInfo backupResolveInfo,
Intent resolvedIntent,
+ @Nullable Intent baseIntentToSend,
ComponentName chooserTargetComponentName,
CharSequence chooserTargetUnsanitizedTitle,
Icon chooserTargetIcon,
@@ -183,9 +175,7 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
float modifiedScore,
@Nullable ShortcutInfo shortcutInfo,
@Nullable AppTarget appTarget,
- Intent referrerFillInIntent,
- @Nullable Intent fillInIntent,
- int fillInFlags) {
+ Intent referrerFillInIntent) {
mSourceInfo = sourceInfo;
mBackupResolveInfo = backupResolveInfo;
mResolvedIntent = resolvedIntent;
@@ -193,8 +183,6 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
mShortcutInfo = shortcutInfo;
mAppTarget = appTarget;
mReferrerFillInIntent = referrerFillInIntent;
- mFillInIntent = fillInIntent;
- mFillInFlags = fillInFlags;
mChooserTargetComponentName = chooserTargetComponentName;
mChooserTargetUnsanitizedTitle = chooserTargetUnsanitizedTitle;
mChooserTargetIcon = chooserTargetIcon;
@@ -210,9 +198,8 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
mAllSourceIntents = getAllSourceIntents(sourceInfo);
mBaseIntentToSend = getBaseIntentToSend(
+ baseIntentToSend,
mResolvedIntent,
- mFillInIntent,
- mFillInFlags,
mReferrerFillInIntent);
mHashProvider = context -> {
@@ -263,11 +250,12 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
};
}
- private SelectableTargetInfo(SelectableTargetInfo other, Intent fillInIntent, int flags) {
+ private SelectableTargetInfo(SelectableTargetInfo other, Intent baseIntentToSend) {
this(
other.mSourceInfo,
other.mBackupResolveInfo,
other.mResolvedIntent,
+ baseIntentToSend,
other.mChooserTargetComponentName,
other.mChooserTargetUnsanitizedTitle,
other.mChooserTargetIcon,
@@ -275,14 +263,25 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
other.mModifiedScore,
other.mShortcutInfo,
other.mAppTarget,
- other.mReferrerFillInIntent,
- fillInIntent,
- flags);
+ other.mReferrerFillInIntent);
}
@Override
- public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
- return new SelectableTargetInfo(this, fillInIntent, flags);
+ @Nullable
+ public TargetInfo tryToCloneWithAppliedRefinement(Intent proposedRefinement) {
+ Intent matchingBase =
+ getAllSourceIntents()
+ .stream()
+ .filter(i -> i.filterEquals(proposedRefinement))
+ .findFirst()
+ .orElse(null);
+ if (matchingBase == null) {
+ return null;
+ }
+
+ Intent merged = new Intent(matchingBase);
+ merged.fillIn(proposedRefinement, 0);
+ return new SelectableTargetInfo(this, merged);
}
@Override
@@ -332,12 +331,7 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
}
@Override
- public boolean start(Activity activity, Bundle options) {
- return mActivityStarter.start(activity, options);
- }
-
- @Override
- public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
+ public boolean startAsCaller(Activity activity, Bundle options, int userId) {
return mActivityStarter.startAsCaller(activity, options, userId);
}
@@ -346,6 +340,12 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
return mActivityStarter.startAsUser(activity, options, user);
}
+ @Nullable
+ @Override
+ public Intent getTargetIntent() {
+ return mBaseIntentToSend;
+ }
+
@Override
public ResolveInfo getResolveInfo() {
return mResolveInfo;
@@ -418,18 +418,14 @@ public final class SelectableTargetInfo extends ChooserTargetInfo {
@Nullable
private static Intent getBaseIntentToSend(
- @Nullable Intent resolvedIntent,
- Intent fillInIntent,
- int fillInFlags,
+ @Nullable Intent providedBase,
+ @Nullable Intent fallbackBase,
Intent referrerFillInIntent) {
- Intent result = resolvedIntent;
+ Intent result = (providedBase != null) ? providedBase : fallbackBase;
if (result == null) {
Log.e(TAG, "ChooserTargetInfo: no base intent available to send");
} else {
result = new Intent(result);
- if (fillInIntent != null) {
- result.fillIn(fillInIntent, fillInFlags);
- }
result.fillIn(referrerFillInIntent, 0);
}
return result;
diff --git a/java/src/com/android/intentresolver/chooser/TargetInfo.java b/java/src/com/android/intentresolver/chooser/TargetInfo.java
index 72dd1b0b..2f48704c 100644
--- a/java/src/com/android/intentresolver/chooser/TargetInfo.java
+++ b/java/src/com/android/intentresolver/chooser/TargetInfo.java
@@ -32,8 +32,6 @@ 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;
@@ -88,6 +86,12 @@ public interface TargetInfo {
Intent getResolvedIntent();
/**
+ * Get the target intent, the one that will be used with one of the <code>start</code> methods.
+ * @return the intent with target will be launced with.
+ */
+ @Nullable Intent getTargetIntent();
+
+ /**
* 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. This may be
@@ -118,24 +122,15 @@ public interface TargetInfo {
}
/**
- * Start the activity referenced by this target.
- *
- * @param activity calling Activity performing the launch
- * @param options ActivityOptions bundle
- * @return true if the start completed successfully
- */
- boolean start(Activity activity, Bundle options);
-
- /**
- * Start the activity referenced by this target as if the ResolverActivity's caller
- * was performing the start operation.
+ * Start the activity referenced by this target as if the Activity's caller was performing the
+ * start operation.
*
* @param activity calling Activity (actually) performing the launch
* @param options ActivityOptions bundle
* @param userId userId to start as or {@link UserHandle#USER_NULL} for activity's caller
* @return true if the start completed successfully
*/
- boolean startAsCaller(ResolverActivity activity, Bundle options, int userId);
+ boolean startAsCaller(Activity activity, Bundle options, int userId);
/**
* Start the activity referenced by this target as a given user.
@@ -187,10 +182,25 @@ public interface TargetInfo {
default boolean hasDisplayIcon() {
return getDisplayIconHolder().getDisplayIcon() != null;
}
+
/**
- * Clone this target with the given fill-in information.
+ * Attempt to apply a {@code proposedRefinement} that the {@link ChooserRefinementManager}
+ * received from the caller's refinement flow. This may succeed only if the target has a source
+ * intent that matches the filtering parameters of the proposed refinement (according to
+ * {@link Intent#filterEquals()}). Then the first such match is the "base intent," and the
+ * proposed refinement is merged into that base (via {@link Intent#fillIn()}; this can never
+ * result in a change to the {@link Intent#filterEquals()} status of the base, but may e.g. add
+ * new "extras" that weren't previously given in the base intent).
+ *
+ * @return a copy of this {@link TargetInfo} where the "base intent to send" is the result of
+ * merging the refinement into the best-matching source intent, if possible. If there is no
+ * suitable match for the proposed refinement, or if merging fails for any other reason, this
+ * returns null.
+ *
+ * @see android.content.Intent#fillIn(Intent, int)
*/
- TargetInfo cloneFilledIn(Intent fillInIntent, int flags);
+ @Nullable
+ TargetInfo tryToCloneWithAppliedRefinement(Intent proposedRefinement);
/**
* @return the list of supported source intents deduped against this single target
diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
new file mode 100644
index 00000000..205be444
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
@@ -0,0 +1,310 @@
+/*
+ * 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.contentpreview;
+
+import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE;
+import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE;
+import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ContentInterface;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.RemoteException;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.ImageLoader;
+import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.widget.ActionRow;
+import com.android.intentresolver.widget.ImagePreviewView;
+import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+import java.util.stream.Collectors;
+
+/**
+ * Collection of helpers for building the content preview UI displayed in
+ * {@link com.android.intentresolver.ChooserActivity}.
+ *
+ * A content preview façade.
+ */
+public final class ChooserContentPreviewUi {
+ /**
+ * Delegate to build the default system action buttons to display in the preview layout, if/when
+ * they're determined to be appropriate for the particular preview we display.
+ * TODO: clarify why action buttons are part of preview logic.
+ */
+ public interface ActionFactory {
+ /** Create an action that copies the share content to the clipboard. */
+ ActionRow.Action createCopyButton();
+
+ /** Create an action that opens the share content in a system-default editor. */
+ @Nullable
+ ActionRow.Action createEditButton();
+
+ /** Create an "Share to Nearby" action. */
+ @Nullable
+ ActionRow.Action createNearbyButton();
+
+ /** Create custom actions */
+ List<ActionRow.Action> createCustomActions();
+
+ /**
+ * Provides a share modification action, if any.
+ */
+ @Nullable
+ Runnable getModifyShareAction();
+
+ /**
+ * <p>
+ * Creates an exclude-text action that can be called when the user changes shared text
+ * status in the Media + Text preview.
+ * </p>
+ * <p>
+ * <code>true</code> argument value indicates that the text should be excluded.
+ * </p>
+ */
+ Consumer<Boolean> getExcludeSharedTextAction();
+ }
+
+ /**
+ * 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 com.android.intentresolver.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);
+ }
+
+ private final ContentPreviewUi mContentPreviewUi;
+
+ public ChooserContentPreviewUi(
+ Intent targetIntent,
+ ContentInterface contentResolver,
+ ImageMimeTypeClassifier imageClassifier,
+ ImageLoader imageLoader,
+ ActionFactory actionFactory,
+ TransitionElementStatusCallback transitionElementStatusCallback,
+ FeatureFlagRepository featureFlagRepository) {
+
+ mContentPreviewUi = createContentPreview(
+ targetIntent,
+ contentResolver,
+ imageClassifier,
+ imageLoader,
+ actionFactory,
+ transitionElementStatusCallback,
+ featureFlagRepository);
+ if (mContentPreviewUi.getType() != CONTENT_PREVIEW_IMAGE) {
+ transitionElementStatusCallback.onAllTransitionElementsReady();
+ }
+ }
+
+ private ContentPreviewUi createContentPreview(
+ Intent targetIntent,
+ ContentInterface contentResolver,
+ ImageMimeTypeClassifier imageClassifier,
+ ImageLoader imageLoader,
+ ActionFactory actionFactory,
+ TransitionElementStatusCallback transitionElementStatusCallback,
+ FeatureFlagRepository featureFlagRepository) {
+ int type = findPreferredContentPreview(targetIntent, contentResolver, imageClassifier);
+ switch (type) {
+ case CONTENT_PREVIEW_TEXT:
+ return createTextPreview(
+ targetIntent, actionFactory, imageLoader, featureFlagRepository);
+
+ case CONTENT_PREVIEW_FILE:
+ return new FileContentPreviewUi(
+ extractContentUris(targetIntent),
+ actionFactory,
+ imageLoader,
+ contentResolver,
+ featureFlagRepository);
+
+ case CONTENT_PREVIEW_IMAGE:
+ return createImagePreview(
+ targetIntent,
+ actionFactory,
+ contentResolver,
+ imageClassifier,
+ imageLoader,
+ transitionElementStatusCallback,
+ featureFlagRepository);
+ }
+
+ return new NoContextPreviewUi(type);
+ }
+
+ public int getPreferredContentPreview() {
+ return mContentPreviewUi.getType();
+ }
+
+ /**
+ * Display a content preview of the specified {@code previewType} to preview the content of the
+ * specified {@code intent}.
+ */
+ public ViewGroup displayContentPreview(
+ Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
+
+ return mContentPreviewUi.display(resources, layoutInflater, parent);
+ }
+
+ /** Determine the most appropriate type of preview to show for the provided {@link Intent}. */
+ @ContentPreviewType
+ private static int findPreferredContentPreview(
+ Intent targetIntent,
+ ContentInterface 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. */
+ final String action = targetIntent.getAction();
+ final String type = targetIntent.getType();
+ final boolean isSend = Intent.ACTION_SEND.equals(action);
+ final boolean isSendMultiple = Intent.ACTION_SEND_MULTIPLE.equals(action);
+
+ if (!(isSend || isSendMultiple)
+ || (type != null && ClipDescription.compareMimeTypes(type, "text/*"))) {
+ return CONTENT_PREVIEW_TEXT;
+ }
+
+ if (isSend) {
+ Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+ return findPreferredContentPreview(uri, resolver, imageClassifier);
+ }
+
+ 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;
+ }
+
+ @ContentPreviewType
+ private static int findPreferredContentPreview(
+ Uri uri, ContentInterface resolver, ImageMimeTypeClassifier imageClassifier) {
+ if (uri == null) {
+ return CONTENT_PREVIEW_TEXT;
+ }
+
+ String mimeType = null;
+ try {
+ mimeType = resolver.getType(uri);
+ } catch (RemoteException ignored) {
+ }
+ return imageClassifier.isImageType(mimeType) ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE;
+ }
+
+ private static TextContentPreviewUi createTextPreview(
+ Intent targetIntent,
+ ChooserContentPreviewUi.ActionFactory actionFactory,
+ ImageLoader imageLoader,
+ FeatureFlagRepository featureFlagRepository) {
+ CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
+ String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE);
+ ClipData previewData = targetIntent.getClipData();
+ Uri previewThumbnail = null;
+ if (previewData != null) {
+ if (previewData.getItemCount() > 0) {
+ ClipData.Item previewDataItem = previewData.getItemAt(0);
+ previewThumbnail = previewDataItem.getUri();
+ }
+ }
+ return new TextContentPreviewUi(
+ sharingText,
+ previewTitle,
+ previewThumbnail,
+ actionFactory,
+ imageLoader,
+ featureFlagRepository);
+ }
+
+ static ImageContentPreviewUi createImagePreview(
+ Intent targetIntent,
+ ChooserContentPreviewUi.ActionFactory actionFactory,
+ ContentInterface contentResolver,
+ ChooserContentPreviewUi.ImageMimeTypeClassifier imageClassifier,
+ ImageLoader imageLoader,
+ ImagePreviewView.TransitionElementStatusCallback transitionElementStatusCallback,
+ FeatureFlagRepository featureFlagRepository) {
+ CharSequence text = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
+ String action = targetIntent.getAction();
+ // TODO: why don't we use image classifier for single-element ACTION_SEND?
+ final List<Uri> imageUris = Intent.ACTION_SEND.equals(action)
+ ? extractContentUris(targetIntent)
+ : extractContentUris(targetIntent)
+ .stream()
+ .filter(uri -> {
+ String type = null;
+ try {
+ type = contentResolver.getType(uri);
+ } catch (RemoteException ignored) {
+ }
+ return imageClassifier.isImageType(type);
+ })
+ .collect(Collectors.toList());
+ return new ImageContentPreviewUi(
+ imageUris,
+ text,
+ actionFactory,
+ imageLoader,
+ transitionElementStatusCallback,
+ featureFlagRepository);
+ }
+
+ private static List<Uri> extractContentUris(Intent targetIntent) {
+ List<Uri> uris = new ArrayList<>();
+ if (Intent.ACTION_SEND.equals(targetIntent.getAction())) {
+ Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+ if (ContentPreviewUi.validForContentPreview(uri)) {
+ uris.add(uri);
+ }
+ } else {
+ List<Uri> receivedUris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ if (receivedUris != null) {
+ for (Uri uri : receivedUris) {
+ if (ContentPreviewUi.validForContentPreview(uri)) {
+ uris.add(uri);
+ }
+ }
+ }
+ }
+ return uris;
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java
new file mode 100644
index 00000000..ebab147d
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewType.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+
+@Retention(SOURCE)
+@IntDef({ContentPreviewType.CONTENT_PREVIEW_FILE,
+ ContentPreviewType.CONTENT_PREVIEW_IMAGE,
+ ContentPreviewType.CONTENT_PREVIEW_TEXT})
+public @interface ContentPreviewType {
+ // Starting at 1 since 0 is considered "undefined" for some of the database transformations
+ // of tron logs.
+ int CONTENT_PREVIEW_IMAGE = 1;
+ int CONTENT_PREVIEW_FILE = 2;
+ int CONTENT_PREVIEW_TEXT = 3;
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
new file mode 100644
index 00000000..39856e66
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview;
+
+import static android.content.ContentProvider.getUserIdFromUri;
+
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.view.animation.DecelerateInterpolator;
+
+import androidx.annotation.LayoutRes;
+
+import com.android.intentresolver.R;
+import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.flags.Flags;
+import com.android.intentresolver.widget.ActionRow;
+import com.android.intentresolver.widget.RoundedRectImageView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+abstract class ContentPreviewUi {
+ private static final int IMAGE_FADE_IN_MILLIS = 150;
+ static final String TAG = "ChooserPreview";
+
+ @ContentPreviewType
+ public abstract int getType();
+
+ public abstract ViewGroup display(
+ Resources resources, LayoutInflater layoutInflater, ViewGroup parent);
+
+ protected static int getActionRowLayout(FeatureFlagRepository featureFlagRepository) {
+ return featureFlagRepository.isEnabled(Flags.SHARESHEET_CUSTOM_ACTIONS)
+ ? R.layout.scrollable_chooser_action_row
+ : R.layout.chooser_action_row;
+ }
+
+ protected static ActionRow inflateActionRow(ViewGroup parent, @LayoutRes int actionRowLayout) {
+ final ViewStub stub = parent.findViewById(com.android.intentresolver.R.id.action_row_stub);
+ if (stub != null) {
+ stub.setLayoutResource(actionRowLayout);
+ stub.inflate();
+ }
+ return parent.findViewById(com.android.internal.R.id.chooser_action_row);
+ }
+
+ protected static List<ActionRow.Action> createActions(
+ List<ActionRow.Action> systemActions,
+ List<ActionRow.Action> customActions,
+ FeatureFlagRepository featureFlagRepository) {
+ ArrayList<ActionRow.Action> actions =
+ new ArrayList<>(systemActions.size() + customActions.size());
+ actions.addAll(systemActions);
+ if (featureFlagRepository.isEnabled(Flags.SHARESHEET_CUSTOM_ACTIONS)) {
+ actions.addAll(customActions);
+ }
+ return actions;
+ }
+
+ /**
+ * Indicate if the incoming content URI should be allowed.
+ *
+ * @param uri the uri to test
+ * @return true if the URI is allowed for content preview
+ */
+ protected static boolean validForContentPreview(Uri uri) throws SecurityException {
+ if (uri == null) {
+ return false;
+ }
+ int userId = getUserIdFromUri(uri, UserHandle.USER_CURRENT);
+ if (userId != UserHandle.USER_CURRENT && userId != UserHandle.myUserId()) {
+ Log.e(ContentPreviewUi.TAG, "dropped invalid content URI belonging to user " + userId);
+ return false;
+ }
+ return true;
+ }
+
+ protected static void updateViewWithImage(RoundedRectImageView imageView, Bitmap image) {
+ if (image == null) {
+ imageView.setVisibility(View.GONE);
+ return;
+ }
+ imageView.setVisibility(View.VISIBLE);
+ imageView.setAlpha(0.0f);
+ imageView.setImageBitmap(image);
+
+ ValueAnimator fadeAnim = ObjectAnimator.ofFloat(imageView, "alpha", 0.0f, 1.0f);
+ fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
+ fadeAnim.setDuration(IMAGE_FADE_IN_MILLIS);
+ fadeAnim.start();
+ }
+
+ protected static void displayPayloadReselectionAction(
+ ViewGroup layout,
+ ChooserContentPreviewUi.ActionFactory actionFactory,
+ FeatureFlagRepository featureFlagRepository) {
+ Runnable modifyShareAction = actionFactory.getModifyShareAction();
+ if (modifyShareAction != null && layout != null
+ && featureFlagRepository.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)) {
+ View modifyShareView = layout.findViewById(R.id.reselection_action);
+ if (modifyShareView != null) {
+ modifyShareView.setVisibility(View.VISIBLE);
+ modifyShareView.setOnClickListener(view -> modifyShareAction.run());
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java
new file mode 100644
index 00000000..7cd71475
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview;
+
+import android.content.ContentInterface;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.RemoteException;
+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.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.LayoutRes;
+
+import com.android.intentresolver.ImageLoader;
+import com.android.intentresolver.R;
+import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.widget.ActionRow;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class FileContentPreviewUi extends ContentPreviewUi {
+ private static final String PLURALS_COUNT = "count";
+ private static final String PLURALS_FILE_NAME = "file_name";
+
+ private final List<Uri> mUris;
+ private final ChooserContentPreviewUi.ActionFactory mActionFactory;
+ private final ImageLoader mImageLoader;
+ private final ContentInterface mContentResolver;
+ private final FeatureFlagRepository mFeatureFlagRepository;
+
+ FileContentPreviewUi(List<Uri> uris,
+ ChooserContentPreviewUi.ActionFactory actionFactory,
+ ImageLoader imageLoader,
+ ContentInterface contentResolver,
+ FeatureFlagRepository featureFlagRepository) {
+ mUris = uris;
+ mActionFactory = actionFactory;
+ mImageLoader = imageLoader;
+ mContentResolver = contentResolver;
+ mFeatureFlagRepository = featureFlagRepository;
+ }
+
+ @Override
+ public int getType() {
+ return ContentPreviewType.CONTENT_PREVIEW_FILE;
+ }
+
+ @Override
+ public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
+ ViewGroup layout = displayInternal(resources, layoutInflater, parent);
+ displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository);
+ return layout;
+ }
+
+ private ViewGroup displayInternal(
+ Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
+ @LayoutRes int actionRowLayout = getActionRowLayout(mFeatureFlagRepository);
+ ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+ R.layout.chooser_grid_preview_file, parent, false);
+
+ final int uriCount = mUris.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;
+ }
+
+ if (uriCount == 1) {
+ loadFileUriIntoView(mUris.get(0), contentPreviewLayout, mImageLoader, mContentResolver);
+ } else {
+ FileInfo fileInfo = extractFileInfo(mUris.get(0), mContentResolver);
+ 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);
+ }
+
+ final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
+ if (actionRow != null) {
+ actionRow.setActions(
+ createActions(
+ createFilePreviewActions(),
+ mActionFactory.createCustomActions(),
+ mFeatureFlagRepository));
+ }
+
+ return contentPreviewLayout;
+ }
+
+ private List<ActionRow.Action> createFilePreviewActions() {
+ List<ActionRow.Action> actions = new ArrayList<>(1);
+ //TODO(b/120417119):
+ // add action buttonFactory.createCopyButton()
+ ActionRow.Action action = mActionFactory.createNearbyButton();
+ if (action != null) {
+ actions.add(action);
+ }
+ return actions;
+ }
+
+ private static void loadFileUriIntoView(
+ final Uri uri,
+ final View parent,
+ final ImageLoader imageLoader,
+ final ContentInterface 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) {
+ imageLoader.loadImage(
+ uri,
+ (bitmap) -> updateViewWithImage(
+ parent.findViewById(
+ com.android.internal.R.id.content_preview_file_thumbnail),
+ bitmap));
+ } else {
+ View thumbnailView = parent.findViewById(
+ com.android.internal.R.id.content_preview_file_thumbnail);
+ thumbnailView.setVisibility(View.GONE);
+
+ ImageView fileIconView = parent.findViewById(
+ com.android.internal.R.id.content_preview_file_icon);
+ fileIconView.setVisibility(View.VISIBLE);
+ fileIconView.setImageResource(R.drawable.chooser_file_generic);
+ }
+ }
+
+ private static FileInfo extractFileInfo(Uri uri, ContentInterface 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) {
+ // 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");
+ }
+
+ if (TextUtils.isEmpty(fileName)) {
+ fileName = uri.getPath();
+ fileName = fileName == null ? "" : fileName;
+ int index = fileName.lastIndexOf('/');
+ if (index != -1) {
+ fileName = fileName.substring(index + 1);
+ }
+ }
+
+ return new FileInfo(fileName, hasThumbnail);
+ }
+
+ private static Cursor queryResolver(ContentInterface resolver, Uri uri) {
+ try {
+ return resolver.query(uri, null, null, null);
+ } catch (RemoteException e) {
+ return null;
+ }
+ }
+
+ private static class FileInfo {
+ public final String name;
+ public final boolean hasThumbnail;
+
+ FileInfo(String name, boolean hasThumbnail) {
+ this.name = name;
+ this.hasThumbnail = hasThumbnail;
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java
new file mode 100644
index 00000000..db26ab1b
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/ImageContentPreviewUi.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview;
+
+import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE;
+
+import android.content.res.Resources;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.text.util.Linkify;
+import android.transition.TransitionManager;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.widget.CheckBox;
+import android.widget.TextView;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.ImageLoader;
+import com.android.intentresolver.R;
+import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.flags.Flags;
+import com.android.intentresolver.widget.ActionRow;
+import com.android.intentresolver.widget.ImagePreviewView;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Consumer;
+
+class ImageContentPreviewUi extends ContentPreviewUi {
+ private final List<Uri> mImageUris;
+ @Nullable
+ private final CharSequence mText;
+ private final ChooserContentPreviewUi.ActionFactory mActionFactory;
+ private final ImageLoader mImageLoader;
+ private final ImagePreviewView.TransitionElementStatusCallback mTransitionElementStatusCallback;
+ private final FeatureFlagRepository mFeatureFlagRepository;
+
+ ImageContentPreviewUi(
+ List<Uri> imageUris,
+ @Nullable CharSequence text,
+ ChooserContentPreviewUi.ActionFactory actionFactory,
+ ImageLoader imageLoader,
+ ImagePreviewView.TransitionElementStatusCallback transitionElementStatusCallback,
+ FeatureFlagRepository featureFlagRepository) {
+ mImageUris = imageUris;
+ mText = text;
+ mActionFactory = actionFactory;
+ mImageLoader = imageLoader;
+ mTransitionElementStatusCallback = transitionElementStatusCallback;
+ mFeatureFlagRepository = featureFlagRepository;
+
+ mImageLoader.prePopulate(mImageUris);
+ }
+
+ @Override
+ public int getType() {
+ return CONTENT_PREVIEW_IMAGE;
+ }
+
+ @Override
+ public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
+ ViewGroup layout = displayInternal(layoutInflater, parent);
+ displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository);
+ return layout;
+ }
+
+ private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) {
+ @LayoutRes int actionRowLayout = getActionRowLayout(mFeatureFlagRepository);
+ ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+ R.layout.chooser_grid_preview_image, parent, false);
+ ImagePreviewView imagePreview = inflateImagePreviewView(contentPreviewLayout);
+
+ final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
+ if (actionRow != null) {
+ actionRow.setActions(
+ createActions(
+ createImagePreviewActions(),
+ mActionFactory.createCustomActions(),
+ mFeatureFlagRepository));
+ }
+
+ if (mImageUris.size() == 0) {
+ Log.i(
+ TAG,
+ "Attempted to display image preview area with zero"
+ + " available images detected in EXTRA_STREAM list");
+ ((View) imagePreview).setVisibility(View.GONE);
+ mTransitionElementStatusCallback.onAllTransitionElementsReady();
+ return contentPreviewLayout;
+ }
+
+ setTextInImagePreviewVisibility(contentPreviewLayout, mActionFactory);
+ imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback);
+ imagePreview.setImages(mImageUris, mImageLoader);
+
+ return contentPreviewLayout;
+ }
+
+ private List<ActionRow.Action> createImagePreviewActions() {
+ ArrayList<ActionRow.Action> actions = new ArrayList<>(2);
+ //TODO: add copy action;
+ ActionRow.Action action = mActionFactory.createNearbyButton();
+ if (action != null) {
+ actions.add(action);
+ }
+ action = mActionFactory.createEditButton();
+ if (action != null) {
+ actions.add(action);
+ }
+ return actions;
+ }
+
+ private ImagePreviewView inflateImagePreviewView(ViewGroup previewLayout) {
+ ViewStub stub = previewLayout.findViewById(R.id.image_preview_stub);
+ if (stub != null) {
+ int layoutId =
+ mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW)
+ ? R.layout.scrollable_image_preview_view
+ : R.layout.chooser_image_preview_view;
+ stub.setLayoutResource(layoutId);
+ stub.inflate();
+ }
+ return previewLayout.findViewById(
+ com.android.internal.R.id.content_preview_image_area);
+ }
+
+ private void setTextInImagePreviewVisibility(
+ ViewGroup contentPreview, ChooserContentPreviewUi.ActionFactory actionFactory) {
+ int visibility = mFeatureFlagRepository.isEnabled(Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW)
+ && !TextUtils.isEmpty(mText)
+ ? View.VISIBLE
+ : View.GONE;
+
+ final TextView textView = contentPreview
+ .requireViewById(com.android.internal.R.id.content_preview_text);
+ CheckBox actionView = contentPreview
+ .requireViewById(R.id.include_text_action);
+ textView.setVisibility(visibility);
+ boolean isLink = visibility == View.VISIBLE && HttpUriMatcher.isHttpUri(mText.toString());
+ textView.setAutoLinkMask(isLink ? Linkify.WEB_URLS : 0);
+ textView.setText(mText);
+
+ if (visibility == View.VISIBLE) {
+ final int[] actionLabels = isLink
+ ? new int[] { R.string.include_link, R.string.exclude_link }
+ : new int[] { R.string.include_text, R.string.exclude_text };
+ final Consumer<Boolean> shareTextAction = actionFactory.getExcludeSharedTextAction();
+ actionView.setChecked(true);
+ actionView.setText(actionLabels[1]);
+ shareTextAction.accept(false);
+ actionView.setOnCheckedChangeListener((view, isChecked) -> {
+ view.setText(actionLabels[isChecked ? 1 : 0]);
+ TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent());
+ textView.setVisibility(isChecked ? View.VISIBLE : View.GONE);
+ shareTextAction.accept(!isChecked);
+ });
+ }
+ actionView.setVisibility(visibility);
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/IsHttpUri.kt b/java/src/com/android/intentresolver/contentpreview/IsHttpUri.kt
new file mode 100644
index 00000000..80232537
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/IsHttpUri.kt
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:JvmName("HttpUriMatcher")
+package com.android.intentresolver.contentpreview
+
+import java.net.URI
+
+internal fun String.isHttpUri() =
+ kotlin.runCatching {
+ URI(this).scheme.takeIf { scheme ->
+ "http".compareTo(scheme, true) == 0 || "https".compareTo(scheme, true) == 0
+ }
+ }.getOrNull() != null
diff --git a/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt b/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt
new file mode 100644
index 00000000..90016932
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/NoContextPreviewUi.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview
+
+import android.content.res.Resources
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.ViewGroup
+
+internal class NoContextPreviewUi(private val type: Int) : ContentPreviewUi() {
+ override fun getType(): Int = type
+
+ override fun display(
+ resources: Resources?, layoutInflater: LayoutInflater?, parent: ViewGroup?
+ ): ViewGroup? {
+ Log.e(TAG, "Unexpected content preview type: $type")
+ return null
+ }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java
new file mode 100644
index 00000000..7901e4cb
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview;
+
+import android.content.res.Resources;
+import android.net.Uri;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.Nullable;
+
+import com.android.intentresolver.ImageLoader;
+import com.android.intentresolver.R;
+import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.widget.ActionRow;
+
+import java.util.ArrayList;
+import java.util.List;
+
+class TextContentPreviewUi extends ContentPreviewUi {
+ @Nullable
+ private final CharSequence mSharingText;
+ @Nullable
+ private final CharSequence mPreviewTitle;
+ @Nullable
+ private final Uri mPreviewThumbnail;
+ private final ImageLoader mImageLoader;
+ private final ChooserContentPreviewUi.ActionFactory mActionFactory;
+ private final FeatureFlagRepository mFeatureFlagRepository;
+
+ TextContentPreviewUi(
+ @Nullable CharSequence sharingText,
+ @Nullable CharSequence previewTitle,
+ @Nullable Uri previewThumbnail,
+ ChooserContentPreviewUi.ActionFactory actionFactory,
+ ImageLoader imageLoader,
+ FeatureFlagRepository featureFlagRepository) {
+ mSharingText = sharingText;
+ mPreviewTitle = previewTitle;
+ mPreviewThumbnail = previewThumbnail;
+ mImageLoader = imageLoader;
+ mActionFactory = actionFactory;
+ mFeatureFlagRepository = featureFlagRepository;
+ }
+
+ @Override
+ public int getType() {
+ return ContentPreviewType.CONTENT_PREVIEW_TEXT;
+ }
+
+ @Override
+ public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) {
+ ViewGroup layout = displayInternal(layoutInflater, parent);
+ displayPayloadReselectionAction(layout, mActionFactory, mFeatureFlagRepository);
+ return layout;
+ }
+
+ private ViewGroup displayInternal(
+ LayoutInflater layoutInflater,
+ ViewGroup parent) {
+ @LayoutRes int actionRowLayout = getActionRowLayout(mFeatureFlagRepository);
+ ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+ R.layout.chooser_grid_preview_text, parent, false);
+
+ final ActionRow actionRow = inflateActionRow(contentPreviewLayout, actionRowLayout);
+ if (actionRow != null) {
+ actionRow.setActions(
+ createActions(
+ createTextPreviewActions(),
+ mActionFactory.createCustomActions(),
+ mFeatureFlagRepository));
+ }
+
+ if (mSharingText == 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(mSharingText);
+ }
+
+ if (TextUtils.isEmpty(mPreviewTitle)) {
+ 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(mPreviewTitle);
+
+ ImageView previewThumbnailView = contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_thumbnail);
+ if (!validForContentPreview(mPreviewThumbnail)) {
+ previewThumbnailView.setVisibility(View.GONE);
+ } else {
+ mImageLoader.loadImage(
+ mPreviewThumbnail,
+ (bitmap) -> updateViewWithImage(
+ contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_thumbnail),
+ bitmap));
+ }
+ }
+
+ return contentPreviewLayout;
+ }
+
+ private List<ActionRow.Action> createTextPreviewActions() {
+ ArrayList<ActionRow.Action> actions = new ArrayList<>(2);
+ actions.add(mActionFactory.createCopyButton());
+ ActionRow.Action nearbyAction = mActionFactory.createNearbyButton();
+ if (nearbyAction != null) {
+ actions.add(nearbyAction);
+ }
+ return actions;
+ }
+}
diff --git a/java/src/com/android/intentresolver/flags/DeviceConfigProxy.kt b/java/src/com/android/intentresolver/flags/DeviceConfigProxy.kt
new file mode 100644
index 00000000..d1494fe7
--- /dev/null
+++ b/java/src/com/android/intentresolver/flags/DeviceConfigProxy.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.flags
+
+import android.provider.DeviceConfig
+import com.android.systemui.flags.ParcelableFlag
+
+internal class DeviceConfigProxy {
+ fun isEnabled(flag: ParcelableFlag<Boolean>): Boolean? {
+ return runCatching {
+ val hasProperty = DeviceConfig.getProperty(flag.namespace, flag.name) != null
+ if (hasProperty) {
+ DeviceConfig.getBoolean(flag.namespace, flag.name, flag.default)
+ } else {
+ null
+ }
+ }.getOrDefault(null)
+ }
+}
diff --git a/java/src/com/android/intentresolver/flags/FeatureFlagRepository.kt b/java/src/com/android/intentresolver/flags/FeatureFlagRepository.kt
new file mode 100644
index 00000000..5b5d769c
--- /dev/null
+++ b/java/src/com/android/intentresolver/flags/FeatureFlagRepository.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.flags
+
+import com.android.systemui.flags.ReleasedFlag
+import com.android.systemui.flags.UnreleasedFlag
+
+interface FeatureFlagRepository {
+ fun isEnabled(flag: UnreleasedFlag): Boolean
+ fun isEnabled(flag: ReleasedFlag): Boolean
+}
diff --git a/java/src/com/android/intentresolver/flags/Flags.kt b/java/src/com/android/intentresolver/flags/Flags.kt
new file mode 100644
index 00000000..f4dbeddb
--- /dev/null
+++ b/java/src/com/android/intentresolver/flags/Flags.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.flags
+
+import com.android.systemui.flags.UnreleasedFlag
+
+// Flag id, name and namespace should be kept in sync with [com.android.systemui.flags.Flags] to
+// make the flags available in the flag flipper app (see go/sysui-flags).
+object Flags {
+ const val SHARESHEET_CUSTOM_ACTIONS_NAME = "sharesheet_custom_actions"
+ const val SHARESHEET_RESELECTION_ACTION_NAME = "sharesheet_reselection_action"
+ const val SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME = "sharesheet_image_text_preview"
+ const val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME = "sharesheet_scrollable_image_preview"
+
+ // TODO(b/266983432) Tracking Bug
+ @JvmField
+ val SHARESHEET_CUSTOM_ACTIONS = unreleasedFlag(
+ 1501, SHARESHEET_CUSTOM_ACTIONS_NAME, teamfood = true
+ )
+
+ // TODO(b/266982749) Tracking Bug
+ @JvmField
+ val SHARESHEET_RESELECTION_ACTION = unreleasedFlag(
+ 1502, SHARESHEET_RESELECTION_ACTION_NAME, teamfood = true
+ )
+
+ // TODO(b/266983474) Tracking Bug
+ @JvmField
+ val SHARESHEET_IMAGE_AND_TEXT_PREVIEW = unreleasedFlag(
+ id = 1503, name = SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME, teamfood = true
+ )
+
+ // TODO(b/267355521) Tracking Bug
+ @JvmField
+ val SHARESHEET_SCROLLABLE_IMAGE_PREVIEW = unreleasedFlag(
+ 1504, SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME, teamfood = true
+ )
+
+ private fun unreleasedFlag(id: Int, name: String, teamfood: Boolean = false) =
+ UnreleasedFlag(id, name, "systemui", teamfood)
+}
diff --git a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
index 271c6f98..ea767568 100644
--- a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
@@ -30,8 +30,8 @@ import android.os.UserHandle;
import android.util.Log;
import com.android.intentresolver.ChooserActivityLogger;
+import com.android.intentresolver.ResolvedComponentInfo;
import com.android.intentresolver.ResolverActivity;
-import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
import java.text.Collator;
import java.util.ArrayList;
diff --git a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
index c6bb2b85..c986ef15 100644
--- a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
@@ -32,7 +32,7 @@ import android.os.UserHandle;
import android.util.Log;
import com.android.intentresolver.ChooserActivityLogger;
-import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
+import com.android.intentresolver.ResolvedComponentInfo;
import java.util.ArrayList;
import java.util.Comparator;
diff --git a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
index 4382f109..0431078c 100644
--- a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
@@ -38,7 +38,7 @@ import android.service.resolver.ResolverTarget;
import android.util.Log;
import com.android.intentresolver.ChooserActivityLogger;
-import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
+import com.android.intentresolver.ResolvedComponentInfo;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java
deleted file mode 100644
index 1cfa2c8d..00000000
--- a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.java
+++ /dev/null
@@ -1,426 +0,0 @@
-/*
- * Copyright (C) 2022 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver.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/ShortcutLoader.kt b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
new file mode 100644
index 00000000..6f7542f1
--- /dev/null
+++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
@@ -0,0 +1,326 @@
+/*
+ * 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.ShortcutInfo
+import android.content.pm.ShortcutManager
+import android.content.pm.ShortcutManager.ShareShortcutInfo
+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.OpenForTesting
+import androidx.annotation.VisibleForTesting
+import androidx.annotation.WorkerThread
+import com.android.intentresolver.chooser.DisplayResolveInfo
+import java.lang.RuntimeException
+import java.util.ArrayList
+import java.util.HashMap
+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.
+ *
+ *
+ * A ShortcutLoader instance can be viewed as a per-profile singleton hot stream of shortcut
+ * updates. The shortcut loading is triggered by the [queryShortcuts],
+ * the processing will happen on the [backgroundExecutor] and the result is delivered
+ * through the [callback] on the [callbackExecutor], the main thread.
+ *
+ *
+ * The current version does not improve on the legacy in a way that it does not guarantee that
+ * each invocation of the [queryShortcuts] 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
+ * [queryShortcuts] may result in two callbacks where shortcuts are
+ * processed against the latest input.
+ *
+ */
+@OpenForTesting
+open class ShortcutLoader @VisibleForTesting constructor(
+ private val context: Context,
+ private val appPredictor: AppPredictorProxy?,
+ private val userHandle: UserHandle,
+ private val isPersonalProfile: Boolean,
+ private val targetIntentFilter: IntentFilter?,
+ private val backgroundExecutor: Executor,
+ private val callbackExecutor: Executor,
+ private val callback: Consumer<Result>
+) {
+ private val shortcutToChooserTargetConverter = ShortcutToChooserTargetConverter()
+ private val userManager = context.getSystemService(Context.USER_SERVICE) as UserManager
+ private val activeRequest = AtomicReference(NO_REQUEST)
+ private val appPredictorCallback = AppPredictor.Callback { onAppPredictorCallback(it) }
+ private var isDestroyed = false
+
+ @MainThread
+ constructor(
+ context: Context,
+ appPredictor: AppPredictor?,
+ userHandle: UserHandle,
+ targetIntentFilter: IntentFilter?,
+ callback: Consumer<Result>
+ ) : this(
+ context,
+ appPredictor?.let { AppPredictorProxy(it) },
+ userHandle, userHandle == UserHandle.of(ActivityManager.getCurrentUser()),
+ targetIntentFilter,
+ AsyncTask.SERIAL_EXECUTOR,
+ context.mainExecutor,
+ callback
+ )
+
+ init {
+ appPredictor?.registerPredictionUpdates(callbackExecutor, appPredictorCallback)
+ }
+
+ /**
+ * Unsubscribe from app predictor if one was provided.
+ */
+ @OpenForTesting
+ @MainThread
+ open fun destroy() {
+ isDestroyed = true
+ appPredictor?.unregisterPredictionUpdates(appPredictorCallback)
+ }
+
+ /**
+ * 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
+ */
+ @OpenForTesting
+ @MainThread
+ open fun queryShortcuts(appTargets: Array<DisplayResolveInfo>) {
+ if (isDestroyed) return
+ activeRequest.set(Request(appTargets))
+ backgroundExecutor.execute { loadShortcuts() }
+ }
+
+ @WorkerThread
+ private fun 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 fun queryDirectShareTargets(skipAppPredictionService: Boolean) {
+ if (!skipAppPredictionService && appPredictor != null) {
+ appPredictor.requestPredictionUpdate()
+ return
+ }
+ // Default to just querying ShortcutManager if AppPredictor not present.
+ if (targetIntentFilter == null) return
+ val shortcuts = queryShortcutManager(targetIntentFilter)
+ sendShareShortcutInfoList(shortcuts, false, null)
+ }
+
+ @WorkerThread
+ private fun queryShortcutManager(targetIntentFilter: IntentFilter): List<ShareShortcutInfo> {
+ val selectedProfileContext = context.createContextAsUser(userHandle, 0 /* flags */)
+ val sm = selectedProfileContext
+ .getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager?
+ val pm = context.createContextAsUser(userHandle, 0 /* flags */).packageManager
+ return sm?.getShareTargets(targetIntentFilter)
+ ?.filter { pm.isPackageEnabled(it.targetComponent.packageName) }
+ ?: emptyList()
+ }
+
+ @WorkerThread
+ private fun onAppPredictorCallback(appPredictorTargets: List<AppTarget>) {
+ if (appPredictorTargets.isEmpty() && shouldQueryDirectShareTargets()) {
+ // APS may be disabled, so try querying targets ourselves.
+ queryDirectShareTargets(true)
+ return
+ }
+ val pm = context.createContextAsUser(userHandle, 0).packageManager
+ val pair = appPredictorTargets.toShortcuts(pm)
+ sendShareShortcutInfoList(pair.shortcuts, true, pair.appTargets)
+ }
+
+ @WorkerThread
+ private fun List<AppTarget>.toShortcuts(pm: PackageManager): ShortcutsAppTargetsPair =
+ fold(
+ ShortcutsAppTargetsPair(ArrayList(size), ArrayList(size))
+ ) { acc, appTarget ->
+ val shortcutInfo = appTarget.shortcutInfo
+ val packageName = appTarget.packageName
+ val className = appTarget.className
+ if (shortcutInfo != null && className != null && pm.isPackageEnabled(packageName)) {
+ (acc.shortcuts as ArrayList<ShareShortcutInfo>).add(
+ ShareShortcutInfo(shortcutInfo, ComponentName(packageName, className))
+ )
+ (acc.appTargets as ArrayList<AppTarget>).add(appTarget)
+ }
+ acc
+ }
+
+ @WorkerThread
+ private fun sendShareShortcutInfoList(
+ shortcuts: List<ShareShortcutInfo>,
+ isFromAppPredictor: Boolean,
+ appPredictorTargets: List<AppTarget>?
+ ) {
+ if (appPredictorTargets != null && appPredictorTargets.size != shortcuts.size) {
+ throw RuntimeException(
+ "resultList and appTargets must have the same size."
+ + " resultList.size()=" + shortcuts.size
+ + " appTargets.size()=" + appPredictorTargets.size
+ )
+ }
+ val directShareAppTargetCache = HashMap<ChooserTarget, AppTarget>()
+ val directShareShortcutInfoCache = HashMap<ChooserTarget, ShortcutInfo>()
+ // 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.
+ val appTargets = activeRequest.get().appTargets
+ val resultRecords: MutableList<ShortcutResultInfo> = ArrayList()
+ for (displayResolveInfo in appTargets) {
+ val matchingShortcuts = shortcuts.filter {
+ it.targetComponent == displayResolveInfo.resolvedComponentName
+ }
+ if (matchingShortcuts.isEmpty()) continue
+ val chooserTargets = shortcutToChooserTargetConverter.convertToChooserTarget(
+ matchingShortcuts,
+ shortcuts,
+ appPredictorTargets,
+ directShareAppTargetCache,
+ directShareShortcutInfoCache
+ )
+ val resultRecord = ShortcutResultInfo(displayResolveInfo, chooserTargets)
+ resultRecords.add(resultRecord)
+ }
+ postReport(
+ Result(
+ isFromAppPredictor,
+ appTargets,
+ resultRecords.toTypedArray(),
+ directShareAppTargetCache,
+ directShareShortcutInfoCache
+ )
+ )
+ }
+
+ private fun postReport(result: Result) = callbackExecutor.execute { report(result) }
+
+ @MainThread
+ private fun report(result: Result) {
+ if (isDestroyed) return
+ callback.accept(result)
+ }
+
+ /**
+ * Returns `false` if `userHandle` is the work profile and it's either
+ * in quiet mode or not running.
+ */
+ private fun shouldQueryDirectShareTargets(): Boolean = isPersonalProfile || isProfileActive
+
+ @get:VisibleForTesting
+ protected val isProfileActive: Boolean
+ get() = userManager.isUserRunning(userHandle)
+ && userManager.isUserUnlocked(userHandle)
+ && !userManager.isQuietModeEnabled(userHandle)
+
+ private class Request(val appTargets: Array<DisplayResolveInfo>)
+
+ /**
+ * Resolved shortcuts with corresponding app targets.
+ */
+ class Result(
+ val isFromAppPredictor: Boolean,
+ /**
+ * Input app targets (see [ShortcutLoader.queryShortcuts] the
+ * shortcuts were process against.
+ */
+ val appTargets: Array<DisplayResolveInfo>,
+ /**
+ * Shortcuts grouped by app target.
+ */
+ val shortcutsByApp: Array<ShortcutResultInfo>,
+ val directShareAppTargetCache: Map<ChooserTarget, AppTarget>,
+ val directShareShortcutInfoCache: Map<ChooserTarget, ShortcutInfo>
+ )
+
+ /**
+ * Shortcuts grouped by app.
+ */
+ class ShortcutResultInfo(
+ val appTarget: DisplayResolveInfo,
+ val shortcuts: List<ChooserTarget?>
+ )
+
+ private class ShortcutsAppTargetsPair(
+ val shortcuts: List<ShareShortcutInfo>,
+ val appTargets: List<AppTarget>?
+ )
+
+ /**
+ * A wrapper around AppPredictor to facilitate unit-testing.
+ */
+ @VisibleForTesting
+ open class AppPredictorProxy internal constructor(private val mAppPredictor: AppPredictor) {
+ /**
+ * [AppPredictor.registerPredictionUpdates]
+ */
+ open fun registerPredictionUpdates(
+ callbackExecutor: Executor, callback: AppPredictor.Callback
+ ) = mAppPredictor.registerPredictionUpdates(callbackExecutor, callback)
+
+ /**
+ * [AppPredictor.unregisterPredictionUpdates]
+ */
+ open fun unregisterPredictionUpdates(callback: AppPredictor.Callback) =
+ mAppPredictor.unregisterPredictionUpdates(callback)
+
+ /**
+ * [AppPredictor.requestPredictionUpdate]
+ */
+ open fun requestPredictionUpdate() = mAppPredictor.requestPredictionUpdate()
+ }
+
+ companion object {
+ private const val TAG = "ShortcutLoader"
+ private val NO_REQUEST = Request(arrayOf())
+
+ private fun PackageManager.isPackageEnabled(packageName: String): Boolean {
+ if (TextUtils.isEmpty(packageName)) {
+ return false
+ }
+ return runCatching {
+ val appInfo = getApplicationInfo(
+ packageName,
+ PackageManager.ApplicationInfoFlags.of(PackageManager.GET_META_DATA.toLong())
+ )
+ appInfo.enabled && (appInfo.flags and ApplicationInfo.FLAG_SUSPENDED) == 0
+ }.getOrDefault(false)
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt
new file mode 100644
index 00000000..ca94a95d
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ChooserImagePreviewView.kt
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.widget
+
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.net.Uri
+import android.util.AttributeSet
+import android.view.LayoutInflater
+import android.view.animation.DecelerateInterpolator
+import android.widget.RelativeLayout
+import androidx.core.view.isVisible
+import com.android.intentresolver.R
+import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
+import com.android.internal.R as IntR
+
+private const val IMAGE_FADE_IN_MILLIS = 150L
+
+class ChooserImagePreviewView : RelativeLayout, ImagePreviewView {
+
+ constructor(context: Context) : this(context, null)
+ constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
+
+ constructor(
+ context: Context, attrs: AttributeSet?, defStyleAttr: Int
+ ) : this(context, attrs, defStyleAttr, 0)
+
+ constructor(
+ context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int
+ ) : super(context, attrs, defStyleAttr, defStyleRes)
+
+ private val coroutineScope = MainScope()
+ private lateinit var mainImage: RoundedRectImageView
+ private lateinit var secondLargeImage: RoundedRectImageView
+ private lateinit var secondSmallImage: RoundedRectImageView
+ private lateinit var thirdImage: RoundedRectImageView
+
+ private var loadImageJob: Job? = null
+ private var transitionStatusElementCallback: TransitionElementStatusCallback? = null
+
+ override fun onFinishInflate() {
+ LayoutInflater.from(context)
+ .inflate(R.layout.chooser_image_preview_view_internals, this, true)
+ mainImage = requireViewById(IntR.id.content_preview_image_1_large)
+ secondLargeImage = requireViewById(IntR.id.content_preview_image_2_large)
+ secondSmallImage = requireViewById(IntR.id.content_preview_image_2_small)
+ thirdImage = requireViewById(IntR.id.content_preview_image_3_small)
+ }
+
+ /**
+ * Specifies a transition animation target readiness callback. The callback will be
+ * invoked once when views preparation is done.
+ * Should be called before [setImages].
+ */
+ override fun setTransitionElementStatusCallback(callback: TransitionElementStatusCallback?) {
+ transitionStatusElementCallback = callback
+ }
+
+ override fun setImages(uris: List<Uri>, imageLoader: ImageLoader) {
+ loadImageJob?.cancel()
+ loadImageJob = coroutineScope.launch {
+ when (uris.size) {
+ 0 -> hideAllViews()
+ 1 -> showOneImage(uris, imageLoader)
+ 2 -> showTwoImages(uris, imageLoader)
+ else -> showThreeImages(uris, imageLoader)
+ }
+ }
+ }
+
+ private fun hideAllViews() {
+ mainImage.isVisible = false
+ secondLargeImage.isVisible = false
+ secondSmallImage.isVisible = false
+ thirdImage.isVisible = false
+ invokeTransitionViewReadyCallback()
+ }
+
+ private suspend fun showOneImage(uris: List<Uri>, imageLoader: ImageLoader) {
+ secondLargeImage.isVisible = false
+ secondSmallImage.isVisible = false
+ thirdImage.isVisible = false
+ showImages(uris, imageLoader, mainImage)
+ }
+
+ private suspend fun showTwoImages(uris: List<Uri>, imageLoader: ImageLoader) {
+ secondSmallImage.isVisible = false
+ thirdImage.isVisible = false
+ showImages(uris, imageLoader, mainImage, secondLargeImage)
+ }
+
+ private suspend fun showThreeImages(uris: List<Uri>, imageLoader: ImageLoader) {
+ secondLargeImage.isVisible = false
+ showImages(uris, imageLoader, mainImage, secondSmallImage, thirdImage)
+ thirdImage.setExtraImageCount(uris.size - 3)
+ }
+
+ private suspend fun showImages(
+ uris: List<Uri>, imageLoader: ImageLoader, vararg views: RoundedRectImageView
+ ) = coroutineScope {
+ for (i in views.indices) {
+ launch {
+ loadImageIntoView(views[i], uris[i], imageLoader)
+ }
+ }
+ }
+
+ private suspend fun loadImageIntoView(
+ view: RoundedRectImageView, uri: Uri, imageLoader: ImageLoader
+ ) {
+ val bitmap = runCatching {
+ imageLoader(uri)
+ }.getOrDefault(null)
+ if (bitmap == null) {
+ view.isVisible = false
+ if (view === mainImage) {
+ invokeTransitionViewReadyCallback()
+ }
+ } else {
+ view.isVisible = true
+ view.setImageBitmap(bitmap)
+
+ view.alpha = 0f
+ ObjectAnimator.ofFloat(view, "alpha", 0.0f, 1.0f).apply {
+ interpolator = DecelerateInterpolator(1.0f)
+ duration = IMAGE_FADE_IN_MILLIS
+ start()
+ }
+ if (view === mainImage && transitionStatusElementCallback != null) {
+ view.waitForPreDraw()
+ invokeTransitionViewReadyCallback()
+ }
+ }
+ }
+
+ private fun invokeTransitionViewReadyCallback() {
+ transitionStatusElementCallback?.apply {
+ if (mainImage.isVisible && mainImage.drawable != null) {
+ mainImage.transitionName?.let { onTransitionElementReady(it) }
+ }
+ onAllTransitionElementsReady()
+ }
+ transitionStatusElementCallback = null
+ }
+}
diff --git a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt
index a37ef954..a166ef27 100644
--- a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt
+++ b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt
@@ -16,163 +16,34 @@
package com.android.intentresolver.widget
-import android.animation.ObjectAnimator
-import android.content.Context
import android.graphics.Bitmap
import android.net.Uri
-import android.util.AttributeSet
-import android.view.LayoutInflater
-import android.view.View
-import android.view.ViewTreeObserver
-import android.view.animation.DecelerateInterpolator
-import android.widget.RelativeLayout
-import androidx.core.view.isVisible
-import com.android.intentresolver.R
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.MainScope
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-import java.util.function.Consumer
-import com.android.internal.R as IntR
-typealias ImageLoader = suspend (Uri) -> Bitmap?
+internal typealias ImageLoader = suspend (Uri) -> Bitmap?
-private const val IMAGE_FADE_IN_MILLIS = 150L
-
-class ImagePreviewView : RelativeLayout {
-
- constructor(context: Context) : this(context, null)
- constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
-
- constructor(
- context: Context, attrs: AttributeSet?, defStyleAttr: Int
- ) : this(context, attrs, defStyleAttr, 0)
-
- constructor(
- context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int
- ) : super(context, attrs, defStyleAttr, defStyleRes)
-
- private val coroutineScope = MainScope()
- private lateinit var mainImage: RoundedRectImageView
- private lateinit var secondLargeImage: RoundedRectImageView
- private lateinit var secondSmallImage: RoundedRectImageView
- private lateinit var thirdImage: RoundedRectImageView
-
- private var loadImageJob: Job? = null
- private var onTransitionViewReadyCallback: Consumer<Boolean>? = null
-
- override fun onFinishInflate() {
- LayoutInflater.from(context).inflate(R.layout.image_preview_view, this, true)
- mainImage = requireViewById(IntR.id.content_preview_image_1_large)
- secondLargeImage = requireViewById(IntR.id.content_preview_image_2_large)
- secondSmallImage = requireViewById(IntR.id.content_preview_image_2_small)
- thirdImage = requireViewById(IntR.id.content_preview_image_3_small)
- }
+interface ImagePreviewView {
+ fun setTransitionElementStatusCallback(callback: TransitionElementStatusCallback?)
+ fun setImages(uris: List<Uri>, imageLoader: ImageLoader)
/**
- * Specifies a transition animation target name and a readiness callback. The callback will be
- * invoked once when the view preparation is done i.e. either when an image is loaded into it
- * and it is laid out (and it is ready to be draw) or image loading has failed.
- * Should be called before [setImages].
- * @param name, transition name
- * @param onViewReady, a callback that will be invoked with `true` if the view is ready to
- * receive transition animation (the image was loaded successfully) and with `false` otherwise.
+ * [ImagePreviewView] progressively prepares views for shared element transition and reports
+ * each successful preparation with [onTransitionElementReady] call followed by
+ * closing [onAllTransitionElementsReady] invocation. Thus the overall invocation pattern is
+ * zero or more [onTransitionElementReady] calls followed by the final
+ * [onAllTransitionElementsReady] call.
*/
- fun setSharedElementTransitionTarget(name: String, onViewReady: Consumer<Boolean>) {
- mainImage.transitionName = name
- onTransitionViewReadyCallback = onViewReady
- }
-
- fun setImages(uris: List<Uri>, imageLoader: ImageLoader) {
- loadImageJob?.cancel()
- loadImageJob = coroutineScope.launch {
- when (uris.size) {
- 0 -> hideAllViews()
- 1 -> showOneImage(uris, imageLoader)
- 2 -> showTwoImages(uris, imageLoader)
- else -> showThreeImages(uris, imageLoader)
- }
- }
- }
-
- private fun hideAllViews() {
- mainImage.isVisible = false
- secondLargeImage.isVisible = false
- secondSmallImage.isVisible = false
- thirdImage.isVisible = false
- invokeTransitionViewReadyCallback(runTransitionAnimation = false)
- }
-
- private suspend fun showOneImage(uris: List<Uri>, imageLoader: ImageLoader) {
- secondLargeImage.isVisible = false
- secondSmallImage.isVisible = false
- thirdImage.isVisible = false
- showImages(uris, imageLoader, mainImage)
- }
-
- private suspend fun showTwoImages(uris: List<Uri>, imageLoader: ImageLoader) {
- secondSmallImage.isVisible = false
- thirdImage.isVisible = false
- showImages(uris, imageLoader, mainImage, secondLargeImage)
- }
-
- private suspend fun showThreeImages(uris: List<Uri>, imageLoader: ImageLoader) {
- secondLargeImage.isVisible = false
- showImages(uris, imageLoader, mainImage, secondSmallImage, thirdImage)
- thirdImage.setExtraImageCount(uris.size - 3)
- }
-
- private suspend fun showImages(
- uris: List<Uri>, imageLoader: ImageLoader, vararg views: RoundedRectImageView
- ) = coroutineScope {
- for (i in views.indices) {
- launch {
- loadImageIntoView(views[i], uris[i], imageLoader)
- }
- }
- }
-
- private suspend fun loadImageIntoView(
- view: RoundedRectImageView, uri: Uri, imageLoader: ImageLoader
- ) {
- val bitmap = runCatching {
- imageLoader(uri)
- }.getOrDefault(null)
- if (bitmap == null) {
- view.isVisible = false
- if (view === mainImage) {
- invokeTransitionViewReadyCallback(runTransitionAnimation = false)
- }
- } else {
- view.isVisible = true
- view.setImageBitmap(bitmap)
-
- view.alpha = 0f
- ObjectAnimator.ofFloat(view, "alpha", 0.0f, 1.0f).apply {
- interpolator = DecelerateInterpolator(1.0f)
- duration = IMAGE_FADE_IN_MILLIS
- start()
- }
- if (view === mainImage && onTransitionViewReadyCallback != null) {
- setupPreDrawListener(mainImage)
- }
- }
- }
-
- private fun setupPreDrawListener(view: View) {
- view.viewTreeObserver.addOnPreDrawListener(
- object : ViewTreeObserver.OnPreDrawListener {
- override fun onPreDraw(): Boolean {
- view.viewTreeObserver.removeOnPreDrawListener(this)
- invokeTransitionViewReadyCallback(runTransitionAnimation = true)
- return true
- }
- }
- )
- }
-
- private fun invokeTransitionViewReadyCallback(runTransitionAnimation: Boolean) {
- onTransitionViewReadyCallback?.accept(runTransitionAnimation)
- onTransitionViewReadyCallback = null
+ interface TransitionElementStatusCallback {
+ /**
+ * Invoked when a view for a shared transition animation element is ready i.e. the image
+ * is loaded and the view is laid out.
+ * @param name shared element name.
+ */
+ fun onTransitionElementReady(name: String)
+
+ /**
+ * Indicates that all supported transition elements have been reported with
+ * [onTransitionElementReady].
+ */
+ fun onAllTransitionElementsReady()
}
}
diff --git a/java/src/com/android/intentresolver/widget/RecyclerViewExtensions.kt b/java/src/com/android/intentresolver/widget/RecyclerViewExtensions.kt
new file mode 100644
index 00000000..a7906001
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/RecyclerViewExtensions.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.widget
+
+import android.view.View
+import androidx.recyclerview.widget.RecyclerView
+
+internal val RecyclerView.areAllChildrenVisible: Boolean
+ get() {
+ val count = getChildCount()
+ if (count == 0) return true
+ val first = getChildAt(0)
+ val last = getChildAt(count - 1)
+ val itemCount = adapter?.itemCount ?: 0
+ return getChildAdapterPosition(first) == 0
+ && getChildAdapterPosition(last) == itemCount - 1
+ && isFullyVisible(first)
+ && isFullyVisible(last)
+ }
+
+private fun RecyclerView.isFullyVisible(view: View): Boolean =
+ view.left >= paddingLeft && view.right <= width - paddingRight
diff --git a/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt b/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt
index a941b97a..f2a8b9e8 100644
--- a/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt
+++ b/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt
@@ -50,21 +50,6 @@ class ScrollableActionRow : RecyclerView, ActionRow {
)
}
- private val areAllChildrenVisible: Boolean
- get() {
- val count = getChildCount()
- if (count == 0) return true
- val first = getChildAt(0)
- val last = getChildAt(count - 1)
- return getChildAdapterPosition(first) == 0
- && getChildAdapterPosition(last) == actionsAdapter.itemCount - 1
- && isFullyVisible(first)
- && isFullyVisible(last)
- }
-
- private fun isFullyVisible(view: View): Boolean =
- view.left >= paddingLeft && view.right <= width - paddingRight
-
private class Adapter(private val context: Context) : RecyclerView.Adapter<ViewHolder>() {
private val iconSize: Int =
context.resources.getDimensionPixelSize(R.dimen.chooser_action_view_icon_size)
@@ -103,11 +88,12 @@ class ScrollableActionRow : RecyclerView, ActionRow {
) : RecyclerView.ViewHolder(view) {
fun bind(action: ActionRow.Action) {
- if (action.icon != null) {
- action.icon.setBounds(0, 0, iconSize, iconSize)
+ action.icon?.let { icon ->
+ icon.setBounds(0, 0, iconSize, iconSize)
// some drawables (edit) does not gets tinted when set to the top of the text
// with TextView#setCompoundDrawableRelative
- view.setCompoundDrawablesRelative(null, action.icon, null, null)
+ tintIcon(icon, view)
+ view.setCompoundDrawablesRelative(null, icon, null, null)
}
view.text = action.label ?: ""
view.setOnClickListener {
diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
new file mode 100644
index 00000000..467c404a
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.widget
+
+import android.content.Context
+import android.graphics.Rect
+import android.net.Uri
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import com.android.intentresolver.R
+import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.MainScope
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.plus
+
+private const val TRANSITION_NAME = "screenshot_preview_image"
+
+class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
+ constructor(context: Context) : this(context, null)
+ constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
+ constructor(
+ context: Context, attrs: AttributeSet?, defStyleAttr: Int
+ ) : super(context, attrs, defStyleAttr) {
+ layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
+ adapter = Adapter(context)
+ val spacing = TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP, 5f, context.resources.displayMetrics
+ ).toInt()
+ addItemDecoration(SpacingDecoration(spacing))
+ }
+
+ private val previewAdapter get() = adapter as Adapter
+
+ override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
+ super.onLayout(changed, l, t, r, b)
+ setOverScrollMode(
+ if (areAllChildrenVisible) View.OVER_SCROLL_NEVER else View.OVER_SCROLL_ALWAYS
+ )
+ }
+
+ override fun setTransitionElementStatusCallback(callback: TransitionElementStatusCallback?) {
+ previewAdapter.transitionStatusElementCallback = callback
+ }
+
+ override fun setImages(uris: List<Uri>, imageLoader: ImageLoader) {
+ previewAdapter.setImages(uris, imageLoader)
+ }
+
+ private class Adapter(private val context: Context) : RecyclerView.Adapter<ViewHolder>() {
+ private val uris = ArrayList<Uri>()
+ private var imageLoader: ImageLoader? = null
+ var transitionStatusElementCallback: TransitionElementStatusCallback? = null
+
+ fun setImages(uris: List<Uri>, imageLoader: ImageLoader) {
+ this.uris.clear()
+ this.uris.addAll(uris)
+ this.imageLoader = imageLoader
+ notifyDataSetChanged()
+ }
+
+ override fun onCreateViewHolder(parent: ViewGroup, itemType: Int): ViewHolder {
+ return ViewHolder(
+ LayoutInflater.from(context)
+ .inflate(R.layout.image_preview_image_item, parent, false)
+ )
+ }
+
+ override fun getItemCount(): Int = uris.size
+
+ override fun onBindViewHolder(vh: ViewHolder, position: Int) {
+ vh.bind(
+ uris[position],
+ imageLoader ?: error("ImageLoader is missing"),
+ if (position == 0 && transitionStatusElementCallback != null) {
+ this::onTransitionElementReady
+ } else {
+ null
+ }
+ )
+ }
+
+ override fun onViewRecycled(vh: ViewHolder) {
+ vh.unbind()
+ }
+
+ override fun onFailedToRecycleView(vh: ViewHolder): Boolean {
+ vh.unbind()
+ return super.onFailedToRecycleView(vh)
+ }
+
+ private fun onTransitionElementReady(name: String) {
+ transitionStatusElementCallback?.apply {
+ onTransitionElementReady(name)
+ onAllTransitionElementsReady()
+ }
+ transitionStatusElementCallback = null
+ }
+ }
+
+ private class ViewHolder(view: View) : RecyclerView.ViewHolder(view) {
+ private val image = view.requireViewById<ImageView>(R.id.image)
+ private var scope: CoroutineScope? = null
+
+ fun bind(
+ uri: Uri,
+ imageLoader: ImageLoader,
+ previewReadyCallback: ((String) -> Unit)?
+ ) {
+ image.setImageDrawable(null)
+ image.transitionName = if (previewReadyCallback != null) {
+ TRANSITION_NAME
+ } else {
+ null
+ }
+ resetScope().launch {
+ loadImage(uri, imageLoader, previewReadyCallback)
+ }
+ }
+
+ private suspend fun loadImage(
+ uri: Uri,
+ imageLoader: ImageLoader,
+ previewReadyCallback: ((String) -> Unit)?
+ ) {
+ val bitmap = runCatching {
+ // it's expected for all loading/caching optimizations to be implemented by the
+ // loader
+ imageLoader(uri)
+ }.getOrNull()
+ image.setImageBitmap(bitmap)
+ previewReadyCallback?.let { callback ->
+ image.waitForPreDraw()
+ callback(TRANSITION_NAME)
+ }
+ }
+
+ private fun resetScope(): CoroutineScope =
+ (MainScope() + Dispatchers.Main.immediate).also {
+ scope?.cancel()
+ scope = it
+ }
+
+ fun unbind() {
+ scope?.cancel()
+ scope = null
+ }
+ }
+
+ private class SpacingDecoration(private val margin: Int) : RecyclerView.ItemDecoration() {
+ override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State) {
+ outRect.set(margin, 0, margin, 0)
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/widget/ViewExtensions.kt b/java/src/com/android/intentresolver/widget/ViewExtensions.kt
new file mode 100644
index 00000000..11b7c146
--- /dev/null
+++ b/java/src/com/android/intentresolver/widget/ViewExtensions.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.widget
+
+import android.util.Log
+import android.view.View
+import androidx.core.view.OneShotPreDrawListener
+import kotlinx.coroutines.suspendCancellableCoroutine
+import java.util.concurrent.atomic.AtomicBoolean
+
+internal suspend fun View.waitForPreDraw(): Unit = suspendCancellableCoroutine { continuation ->
+ val isResumed = AtomicBoolean(false)
+ val callback = OneShotPreDrawListener.add(
+ this,
+ Runnable {
+ if (isResumed.compareAndSet(false, true)) {
+ continuation.resumeWith(Result.success(Unit))
+ } else {
+ // it's not really expected but in some unknown corner-case let's not crash
+ Log.e("waitForPreDraw", "An attempt to resume a completed coroutine", Exception())
+ }
+ }
+ )
+ continuation.invokeOnCancellation { callback.removeListener() }
+}
diff --git a/java/tests/Android.bp b/java/tests/Android.bp
index 2913d128..4e835ec8 100644
--- a/java/tests/Android.bp
+++ b/java/tests/Android.bp
@@ -21,11 +21,16 @@ android_test {
"IntentResolver-core",
"androidx.test.rules",
"androidx.test.ext.junit",
+ "androidx.test.espresso.contrib",
"mockito-target-minus-junit4",
"androidx.test.espresso.core",
+ "androidx.lifecycle_lifecycle-common-java8",
+ "androidx.lifecycle_lifecycle-extensions",
+ "androidx.lifecycle_lifecycle-runtime-ktx",
"truth-prebuilt",
"testables",
"testng",
+ "kotlinx_coroutines_test",
],
test_suites: ["general-tests"],
sdk_version: "core_platform",
diff --git a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt
new file mode 100644
index 00000000..af134fcd
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.app.Activity
+import android.app.PendingIntent
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.res.Resources
+import android.graphics.drawable.Icon
+import android.service.chooser.ChooserAction
+import android.view.View
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.intentresolver.flags.FeatureFlagRepository
+import com.android.intentresolver.flags.Flags
+import com.google.common.collect.ImmutableList
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Assert.assertEquals
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito
+import java.util.concurrent.Callable
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+import java.util.function.Consumer
+
+@RunWith(AndroidJUnit4::class)
+class ChooserActionFactoryTest {
+ private val context = InstrumentationRegistry.getInstrumentation().getContext()
+
+ private val logger = mock<ChooserActivityLogger>()
+ private val flags = mock<FeatureFlagRepository>()
+ private val actionLabel = "Action label"
+ private val testAction = "com.android.intentresolver.testaction"
+ private val countdown = CountDownLatch(1)
+ private val testReceiver: BroadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent) {
+ // Just doing at most a single countdown per test.
+ countdown.countDown()
+ }
+ }
+ private object resultConsumer : Consumer<Int> {
+ var latestReturn = Integer.MIN_VALUE
+
+ override fun accept(resultCode: Int) {
+ latestReturn = resultCode
+ }
+
+ }
+
+ @Before
+ fun setup() {
+ whenever(flags.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)).thenReturn(true)
+ context.registerReceiver(testReceiver, IntentFilter(testAction))
+ }
+
+ @After
+ fun teardown() {
+ context.unregisterReceiver(testReceiver)
+ }
+
+ @Test
+ fun testCreateCustomActions() {
+ val factory = createFactory()
+
+ val customActions = factory.createCustomActions()
+
+ assertThat(customActions.size).isEqualTo(1)
+ assertThat(customActions[0].label).isEqualTo(actionLabel)
+
+ // click it
+ customActions[0].onClicked.run()
+
+ Mockito.verify(logger).logCustomActionSelected(eq(0))
+ assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn)
+ // Verify the pendingintent has been called
+ countdown.await(500, TimeUnit.MILLISECONDS)
+ }
+
+ @Test
+ fun testNoModifyShareAction() {
+ val factory = createFactory(includeModifyShare = false)
+
+ assertThat(factory.modifyShareAction).isNull()
+ }
+
+ @Test
+ fun testNoModifyShareAction_flagDisabled() {
+ whenever(flags.isEnabled(Flags.SHARESHEET_RESELECTION_ACTION)).thenReturn(false)
+ val factory = createFactory(includeModifyShare = true)
+
+ assertThat(factory.modifyShareAction).isNull()
+ }
+
+ @Test
+ fun testModifyShareAction() {
+ val factory = createFactory(includeModifyShare = true)
+
+ factory.modifyShareAction!!.run()
+
+ Mockito.verify(logger).logActionSelected(
+ eq(ChooserActivityLogger.SELECTION_TYPE_MODIFY_SHARE))
+ assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn)
+ // Verify the pendingintent has been called
+ countdown.await(500, TimeUnit.MILLISECONDS)
+ }
+
+ private fun createFactory(includeModifyShare: Boolean = false): ChooserActionFactory {
+ val testPendingIntent = PendingIntent.getActivity(context, 0, Intent(testAction),0)
+ val targetIntent = Intent()
+ val action = ChooserAction.Builder(
+ Icon.createWithResource("", Resources.ID_NULL),
+ actionLabel,
+ testPendingIntent
+ ).build()
+ val chooserRequest = mock<ChooserRequestParameters>()
+ whenever(chooserRequest.targetIntent).thenReturn(targetIntent)
+ whenever(chooserRequest.chooserActions).thenReturn(ImmutableList.of(action))
+
+ if (includeModifyShare) {
+ whenever(chooserRequest.modifyShareAction).thenReturn(testPendingIntent)
+ }
+
+ return ChooserActionFactory(
+ context,
+ chooserRequest,
+ flags,
+ mock<ChooserIntegratedDeviceComponents>(),
+ logger,
+ Consumer<Boolean>{},
+ Callable<View?>{null},
+ mock<ChooserActionFactory.ActionActivityStarter>(),
+ resultConsumer)
+ }
+} \ No newline at end of file
diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java
index 705a3228..aa42c24c 100644
--- a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java
+++ b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java
@@ -36,6 +36,7 @@ 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.intentresolver.contentpreview.ContentPreviewType;
import com.android.internal.logging.InstanceId;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
@@ -112,24 +113,26 @@ public final class ChooserActivityLoggerTest {
@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 int previewType = ContentPreviewType.CONTENT_PREVIEW_FILE;
final String intentAction = Intent.ACTION_SENDTO;
+ final int numCustomActions = 3;
+ final boolean modifyShareProvided = true;
mChooserLogger.logShareStarted(
- eventId,
packageName,
mimeType,
appProvidedDirectTargets,
appProvidedAppTargets,
workProfile,
previewType,
- intentAction);
+ intentAction,
+ numCustomActions,
+ modifyShareProvided);
verify(mFrameworkLog).write(
eq(FrameworkStatsLog.SHARESHEET_STARTED),
@@ -141,7 +144,9 @@ public final class ChooserActivityLoggerTest {
eq(appProvidedAppTargets),
eq(workProfile),
eq(FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_FILE),
- eq(FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_SENDTO));
+ eq(FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_SENDTO),
+ /* custom actions provided */ eq(numCustomActions),
+ /* reselection action provided */ eq(modifyShareProvided));
}
@Test
@@ -203,6 +208,17 @@ public final class ChooserActivityLoggerTest {
}
@Test
+ public void testLogCustomActionSelected() {
+ final int position = 4;
+ mChooserLogger.logCustomActionSelected(position);
+
+ verify(mFrameworkLog).write(
+ eq(FrameworkStatsLog.RANKING_SELECTED),
+ eq(SharesheetTargetSelectedEvent.SHARESHEET_CUSTOM_ACTION_SELECTED.getId()),
+ any(), anyInt(), eq(position), eq(false));
+ }
+
+ @Test
public void testLogDirectShareTargetReceived() {
final int category = MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER;
final int latency = 123;
@@ -218,7 +234,7 @@ public final class ChooserActivityLoggerTest {
@Test
public void testLogActionShareWithPreview() {
- final int previewType = ChooserContentPreviewUi.CONTENT_PREVIEW_TEXT;
+ final int previewType = ContentPreviewType.CONTENT_PREVIEW_TEXT;
mChooserLogger.logActionShareWithPreview(previewType);
diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
index 5df0d4a2..f0c459e5 100644
--- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
+++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
@@ -29,8 +29,8 @@ 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.flags.FeatureFlagRepository;
import com.android.intentresolver.shortcuts.ShortcutLoader;
import java.util.function.Consumer;
@@ -58,8 +58,8 @@ public class ChooserActivityOverrideData {
public Function<TargetInfo, Boolean> onSafelyStartCallback;
public Function2<UserHandle, Consumer<ShortcutLoader.Result>, ShortcutLoader>
shortcutLoaderFactory = (userHandle, callback) -> null;
- public ResolverListController resolverListController;
- public ResolverListController workResolverListController;
+ public ChooserActivity.ChooserListController resolverListController;
+ public ChooserActivity.ChooserListController workResolverListController;
public Boolean isVoiceInteraction;
public boolean isImageType;
public Cursor resolverCursor;
@@ -72,10 +72,11 @@ public class ChooserActivityOverrideData {
public boolean hasCrossProfileIntents;
public boolean isQuietModeEnabled;
public Integer myUserId;
- public QuietModeManager mQuietModeManager;
+ public WorkProfileAvailabilityManager mWorkProfileAvailability;
public MyUserIdProvider mMyUserIdProvider;
public CrossProfileIntentsChecker mCrossProfileIntentsChecker;
public PackageManager packageManager;
+ public FeatureFlagRepository featureFlagRepository;
public void reset() {
onSafelyStartCallback = null;
@@ -85,8 +86,8 @@ public class ChooserActivityOverrideData {
isImageType = false;
resolverCursor = null;
resolverForceException = false;
- resolverListController = mock(ResolverListController.class);
- workResolverListController = mock(ResolverListController.class);
+ resolverListController = mock(ChooserActivity.ChooserListController.class);
+ workResolverListController = mock(ChooserActivity.ChooserListController.class);
chooserActivityLogger = mock(ChooserActivityLogger.class);
alternateProfileSetting = 0;
resources = null;
@@ -95,23 +96,26 @@ public class ChooserActivityOverrideData {
isQuietModeEnabled = false;
myUserId = null;
packageManager = null;
- mQuietModeManager = new QuietModeManager() {
+ mWorkProfileAvailability = new WorkProfileAvailabilityManager(null, null, null) {
@Override
- public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) {
+ public boolean isQuietModeEnabled() {
return isQuietModeEnabled;
}
@Override
- public void requestQuietModeEnabled(boolean enabled,
- UserHandle workProfileUserHandle) {
- isQuietModeEnabled = enabled;
+ public boolean isWorkProfileUserUnlocked() {
+ return true;
}
@Override
- public void markWorkProfileEnabledBroadcastReceived() {
+ public void requestQuietModeEnabled(boolean enabled) {
+ isQuietModeEnabled = enabled;
}
@Override
+ public void markWorkProfileEnabledBroadcastReceived() {}
+
+ @Override
public boolean isWaitingToEnableWorkProfile() {
return false;
}
@@ -128,6 +132,7 @@ public class ChooserActivityOverrideData {
mCrossProfileIntentsChecker = mock(CrossProfileIntentsChecker.class);
when(mCrossProfileIntentsChecker.hasCrossProfileIntents(any(), anyInt(), anyInt()))
.thenAnswer(invocation -> hasCrossProfileIntents);
+ featureFlagRepository = null;
}
private ChooserActivityOverrideData() {}
diff --git a/java/tests/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt b/java/tests/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt
new file mode 100644
index 00000000..9a5dabdb
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ChooserIntegratedDeviceComponentsTest.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.content.ComponentName
+import android.provider.Settings
+import android.testing.TestableContext
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class ChooserIntegratedDeviceComponentsTest {
+ private val secureSettings = mock<SecureSettings>()
+ private val testableContext =
+ TestableContext(InstrumentationRegistry.getInstrumentation().getContext())
+
+ @Test
+ fun testEditorAndNearby() {
+ val resources = testableContext.getOrCreateTestableResources()
+
+ resources.addOverride(R.string.config_systemImageEditor, "")
+ resources.addOverride(R.string.config_defaultNearbySharingComponent, "")
+
+ var components = ChooserIntegratedDeviceComponents.get(testableContext, secureSettings)
+
+ assertThat(components.editSharingComponent).isNull()
+ assertThat(components.nearbySharingComponent).isNull()
+
+ val editor = ComponentName.unflattenFromString("com.android/com.android.Editor")
+ val nearby = ComponentName.unflattenFromString("com.android/com.android.nearby")
+
+ resources.addOverride(R.string.config_systemImageEditor, editor?.flattenToString())
+ resources.addOverride(
+ R.string.config_defaultNearbySharingComponent, nearby?.flattenToString())
+
+ components = ChooserIntegratedDeviceComponents.get(testableContext, secureSettings)
+
+ assertThat(components.editSharingComponent).isEqualTo(editor)
+ assertThat(components.nearbySharingComponent).isEqualTo(nearby)
+
+ val anotherNearby =
+ ComponentName.unflattenFromString("com.android/com.android.another_nearby")
+ whenever(
+ secureSettings.getString(
+ any(),
+ eq(Settings.Secure.NEARBY_SHARING_COMPONENT)
+ )
+ ).thenReturn(anotherNearby?.flattenToString())
+
+ components = ChooserIntegratedDeviceComponents.get(testableContext, secureSettings)
+
+ assertThat(components.nearbySharingComponent).isEqualTo(anotherNearby)
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.kt b/java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.kt
new file mode 100644
index 00000000..50c37c7f
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ChooserRefinementManagerTest.kt
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.content.Context
+import android.content.Intent
+import android.content.IntentSender
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.intentresolver.chooser.TargetInfo
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Mockito
+import java.util.function.Consumer
+import org.junit.Assert.assertEquals
+
+@RunWith(AndroidJUnit4::class)
+class ChooserRefinementManagerTest {
+ @Test
+ fun testMaybeHandleSelection() {
+ val intentSender = mock<IntentSender>()
+ val refinementManager = ChooserRefinementManager(
+ mock<Context>(),
+ intentSender,
+ Consumer<TargetInfo>{},
+ Runnable{})
+
+ val intents = listOf(Intent(Intent.ACTION_VIEW), Intent(Intent.ACTION_EDIT))
+ val targetInfo = mock<TargetInfo>{
+ whenever(allSourceIntents).thenReturn(intents)
+ }
+
+ refinementManager.maybeHandleSelection(targetInfo)
+
+ val intentCaptor = ArgumentCaptor.forClass(Intent::class.java)
+ Mockito.verify(intentSender).sendIntent(
+ any(), eq(0), intentCaptor.capture(), eq(null), eq(null))
+
+ val intent = intentCaptor.value
+ assertEquals(intents[0], intent.getParcelableExtra(Intent.EXTRA_INTENT, Intent::class.java))
+
+ val alternates =
+ intent.getParcelableArrayExtra(Intent.EXTRA_ALTERNATE_INTENTS, Intent::class.java)
+ assertEquals(1, alternates?.size)
+ assertEquals(intents[1], alternates?.get(0))
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
index 97de97f5..d4ae666b 100644
--- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
+++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
@@ -16,8 +16,6 @@
package com.android.intentresolver;
-import static org.mockito.Mockito.when;
-
import android.annotation.Nullable;
import android.app.prediction.AppPredictor;
import android.app.usage.UsageStatsManager;
@@ -30,17 +28,14 @@ import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.res.Resources;
import android.database.Cursor;
-import android.graphics.Bitmap;
import android.net.Uri;
import android.os.UserHandle;
-import android.util.Size;
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.MyUserIdProvider;
-import com.android.intentresolver.AbstractMultiProfilePagerAdapter.QuietModeManager;
import com.android.intentresolver.chooser.DisplayResolveInfo;
-import com.android.intentresolver.chooser.NotSelectableTargetInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.flags.FeatureFlagRepository;
import com.android.intentresolver.grid.ChooserGridAdapter;
import com.android.intentresolver.shortcuts.ShortcutLoader;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -121,15 +116,13 @@ public class ChooserWrapperActivity
}
@Override
- protected ComponentName getNearbySharingComponent() {
- // an arbitrary pre-installed activity that handles this type of intent
- return ComponentName.unflattenFromString("com.google.android.apps.messaging/"
- + "com.google.android.apps.messaging.ui.conversationlist.ShareIntentActivity");
- }
-
- @Override
- protected TargetInfo getNearbySharingTarget(Intent originalIntent) {
- return NotSelectableTargetInfo.newEmptyTargetInfo();
+ protected ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() {
+ return new ChooserIntegratedDeviceComponents(
+ /* editSharingComponent=*/ null,
+ // An arbitrary pre-installed activity that handles this type of intent:
+ /* nearbySharingComponent=*/ new ComponentName(
+ "com.google.android.apps.messaging",
+ ".ui.conversationlist.ShareIntentActivity"));
}
@Override
@@ -165,15 +158,15 @@ public class ChooserWrapperActivity
}
@Override
- protected QuietModeManager createQuietModeManager() {
- if (sOverrides.mQuietModeManager != null) {
- return sOverrides.mQuietModeManager;
+ protected WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() {
+ if (sOverrides.mWorkProfileAvailability != null) {
+ return sOverrides.mWorkProfileAvailability;
}
- return super.createQuietModeManager();
+ return super.createWorkProfileAvailabilityManager();
}
@Override
- public void safelyStartActivity(com.android.intentresolver.chooser.TargetInfo cti) {
+ public void safelyStartActivity(TargetInfo cti) {
if (sOverrides.onSafelyStartCallback != null
&& sOverrides.onSafelyStartCallback.apply(cti)) {
return;
@@ -182,12 +175,10 @@ public class ChooserWrapperActivity
}
@Override
- protected ResolverListController createListController(UserHandle userHandle) {
+ protected ChooserListController 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;
}
@@ -208,11 +199,10 @@ public class ChooserWrapperActivity
}
@Override
- protected Bitmap loadThumbnail(Uri uri, Size size) {
- if (sOverrides.previewThumbnail != null) {
- return sOverrides.previewThumbnail;
- }
- return super.loadThumbnail(uri, size);
+ protected ImageLoader createPreviewImageLoader() {
+ return new TestPreviewImageLoader(
+ super.createPreviewImageLoader(),
+ () -> sOverrides.previewThumbnail);
}
@Override
@@ -290,4 +280,12 @@ public class ChooserWrapperActivity
return super.createShortcutLoader(
context, appPredictor, userHandle, targetIntentFilter, callback);
}
+
+ @Override
+ protected FeatureFlagRepository createFeatureFlagRepository() {
+ if (sOverrides.featureFlagRepository != null) {
+ return sOverrides.featureFlagRepository;
+ }
+ return super.createFeatureFlagRepository();
+ }
}
diff --git a/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt b/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt
new file mode 100644
index 00000000..9ea9dfa7
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.content.res.Resources
+import android.view.View
+import android.view.Window
+import androidx.activity.ComponentActivity
+import androidx.lifecycle.Lifecycle
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+private const val TIMEOUT_MS = 200
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class EnterTransitionAnimationDelegateTest {
+ private val elementName = "shared-element"
+ private val scheduler = TestCoroutineScheduler()
+ private val dispatcher = StandardTestDispatcher(scheduler)
+ private val lifecycleOwner = TestLifecycleOwner()
+
+ private val transitionTargetView = mock<View> {
+ // avoid the request-layout path in the delegate
+ whenever(isInLayout).thenReturn(true)
+ }
+
+ private val windowMock = mock<Window>()
+ private val resourcesMock = mock<Resources> {
+ whenever(getInteger(anyInt())).thenReturn(TIMEOUT_MS)
+ }
+ private val activity = mock<ComponentActivity> {
+ whenever(lifecycle).thenReturn(lifecycleOwner.lifecycle)
+ whenever(resources).thenReturn(resourcesMock)
+ whenever(isActivityTransitionRunning).thenReturn(true)
+ whenever(window).thenReturn(windowMock)
+ }
+
+ private val testSubject = EnterTransitionAnimationDelegate(activity) {
+ transitionTargetView
+ }
+
+ @Before
+ fun setup() {
+ Dispatchers.setMain(dispatcher)
+ lifecycleOwner.state = Lifecycle.State.CREATED
+ }
+
+ @After
+ fun cleanup() {
+ lifecycleOwner.state = Lifecycle.State.DESTROYED
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun test_postponeTransition_timeout() {
+ testSubject.postponeTransition()
+ testSubject.markOffsetCalculated()
+
+ scheduler.advanceTimeBy(TIMEOUT_MS + 1L)
+ verify(activity, times(1)).startPostponedEnterTransition()
+ verify(windowMock, never()).setWindowAnimations(anyInt())
+ }
+
+ @Test
+ fun test_postponeTransition_animation_resumes_only_once() {
+ testSubject.postponeTransition()
+ testSubject.markOffsetCalculated()
+ testSubject.onTransitionElementReady(elementName)
+ testSubject.markOffsetCalculated()
+ testSubject.onTransitionElementReady(elementName)
+
+ scheduler.advanceTimeBy(TIMEOUT_MS + 1L)
+ verify(activity, times(1)).startPostponedEnterTransition()
+ }
+
+ @Test
+ fun test_postponeTransition_resume_animation_conditions() {
+ testSubject.postponeTransition()
+ verify(activity, never()).startPostponedEnterTransition()
+
+ testSubject.markOffsetCalculated()
+ verify(activity, never()).startPostponedEnterTransition()
+
+ testSubject.onAllTransitionElementsReady()
+ verify(activity, times(1)).startPostponedEnterTransition()
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/FeatureFlagRule.kt b/java/tests/src/com/android/intentresolver/FeatureFlagRule.kt
new file mode 100644
index 00000000..3fa01bcc
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/FeatureFlagRule.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import com.android.systemui.flags.BooleanFlag
+import org.junit.rules.TestRule
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+/**
+ * Ignores tests annotated with [RequireFeatureFlags] which flag requirements does not
+ * meet in the active flag set.
+ * @param flags active flag set
+ */
+internal class FeatureFlagRule(flags: Map<BooleanFlag, Boolean>) : TestRule {
+ private val flags = flags.entries.fold(HashMap<String, Boolean>()) { map, (key, value) ->
+ map.apply {
+ put(key.name, value)
+ }
+ }
+ private val skippingStatement = object : Statement() {
+ override fun evaluate() = Unit
+ }
+
+ override fun apply(base: Statement, description: Description): Statement {
+ val annotation = description.annotations.firstOrNull {
+ it is RequireFeatureFlags
+ } as? RequireFeatureFlags
+ ?: return base
+
+ if (annotation.flags.size != annotation.values.size) {
+ error("${description.className}#${description.methodName}: inconsistent number of" +
+ " flags and values in $annotation")
+ }
+ for (i in annotation.flags.indices) {
+ val flag = annotation.flags[i]
+ val value = annotation.values[i]
+ if (flags.getOrDefault(flag, !value) != value) return skippingStatement
+ }
+ return base
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt b/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt
new file mode 100644
index 00000000..f327e19e
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.content.ContentResolver
+import android.content.Context
+import android.content.res.Resources
+import android.net.Uri
+import android.util.Size
+import androidx.lifecycle.Lifecycle
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestCoroutineScheduler
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.resetMain
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@OptIn(ExperimentalCoroutinesApi::class)
+class ImagePreviewImageLoaderTest {
+ private val imageSize = Size(300, 300)
+ private val uriOne = Uri.parse("content://org.package.app/image-1.png")
+ private val uriTwo = Uri.parse("content://org.package.app/image-2.png")
+ private val contentResolver = mock<ContentResolver>()
+ private val resources = mock<Resources> {
+ whenever(getDimensionPixelSize(R.dimen.chooser_preview_image_max_dimen))
+ .thenReturn(imageSize.width)
+ }
+ private val context = mock<Context> {
+ whenever(this.resources).thenReturn(this@ImagePreviewImageLoaderTest.resources)
+ whenever(this.contentResolver).thenReturn(this@ImagePreviewImageLoaderTest.contentResolver)
+ }
+ private val scheduler = TestCoroutineScheduler()
+ private val lifecycleOwner = TestLifecycleOwner()
+ private val dispatcher = UnconfinedTestDispatcher(scheduler)
+ private val testSubject = ImagePreviewImageLoader(
+ context, lifecycleOwner.lifecycle, 1, dispatcher
+ )
+
+ @Before
+ fun setup() {
+ Dispatchers.setMain(dispatcher)
+ lifecycleOwner.state = Lifecycle.State.CREATED
+ }
+
+ @After
+ fun cleanup() {
+ lifecycleOwner.state = Lifecycle.State.DESTROYED
+ Dispatchers.resetMain()
+ }
+
+ @Test
+ fun test_prePopulate() = runTest {
+ testSubject.prePopulate(listOf(uriOne, uriTwo))
+
+ verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null)
+ verify(contentResolver, never()).loadThumbnail(uriTwo, imageSize, null)
+
+ testSubject(uriOne)
+ verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null)
+ }
+
+ @Test
+ fun test_invoke_return_cached_image() = runTest {
+ testSubject(uriOne)
+ testSubject(uriOne)
+
+ verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull())
+ }
+
+ @Test
+ fun test_invoke_old_records_evicted_from_the_cache() = runTest {
+ testSubject(uriOne)
+ testSubject(uriTwo)
+ testSubject(uriTwo)
+ testSubject(uriOne)
+
+ verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null)
+ verify(contentResolver, times(1)).loadThumbnail(uriTwo, imageSize, null)
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt b/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt
index 159c6d6a..aaa7a282 100644
--- a/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt
+++ b/java/tests/src/com/android/intentresolver/MockitoKotlinHelpers.kt
@@ -26,6 +26,7 @@ package com.android.intentresolver
import org.mockito.ArgumentCaptor
import org.mockito.ArgumentMatcher
+import org.mockito.ArgumentMatchers
import org.mockito.Mockito
import org.mockito.stubbing.OngoingStubbing
@@ -144,3 +145,5 @@ inline fun <reified T : Any> withArgCaptor(block: KotlinArgumentCaptor<T>.() ->
*/
inline fun <reified T : Any> captureMany(block: KotlinArgumentCaptor<T>.() -> Unit): List<T> =
kotlinArgumentCaptor<T>().apply{ block() }.allValues
+
+inline fun <reified T> anyOrNull() = ArgumentMatchers.argThat(ArgumentMatcher<T?> { true })
diff --git a/java/tests/src/com/android/intentresolver/RequireFeatureFlags.kt b/java/tests/src/com/android/intentresolver/RequireFeatureFlags.kt
new file mode 100644
index 00000000..1ddf7462
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/RequireFeatureFlags.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+/**
+ * Specifies expected feature flag values for a test.
+ */
+@Target(AnnotationTarget.FUNCTION)
+annotation class RequireFeatureFlags(val flags: Array<String>, val values: BooleanArray)
diff --git a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java
index 62c16ff5..ae1b99f8 100644
--- a/java/tests/src/com/android/intentresolver/ResolverActivityTest.java
+++ b/java/tests/src/com/android/intentresolver/ResolverActivityTest.java
@@ -54,7 +54,6 @@ import androidx.test.espresso.NoMatchingViewException;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
-import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
import com.android.intentresolver.widget.ResolverDrawerLayout;
import com.android.internal.R;
@@ -101,10 +100,7 @@ public class ResolverActivityTest {
Intent sendIntent = createSendImageIntent();
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource());
@@ -133,10 +129,7 @@ public class ResolverActivityTest {
Intent sendIntent = createSendImageIntent();
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
waitForIdle();
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
@@ -178,10 +171,7 @@ public class ResolverActivityTest {
Intent sendIntent = createSendImageIntent();
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
waitForIdle();
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
@@ -211,10 +201,7 @@ public class ResolverActivityTest {
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);
+ setupResolverControllers(resolvedComponentInfos);
when(sOverrides.resolverListController.getLastChosen())
.thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0));
@@ -277,10 +264,7 @@ public class ResolverActivityTest {
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);
+ setupResolverControllers(resolvedComponentInfos);
final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
Espresso.registerIdlingResources(activity.getAdapter().getLabelIdlingResource());
@@ -319,10 +303,7 @@ public class ResolverActivityTest {
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);
+ setupResolverControllers(resolvedComponentInfos);
when(sOverrides.resolverListController.getLastChosen())
.thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0));
@@ -761,10 +742,7 @@ public class ResolverActivityTest {
List<ResolvedComponentInfo> resolvedComponentInfos =
createResolvedComponentsForTest(2);
- when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
when(sOverrides.resolverListController.getLastChosen())
.thenReturn(resolvedComponentInfos.get(1).getResolveInfoAt(0));
@@ -837,22 +815,33 @@ public class ResolverActivityTest {
}
private void setupResolverControllers(
+ List<ResolvedComponentInfo> personalResolvedComponentInfos) {
+ setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>());
+ }
+
+ private void setupResolverControllers(
List<ResolvedComponentInfo> personalResolvedComponentInfos,
List<ResolvedComponentInfo> workResolvedComponentInfos) {
- when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+ when(sOverrides.resolverListController.getResolversForIntentAsUser(
Mockito.anyBoolean(),
Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
- when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.SYSTEM)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ when(sOverrides.workResolverListController.getResolversForIntentAsUser(
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));
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ when(sOverrides.workResolverListController.getResolversForIntentAsUser(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.of(10))))
+ .thenReturn(new ArrayList<>(workResolvedComponentInfos));
}
}
diff --git a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
index fb928e09..b6b32b5a 100644
--- a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
+++ b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
@@ -36,20 +36,33 @@ public class ResolverDataProvider {
static private int USER_SOMEONE_ELSE = 10;
- static ResolverActivity.ResolvedComponentInfo createResolvedComponentInfo(int i) {
- return new ResolverActivity.ResolvedComponentInfo(createComponentName(i),
- createResolverIntent(i), createResolveInfo(i, UserHandle.USER_CURRENT));
+ static ResolvedComponentInfo createResolvedComponentInfo(int i) {
+ return new ResolvedComponentInfo(
+ createComponentName(i),
+ createResolverIntent(i),
+ createResolveInfo(i, UserHandle.USER_CURRENT));
}
- static ResolverActivity.ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i) {
- return new ResolverActivity.ResolvedComponentInfo(createComponentName(i),
- createResolverIntent(i), createResolveInfo(i, USER_SOMEONE_ELSE));
+ static ResolvedComponentInfo createResolvedComponentInfo(
+ ComponentName componentName, Intent intent) {
+ return new ResolvedComponentInfo(
+ componentName,
+ intent,
+ createResolveInfo(componentName, UserHandle.USER_CURRENT));
}
- static ResolverActivity.ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i,
- int userId) {
- return new ResolverActivity.ResolvedComponentInfo(createComponentName(i),
- createResolverIntent(i), createResolveInfo(i, userId));
+ static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i) {
+ return new ResolvedComponentInfo(
+ createComponentName(i),
+ createResolverIntent(i),
+ createResolveInfo(i, USER_SOMEONE_ELSE));
+ }
+
+ static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i, int userId) {
+ return new ResolvedComponentInfo(
+ createComponentName(i),
+ createResolverIntent(i),
+ createResolveInfo(i, userId));
}
public static ComponentName createComponentName(int i) {
@@ -64,6 +77,13 @@ public class ResolverDataProvider {
return resolveInfo;
}
+ public static ResolveInfo createResolveInfo(ComponentName componentName, int userId) {
+ final ResolveInfo resolveInfo = new ResolveInfo();
+ resolveInfo.activityInfo = createActivityInfo(componentName);
+ resolveInfo.targetUserId = userId;
+ return resolveInfo;
+ }
+
static ActivityInfo createActivityInfo(int i) {
ActivityInfo ai = new ActivityInfo();
ai.name = "activity_name" + i;
@@ -75,6 +95,18 @@ public class ResolverDataProvider {
return ai;
}
+ static ActivityInfo createActivityInfo(ComponentName componentName) {
+ ActivityInfo ai = new ActivityInfo();
+ ai.name = componentName.getClassName();
+ ai.packageName = componentName.getPackageName();
+ ai.enabled = true;
+ ai.exported = true;
+ ai.permission = null;
+ ai.applicationInfo = createApplicationInfo();
+ ai.applicationInfo.packageName = componentName.getPackageName();
+ return ai;
+ }
+
static ApplicationInfo createApplicationInfo() {
ApplicationInfo ai = new ApplicationInfo();
ai.name = "app_name";
diff --git a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java
index 239bffe0..d67b73af 100644
--- a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java
+++ b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java
@@ -31,7 +31,6 @@ 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;
@@ -88,11 +87,11 @@ public class ResolverWrapperActivity extends ResolverActivity {
}
@Override
- protected QuietModeManager createQuietModeManager() {
- if (sOverrides.mQuietModeManager != null) {
- return sOverrides.mQuietModeManager;
+ protected WorkProfileAvailabilityManager createWorkProfileAvailabilityManager() {
+ if (sOverrides.mWorkProfileAvailability != null) {
+ return sOverrides.mWorkProfileAvailability;
}
- return super.createQuietModeManager();
+ return super.createWorkProfileAvailabilityManager();
}
ResolverWrapperAdapter getAdapter() {
@@ -130,10 +129,8 @@ public class ResolverWrapperActivity extends ResolverActivity {
@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;
}
@@ -175,7 +172,7 @@ public class ResolverWrapperActivity extends ResolverActivity {
public Integer myUserId;
public boolean hasCrossProfileIntents;
public boolean isQuietModeEnabled;
- public QuietModeManager mQuietModeManager;
+ public WorkProfileAvailabilityManager mWorkProfileAvailability;
public MyUserIdProvider mMyUserIdProvider;
public CrossProfileIntentsChecker mCrossProfileIntentsChecker;
@@ -190,23 +187,26 @@ public class ResolverWrapperActivity extends ResolverActivity {
hasCrossProfileIntents = true;
isQuietModeEnabled = false;
- mQuietModeManager = new QuietModeManager() {
+ mWorkProfileAvailability = new WorkProfileAvailabilityManager(null, null, null) {
@Override
- public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) {
+ public boolean isQuietModeEnabled() {
return isQuietModeEnabled;
}
@Override
- public void requestQuietModeEnabled(boolean enabled,
- UserHandle workProfileUserHandle) {
- isQuietModeEnabled = enabled;
+ public boolean isWorkProfileUserUnlocked() {
+ return true;
}
@Override
- public void markWorkProfileEnabledBroadcastReceived() {
+ public void requestQuietModeEnabled(boolean enabled) {
+ isQuietModeEnabled = enabled;
}
@Override
+ public void markWorkProfileEnabledBroadcastReceived() {}
+
+ @Override
public boolean isWaitingToEnableWorkProfile() {
return false;
}
diff --git a/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt b/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt
new file mode 100644
index 00000000..b9047712
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/TestFeatureFlagRepository.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import com.android.intentresolver.flags.FeatureFlagRepository
+import com.android.systemui.flags.BooleanFlag
+import com.android.systemui.flags.ReleasedFlag
+import com.android.systemui.flags.UnreleasedFlag
+
+class TestFeatureFlagRepository(
+ private val overrides: Map<BooleanFlag, Boolean>
+) : FeatureFlagRepository {
+ override fun isEnabled(flag: UnreleasedFlag): Boolean = getValue(flag)
+ override fun isEnabled(flag: ReleasedFlag): Boolean = getValue(flag)
+
+ private fun getValue(flag: BooleanFlag) = overrides.getOrDefault(flag, flag.default)
+}
diff --git a/java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt b/java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt
new file mode 100644
index 00000000..f47e343f
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+
+internal class TestLifecycleOwner : LifecycleOwner {
+ private val lifecycleRegistry = LifecycleRegistry.createUnsafe(this)
+
+ override fun getLifecycle(): Lifecycle = lifecycleRegistry
+
+ var state: Lifecycle.State
+ get() = lifecycle.currentState
+ set(value) {
+ lifecycleRegistry.currentState = value
+ }
+} \ No newline at end of file
diff --git a/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt b/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt
new file mode 100644
index 00000000..cfe041dd
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.graphics.Bitmap
+import android.net.Uri
+import java.util.function.Consumer
+
+internal class TestPreviewImageLoader(
+ private val imageLoader: ImageLoader,
+ private val imageOverride: () -> Bitmap?
+) : ImageLoader {
+ override fun loadImage(uri: Uri, callback: Consumer<Bitmap?>) {
+ val override = imageOverride()
+ if (override != null) {
+ callback.accept(override)
+ } else {
+ imageLoader.loadImage(uri, callback)
+ }
+ }
+
+ override suspend fun invoke(uri: Uri): Bitmap? = imageOverride() ?: imageLoader(uri)
+ override fun prePopulate(uris: List<Uri>) = Unit
+}
diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
index af2557ef..9ffd02d4 100644
--- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
+++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
@@ -20,6 +20,7 @@ import static android.app.Activity.RESULT_OK;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.action.ViewActions.longClick;
import static androidx.test.espresso.action.ViewActions.swipeUp;
import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
import static androidx.test.espresso.assertion.ViewAssertions.matches;
@@ -55,13 +56,16 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.PendingIntent;
import android.app.usage.UsageStatsManager;
+import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
@@ -69,6 +73,7 @@ import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutManager.ShareShortcutInfo;
import android.content.res.Configuration;
+import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
@@ -79,25 +84,29 @@ import android.net.Uri;
import android.os.Bundle;
import android.os.UserHandle;
import android.provider.DeviceConfig;
+import android.service.chooser.ChooserAction;
import android.service.chooser.ChooserTarget;
import android.util.HashedStringCache;
import android.util.Pair;
import android.util.SparseArray;
import android.view.View;
+import android.view.ViewGroup;
import androidx.annotation.CallSuper;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.espresso.contrib.RecyclerViewActions;
import androidx.test.espresso.matcher.BoundedDiagnosingMatcher;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
-import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.flags.Flags;
import com.android.intentresolver.shortcuts.ShortcutLoader;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.systemui.flags.BooleanFlag;
import org.hamcrest.Description;
import org.hamcrest.Matcher;
@@ -106,6 +115,8 @@ import org.junit.Before;
import org.junit.Ignore;
import org.junit.Rule;
import org.junit.Test;
+import org.junit.rules.RuleChain;
+import org.junit.rules.TestRule;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.mockito.ArgumentCaptor;
@@ -117,6 +128,8 @@ import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -150,14 +163,40 @@ public class UnbundledChooserActivityTest {
return mock;
};
+ private static final List<BooleanFlag> ALL_FLAGS =
+ Arrays.asList(
+ Flags.SHARESHEET_CUSTOM_ACTIONS,
+ Flags.SHARESHEET_RESELECTION_ACTION,
+ Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW,
+ Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW);
+
+ private static final Map<BooleanFlag, Boolean> ALL_FLAGS_OFF =
+ createAllFlagsOverride(false);
+ private static final Map<BooleanFlag, Boolean> ALL_FLAGS_ON =
+ createAllFlagsOverride(true);
+
@Parameterized.Parameters
public static Collection packageManagers() {
return Arrays.asList(new Object[][] {
- {0, "Default PackageManager", DEFAULT_PM},
- {1, "No App Prediction Service", NO_APP_PREDICTION_SERVICE_PM}
+ // Default PackageManager and all flags off
+ { DEFAULT_PM, ALL_FLAGS_OFF },
+ // Default PackageManager and all flags on
+ { DEFAULT_PM, ALL_FLAGS_ON },
+ // No App Prediction Service and all flags off
+ { NO_APP_PREDICTION_SERVICE_PM, ALL_FLAGS_OFF },
+ // No App Prediction Service and all flags on
+ { NO_APP_PREDICTION_SERVICE_PM, ALL_FLAGS_ON }
});
}
+ private static Map<BooleanFlag, Boolean> createAllFlagsOverride(boolean value) {
+ HashMap<BooleanFlag, Boolean> overrides = new HashMap<>(ALL_FLAGS.size());
+ for (BooleanFlag flag : ALL_FLAGS) {
+ overrides.put(flag, value);
+ }
+ return overrides;
+ }
+
/* --------
* Subclasses can override the following methods to customize test behavior.
* --------
@@ -177,6 +216,8 @@ public class UnbundledChooserActivityTest {
.adoptShellPermissionIdentity();
cleanOverrideData();
+ ChooserActivityOverrideData.getInstance().featureFlagRepository =
+ new TestFeatureFlagRepository(mFlags);
}
/**
@@ -209,11 +250,13 @@ public class UnbundledChooserActivityTest {
* --------
*/
+ @Rule
+ public final TestRule mRule;
+
// Shared test code references the activity under test as ChooserActivity, the common ancestor
// of any (inheritance-based) chooser implementation. For testing purposes, that activity will
// usually be cast to IChooserWrapper to expose instrumentation.
- @Rule
- public ActivityTestRule<ChooserActivity> mActivityRule =
+ private ActivityTestRule<ChooserActivity> mActivityRule =
new ActivityTestRule<>(ChooserActivity.class, false, false) {
@Override
public ChooserActivity launchActivity(Intent clientIntent) {
@@ -240,16 +283,20 @@ public class UnbundledChooserActivityTest {
private static final int CONTENT_PREVIEW_IMAGE = 1;
private static final int CONTENT_PREVIEW_FILE = 2;
private static final int CONTENT_PREVIEW_TEXT = 3;
- private Function<PackageManager, PackageManager> mPackageManagerOverride;
- private int mTestNum;
+
+ private final Function<PackageManager, PackageManager> mPackageManagerOverride;
+ private final Map<BooleanFlag, Boolean> mFlags;
public UnbundledChooserActivityTest(
- int testNum,
- String testName,
- Function<PackageManager, PackageManager> packageManagerOverride) {
+ Function<PackageManager, PackageManager> packageManagerOverride,
+ Map<BooleanFlag, Boolean> flags) {
mPackageManagerOverride = packageManagerOverride;
- mTestNum = testNum;
+ mFlags = flags;
+
+ mRule = RuleChain
+ .outerRule(new FeatureFlagRule(flags))
+ .around(mActivityRule);
}
private void setDeviceConfigProperty(
@@ -284,16 +331,7 @@ public class UnbundledChooserActivityTest {
Intent viewIntent = createViewTextIntent();
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(
Intent.createChooser(viewIntent, "chooser test"));
@@ -308,16 +346,7 @@ public class UnbundledChooserActivityTest {
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);
+ setupResolverControllers(resolvedComponentInfos);
mActivityRule.launchActivity(Intent.createChooser(sendIntent, "chooser test"));
waitForIdle();
onView(withId(android.R.id.title))
@@ -329,16 +358,7 @@ public class UnbundledChooserActivityTest {
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);
+ setupResolverControllers(resolvedComponentInfos);
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
onView(withId(android.R.id.title))
@@ -350,16 +370,7 @@ public class UnbundledChooserActivityTest {
Intent sendIntent = createSendTextIntentWithPreview(null, null);
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
onView(withId(com.android.internal.R.id.content_preview_title))
@@ -374,16 +385,7 @@ public class UnbundledChooserActivityTest {
Intent sendIntent = createSendTextIntentWithPreview(previewTitle, null);
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
onView(withId(com.android.internal.R.id.content_preview_title))
@@ -401,16 +403,7 @@ public class UnbundledChooserActivityTest {
Uri.parse("tel:(+49)12345789"));
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
onView(withId(com.android.internal.R.id.content_preview_title))
@@ -428,16 +421,7 @@ public class UnbundledChooserActivityTest {
ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap();
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
onView(withId(com.android.internal.R.id.content_preview_title))
@@ -451,16 +435,7 @@ public class UnbundledChooserActivityTest {
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);
+ setupResolverControllers(resolvedComponentInfos);
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
@@ -506,16 +481,7 @@ public class UnbundledChooserActivityTest {
}
resolvedComponentInfos.addAll(infosToStack);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
@@ -547,16 +513,7 @@ public class UnbundledChooserActivityTest {
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);
+ setupResolverControllers(resolvedComponentInfos);
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
@@ -582,16 +539,7 @@ public class UnbundledChooserActivityTest {
@Ignore // b/148158199
@Test
public void noResultsFromPackageManager() {
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(null);
+ setupResolverControllers(null);
Intent sendIntent = createSendTextIntent();
final ChooserActivity activity =
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
@@ -618,16 +566,7 @@ public class UnbundledChooserActivityTest {
};
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(1);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
Intent sendIntent = createSendTextIntent();
final ChooserActivity activity =
@@ -679,11 +618,7 @@ public class UnbundledChooserActivityTest {
createResolvedComponentsForTestWithOtherProfile(3);
ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0);
- when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
when(ChooserActivityOverrideData.getInstance().resolverListController.getLastChosen())
.thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0));
@@ -716,11 +651,7 @@ public class UnbundledChooserActivityTest {
createResolvedComponentsForTestWithOtherProfile(3);
ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0);
- when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
@@ -745,15 +676,148 @@ public class UnbundledChooserActivityTest {
}
@Test
+ @RequireFeatureFlags(
+ flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME },
+ values = { true })
+ public void testImagePlusTextSharing_ExcludeText() {
+ Intent sendIntent = createSendImageIntent(
+ Uri.parse("android.resource://com.android.frameworks.coretests/"
+ + R.drawable.test320x240));
+ ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap();
+ ChooserActivityOverrideData.getInstance().isImageType = true;
+ sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google");
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList(
+ ResolverDataProvider.createResolvedComponentInfo(
+ new ComponentName("org.imageviewer", "ImageTarget"),
+ sendIntent),
+ ResolverDataProvider.createResolvedComponentInfo(
+ new ComponentName("org.textviewer", "UriTarget"),
+ new Intent("VIEW_TEXT"))
+ );
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withId(R.id.include_text_action))
+ .check(matches(isDisplayed()))
+ .perform(click());
+ waitForIdle();
+
+ AtomicReference<Intent> launchedIntentRef = new AtomicReference<>();
+ ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+ launchedIntentRef.set(targetInfo.getTargetIntent());
+ return true;
+ };
+
+ onView(withText(resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.name))
+ .perform(click());
+ waitForIdle();
+ assertThat(launchedIntentRef.get().hasExtra(Intent.EXTRA_TEXT)).isFalse();
+ }
+
+ @Test
+ @RequireFeatureFlags(
+ flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME },
+ values = { true })
+ public void testImagePlusTextSharing_RemoveAndAddBackText() {
+ Intent sendIntent = createSendImageIntent(
+ Uri.parse("android.resource://com.android.frameworks.coretests/"
+ + R.drawable.test320x240));
+ ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap();
+ ChooserActivityOverrideData.getInstance().isImageType = true;
+ final String text = "https://google.com/search?q=google";
+ sendIntent.putExtra(Intent.EXTRA_TEXT, text);
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList(
+ ResolverDataProvider.createResolvedComponentInfo(
+ new ComponentName("org.imageviewer", "ImageTarget"),
+ sendIntent),
+ ResolverDataProvider.createResolvedComponentInfo(
+ new ComponentName("org.textviewer", "UriTarget"),
+ new Intent("VIEW_TEXT"))
+ );
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withId(R.id.include_text_action))
+ .check(matches(isDisplayed()))
+ .perform(click());
+ waitForIdle();
+ onView(withId(R.id.include_text_action))
+ .perform(click());
+ waitForIdle();
+
+ AtomicReference<Intent> launchedIntentRef = new AtomicReference<>();
+ ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+ launchedIntentRef.set(targetInfo.getTargetIntent());
+ return true;
+ };
+
+ onView(withText(resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.name))
+ .perform(click());
+ waitForIdle();
+ assertThat(launchedIntentRef.get().getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(text);
+ }
+
+ @Test
+ @RequireFeatureFlags(
+ flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME },
+ values = { true })
+ public void testImagePlusTextSharing_TextExclusionDoesNotAffectAlternativeIntent() {
+ Intent sendIntent = createSendImageIntent(
+ Uri.parse("android.resource://com.android.frameworks.coretests/"
+ + R.drawable.test320x240));
+ ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap();
+ ChooserActivityOverrideData.getInstance().isImageType = true;
+ sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google");
+
+ Intent alternativeIntent = createSendTextIntent();
+ final String text = "alternative intent";
+ alternativeIntent.putExtra(Intent.EXTRA_TEXT, text);
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = Arrays.asList(
+ ResolverDataProvider.createResolvedComponentInfo(
+ new ComponentName("org.imageviewer", "ImageTarget"),
+ sendIntent),
+ ResolverDataProvider.createResolvedComponentInfo(
+ new ComponentName("org.textviewer", "UriTarget"),
+ alternativeIntent)
+ );
+
+ setupResolverControllers(resolvedComponentInfos);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withId(R.id.include_text_action))
+ .check(matches(isDisplayed()))
+ .perform(click());
+ waitForIdle();
+
+ AtomicReference<Intent> launchedIntentRef = new AtomicReference<>();
+ ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+ launchedIntentRef.set(targetInfo.getTargetIntent());
+ return true;
+ };
+
+ onView(withText(resolvedComponentInfos.get(1).getResolveInfoAt(0).activityInfo.name))
+ .perform(click());
+ waitForIdle();
+ assertThat(launchedIntentRef.get().getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(text);
+ }
+
+ @Test
public void copyTextToClipboard() throws Exception {
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);
+ setupResolverControllers(resolvedComponentInfos);
final ChooserActivity activity =
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
@@ -777,11 +841,7 @@ public class UnbundledChooserActivityTest {
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);
+ setupResolverControllers(resolvedComponentInfos);
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
@@ -800,11 +860,7 @@ public class UnbundledChooserActivityTest {
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);
+ setupResolverControllers(resolvedComponentInfos);
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
@@ -830,11 +886,7 @@ public class UnbundledChooserActivityTest {
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
@@ -848,7 +900,7 @@ public class UnbundledChooserActivityTest {
@Test
- public void oneVisibleImagePreview() throws InterruptedException {
+ public void oneVisibleImagePreview() {
Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/"
+ R.drawable.test320x240);
@@ -861,30 +913,34 @@ public class UnbundledChooserActivityTest {
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
- onView(withId(com.android.internal.R.id.content_preview_image_1_large))
- .check(matches(isDisplayed()));
- onView(withId(com.android.internal.R.id.content_preview_image_2_large))
- .check(matches(not(isDisplayed())));
- onView(withId(com.android.internal.R.id.content_preview_image_2_small))
- .check(matches(not(isDisplayed())));
- onView(withId(com.android.internal.R.id.content_preview_image_3_small))
- .check(matches(not(isDisplayed())));
+ onView(withId(com.android.internal.R.id.content_preview_image_area))
+ .check((view, exception) -> {
+ if (exception != null) {
+ throw exception;
+ }
+ ViewGroup parent = (ViewGroup) view;
+ ArrayList<View> visibleViews = new ArrayList<>();
+ for (int i = 0, count = parent.getChildCount(); i < count; i++) {
+ View child = parent.getChildAt(i);
+ if (child.getVisibility() == View.VISIBLE) {
+ visibleViews.add(child);
+ }
+ }
+ assertThat(visibleViews.size(), is(1));
+ assertThat(
+ "image preview view is fully visible",
+ isDisplayed().matches(visibleViews.get(0)));
+ });
}
@Test
- public void twoVisibleImagePreview() throws InterruptedException {
+ @RequireFeatureFlags(
+ flags = { Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME },
+ values = { false })
+ public void twoVisibleImagePreview() {
Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/"
+ R.drawable.test320x240);
@@ -898,16 +954,7 @@ public class UnbundledChooserActivityTest {
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
onView(withId(com.android.internal.R.id.content_preview_image_1_large))
@@ -921,7 +968,10 @@ public class UnbundledChooserActivityTest {
}
@Test
- public void threeOrMoreVisibleImagePreview() throws InterruptedException {
+ @RequireFeatureFlags(
+ flags = { Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME },
+ values = { false })
+ public void threeOrMoreVisibleImagePreview() {
Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/"
+ R.drawable.test320x240);
@@ -938,16 +988,7 @@ public class UnbundledChooserActivityTest {
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
onView(withId(com.android.internal.R.id.content_preview_image_1_large))
@@ -961,6 +1002,72 @@ public class UnbundledChooserActivityTest {
}
@Test
+ @RequireFeatureFlags(
+ flags = { Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW_NAME },
+ values = { true })
+ public void testManyVisibleImagePreview_ScrollableImagePreview() {
+ Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/"
+ + R.drawable.test320x240);
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap();
+ ChooserActivityOverrideData.getInstance().isImageType = true;
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withId(com.android.internal.R.id.content_preview_image_area))
+ .perform(RecyclerViewActions.scrollToLastPosition())
+ .check((view, exception) -> {
+ if (exception != null) {
+ throw exception;
+ }
+ RecyclerView recyclerView = (RecyclerView) view;
+ assertThat(recyclerView.getAdapter().getItemCount(), is(uris.size()));
+ });
+ }
+
+ @Test
+ @RequireFeatureFlags(
+ flags = { Flags.SHARESHEET_IMAGE_AND_TEXT_PREVIEW_NAME },
+ values = { true })
+ public void testImageAndTextPreview() {
+ final Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/"
+ + R.drawable.test320x240);
+ final String sharedText = "text-" + System.currentTimeMillis();
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText);
+ ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap();
+ ChooserActivityOverrideData.getInstance().isImageType = true;
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withText(sharedText))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
public void testOnCreateLogging() {
Intent sendIntent = createSendTextIntent();
sendIntent.setType(TEST_MIME_TYPE);
@@ -1007,11 +1114,7 @@ public class UnbundledChooserActivityTest {
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
@@ -1036,16 +1139,7 @@ public class UnbundledChooserActivityTest {
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
@@ -1065,16 +1159,7 @@ public class UnbundledChooserActivityTest {
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
onView(withId(com.android.internal.R.id.content_preview_filename))
@@ -1099,16 +1184,7 @@ public class UnbundledChooserActivityTest {
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
onView(withId(com.android.internal.R.id.content_preview_filename))
@@ -1129,16 +1205,7 @@ public class UnbundledChooserActivityTest {
Intent sendIntent = createSendUriIntentWithPreview(uris);
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
ChooserActivityOverrideData.getInstance().resolverForceException = true;
@@ -1163,16 +1230,7 @@ public class UnbundledChooserActivityTest {
Intent sendIntent = createSendUriIntentWithPreview(uris);
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
Cursor cursor = mock(Cursor.class);
when(cursor.getCount()).thenReturn(1);
@@ -1199,16 +1257,8 @@ public class UnbundledChooserActivityTest {
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);
+ setupResolverControllers(resolvedComponentInfos);
+
when(
ChooserActivityOverrideData
.getInstance()
@@ -1241,16 +1291,7 @@ public class UnbundledChooserActivityTest {
Intent sendIntent = createSendTextIntent();
// We need app targets for direct targets to get displayed
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
// create test shortcut loader factory, remember loaders and their callbacks
SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
@@ -1331,16 +1372,7 @@ public class UnbundledChooserActivityTest {
Intent sendIntent = createSendTextIntent();
// We need app targets for direct targets to get displayed
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
// create test shortcut loader factory, remember loaders and their callbacks
SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
@@ -1425,16 +1457,7 @@ public class UnbundledChooserActivityTest {
Intent sendIntent = createSendTextIntent();
// We need app targets for direct targets to get displayed
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
// create test shortcut loader factory, remember loaders and their callbacks
SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
@@ -1509,16 +1532,7 @@ public class UnbundledChooserActivityTest {
Intent sendIntent = createSendTextIntent();
// We need app targets for direct targets to get displayed
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
// create test shortcut loader factory, remember loaders and their callbacks
SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
@@ -1597,16 +1611,7 @@ public class UnbundledChooserActivityTest {
// We need app targets for direct targets to get displayed
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
// set caller-provided target
Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null);
@@ -1665,6 +1670,91 @@ public class UnbundledChooserActivityTest {
}
@Test
+ @RequireFeatureFlags(
+ flags = { Flags.SHARESHEET_CUSTOM_ACTIONS_NAME },
+ values = { true })
+ public void testLaunchWithCustomAction() throws InterruptedException {
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ Context testContext = InstrumentationRegistry.getInstrumentation().getContext();
+ final String customActionLabel = "Custom Action";
+ final String testAction = "test-broadcast-receiver-action";
+ Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null);
+ chooserIntent.putExtra(
+ Intent.EXTRA_CHOOSER_CUSTOM_ACTIONS,
+ new ChooserAction[] {
+ new ChooserAction.Builder(
+ Icon.createWithResource("", Resources.ID_NULL),
+ customActionLabel,
+ PendingIntent.getBroadcast(
+ testContext,
+ 123,
+ new Intent(testAction),
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT))
+ .build()
+ });
+ // Start activity
+ mActivityRule.launchActivity(chooserIntent);
+ waitForIdle();
+
+ final CountDownLatch broadcastInvoked = new CountDownLatch(1);
+ BroadcastReceiver testReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ broadcastInvoked.countDown();
+ }
+ };
+ testContext.registerReceiver(testReceiver, new IntentFilter(testAction));
+
+ try {
+ onView(withText(customActionLabel)).perform(click());
+ broadcastInvoked.await();
+ } finally {
+ testContext.unregisterReceiver(testReceiver);
+ }
+ }
+
+ @Test
+ @RequireFeatureFlags(
+ flags = { Flags.SHARESHEET_RESELECTION_ACTION_NAME },
+ values = { true })
+ public void testLaunchWithShareModification() throws InterruptedException {
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ Context testContext = InstrumentationRegistry.getInstrumentation().getContext();
+ final String modifyShareAction = "test-broadcast-receiver-action";
+ Intent chooserIntent = Intent.createChooser(createSendTextIntent(), null);
+ chooserIntent.putExtra(
+ Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION,
+ PendingIntent.getBroadcast(
+ testContext,
+ 123,
+ new Intent(modifyShareAction),
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_ONE_SHOT));
+ // Start activity
+ mActivityRule.launchActivity(chooserIntent);
+ waitForIdle();
+
+ final CountDownLatch broadcastInvoked = new CountDownLatch(1);
+ BroadcastReceiver testReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ broadcastInvoked.countDown();
+ }
+ };
+ testContext.registerReceiver(testReceiver, new IntentFilter(modifyShareAction));
+
+ try {
+ onView(withText(R.string.select_text)).perform(click());
+ broadcastInvoked.await();
+ } finally {
+ testContext.unregisterReceiver(testReceiver);
+ }
+ }
+
+ @Test
public void testUpdateMaxTargetsPerRow_columnCountIsUpdated() throws InterruptedException {
updateMaxTargetsPerRowResource(/* targetsPerRow= */ 4);
givenAppTargets(/* appCount= */ 16);
@@ -1715,16 +1805,7 @@ public class UnbundledChooserActivityTest {
Intent sendIntent = createSendTextIntent();
// We need app targets for direct targets to get displayed
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(15);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
// Create direct share target
List<ChooserTarget> serviceTargets = createDirectShareTargets(1,
@@ -2004,16 +2085,7 @@ public class UnbundledChooserActivityTest {
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);
+ setupResolverControllers(resolvedComponentInfos);
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
@@ -2045,16 +2117,7 @@ public class UnbundledChooserActivityTest {
Intent sendIntent = createSendTextIntent();
// We need app targets for direct targets to get displayed
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
// create test shortcut loader factory, remember loaders and their callbacks
SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
@@ -2130,21 +2193,69 @@ public class UnbundledChooserActivityTest {
/* selectionCost= */ anyLong());
}
+ @Test
+ public void testDirectTargetPinningDialog() {
+ Intent sendIntent = createSendTextIntent();
+ // We need app targets for direct targets to get displayed
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ // create test shortcut loader factory, remember loaders and their callbacks
+ SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>> shortcutLoaders =
+ new SparseArray<>();
+ ChooserActivityOverrideData.getInstance().shortcutLoaderFactory =
+ (userHandle, callback) -> {
+ Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>> pair =
+ new Pair<>(mock(ShortcutLoader.class), callback);
+ shortcutLoaders.put(userHandle.getIdentifier(), pair);
+ return pair.first;
+ };
+
+ // Start activity
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // verify that ShortcutLoader was queried
+ ArgumentCaptor<DisplayResolveInfo[]> appTargets =
+ ArgumentCaptor.forClass(DisplayResolveInfo[].class);
+ verify(shortcutLoaders.get(0).first, times(1))
+ .queryShortcuts(appTargets.capture());
+
+ // send shortcuts
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(
+ 1,
+ resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+ ShortcutLoader.Result result = new ShortcutLoader.Result(
+ // TODO: test another value as well
+ false,
+ appTargets.getValue(),
+ new ShortcutLoader.ShortcutResultInfo[] {
+ new ShortcutLoader.ShortcutResultInfo(
+ appTargets.getValue()[0],
+ serviceTargets
+ )
+ },
+ new HashMap<>(),
+ new HashMap<>()
+ );
+ activity.getMainExecutor().execute(() -> shortcutLoaders.get(0).second.accept(result));
+ waitForIdle();
+
+ // Long-click on the direct target
+ String name = serviceTargets.get(0).getTitle().toString();
+ onView(withText(name)).perform(longClick());
+ waitForIdle();
+
+ onView(withId(R.id.chooser_dialog_content)).check(matches(isDisplayed()));
+ }
+
@Test @Ignore
public void testEmptyDirectRowLogging() throws InterruptedException {
Intent sendIntent = createSendTextIntent();
// We need app targets for direct targets to get displayed
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
// Start activity
final IChooserWrapper activity = (IChooserWrapper)
@@ -2168,16 +2279,7 @@ public class UnbundledChooserActivityTest {
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);
+ setupResolverControllers(resolvedComponentInfos);
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
@@ -2239,16 +2341,7 @@ public class UnbundledChooserActivityTest {
public void testOneInitialIntent_noAutolaunch() {
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTest(1);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ setupResolverControllers(personalResolvedComponentInfos);
Intent chooserIntent = createChooserIntent(createSendTextIntent(),
new Intent[] {new Intent("action.fake")});
ResolveInfo[] chosen = new ResolveInfo[1];
@@ -2374,12 +2467,7 @@ public class UnbundledChooserActivityTest {
// Create 4 ranked app targets.
List<ResolvedComponentInfo> personalResolvedComponentInfos =
createResolvedComponentsForTest(4);
- when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ setupResolverControllers(personalResolvedComponentInfos);
// Create caller target which is duplicate with one of app targets
Intent chooserIntent = createChooserIntent(createSendTextIntent(),
new Intent[] {new Intent("action.fake")});
@@ -2646,28 +2734,35 @@ public class UnbundledChooserActivityTest {
}
private void setupResolverControllers(
+ List<ResolvedComponentInfo> personalResolvedComponentInfos) {
+ setupResolverControllers(personalResolvedComponentInfos, new ArrayList<>());
+ }
+
+ private void setupResolverControllers(
List<ResolvedComponentInfo> personalResolvedComponentInfos,
List<ResolvedComponentInfo> workResolvedComponentInfos) {
when(
ChooserActivityOverrideData
.getInstance()
.resolverListController
- .getResolversForIntent(
+ .getResolversForIntentAsUser(
Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.anyBoolean(),
- Mockito.isA(List.class)))
+ Mockito.isA(List.class),
+ eq(UserHandle.SYSTEM)))
.thenReturn(new ArrayList<>(personalResolvedComponentInfos));
when(
ChooserActivityOverrideData
.getInstance()
.workResolverListController
- .getResolversForIntent(
+ .getResolversForIntentAsUser(
Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(new ArrayList<>(workResolvedComponentInfos));
+ Mockito.isA(List.class),
+ eq(UserHandle.SYSTEM)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
when(
ChooserActivityOverrideData
.getInstance()
@@ -2677,8 +2772,8 @@ public class UnbundledChooserActivityTest {
Mockito.anyBoolean(),
Mockito.anyBoolean(),
Mockito.isA(List.class),
- eq(UserHandle.SYSTEM)))
- .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ eq(UserHandle.of(10))))
+ .thenReturn(new ArrayList<>(workResolvedComponentInfos));
}
private static GridRecyclerSpanCountMatcher withGridColumnCount(int columnCount) {
@@ -2717,16 +2812,7 @@ public class UnbundledChooserActivityTest {
private void givenAppTargets(int appCount) {
List<ResolvedComponentInfo> resolvedComponentInfos =
createResolvedComponentsForTest(appCount);
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resolverListController
- .getResolversForIntent(
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(resolvedComponentInfos);
+ setupResolverControllers(resolvedComponentInfos);
}
private void updateMaxTargetsPerRowResource(int targetsPerRow) {
diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java
index f1febed2..87dc1b9d 100644
--- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java
+++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityWorkProfileTest.java
@@ -48,7 +48,6 @@ import androidx.test.InstrumentationRegistry;
import androidx.test.espresso.NoMatchingViewException;
import androidx.test.rule.ActivityTestRule;
-import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
import com.android.intentresolver.UnbundledChooserActivityWorkProfileTest.TestCase.Tab;
import com.android.internal.R;
@@ -274,21 +273,27 @@ public class UnbundledChooserActivityWorkProfileTest {
private void setupResolverControllers(
List<ResolvedComponentInfo> personalResolvedComponentInfos,
List<ResolvedComponentInfo> workResolvedComponentInfos) {
- when(sOverrides.resolverListController.getResolversForIntent(Mockito.anyBoolean(),
+ when(sOverrides.resolverListController.getResolversForIntentAsUser(
Mockito.anyBoolean(),
Mockito.anyBoolean(),
- Mockito.isA(List.class)))
- .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
- when(sOverrides.workResolverListController.getResolversForIntent(Mockito.anyBoolean(),
Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.SYSTEM)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ when(sOverrides.workResolverListController.getResolversForIntentAsUser(
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));
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ when(sOverrides.workResolverListController.getResolversForIntentAsUser(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(WORK_USER_HANDLE)))
+ .thenReturn(new ArrayList<>(workResolvedComponentInfos));
}
private void waitForIdle() {
diff --git a/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt b/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt
new file mode 100644
index 00000000..e9c755d3
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/chooser/ImmutableTargetInfoTest.kt
@@ -0,0 +1,497 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *3
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.Activity
+import android.app.prediction.AppTarget
+import android.app.prediction.AppTargetId
+import android.content.ComponentName
+import android.content.Intent
+import android.content.pm.ResolveInfo
+import android.os.Bundle
+import android.os.UserHandle
+import com.android.intentresolver.createShortcutInfo
+import com.android.intentresolver.mock
+import com.android.intentresolver.ResolverActivity
+import com.android.intentresolver.ResolverDataProvider
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class ImmutableTargetInfoTest {
+ private val resolvedIntent = Intent("resolved")
+ private val targetIntent = Intent("target")
+ private val referrerFillInIntent = Intent("referrer_fillin")
+ private val resolvedComponentName = ComponentName("resolved", "component")
+ private val chooserTargetComponentName = ComponentName("chooser", "target")
+ private val resolveInfo = ResolverDataProvider.createResolveInfo(1, 0)
+ private val displayLabel: CharSequence = "Display Label"
+ private val extendedInfo: CharSequence = "Extended Info"
+ private val displayIconHolder: TargetInfo.IconHolder = mock()
+ private val sourceIntent1 = Intent("source1")
+ private val sourceIntent2 = Intent("source2")
+ private val displayTarget1 = DisplayResolveInfo.newDisplayResolveInfo(
+ Intent("display1"),
+ ResolverDataProvider.createResolveInfo(2, 0),
+ "display1 label",
+ "display1 extended info",
+ Intent("display1_resolved"),
+ /* resolveInfoPresentationGetter= */ null)
+ private val displayTarget2 = DisplayResolveInfo.newDisplayResolveInfo(
+ Intent("display2"),
+ ResolverDataProvider.createResolveInfo(3, 0),
+ "display2 label",
+ "display2 extended info",
+ Intent("display2_resolved"),
+ /* resolveInfoPresentationGetter= */ null)
+ private val directShareShortcutInfo = createShortcutInfo(
+ "shortcutid", ResolverDataProvider.createComponentName(4), 4)
+ private val directShareAppTarget = AppTarget(
+ AppTargetId("apptargetid"),
+ "test.directshare",
+ "target",
+ UserHandle.CURRENT)
+ private val displayResolveInfo = DisplayResolveInfo.newDisplayResolveInfo(
+ Intent("displayresolve"),
+ ResolverDataProvider.createResolveInfo(5, 0),
+ "displayresolve label",
+ "displayresolve extended info",
+ Intent("display_resolved"),
+ /* resolveInfoPresentationGetter= */ null)
+ private val hashProvider: ImmutableTargetInfo.TargetHashProvider = mock()
+
+ @Test
+ fun testBasicProperties() { // Fields that are reflected back w/o logic.
+ // TODO: we could consider passing copies of all the values into the builder so that we can
+ // verify that they're not mutated (e.g. no extras added to the intents). For now that
+ // should be obvious from the implementation.
+ val info = ImmutableTargetInfo.newBuilder()
+ .setResolvedIntent(resolvedIntent)
+ .setTargetIntent(targetIntent)
+ .setReferrerFillInIntent(referrerFillInIntent)
+ .setResolvedComponentName(resolvedComponentName)
+ .setChooserTargetComponentName(chooserTargetComponentName)
+ .setResolveInfo(resolveInfo)
+ .setDisplayLabel(displayLabel)
+ .setExtendedInfo(extendedInfo)
+ .setDisplayIconHolder(displayIconHolder)
+ .setAlternateSourceIntents(listOf(sourceIntent1, sourceIntent2))
+ .setAllDisplayTargets(listOf(displayTarget1, displayTarget2))
+ .setIsSuspended(true)
+ .setIsPinned(true)
+ .setModifiedScore(42.0f)
+ .setDirectShareShortcutInfo(directShareShortcutInfo)
+ .setDirectShareAppTarget(directShareAppTarget)
+ .setDisplayResolveInfo(displayResolveInfo)
+ .setHashProvider(hashProvider)
+ .build()
+
+ assertThat(info.resolvedIntent).isEqualTo(resolvedIntent)
+ assertThat(info.targetIntent).isEqualTo(targetIntent)
+ assertThat(info.referrerFillInIntent).isEqualTo(referrerFillInIntent)
+ assertThat(info.resolvedComponentName).isEqualTo(resolvedComponentName)
+ assertThat(info.chooserTargetComponentName).isEqualTo(chooserTargetComponentName)
+ assertThat(info.resolveInfo).isEqualTo(resolveInfo)
+ assertThat(info.displayLabel).isEqualTo(displayLabel)
+ assertThat(info.extendedInfo).isEqualTo(extendedInfo)
+ assertThat(info.displayIconHolder).isEqualTo(displayIconHolder)
+ assertThat(info.allSourceIntents).containsExactly(
+ resolvedIntent, sourceIntent1, sourceIntent2)
+ assertThat(info.allDisplayTargets).containsExactly(displayTarget1, displayTarget2)
+ assertThat(info.isSuspended).isTrue()
+ assertThat(info.isPinned).isTrue()
+ assertThat(info.modifiedScore).isEqualTo(42.0f)
+ assertThat(info.directShareShortcutInfo).isEqualTo(directShareShortcutInfo)
+ assertThat(info.directShareAppTarget).isEqualTo(directShareAppTarget)
+ assertThat(info.displayResolveInfo).isEqualTo(displayResolveInfo)
+ assertThat(info.isEmptyTargetInfo).isFalse()
+ assertThat(info.isPlaceHolderTargetInfo).isFalse()
+ assertThat(info.isNotSelectableTargetInfo).isFalse()
+ assertThat(info.isSelectableTargetInfo).isFalse()
+ assertThat(info.isChooserTargetInfo).isFalse()
+ assertThat(info.isMultiDisplayResolveInfo).isFalse()
+ assertThat(info.isDisplayResolveInfo).isFalse()
+ assertThat(info.hashProvider).isEqualTo(hashProvider)
+ }
+
+ @Test
+ fun testToBuilderPreservesBasicProperties() {
+ // Note this is set up exactly as in `testBasicProperties`, but the assertions will be made
+ // against a *copy* of the object instead.
+ val infoToCopyFrom = ImmutableTargetInfo.newBuilder()
+ .setResolvedIntent(resolvedIntent)
+ .setTargetIntent(targetIntent)
+ .setReferrerFillInIntent(referrerFillInIntent)
+ .setResolvedComponentName(resolvedComponentName)
+ .setChooserTargetComponentName(chooserTargetComponentName)
+ .setResolveInfo(resolveInfo)
+ .setDisplayLabel(displayLabel)
+ .setExtendedInfo(extendedInfo)
+ .setDisplayIconHolder(displayIconHolder)
+ .setAlternateSourceIntents(listOf(sourceIntent1, sourceIntent2))
+ .setAllDisplayTargets(listOf(displayTarget1, displayTarget2))
+ .setIsSuspended(true)
+ .setIsPinned(true)
+ .setModifiedScore(42.0f)
+ .setDirectShareShortcutInfo(directShareShortcutInfo)
+ .setDirectShareAppTarget(directShareAppTarget)
+ .setDisplayResolveInfo(displayResolveInfo)
+ .setHashProvider(hashProvider)
+ .build()
+
+ val info = infoToCopyFrom.toBuilder().build()
+
+ assertThat(info.resolvedIntent).isEqualTo(resolvedIntent)
+ assertThat(info.targetIntent).isEqualTo(targetIntent)
+ assertThat(info.referrerFillInIntent).isEqualTo(referrerFillInIntent)
+ assertThat(info.resolvedComponentName).isEqualTo(resolvedComponentName)
+ assertThat(info.chooserTargetComponentName).isEqualTo(chooserTargetComponentName)
+ assertThat(info.resolveInfo).isEqualTo(resolveInfo)
+ assertThat(info.displayLabel).isEqualTo(displayLabel)
+ assertThat(info.extendedInfo).isEqualTo(extendedInfo)
+ assertThat(info.displayIconHolder).isEqualTo(displayIconHolder)
+ assertThat(info.allSourceIntents).containsExactly(
+ resolvedIntent, sourceIntent1, sourceIntent2)
+ assertThat(info.allDisplayTargets).containsExactly(displayTarget1, displayTarget2)
+ assertThat(info.isSuspended).isTrue()
+ assertThat(info.isPinned).isTrue()
+ assertThat(info.modifiedScore).isEqualTo(42.0f)
+ assertThat(info.directShareShortcutInfo).isEqualTo(directShareShortcutInfo)
+ assertThat(info.directShareAppTarget).isEqualTo(directShareAppTarget)
+ assertThat(info.displayResolveInfo).isEqualTo(displayResolveInfo)
+ assertThat(info.isEmptyTargetInfo).isFalse()
+ assertThat(info.isPlaceHolderTargetInfo).isFalse()
+ assertThat(info.isNotSelectableTargetInfo).isFalse()
+ assertThat(info.isSelectableTargetInfo).isFalse()
+ assertThat(info.isChooserTargetInfo).isFalse()
+ assertThat(info.isMultiDisplayResolveInfo).isFalse()
+ assertThat(info.isDisplayResolveInfo).isFalse()
+ assertThat(info.hashProvider).isEqualTo(hashProvider)
+ }
+
+ @Test
+ fun testBaseIntentToSend_defaultsToResolvedIntent() {
+ val info = ImmutableTargetInfo.newBuilder().setResolvedIntent(resolvedIntent).build()
+ assertThat(info.baseIntentToSend.filterEquals(resolvedIntent)).isTrue()
+ }
+
+ @Test
+ fun testBaseIntentToSend_fillsInFromReferrerIntent() {
+ val originalIntent = Intent()
+ originalIntent.setPackage("original")
+
+ val referrerFillInIntent = Intent("REFERRER_FILL_IN")
+ referrerFillInIntent.setPackage("referrer")
+
+ val info = ImmutableTargetInfo.newBuilder()
+ .setResolvedIntent(originalIntent)
+ .setReferrerFillInIntent(referrerFillInIntent)
+ .build()
+
+ assertThat(info.baseIntentToSend.getPackage()).isEqualTo("original") // Only fill if empty.
+ assertThat(info.baseIntentToSend.action).isEqualTo("REFERRER_FILL_IN")
+ }
+
+ @Test
+ fun testBaseIntentToSend_fillsInFromRefinementIntent() {
+ val originalIntent = Intent()
+ originalIntent.putExtra("ORIGINAL", true)
+
+ val refinementIntent = Intent()
+ refinementIntent.putExtra("REFINEMENT", true)
+
+ val originalInfo = ImmutableTargetInfo.newBuilder()
+ .setResolvedIntent(originalIntent)
+ .build()
+ val info = originalInfo.tryToCloneWithAppliedRefinement(refinementIntent)
+
+ assertThat(info.baseIntentToSend.getBooleanExtra("ORIGINAL", false)).isTrue()
+ assertThat(info.baseIntentToSend.getBooleanExtra("REFINEMENT", false)).isTrue()
+ }
+
+ @Test
+ fun testBaseIntentToSend_twoFillInSourcesFavorsRefinementRequest() {
+ val originalIntent = Intent("REFINE_ME")
+ originalIntent.setPackage("original")
+
+ val referrerFillInIntent = Intent("REFERRER_FILL_IN")
+ referrerFillInIntent.setPackage("referrer_pkg")
+ referrerFillInIntent.setType("test/referrer")
+
+ val infoWithReferrerFillIn = ImmutableTargetInfo.newBuilder()
+ .setResolvedIntent(originalIntent)
+ .setReferrerFillInIntent(referrerFillInIntent)
+ .build()
+
+ val refinementIntent = Intent("REFINE_ME")
+ refinementIntent.setPackage("original") // Has to match for refinement.
+
+ val info = infoWithReferrerFillIn.tryToCloneWithAppliedRefinement(refinementIntent)
+
+ assertThat(info.baseIntentToSend.getPackage()).isEqualTo("original") // Set all along.
+ assertThat(info.baseIntentToSend.action).isEqualTo("REFINE_ME") // Refinement wins.
+ assertThat(info.baseIntentToSend.type).isEqualTo("test/referrer") // Left for referrer.
+ }
+
+ @Test
+ fun testBaseIntentToSend_doubleRefinementPreservesReferrerFillInButNotOriginalRefinement() {
+ val originalIntent = Intent("REFINE_ME")
+ val referrerFillInIntent = Intent("REFERRER_FILL_IN")
+ referrerFillInIntent.putExtra("TEST", "REFERRER")
+ val refinementIntent1 = Intent("REFINE_ME")
+ refinementIntent1.putExtra("TEST1", "1")
+ val refinementIntent2 = Intent("REFINE_ME")
+ refinementIntent2.putExtra("TEST2", "2")
+
+ val originalInfo = ImmutableTargetInfo.newBuilder()
+ .setResolvedIntent(originalIntent)
+ .setReferrerFillInIntent(referrerFillInIntent)
+ .build()
+
+ val refined1 = originalInfo.tryToCloneWithAppliedRefinement(refinementIntent1)
+ val refined2 = refined1.tryToCloneWithAppliedRefinement(refinementIntent2) // Cloned clone.
+
+ // Both clones get the same values filled in from the referrer intent.
+ assertThat(refined1.baseIntentToSend.getStringExtra("TEST")).isEqualTo("REFERRER")
+ assertThat(refined2.baseIntentToSend.getStringExtra("TEST")).isEqualTo("REFERRER")
+ // Each clone has the respective value that was set in their own refinement request.
+ assertThat(refined1.baseIntentToSend.getStringExtra("TEST1")).isEqualTo("1")
+ assertThat(refined2.baseIntentToSend.getStringExtra("TEST2")).isEqualTo("2")
+ // The clones don't have the data from each other's refinements, even though the intent
+ // field is empty (thus able to be populated by filling-in).
+ assertThat(refined1.baseIntentToSend.getStringExtra("TEST2")).isNull()
+ assertThat(refined2.baseIntentToSend.getStringExtra("TEST1")).isNull()
+ }
+
+ @Test
+ fun testBaseIntentToSend_refinementToAlternateSourceIntent() {
+ val originalIntent = Intent("DONT_REFINE_ME")
+ originalIntent.putExtra("originalIntent", true)
+ val mismatchedAlternate = Intent("DOESNT_MATCH")
+ mismatchedAlternate.putExtra("mismatchedAlternate", true)
+ val targetAlternate = Intent("REFINE_ME")
+ targetAlternate.putExtra("targetAlternate", true)
+ val extraMatch = Intent("REFINE_ME")
+ extraMatch.putExtra("extraMatch", true)
+
+ val originalInfo = ImmutableTargetInfo.newBuilder()
+ .setResolvedIntent(originalIntent)
+ .setAllSourceIntents(listOf(
+ originalIntent, mismatchedAlternate, targetAlternate, extraMatch))
+ .build()
+
+ val refinement = Intent("REFINE_ME") // First match is `targetAlternate`
+ refinement.putExtra("refinement", true)
+
+ val refinedResult = originalInfo.tryToCloneWithAppliedRefinement(refinement)
+ assertThat(refinedResult.baseIntentToSend.getBooleanExtra("refinement", false)).isTrue()
+ assertThat(refinedResult.baseIntentToSend.getBooleanExtra("targetAlternate", false))
+ .isTrue()
+ // None of the other source intents got merged in (not even the later one that matched):
+ assertThat(refinedResult.baseIntentToSend.getBooleanExtra("originalIntent", false))
+ .isFalse()
+ assertThat(refinedResult.baseIntentToSend.getBooleanExtra("mismatchedAlternate", false))
+ .isFalse()
+ assertThat(refinedResult.baseIntentToSend.getBooleanExtra("extraMatch", false)).isFalse()
+ }
+
+ @Test
+ fun testBaseIntentToSend_noSourceIntentMatchingProposedRefinement() {
+ val originalIntent = Intent("DONT_REFINE_ME")
+ originalIntent.putExtra("originalIntent", true)
+ val mismatchedAlternate = Intent("DOESNT_MATCH")
+ mismatchedAlternate.putExtra("mismatchedAlternate", true)
+
+ val originalInfo = ImmutableTargetInfo.newBuilder()
+ .setResolvedIntent(originalIntent)
+ .setAllSourceIntents(listOf(originalIntent, mismatchedAlternate))
+ .build()
+
+ val refinement = Intent("PROPOSED_REFINEMENT")
+ assertThat(originalInfo.tryToCloneWithAppliedRefinement(refinement)).isNull()
+ }
+
+ @Test
+ fun testLegacySubclassRelationships_empty() {
+ val info = ImmutableTargetInfo.newBuilder()
+ .setLegacyType(ImmutableTargetInfo.LegacyTargetType.EMPTY_TARGET_INFO)
+ .build()
+
+ assertThat(info.isEmptyTargetInfo).isTrue()
+ assertThat(info.isPlaceHolderTargetInfo).isFalse()
+ assertThat(info.isNotSelectableTargetInfo).isTrue()
+ assertThat(info.isSelectableTargetInfo).isFalse()
+ assertThat(info.isChooserTargetInfo).isTrue()
+ assertThat(info.isMultiDisplayResolveInfo).isFalse()
+ assertThat(info.isDisplayResolveInfo).isFalse()
+ }
+
+ @Test
+ fun testLegacySubclassRelationships_placeholder() {
+ val info = ImmutableTargetInfo.newBuilder()
+ .setLegacyType(ImmutableTargetInfo.LegacyTargetType.PLACEHOLDER_TARGET_INFO)
+ .build()
+
+ assertThat(info.isEmptyTargetInfo).isFalse()
+ assertThat(info.isPlaceHolderTargetInfo).isTrue()
+ assertThat(info.isNotSelectableTargetInfo).isTrue()
+ assertThat(info.isSelectableTargetInfo).isFalse()
+ assertThat(info.isChooserTargetInfo).isTrue()
+ assertThat(info.isMultiDisplayResolveInfo).isFalse()
+ assertThat(info.isDisplayResolveInfo).isFalse()
+ }
+
+ @Test
+ fun testLegacySubclassRelationships_selectable() {
+ val info = ImmutableTargetInfo.newBuilder()
+ .setLegacyType(ImmutableTargetInfo.LegacyTargetType.SELECTABLE_TARGET_INFO)
+ .build()
+
+ assertThat(info.isEmptyTargetInfo).isFalse()
+ assertThat(info.isPlaceHolderTargetInfo).isFalse()
+ assertThat(info.isNotSelectableTargetInfo).isFalse()
+ assertThat(info.isSelectableTargetInfo).isTrue()
+ assertThat(info.isChooserTargetInfo).isTrue()
+ assertThat(info.isMultiDisplayResolveInfo).isFalse()
+ assertThat(info.isDisplayResolveInfo).isFalse()
+ }
+
+ @Test
+ fun testLegacySubclassRelationships_displayResolveInfo() {
+ val info = ImmutableTargetInfo.newBuilder()
+ .setLegacyType(ImmutableTargetInfo.LegacyTargetType.DISPLAY_RESOLVE_INFO)
+ .build()
+
+ assertThat(info.isEmptyTargetInfo).isFalse()
+ assertThat(info.isPlaceHolderTargetInfo).isFalse()
+ assertThat(info.isNotSelectableTargetInfo).isFalse()
+ assertThat(info.isSelectableTargetInfo).isFalse()
+ assertThat(info.isChooserTargetInfo).isFalse()
+ assertThat(info.isMultiDisplayResolveInfo).isFalse()
+ assertThat(info.isDisplayResolveInfo).isTrue()
+ }
+
+ @Test
+ fun testLegacySubclassRelationships_multiDisplayResolveInfo() {
+ val info = ImmutableTargetInfo.newBuilder()
+ .setLegacyType(ImmutableTargetInfo.LegacyTargetType.MULTI_DISPLAY_RESOLVE_INFO)
+ .build()
+
+ assertThat(info.isEmptyTargetInfo).isFalse()
+ assertThat(info.isPlaceHolderTargetInfo).isFalse()
+ assertThat(info.isNotSelectableTargetInfo).isFalse()
+ assertThat(info.isSelectableTargetInfo).isFalse()
+ assertThat(info.isChooserTargetInfo).isFalse()
+ assertThat(info.isMultiDisplayResolveInfo).isTrue()
+ assertThat(info.isDisplayResolveInfo).isTrue()
+ }
+
+ @Test
+ fun testActivityStarter_correctNumberOfInvocations_startAsCaller() {
+ val activityStarter = object : TestActivityStarter() {
+ override fun startAsUser(
+ target: TargetInfo, activity: Activity, options: Bundle, user: UserHandle
+ ): Boolean {
+ throw RuntimeException("Wrong API used: startAsUser")
+ }
+ }
+
+ val info = ImmutableTargetInfo.newBuilder().setActivityStarter(activityStarter).build()
+ val activity: ResolverActivity = mock()
+ val options = Bundle()
+ options.putInt("TEST_KEY", 1)
+
+ info.startAsCaller(activity, options, 42)
+
+ assertThat(activityStarter.totalInvocations).isEqualTo(1)
+ assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info)
+ assertThat(activityStarter.lastInvocationActivity).isEqualTo(activity)
+ assertThat(activityStarter.lastInvocationOptions).isEqualTo(options)
+ assertThat(activityStarter.lastInvocationUserId).isEqualTo(42)
+ assertThat(activityStarter.lastInvocationAsCaller).isTrue()
+ }
+
+ @Test
+ fun testActivityStarter_correctNumberOfInvocations_startAsUser() {
+ val activityStarter = object : TestActivityStarter() {
+ override fun startAsCaller(
+ target: TargetInfo, activity: Activity, options: Bundle, userId: Int): Boolean {
+ throw RuntimeException("Wrong API used: startAsCaller")
+ }
+ }
+
+ val info = ImmutableTargetInfo.newBuilder().setActivityStarter(activityStarter).build()
+ val activity: Activity = mock()
+ val options = Bundle()
+ options.putInt("TEST_KEY", 1)
+
+ info.startAsUser(activity, options, UserHandle.of(42))
+
+ assertThat(activityStarter.totalInvocations).isEqualTo(1)
+ assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info)
+ assertThat(activityStarter.lastInvocationActivity).isEqualTo(activity)
+ assertThat(activityStarter.lastInvocationOptions).isEqualTo(options)
+ assertThat(activityStarter.lastInvocationUserId).isEqualTo(42)
+ assertThat(activityStarter.lastInvocationAsCaller).isFalse()
+ }
+
+ @Test
+ fun testActivityStarter_invokedWithRespectiveTargetInfoAfterCopy() {
+ val activityStarter = TestActivityStarter()
+ val info1 = ImmutableTargetInfo.newBuilder().setActivityStarter(activityStarter).build()
+ val info2 = info1.toBuilder().build()
+
+ info1.startAsCaller(mock(), Bundle(), 42)
+ assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info1)
+ info2.startAsCaller(mock(), Bundle(), 42)
+ assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info2)
+ info2.startAsUser(mock(), Bundle(), UserHandle.of(42))
+ assertThat(activityStarter.lastInvocationTargetInfo).isEqualTo(info2)
+
+ assertThat(activityStarter.totalInvocations).isEqualTo(3) // Instance is still shared.
+ }
+}
+
+private open class TestActivityStarter : ImmutableTargetInfo.TargetActivityStarter {
+ var totalInvocations = 0
+ var lastInvocationTargetInfo: TargetInfo? = null
+ var lastInvocationActivity: Activity? = null
+ var lastInvocationOptions: Bundle? = null
+ var lastInvocationUserId: Integer? = null
+ var lastInvocationAsCaller = false
+
+ override fun startAsCaller(
+ target: TargetInfo, activity: Activity, options: Bundle, userId: Int): Boolean {
+ ++totalInvocations
+ lastInvocationTargetInfo = target
+ lastInvocationActivity = activity
+ lastInvocationOptions = options
+ lastInvocationUserId = Integer(userId)
+ lastInvocationAsCaller = true
+ return true
+ }
+
+ override fun startAsUser(
+ target: TargetInfo, activity: Activity, options: Bundle, user: UserHandle): Boolean {
+ ++totalInvocations
+ lastInvocationTargetInfo = target
+ lastInvocationActivity = activity
+ lastInvocationOptions = options
+ lastInvocationUserId = Integer(user.identifier)
+ lastInvocationAsCaller = false
+ return true
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt
index 7c2b07a9..dddbcccb 100644
--- a/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt
+++ b/java/tests/src/com/android/intentresolver/chooser/TargetInfoTest.kt
@@ -22,18 +22,36 @@ import android.content.ComponentName
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.pm.ResolveInfo
+import android.graphics.drawable.AnimatedVectorDrawable
import android.os.UserHandle
+import android.test.UiThreadTest
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.intentresolver.ResolverDataProvider
import com.android.intentresolver.createChooserTarget
import com.android.intentresolver.createShortcutInfo
import com.android.intentresolver.mock
-import com.android.intentresolver.ResolverDataProvider
+import com.android.intentresolver.whenever
import com.google.common.truth.Truth.assertThat
+import org.junit.Before
import org.junit.Test
+import org.mockito.Mockito.any
+import org.mockito.Mockito.never
+import org.mockito.Mockito.spy
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
class TargetInfoTest {
private val context = InstrumentationRegistry.getInstrumentation().getContext()
+ @Before
+ fun setup() {
+ // SelectableTargetInfo reads DeviceConfig and needs a permission for that.
+ InstrumentationRegistry
+ .getInstrumentation()
+ .getUiAutomation()
+ .adoptShellPermissionIdentity("android.permission.READ_DEVICE_CONFIG")
+ }
+
@Test
fun testNewEmptyTargetInfo() {
val info = NotSelectableTargetInfo.newEmptyTargetInfo()
@@ -43,13 +61,19 @@ class TargetInfoTest {
assertThat(info.getDisplayIconHolder().getDisplayIcon()).isNull()
}
+ @UiThreadTest // AnimatedVectorDrawable needs to start from a thread with a Looper.
@Test
fun testNewPlaceholderTargetInfo() {
val info = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context)
- assertThat(info.isPlaceHolderTargetInfo()).isTrue()
- assertThat(info.isChooserTargetInfo()).isTrue() // From legacy inheritance model.
+ assertThat(info.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.
+ assertThat(info.displayIconHolder.displayIcon)
+ .isInstanceOf(AnimatedVectorDrawable::class.java)
+ // TODO: assert that the animation is pre-started/running (IIUC this requires synchronizing
+ // with some "render thread" per the `AnimatedVectorDrawable` docs). I believe this is
+ // possible using `AnimatorTestRule` but I couldn't find any sample usage in Kotlin nor get
+ // it working myself.
}
@Test
@@ -125,6 +149,42 @@ class TargetInfoTest {
}
@Test
+ fun testSelectableTargetInfo_noSourceIntentMatchingProposedRefinement() {
+ val resolvedIntent = Intent("DONT_REFINE_ME")
+ resolvedIntent.putExtra("resolvedIntent", true)
+
+ val baseDisplayInfo = DisplayResolveInfo.newDisplayResolveInfo(
+ resolvedIntent,
+ ResolverDataProvider.createResolveInfo(1, 0),
+ "label",
+ "extended info",
+ resolvedIntent,
+ /* resolveInfoPresentationGetter= */ null)
+ val chooserTarget = createChooserTarget(
+ "title", 0.3f, ResolverDataProvider.createComponentName(2), "test_shortcut_id")
+ val shortcutInfo = createShortcutInfo("id", ResolverDataProvider.createComponentName(3), 3)
+ val appTarget = AppTarget(
+ AppTargetId("id"),
+ chooserTarget.componentName.packageName,
+ chooserTarget.componentName.className,
+ UserHandle.CURRENT)
+
+ val targetInfo = SelectableTargetInfo.newSelectableTargetInfo(
+ baseDisplayInfo,
+ mock(),
+ resolvedIntent,
+ chooserTarget,
+ 0.1f,
+ shortcutInfo,
+ appTarget,
+ mock(),
+ )
+
+ val refinement = Intent("PROPOSED_REFINEMENT")
+ assertThat(targetInfo.tryToCloneWithAppliedRefinement(refinement)).isNull()
+ }
+
+ @Test
fun testNewDisplayResolveInfo() {
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending")
@@ -145,6 +205,64 @@ class TargetInfoTest {
}
@Test
+ fun test_DisplayResolveInfo_refinementToAlternateSourceIntent() {
+ val originalIntent = Intent("DONT_REFINE_ME")
+ originalIntent.putExtra("originalIntent", true)
+ val mismatchedAlternate = Intent("DOESNT_MATCH")
+ mismatchedAlternate.putExtra("mismatchedAlternate", true)
+ val targetAlternate = Intent("REFINE_ME")
+ targetAlternate.putExtra("targetAlternate", true)
+ val extraMatch = Intent("REFINE_ME")
+ extraMatch.putExtra("extraMatch", true)
+
+ val originalInfo = DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent,
+ ResolverDataProvider.createResolveInfo(3, 0),
+ "label",
+ "extended info",
+ originalIntent,
+ /* resolveInfoPresentationGetter= */ null)
+ originalInfo.addAlternateSourceIntent(mismatchedAlternate)
+ originalInfo.addAlternateSourceIntent(targetAlternate)
+ originalInfo.addAlternateSourceIntent(extraMatch)
+
+ val refinement = Intent("REFINE_ME") // First match is `targetAlternate`
+ refinement.putExtra("refinement", true)
+
+ val refinedResult = originalInfo.tryToCloneWithAppliedRefinement(refinement)
+ // Note `DisplayResolveInfo` targets merge refinements directly into their `resolvedIntent`.
+ assertThat(refinedResult.resolvedIntent.getBooleanExtra("refinement", false)).isTrue()
+ assertThat(refinedResult.resolvedIntent.getBooleanExtra("targetAlternate", false))
+ .isTrue()
+ // None of the other source intents got merged in (not even the later one that matched):
+ assertThat(refinedResult.resolvedIntent.getBooleanExtra("originalIntent", false))
+ .isFalse()
+ assertThat(refinedResult.resolvedIntent.getBooleanExtra("mismatchedAlternate", false))
+ .isFalse()
+ assertThat(refinedResult.resolvedIntent.getBooleanExtra("extraMatch", false)).isFalse()
+ }
+
+ @Test
+ fun testDisplayResolveInfo_noSourceIntentMatchingProposedRefinement() {
+ val originalIntent = Intent("DONT_REFINE_ME")
+ originalIntent.putExtra("originalIntent", true)
+ val mismatchedAlternate = Intent("DOESNT_MATCH")
+ mismatchedAlternate.putExtra("mismatchedAlternate", true)
+
+ val originalInfo = DisplayResolveInfo.newDisplayResolveInfo(
+ originalIntent,
+ ResolverDataProvider.createResolveInfo(3, 0),
+ "label",
+ "extended info",
+ originalIntent,
+ /* resolveInfoPresentationGetter= */ null)
+ originalInfo.addAlternateSourceIntent(mismatchedAlternate)
+
+ val refinement = Intent("PROPOSED_REFINEMENT")
+ assertThat(originalInfo.tryToCloneWithAppliedRefinement(refinement)).isNull()
+ }
+
+ @Test
fun testNewMultiDisplayResolveInfo() {
val intent = Intent(Intent.ACTION_SEND)
intent.putExtra(Intent.EXTRA_TEXT, "testing intent sending")
@@ -186,7 +304,93 @@ class TargetInfoTest {
assertThat(multiTargetInfo.hasSelected()).isTrue()
assertThat(multiTargetInfo.getSelectedTarget()).isEqualTo(secondTargetInfo)
+ val refined = multiTargetInfo.tryToCloneWithAppliedRefinement(intent)
+ assertThat(refined).isInstanceOf(MultiDisplayResolveInfo::class.java)
+ assertThat((refined as MultiDisplayResolveInfo).hasSelected())
+ .isEqualTo(multiTargetInfo.hasSelected())
+
// TODO: consider exercising activity-start behavior.
// TODO: consider exercising DisplayResolveInfo base class behavior.
}
+
+ @Test
+ fun testNewMultiDisplayResolveInfo_getAllSourceIntents_fromSelectedTarget() {
+ val sendImage = Intent("SEND").apply { type = "image/png" }
+ val sendUri = Intent("SEND").apply { type = "text/uri" }
+
+ val resolveInfo = ResolverDataProvider.createResolveInfo(1, 0)
+
+ val imageOnlyTarget = DisplayResolveInfo.newDisplayResolveInfo(
+ sendImage,
+ resolveInfo,
+ "Send Image",
+ "Sends only images",
+ sendImage,
+ /* resolveInfoPresentationGetter= */ null)
+
+ val textOnlyTarget = DisplayResolveInfo.newDisplayResolveInfo(
+ sendUri,
+ resolveInfo,
+ "Send Text",
+ "Sends only text",
+ sendUri,
+ /* resolveInfoPresentationGetter= */ null)
+
+ val imageOrTextTarget = DisplayResolveInfo.newDisplayResolveInfo(
+ sendImage,
+ resolveInfo,
+ "Send Image or Text",
+ "Sends images or text",
+ sendImage,
+ /* resolveInfoPresentationGetter= */ null
+ ).apply {
+ addAlternateSourceIntent(sendUri)
+ }
+
+ val multiTargetInfo = MultiDisplayResolveInfo.newMultiDisplayResolveInfo(
+ listOf(imageOnlyTarget, textOnlyTarget, imageOrTextTarget)
+ )
+
+ multiTargetInfo.setSelected(0)
+ assertThat(multiTargetInfo.selectedTarget).isEqualTo(imageOnlyTarget)
+ assertThat(multiTargetInfo.allSourceIntents).isEqualTo(imageOnlyTarget.allSourceIntents)
+
+ multiTargetInfo.setSelected(1)
+ assertThat(multiTargetInfo.selectedTarget).isEqualTo(textOnlyTarget)
+ assertThat(multiTargetInfo.allSourceIntents).isEqualTo(textOnlyTarget.allSourceIntents)
+
+ multiTargetInfo.setSelected(2)
+ assertThat(multiTargetInfo.selectedTarget).isEqualTo(imageOrTextTarget)
+ assertThat(multiTargetInfo.allSourceIntents).isEqualTo(imageOrTextTarget.allSourceIntents)
+ }
+
+ @Test
+ fun testNewMultiDisplayResolveInfo_tryToCloneWithAppliedRefinement_delegatedToSelectedTarget() {
+ val refined = Intent("SEND")
+ val sendImage = Intent("SEND")
+ val targetOne = spy(
+ DisplayResolveInfo.newDisplayResolveInfo(
+ sendImage,
+ ResolverDataProvider.createResolveInfo(1, 0),
+ "Target One",
+ "Target One",
+ sendImage,
+ /* resolveInfoPresentationGetter= */ null
+ )
+ )
+ val targetTwo = mock<DisplayResolveInfo> {
+ whenever(tryToCloneWithAppliedRefinement(any())).thenReturn(this)
+ }
+
+ val multiTargetInfo = MultiDisplayResolveInfo.newMultiDisplayResolveInfo(
+ listOf(targetOne, targetTwo)
+ )
+
+ multiTargetInfo.setSelected(1)
+ assertThat(multiTargetInfo.selectedTarget).isEqualTo(targetTwo)
+
+ multiTargetInfo.tryToCloneWithAppliedRefinement(refined)
+ verify(targetTwo, times(1)).tryToCloneWithAppliedRefinement(refined)
+ verify(targetOne, never()).tryToCloneWithAppliedRefinement(any())
+ }
}
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt
new file mode 100644
index 00000000..d870a8c2
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt
@@ -0,0 +1,203 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview
+
+import android.content.ClipDescription
+import android.content.ContentInterface
+import android.content.Intent
+import android.graphics.Bitmap
+import android.net.Uri
+import com.android.intentresolver.ImageLoader
+import com.android.intentresolver.TestFeatureFlagRepository
+import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory
+import com.android.intentresolver.flags.Flags
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import com.android.intentresolver.widget.ActionRow
+import com.android.intentresolver.widget.ImagePreviewView
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import java.util.function.Consumer
+
+private const val PROVIDER_NAME = "org.pkg.app"
+class ChooserContentPreviewUiTest {
+ private val contentResolver = mock<ContentInterface>()
+ private val imageClassifier = ChooserContentPreviewUi.ImageMimeTypeClassifier { mimeType ->
+ mimeType != null && ClipDescription.compareMimeTypes(mimeType, "image/*")
+ }
+ private val imageLoader = object : ImageLoader {
+ override fun loadImage(uri: Uri, callback: Consumer<Bitmap?>) {
+ callback.accept(null)
+ }
+ override fun prePopulate(uris: List<Uri>) = Unit
+ override suspend fun invoke(uri: Uri): Bitmap? = null
+ }
+ private val actionFactory = object : ActionFactory {
+ override fun createCopyButton() = ActionRow.Action(label = "Copy", icon = null) {}
+ override fun createEditButton(): ActionRow.Action? = null
+ override fun createNearbyButton(): ActionRow.Action? = null
+ override fun createCustomActions(): List<ActionRow.Action> = emptyList()
+ override fun getModifyShareAction(): Runnable? = null
+ override fun getExcludeSharedTextAction(): Consumer<Boolean> = Consumer<Boolean> {}
+ }
+ private val transitionCallback = mock<ImagePreviewView.TransitionElementStatusCallback>()
+ private val featureFlagRepository = TestFeatureFlagRepository(
+ mapOf(
+ Flags.SHARESHEET_SCROLLABLE_IMAGE_PREVIEW to true
+ )
+ )
+
+ @Test
+ fun test_ChooserContentPreview_non_send_intent_action_to_text_preview() {
+ val targetIntent = Intent(Intent.ACTION_VIEW)
+ val testSubject = ChooserContentPreviewUi(
+ targetIntent,
+ contentResolver,
+ imageClassifier,
+ imageLoader,
+ actionFactory,
+ transitionCallback,
+ featureFlagRepository
+ )
+ assertThat(testSubject.preferredContentPreview)
+ .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT)
+ verify(transitionCallback, times(1)).onAllTransitionElementsReady()
+ }
+
+ @Test
+ fun test_ChooserContentPreview_text_mime_type_to_text_preview() {
+ val targetIntent = Intent(Intent.ACTION_SEND).apply {
+ type = "text/plain"
+ putExtra(Intent.EXTRA_TEXT, "Text Extra")
+ }
+ val testSubject = ChooserContentPreviewUi(
+ targetIntent,
+ contentResolver,
+ imageClassifier,
+ imageLoader,
+ actionFactory,
+ transitionCallback,
+ featureFlagRepository
+ )
+ assertThat(testSubject.preferredContentPreview)
+ .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT)
+ verify(transitionCallback, times(1)).onAllTransitionElementsReady()
+ }
+
+ @Test
+ fun test_ChooserContentPreview_single_image_uri_to_image_preview() {
+ val uri = Uri.parse("content://$PROVIDER_NAME/test.png")
+ val targetIntent = Intent(Intent.ACTION_SEND).apply {
+ putExtra(Intent.EXTRA_STREAM, uri)
+ }
+ whenever(contentResolver.getType(uri)).thenReturn("image/png")
+ val testSubject = ChooserContentPreviewUi(
+ targetIntent,
+ contentResolver,
+ imageClassifier,
+ imageLoader,
+ actionFactory,
+ transitionCallback,
+ featureFlagRepository
+ )
+ assertThat(testSubject.preferredContentPreview)
+ .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
+ verify(transitionCallback, never()).onAllTransitionElementsReady()
+ }
+
+ @Test
+ fun test_ChooserContentPreview_single_non_image_uri_to_file_preview() {
+ val uri = Uri.parse("content://$PROVIDER_NAME/test.pdf")
+ val targetIntent = Intent(Intent.ACTION_SEND).apply {
+ putExtra(Intent.EXTRA_STREAM, uri)
+ }
+ whenever(contentResolver.getType(uri)).thenReturn("application/pdf")
+ val testSubject = ChooserContentPreviewUi(
+ targetIntent,
+ contentResolver,
+ imageClassifier,
+ imageLoader,
+ actionFactory,
+ transitionCallback,
+ featureFlagRepository
+ )
+ assertThat(testSubject.preferredContentPreview)
+ .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
+ verify(transitionCallback, times(1)).onAllTransitionElementsReady()
+ }
+
+ @Test
+ fun test_ChooserContentPreview_multiple_image_uri_to_image_preview() {
+ val uri1 = Uri.parse("content://$PROVIDER_NAME/test.png")
+ val uri2 = Uri.parse("content://$PROVIDER_NAME/test.jpg")
+ val targetIntent = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
+ putExtra(
+ Intent.EXTRA_STREAM,
+ ArrayList<Uri>().apply {
+ add(uri1)
+ add(uri2)
+ }
+ )
+ }
+ whenever(contentResolver.getType(uri1)).thenReturn("image/png")
+ whenever(contentResolver.getType(uri2)).thenReturn("image/jpeg")
+ val testSubject = ChooserContentPreviewUi(
+ targetIntent,
+ contentResolver,
+ imageClassifier,
+ imageLoader,
+ actionFactory,
+ transitionCallback,
+ featureFlagRepository
+ )
+ assertThat(testSubject.preferredContentPreview)
+ .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
+ verify(transitionCallback, never()).onAllTransitionElementsReady()
+ }
+
+ @Test
+ fun test_ChooserContentPreview_some_non_image_uri_to_file_preview() {
+ val uri1 = Uri.parse("content://$PROVIDER_NAME/test.png")
+ val uri2 = Uri.parse("content://$PROVIDER_NAME/test.pdf")
+ val targetIntent = Intent(Intent.ACTION_SEND_MULTIPLE).apply {
+ putExtra(
+ Intent.EXTRA_STREAM,
+ ArrayList<Uri>().apply {
+ add(uri1)
+ add(uri2)
+ }
+ )
+ }
+ whenever(contentResolver.getType(uri1)).thenReturn("image/png")
+ whenever(contentResolver.getType(uri2)).thenReturn("application/pdf")
+ val testSubject = ChooserContentPreviewUi(
+ targetIntent,
+ contentResolver,
+ imageClassifier,
+ imageLoader,
+ actionFactory,
+ transitionCallback,
+ featureFlagRepository
+ )
+ assertThat(testSubject.preferredContentPreview)
+ .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
+ verify(transitionCallback, times(1)).onAllTransitionElementsReady()
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java
index 448718cd..006f3b2d 100644
--- a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java
+++ b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java
@@ -27,7 +27,7 @@ import android.os.Message;
import androidx.test.InstrumentationRegistry;
-import com.android.intentresolver.ResolverActivity;
+import com.android.intentresolver.ResolvedComponentInfo;
import org.junit.Test;
@@ -37,12 +37,12 @@ public class AbstractResolverComparatorTest {
@Test
public void testPinned() {
- ResolverActivity.ResolvedComponentInfo r1 = new ResolverActivity.ResolvedComponentInfo(
+ ResolvedComponentInfo r1 = new ResolvedComponentInfo(
new ComponentName("package", "class"), new Intent(), new ResolveInfo()
);
r1.setPinned(true);
- ResolverActivity.ResolvedComponentInfo r2 = new ResolverActivity.ResolvedComponentInfo(
+ ResolvedComponentInfo r2 = new ResolvedComponentInfo(
new ComponentName("zackage", "zlass"), new Intent(), new ResolveInfo()
);
@@ -60,14 +60,14 @@ public class AbstractResolverComparatorTest {
pmInfo1.activityInfo = new ActivityInfo();
pmInfo1.activityInfo.packageName = "aaa";
- ResolverActivity.ResolvedComponentInfo r1 = new ResolverActivity.ResolvedComponentInfo(
+ ResolvedComponentInfo r1 = new 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(
+ ResolvedComponentInfo r2 = new ResolvedComponentInfo(
new ComponentName("zackage", "zlass"), new Intent(), pmInfo2);
r2.setPinned(true);
@@ -91,7 +91,7 @@ public class AbstractResolverComparatorTest {
}
@Override
- void doCompute(List<ResolverActivity.ResolvedComponentInfo> targets) {}
+ void doCompute(List<ResolvedComponentInfo> targets) {}
@Override
public float getScore(ComponentName name) {
diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
index 5756a0cd..0c817cb2 100644
--- a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
+++ b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
@@ -28,6 +28,8 @@ import android.os.UserHandle
import android.os.UserManager
import androidx.test.filters.SmallTest
import com.android.intentresolver.any
+import com.android.intentresolver.argumentCaptor
+import com.android.intentresolver.capture
import com.android.intentresolver.chooser.DisplayResolveInfo
import com.android.intentresolver.createAppTarget
import com.android.intentresolver.createShareShortcutInfo
@@ -39,8 +41,8 @@ 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.atLeastOnce
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
@@ -56,9 +58,15 @@ class ShortcutLoaderTest {
private val pm = mock<PackageManager> {
whenever(getApplicationInfo(any(), any<ApplicationInfoFlags>())).thenReturn(appInfo)
}
+ val userManager = mock<UserManager> {
+ whenever(isUserRunning(any<UserHandle>())).thenReturn(true)
+ whenever(isUserUnlocked(any<UserHandle>())).thenReturn(true)
+ whenever(isQuietModeEnabled(any<UserHandle>())).thenReturn(false)
+ }
private val context = mock<Context> {
whenever(packageManager).thenReturn(pm)
whenever(createContextAsUser(any(), anyInt())).thenReturn(this)
+ whenever(getSystemService(Context.USER_SERVICE)).thenReturn(userManager)
}
private val executor = ImmediateExecutor()
private val intentFilter = mock<IntentFilter>()
@@ -66,7 +74,7 @@ class ShortcutLoaderTest {
private val callback = mock<Consumer<ShortcutLoader.Result>>()
@Test
- fun test_app_predictor_result() {
+ fun test_queryShortcuts_result_consistency_with_AppPredictor() {
val componentName = ComponentName("pkg", "Class")
val appTarget = mock<DisplayResolveInfo> {
whenever(resolvedComponentName).thenReturn(componentName)
@@ -85,24 +93,22 @@ class ShortcutLoaderTest {
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
+ // an AppTarget that does not belong to any resolved application; should be ignored
createAppTarget(
createShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
)
)
+ val appPredictorCallbackCaptor = argumentCaptor<AppPredictor.Callback>()
+ verify(appPredictor, atLeastOnce())
+ .registerPredictionUpdates(any(), capture(appPredictorCallbackCaptor))
appPredictorCallbackCaptor.value.onTargetsAvailable(shortcuts)
- val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java)
- verify(callback, times(1)).accept(resultCaptor.capture())
+ val resultCaptor = argumentCaptor<ShortcutLoader.Result>()
+ verify(callback, times(1)).accept(capture(resultCaptor))
val result = resultCaptor.value
assertTrue("An app predictor result is expected", result.isFromAppPredictor)
@@ -124,7 +130,7 @@ class ShortcutLoaderTest {
}
@Test
- fun test_shortcut_manager_result() {
+ fun test_queryShortcuts_result_consistency_with_ShortcutManager() {
val componentName = ComponentName("pkg", "Class")
val appTarget = mock<DisplayResolveInfo> {
whenever(resolvedComponentName).thenReturn(componentName)
@@ -153,8 +159,8 @@ class ShortcutLoaderTest {
testSubject.queryShortcuts(appTargets)
- val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java)
- verify(callback, times(1)).accept(resultCaptor.capture())
+ val resultCaptor = argumentCaptor<ShortcutLoader.Result>()
+ verify(callback, times(1)).accept(capture(resultCaptor))
val result = resultCaptor.value
assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor)
@@ -175,7 +181,7 @@ class ShortcutLoaderTest {
}
@Test
- fun test_fallback_to_shortcut_manager() {
+ fun test_queryShortcuts_falls_back_to_ShortcutManager_on_empty_reply() {
val componentName = ComponentName("pkg", "Class")
val appTarget = mock<DisplayResolveInfo> {
whenever(resolvedComponentName).thenReturn(componentName)
@@ -205,13 +211,13 @@ class ShortcutLoaderTest {
testSubject.queryShortcuts(appTargets)
verify(appPredictor, times(1)).requestPredictionUpdate()
- val appPredictorCallbackCaptor = ArgumentCaptor.forClass(AppPredictor.Callback::class.java)
+ val appPredictorCallbackCaptor = argumentCaptor<AppPredictor.Callback>()
verify(appPredictor, times(1))
- .registerPredictionUpdates(any(), appPredictorCallbackCaptor.capture())
+ .registerPredictionUpdates(any(), capture(appPredictorCallbackCaptor))
appPredictorCallbackCaptor.value.onTargetsAvailable(emptyList())
- val resultCaptor = ArgumentCaptor.forClass(ShortcutLoader.Result::class.java)
- verify(callback, times(1)).accept(resultCaptor.capture())
+ val resultCaptor = argumentCaptor<ShortcutLoader.Result>()
+ verify(callback, times(1)).accept(capture(resultCaptor))
val result = resultCaptor.value
assertFalse("An ShortcutManager result is expected", result.isFromAppPredictor)
@@ -232,32 +238,32 @@ class ShortcutLoaderTest {
}
@Test
- fun test_do_not_call_services_for_not_running_work_profile() {
+ fun test_queryShortcuts_do_not_call_services_for_not_running_work_profile() {
testDisabledWorkProfileDoNotCallSystem(isUserRunning = false)
}
@Test
- fun test_do_not_call_services_for_locked_work_profile() {
+ fun test_queryShortcuts_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() {
+ fun test_queryShortcuts_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() {
+ fun test_queryShortcuts_call_services_for_not_running_main_profile() {
testAlwaysCallSystemForMainProfile(isUserRunning = false)
}
@Test
- fun test_call_services_for_locked_main_profile() {
+ fun test_queryShortcuts_call_services_for_locked_main_profile() {
testAlwaysCallSystemForMainProfile(isUserUnlocked = false)
}
@Test
- fun test_call_services_if_quite_mode_is_enabled_for_main_profile() {
+ fun test_queryShortcuts_call_services_if_quite_mode_is_enabled_for_main_profile() {
testAlwaysCallSystemForMainProfile(isQuietModeEnabled = true)
}
@@ -267,7 +273,7 @@ class ShortcutLoaderTest {
isQuietModeEnabled: Boolean = false
) {
val userHandle = UserHandle.of(10)
- val userManager = mock<UserManager> {
+ with(userManager) {
whenever(isUserRunning(userHandle)).thenReturn(isUserRunning)
whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked)
whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled)
@@ -297,7 +303,7 @@ class ShortcutLoaderTest {
isQuietModeEnabled: Boolean = false
) {
val userHandle = UserHandle.of(10)
- val userManager = mock<UserManager> {
+ with(userManager) {
whenever(isUserRunning(userHandle)).thenReturn(isUserRunning)
whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked)
whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled)