summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Joshua Trask <joshtrask@google.com> 2022-09-20 13:58:08 -0400
committer Joshua Trask <joshtrask@google.com> 2022-09-23 12:17:04 -0400
commit02632503da6f22a658bc7a069cfa485a5dc4419a (patch)
tree872cfd42412e3fe856eadf17dc3d54b6c48fa8d3
parent89e88bfea5d47cdd236ca540e0fb3c0f8a72ae98 (diff)
Fork framework chooser code to IntentResolver path
This copies Chooser classes and (as needed) their transitive dependencies, while making minor mechanical changes to fix up references for the new package. This is a large CL, especially since we don't track the history of these files across projects (framework vs. "unbundled"), and the meaningful changes could easily get lost in the noise; reviewers should see accompanying notes at go/chooser-fork-cl. Test: locally re-enabled and ran UnbundledChooserActivityTest (as updated in this CL to match the framework version), then re-disabled before uploading. See notes for more info. Change-Id: I6708e3563d18a9e31894dd36dcd65e9a5815abcf
-rw-r--r--Android.bp7
-rw-r--r--java/res/drawable/bottomsheet_background.xml22
-rw-r--r--java/res/drawable/chooser_action_button_bg.xml33
-rw-r--r--java/res/drawable/chooser_dialog_background.xml21
-rw-r--r--java/res/drawable/chooser_direct_share_icon_placeholder.xml84
-rw-r--r--java/res/drawable/chooser_direct_share_label_placeholder.xml37
-rw-r--r--java/res/drawable/chooser_file_generic.xml24
-rw-r--r--java/res/drawable/chooser_group_background.xml25
-rw-r--r--java/res/drawable/chooser_pinned_background.xml26
-rw-r--r--java/res/drawable/chooser_row_layer_list.xml28
-rw-r--r--java/res/drawable/ic_chooser_group_arrow.xml26
-rw-r--r--java/res/drawable/ic_chooser_pin.xml26
-rw-r--r--java/res/drawable/ic_chooser_pin_dialog.xml25
-rw-r--r--java/res/drawable/ic_drag_handle.xml21
-rw-r--r--java/res/drawable/ic_file_copy.xml25
-rw-r--r--java/res/drawable/iconfactory_adaptive_icon_drawable_wrapper.xml24
-rw-r--r--java/res/drawable/resolver_button_bg.xml40
-rw-r--r--java/res/drawable/resolver_icon_placeholder.xml19
-rw-r--r--java/res/drawable/resolver_outlined_button_bg.xml33
-rw-r--r--java/res/drawable/resolver_profile_tab_bg.xml45
-rw-r--r--java/res/layout/chooser_action_button.xml31
-rw-r--r--java/res/layout/chooser_action_row.xml26
-rw-r--r--java/res/layout/chooser_az_label_row.xml26
-rw-r--r--java/res/layout/chooser_dialog.xml61
-rw-r--r--java/res/layout/chooser_dialog_item.xml46
-rw-r--r--java/res/layout/chooser_grid.xml97
-rw-r--r--java/res/layout/chooser_grid_preview_file.xml80
-rw-r--r--java/res/layout/chooser_grid_preview_image.xml93
-rw-r--r--java/res/layout/chooser_grid_preview_text.xml102
-rw-r--r--java/res/layout/chooser_list_per_profile.xml33
-rw-r--r--java/res/layout/chooser_profile_row.xml33
-rw-r--r--java/res/layout/chooser_row.xml33
-rw-r--r--java/res/layout/chooser_row_direct_share.xml26
-rw-r--r--java/res/layout/miniresolver.xml115
-rw-r--r--java/res/layout/resolve_grid_item.xml65
-rw-r--r--java/res/layout/resolve_list_item.xml74
-rw-r--r--java/res/layout/resolver_different_item_header.xml34
-rw-r--r--java/res/layout/resolver_empty_states.xml91
-rw-r--r--java/res/layout/resolver_list.xml172
-rw-r--r--java/res/layout/resolver_list_per_profile.xml38
-rw-r--r--java/res/layout/resolver_list_with_default.xml206
-rw-r--r--java/res/layout/resolver_profile_tab_button.xml30
-rw-r--r--java/res/values-h480dp/bools.xml20
-rw-r--r--java/res/values-h480dp/dimens.xml23
-rw-r--r--java/res/values-land/dimens.xml23
-rw-r--r--java/res/values-night/colors.xml24
-rw-r--r--java/res/values-sw600dp/dimens.xml24
-rw-r--r--java/res/values/attrs.xml38
-rw-r--r--java/res/values/bools.xml19
-rw-r--r--java/res/values/colors.xml23
-rw-r--r--java/res/values/config.xml43
-rw-r--r--java/res/values/dimens.xml34
-rw-r--r--java/res/values/strings.xml82
-rw-r--r--java/res/values/styles.xml8
-rw-r--r--java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java632
-rw-r--r--java/src/com/android/intentresolver/AbstractResolverComparator.java296
-rw-r--r--java/src/com/android/intentresolver/AppPredictionServiceResolverComparator.java276
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java4125
-rw-r--r--java/src/com/android/intentresolver/ChooserActivityLogger.java246
-rw-r--r--java/src/com/android/intentresolver/ChooserActivityLoggerImpl.java84
-rw-r--r--java/src/com/android/intentresolver/ChooserFlags.java33
-rw-r--r--java/src/com/android/intentresolver/ChooserGridLayoutManager.java81
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java845
-rw-r--r--java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java321
-rw-r--r--java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java92
-rw-r--r--java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java77
-rw-r--r--java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java297
-rw-r--r--java/src/com/android/intentresolver/IntentForwarderActivity.java436
-rw-r--r--java/src/com/android/intentresolver/ResolverActivity.java2301
-rw-r--r--java/src/com/android/intentresolver/ResolverComparatorModel.java57
-rw-r--r--java/src/com/android/intentresolver/ResolverListAdapter.java1163
-rw-r--r--java/src/com/android/intentresolver/ResolverListController.java413
-rw-r--r--java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java279
-rw-r--r--java/src/com/android/intentresolver/ResolverRankerServiceResolverComparator.java599
-rw-r--r--java/src/com/android/intentresolver/ResolverViewPager.java89
-rw-r--r--java/src/com/android/intentresolver/SimpleIconFactory.java752
-rw-r--r--java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java53
-rw-r--r--java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java243
-rw-r--r--java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java100
-rw-r--r--java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java92
-rw-r--r--java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java339
-rw-r--r--java/src/com/android/intentresolver/chooser/TargetInfo.java133
-rw-r--r--java/tests/AndroidManifest.xml1
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserActivityLoggerFake.java134
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java115
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java20
-rw-r--r--java/tests/src/com/android/intentresolver/IChooserWrapper.java44
-rw-r--r--java/tests/src/com/android/intentresolver/MatcherUtils.java51
-rw-r--r--java/tests/src/com/android/intentresolver/ResolverDataProvider.java169
-rw-r--r--java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java3167
90 files changed, 20570 insertions, 49 deletions
diff --git a/Android.bp b/Android.bp
index 9b5d1ddc..2407fc72 100644
--- a/Android.bp
+++ b/Android.bp
@@ -45,6 +45,7 @@ android_library {
static_libs: [
"androidx.annotation_annotation",
+ "unsupportedappusage",
],
plugins: ["java_api_finder"],
@@ -66,6 +67,12 @@ android_app {
static_libs: [
"IntentResolver-core",
],
+ optimize: {
+ // TODO: consider re-enabling after setting up Proguard rules to
+ // preserve the name of the ChooserGridLayoutManager class, which is
+ // referenced by name in the chooser_list_per_profile layout XML.
+ enabled: false,
+ },
apex_available: [
"//apex_available:platform",
"com.android.intentresolver",
diff --git a/java/res/drawable/bottomsheet_background.xml b/java/res/drawable/bottomsheet_background.xml
new file mode 100644
index 00000000..60fd7e21
--- /dev/null
+++ b/java/res/drawable/bottomsheet_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2018 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
+ <corners
+ android:topLeftRadius="@*android:dimen/config_bottomDialogCornerRadius"
+ android:topRightRadius="@*android:dimen/config_bottomDialogCornerRadius"/>
+ <solid android:color="?android:attr/colorBackground" />
+</shape>
diff --git a/java/res/drawable/chooser_action_button_bg.xml b/java/res/drawable/chooser_action_button_bg.xml
new file mode 100644
index 00000000..b984a067
--- /dev/null
+++ b/java/res/drawable/chooser_action_button_bg.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ android:color="?android:attr/colorControlHighlight"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item>
+ <inset
+ android:insetLeft="0dp"
+ android:insetTop="8dp"
+ android:insetRight="0dp"
+ android:insetBottom="8dp">
+ <shape android:shape="rectangle">
+ <corners android:radius="8dp" />
+ <stroke android:width="1dp"
+ android:color="?androidprv:attr/colorAccentPrimaryVariant"/>
+ </shape>
+ </inset>
+ </item>
+</ripple>
diff --git a/java/res/drawable/chooser_dialog_background.xml b/java/res/drawable/chooser_dialog_background.xml
new file mode 100644
index 00000000..9c38cb37
--- /dev/null
+++ b/java/res/drawable/chooser_dialog_background.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<shape android:shape="rectangle" xmlns:android="http://schemas.android.com/apk/res/android">
+ <corners android:radius="?android:attr/dialogCornerRadius" />
+ <solid android:color="?android:attr/colorBackgroundFloating" />
+</shape>
diff --git a/java/res/drawable/chooser_direct_share_icon_placeholder.xml b/java/res/drawable/chooser_direct_share_icon_placeholder.xml
new file mode 100644
index 00000000..9642a0d5
--- /dev/null
+++ b/java/res/drawable/chooser_direct_share_icon_placeholder.xml
@@ -0,0 +1,84 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<animated-vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt">
+ <aapt:attr name="android:drawable">
+ <vector xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:aapt="http://schemas.android.com/aapt"
+ android:width="36dp"
+ android:height="36dp"
+ android:viewportHeight="64"
+ android:viewportWidth="64">
+
+ <group android:name="background">
+ <path android:pathData="M0,0 L 64,0 64,64 0,64 z"
+ android:fillColor="@android:color/transparent"/>
+ </group>
+
+ <!-- Gradient starts offscreen so it is not visible in the first frame before start -->
+ <group android:name="gradient" android:translateX="-128">
+ <path
+ android:pathData="M0,0 L 128,0 128,128 0,128 z">
+ <aapt:attr name="android:fillColor">
+ <gradient
+ android:type="linear"
+ android:startX="0"
+ android:endX="128"
+ android:startY="0"
+ android:endY="0">
+ <item
+ android:color="@android:color/transparent"
+ android:offset="0.0" />
+ <item
+ android:color="@android:color/transparent"
+ android:offset="0.5" />
+ <item
+ android:color="@android:color/transparent"
+ android:offset="1.0" />
+ </gradient>
+ </aapt:attr>
+ </path>
+ </group>
+
+ <!-- Use a foregroud with a cutout shape matching direct share inset for appx applied
+ shadow. Using clip-path is a more elegant solution but leaves awful jaggies around
+ the path's shape. -->
+ <group android:name="cover">
+ <path android:fillColor="@android:color/transparent"
+ android:pathData="M0,0 L64,0 L64,64 L0,64 L0,0 Z M59.0587325,42.453601 C60.3124932,39.2104785 61,35.6855272 61,32 C61,15.9837423 48.0162577,3 32,3 C15.9837423,3 3,15.9837423 3,32 C3,48.0162577 15.9837423,61 32,61 C35.6855272,61 39.2104785,60.3124932 42.453601,59.0587325 C44.3362195,60.2864794 46.5847839,61 49,61 C55.627417,61 61,55.627417 61,49 C61,46.5847839 60.2864794,44.3362195 59.0587325,42.453601 Z"/>
+ </group>
+ </vector>
+ </aapt:attr>
+
+ <!-- This AVD uses special properties so that once started it will loop infinitely with no
+ need for callbacks to restart. -->
+ <target android:name="gradient">
+ <aapt:attr name="android:animation">
+ <objectAnimator
+ android:duration="1700"
+ android:pathData="M -128,0 L 192,0"
+ android:propertyXName="translateX"
+ android:repeatMode="restart"
+ android:repeatCount="infinite"
+ android:startOffset="0">
+ <aapt:attr name="android:interpolator">
+ <pathInterpolator android:pathData="M 0.0,0.0 c0.167,0.167 0.833,0.833 1.0,1.0" />
+ </aapt:attr>
+ </objectAnimator>
+ </aapt:attr>
+ </target>
+</animated-vector>
diff --git a/java/res/drawable/chooser_direct_share_label_placeholder.xml b/java/res/drawable/chooser_direct_share_label_placeholder.xml
new file mode 100644
index 00000000..b21444bf
--- /dev/null
+++ b/java/res/drawable/chooser_direct_share_label_placeholder.xml
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <!-- This drawable is intended to be used as the background of a two line TextView. We only
+ want the height to be ~1 line. Do this cheaply by applying padding to the bottom. -->
+ <item android:bottom="18dp">
+ <shape android:shape="rectangle" >
+
+ <!-- Size used for scaling should the container be different dimensions -->
+ <size android:width="@dimen/chooser_direct_share_label_placeholder_max_width"
+ android:height="18dp"/>
+
+ <!-- Absurd corner radius to ensure pill shape -->
+ <corners android:bottomLeftRadius="100dp"
+ android:bottomRightRadius="100dp"
+ android:topLeftRadius="100dp"
+ android:topRightRadius="100dp" />
+
+ <solid android:color="@color/chooser_gradient_background "/>
+ </shape>
+ </item>
+</layer-list> \ No newline at end of file
diff --git a/java/res/drawable/chooser_file_generic.xml b/java/res/drawable/chooser_file_generic.xml
new file mode 100644
index 00000000..006dfba4
--- /dev/null
+++ b/java/res/drawable/chooser_file_generic.xml
@@ -0,0 +1,24 @@
+<!--
+Copyright (C) 2015 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24.0"
+ android:viewportHeight="24.0">
+ <path
+ android:fillColor="#FF737373"
+ android:pathData="M6 2c-1.1 0,-1.99.9,-1.99 2L4 20c0 1.1.89 2 1.99 2H18c1.1 0 2,-.9 2,-2V8l-6,-6H6zm7 7V3.5L18.5 9H13z"/>
+</vector>
diff --git a/java/res/drawable/chooser_group_background.xml b/java/res/drawable/chooser_group_background.xml
new file mode 100644
index 00000000..036028de
--- /dev/null
+++ b/java/res/drawable/chooser_group_background.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_chooser_group_arrow"
+ android:gravity="end|center_vertical"
+ android:width="12dp"
+ android:height="12dp"
+ android:start="4dp"
+ android:end="0dp" />
+</layer-list>
diff --git a/java/res/drawable/chooser_pinned_background.xml b/java/res/drawable/chooser_pinned_background.xml
new file mode 100644
index 00000000..e8c9910e
--- /dev/null
+++ b/java/res/drawable/chooser_pinned_background.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:drawable="@drawable/ic_chooser_pin"
+ android:gravity="start|top"
+ android:top="4dp"
+ android:width="12dp"
+ android:height="12dp"
+ android:start="0dp"
+ android:end="4dp" />
+</layer-list> \ No newline at end of file
diff --git a/java/res/drawable/chooser_row_layer_list.xml b/java/res/drawable/chooser_row_layer_list.xml
new file mode 100644
index 00000000..43f0cf6f
--- /dev/null
+++ b/java/res/drawable/chooser_row_layer_list.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2019, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android">
+ <item>
+ <shape android:shape="rectangle">
+ <solid android:color="?androidprv:attr/colorAccentSecondary"/>
+ <size android:width="128dp" android:height="2dp"/>
+ <corners android:radius="2dp" />
+ </shape>
+ </item>
+</layer-list>
diff --git a/java/res/drawable/ic_chooser_group_arrow.xml b/java/res/drawable/ic_chooser_group_arrow.xml
new file mode 100644
index 00000000..057c3e76
--- /dev/null
+++ b/java/res/drawable/ic_chooser_group_arrow.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="12dp"
+ android:height="12dp"
+ android:viewportWidth="12"
+ android:viewportHeight="12"
+ android:tint="?android:attr/textColorSecondary">
+ <path
+ android:pathData="M2,4L6,8L10,4L2,4Z"
+ android:fillColor="#FF000000"/>
+</vector>
diff --git a/java/res/drawable/ic_chooser_pin.xml b/java/res/drawable/ic_chooser_pin.xml
new file mode 100644
index 00000000..c6877963
--- /dev/null
+++ b/java/res/drawable/ic_chooser_pin.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="12dp"
+ android:height="12dp"
+ android:viewportWidth="12"
+ android:viewportHeight="12"
+ android:tint="?android:attr/textColorSecondary">
+ <path
+ android:pathData="M8.5,2C8.5,1.45 8.055,1 7.5,1L4.5,1C3.95,1 3.5,1.45 3.5,2L3.5,5.5L2.5,7L2.5,8L5.5,8L5.5,10.5L6,11L6.5,10.5L6.5,8L9.5,8L9.5,7L8.5,5.5L8.5,2Z"
+ android:fillColor="#FF000000" />
+</vector>
diff --git a/java/res/drawable/ic_chooser_pin_dialog.xml b/java/res/drawable/ic_chooser_pin_dialog.xml
new file mode 100644
index 00000000..2ac01c79
--- /dev/null
+++ b/java/res/drawable/ic_chooser_pin_dialog.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M14,4v5c0,1.12 0.37,2.16 1,3H9c0.65,-0.86 1,-1.9 1,-3V4H14M17,2H7C6.45,2 6,2.45 6,3c0,0.55 0.45,1 1,1c0,0 0,0 0,0l1,0v5c0,1.66 -1.34,3 -3,3v2h5.97v7l1,1l1,-1v-7H19v-2c0,0 0,0 0,0c-1.66,0 -3,-1.34 -3,-3V4l1,0c0,0 0,0 0,0c0.55,0 1,-0.45 1,-1C18,2.45 17.55,2 17,2L17,2z"
+ android:fillColor="#FF000000"/>
+</vector>
diff --git a/java/res/drawable/ic_drag_handle.xml b/java/res/drawable/ic_drag_handle.xml
new file mode 100644
index 00000000..9b0e2046
--- /dev/null
+++ b/java/res/drawable/ic_drag_handle.xml
@@ -0,0 +1,21 @@
+<!--
+ Copyright (C) 2016 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle" >
+ <solid android:color="#FFFFFFFF" />
+ <corners android:radius="2dp" />
+</shape>
diff --git a/java/res/drawable/ic_file_copy.xml b/java/res/drawable/ic_file_copy.xml
new file mode 100644
index 00000000..d05b55f1
--- /dev/null
+++ b/java/res/drawable/ic_file_copy.xml
@@ -0,0 +1,25 @@
+<!--
+ Copyright (C) 2019 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:tint="@*android:color/material_grey_600"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
+ <path
+ android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM15,5l6,6v10c0,1.1 -0.9,2 -2,2L7.99,23C6.89,23 6,22.1 6,21l0.01,-14c0,-1.1 0.89,-2 1.99,-2h7zM14,12h5.5L14,6.5L14,12z"
+ android:fillColor="@android:color/white"/>
+</vector>
diff --git a/java/res/drawable/iconfactory_adaptive_icon_drawable_wrapper.xml b/java/res/drawable/iconfactory_adaptive_icon_drawable_wrapper.xml
new file mode 100644
index 00000000..7d6677a9
--- /dev/null
+++ b/java/res/drawable/iconfactory_adaptive_icon_drawable_wrapper.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+ <background android:drawable="@android:color/white"/>
+ <foreground>
+ <drawable
+ class="com.android.intentresolver.SimpleIconFactory$FixedScaleDrawable"/>
+ </foreground>
+</adaptive-icon>
diff --git a/java/res/drawable/resolver_button_bg.xml b/java/res/drawable/resolver_button_bg.xml
new file mode 100644
index 00000000..0ab15e82
--- /dev/null
+++ b/java/res/drawable/resolver_button_bg.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:color="@androidprv:color/resolver_accent_ripple">
+
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners android:radius="12dp" />
+ <solid android:color="@androidprv:color/resolver_accent_ripple" />
+ </shape>
+ </item>
+
+ <item>
+ <inset
+ android:insetLeft="0dp"
+ android:insetTop="6dp"
+ android:insetRight="0dp"
+ android:insetBottom="6dp">
+ <shape android:shape="rectangle">
+ <corners android:radius="12dp" />
+ <solid android:color="@androidprv:color/resolver_profile_tab_selected_bg" />
+ </shape>
+ </inset>
+ </item>
+
+</ripple>
diff --git a/java/res/drawable/resolver_icon_placeholder.xml b/java/res/drawable/resolver_icon_placeholder.xml
new file mode 100644
index 00000000..7236fbe8
--- /dev/null
+++ b/java/res/drawable/resolver_icon_placeholder.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2017 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="oval">
+ <solid android:color="@color/chooser_gradient_background"/>
+ <size android:width="36dp" android:height="36dp"/>
+</shape> \ No newline at end of file
diff --git a/java/res/drawable/resolver_outlined_button_bg.xml b/java/res/drawable/resolver_outlined_button_bg.xml
new file mode 100644
index 00000000..da569e72
--- /dev/null
+++ b/java/res/drawable/resolver_outlined_button_bg.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:color="?android:attr/colorControlHighlight">
+ <item>
+ <inset
+ android:insetLeft="0dp"
+ android:insetTop="6dp"
+ android:insetRight="0dp"
+ android:insetBottom="6dp">
+ <shape android:shape="rectangle">
+ <corners android:radius="8dp" />
+ <stroke android:width="1dp"
+ android:color="?androidprv:attr/colorAccentPrimaryVariant"/>
+ </shape>
+ </inset>
+ </item>
+</ripple>
diff --git a/java/res/drawable/resolver_profile_tab_bg.xml b/java/res/drawable/resolver_profile_tab_bg.xml
new file mode 100644
index 00000000..66cd6d88
--- /dev/null
+++ b/java/res/drawable/resolver_profile_tab_bg.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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.
+ -->
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:color="@androidprv:color/resolver_accent_ripple">
+
+ <item android:id="@android:id/mask">
+ <shape android:shape="rectangle">
+ <corners android:radius="12dp" />
+ <solid android:color="@androidprv:color/resolver_accent_ripple" />
+ </shape>
+ </item>
+
+ <item>
+ <selector android:enterFadeDuration="100">
+ <item android:state_selected="false">
+ <shape android:shape="rectangle">
+ <corners android:radius="12dp" />
+ <solid android:color="?androidprv:attr/colorSurface" />
+ </shape>
+ </item>
+
+ <item android:state_selected="true">
+ <shape android:shape="rectangle">
+ <corners android:radius="12dp" />
+ <solid android:color="@androidprv:color/resolver_profile_tab_selected_bg" />
+ </shape>
+ </item>
+ </selector>
+ </item>
+
+</ripple>
diff --git a/java/res/layout/chooser_action_button.xml b/java/res/layout/chooser_action_button.xml
new file mode 100644
index 00000000..2b68ccca
--- /dev/null
+++ b/java/res/layout/chooser_action_button.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<Button xmlns:android="http://schemas.android.com/apk/res/android"
+ android:gravity="center_vertical|start"
+ android:paddingStart="12dp"
+ android:paddingEnd="12dp"
+ android:drawablePadding="8dp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="12sp"
+ android:maxWidth="192dp"
+ android:singleLine="true"
+ android:clickable="true"
+ android:background="@drawable/chooser_action_button_bg"
+ android:drawableTint="?android:attr/textColorPrimary"
+ android:drawableTintMode="src_in"
+ style="?android:attr/borderlessButtonStyle"
+ />
diff --git a/java/res/layout/chooser_action_row.xml b/java/res/layout/chooser_action_row.xml
new file mode 100644
index 00000000..ea756112
--- /dev/null
+++ b/java/res/layout/chooser_action_row.xml
@@ -0,0 +1,26 @@
+<!--
+ ~ Copyright (C) 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingLeft="@dimen/chooser_edge_margin_normal"
+ android:paddingRight="@dimen/chooser_edge_margin_normal"
+ android:gravity="center"
+ >
+
+</LinearLayout>
diff --git a/java/res/layout/chooser_az_label_row.xml b/java/res/layout/chooser_az_label_row.xml
new file mode 100644
index 00000000..baf07cec
--- /dev/null
+++ b/java/res/layout/chooser_az_label_row.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+ -->
+
+<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:contentDescription="@string/chooser_all_apps_button_label"
+ android:src="@drawable/chooser_row_layer_list"
+ android:paddingTop="16dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:scaleType="center"
+ android:gravity="center"/>
+
diff --git a/java/res/layout/chooser_dialog.xml b/java/res/layout/chooser_dialog.xml
new file mode 100644
index 00000000..ff66bbb9
--- /dev/null
+++ b/java/res/layout/chooser_dialog.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:background="@drawable/chooser_dialog_background"
+ android:orientation="vertical"
+ android:paddingBottom="8dp"
+ android:paddingTop="8dp"
+ android:layout_width="240dp"
+ android:layout_height="wrap_content">
+
+ <LinearLayout
+ android:gravity="start|center_vertical"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:minHeight="56dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView android:id="@android:id/icon"
+ android:layout_marginEnd="16dp"
+ android:layout_width="24dp"
+ android:layout_height="24dp"/>
+
+ <TextView android:id="@android:id/title"
+ android:textSize="16sp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textAppearance="@android:style/TextAppearance.DeviceDefault.WindowTitle"
+ android:text="App name"
+ android:lines="1"
+ android:ellipsize="end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+ </LinearLayout>
+
+ <com.android.internal.widget.RecyclerView
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ androidprv:layoutManager="com.android.internal.widget.LinearLayoutManager"
+ android:id="@androidprv:id/listContainer"
+ android:overScrollMode="never"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+
+</LinearLayout>
diff --git a/java/res/layout/chooser_dialog_item.xml b/java/res/layout/chooser_dialog_item.xml
new file mode 100644
index 00000000..58b07441
--- /dev/null
+++ b/java/res/layout/chooser_dialog_item.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:background="?android:attr/selectableItemBackground"
+ android:clickable="true"
+ android:paddingStart="16dp"
+ android:paddingEnd="16dp"
+ android:orientation="horizontal"
+ android:gravity="start|center_vertical"
+ android:minHeight="56dp"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+
+ <ImageView android:id="@android:id/icon"
+ android:alpha="0.54"
+ android:tint="?android:attr/textColorPrimary"
+ android:layout_marginEnd="16dp"
+ android:layout_width="24dp"
+ android:layout_height="24dp"/>
+
+ <TextView android:id="@androidprv:id/text"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="16sp"
+ android:maxLines="2"
+ android:ellipsize="end"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"/>
+
+</LinearLayout>
diff --git a/java/res/layout/chooser_grid.xml b/java/res/layout/chooser_grid.xml
new file mode 100644
index 00000000..a95b0ebe
--- /dev/null
+++ b/java/res/layout/chooser_grid.xml
@@ -0,0 +1,97 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright 2015, The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT 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.internal.widget.ResolverDrawerLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ androidprv:maxCollapsedHeight="0dp"
+ androidprv:maxCollapsedHeightSmall="56dp"
+ android:maxWidth="@dimen/chooser_width"
+ android:id="@androidprv:id/contentPanel">
+
+ <RelativeLayout
+ android:id="@androidprv:id/chooser_header"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ androidprv:layout_alwaysShow="true"
+ android:elevation="0dp"
+ android:background="@drawable/bottomsheet_background">
+
+ <ImageView
+ android:id="@androidprv:id/drag"
+ android:layout_width="24dp"
+ android:layout_height="4dp"
+ android:src="@drawable/ic_drag_handle"
+ android:layout_marginTop="@dimen/chooser_edge_margin_thin"
+ android:layout_marginBottom="@dimen/chooser_edge_margin_thin"
+ android:tint="?androidprv:attr/colorSurfaceVariant"
+ android:layout_centerHorizontal="true"
+ android:layout_alignParentTop="true" />
+
+ <TextView android:id="@android:id/title"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:textAppearance="@android:style/TextAppearance.DeviceDefault.WindowTitle"
+ android:gravity="center"
+ android:paddingBottom="@dimen/chooser_view_spacing"
+ android:paddingLeft="24dp"
+ android:paddingRight="24dp"
+ android:visibility="gone"
+ android:layout_below="@androidprv:id/drag"
+ android:layout_centerHorizontal="true"/>
+ </RelativeLayout>
+
+ <FrameLayout
+ android:id="@androidprv:id/content_preview_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone" />
+
+ <TabHost
+ android:id="@androidprv:id/profile_tabhost"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:background="?android:attr/colorBackground">
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <TabWidget
+ android:id="@android:id/tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone">
+ </TabWidget>
+ <FrameLayout
+ android:id="@android:id/tabcontent"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <com.android.intentresolver.ResolverViewPager
+ android:id="@androidprv:id/profile_pager"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"/>
+ </FrameLayout>
+ </LinearLayout>
+ </TabHost>
+
+</com.android.internal.widget.ResolverDrawerLayout>
diff --git a/java/res/layout/chooser_grid_preview_file.xml b/java/res/layout/chooser_grid_preview_file.xml
new file mode 100644
index 00000000..6d2e76a0
--- /dev/null
+++ b/java/res/layout/chooser_grid_preview_file.xml
@@ -0,0 +1,80 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright 2019, The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+-->
+<!-- Layout Option: File preview, icon, filename, copy-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:id="@androidprv:id/content_preview_file_area"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:paddingBottom="@dimen/chooser_view_spacing"
+ android:background="?android:attr/colorBackground">
+
+ <LinearLayout
+ android:layout_width="@dimen/chooser_preview_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:orientation="horizontal"
+ android:paddingLeft="@dimen/chooser_edge_margin_normal"
+ android:paddingRight="@dimen/chooser_edge_margin_normal"
+ android:layout_marginBottom="@dimen/chooser_view_spacing"
+ android:id="@androidprv:id/content_preview_file_layout">
+
+ <view class="com.android.intentresolver.ChooserActivity$RoundedRectImageView"
+ android:id="@androidprv:id/content_preview_file_thumbnail"
+ android:layout_width="75dp"
+ android:layout_height="75dp"
+ android:layout_marginRight="16dp"
+ android:adjustViewBounds="true"
+ android:layout_gravity="center_vertical"
+ android:gravity="center"
+ android:scaleType="centerCrop"/>
+ <ImageView
+ android:id="@androidprv:id/content_preview_file_icon"
+ android:layout_width="36dp"
+ android:layout_height="36dp"
+ android:layout_marginRight="16dp"
+ android:adjustViewBounds="true"
+ android:layout_gravity="center_vertical"
+ android:gravity="center"
+ android:scaleType="fitCenter"
+ android:visibility="gone"/>
+ <TextView
+ android:id="@androidprv:id/content_preview_filename"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:ellipsize="middle"
+ android:gravity="start|top"
+ android:paddingRight="24dp"
+ android:singleLine="true"/>
+ </LinearLayout>
+
+ <include
+ android:id="@androidprv:id/chooser_action_row"
+ layout="@layout/chooser_action_row"
+ android:layout_width="@dimen/chooser_preview_width"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/chooser_view_spacing"
+ android:layout_gravity="center"
+ />
+</LinearLayout>
+
diff --git a/java/res/layout/chooser_grid_preview_image.xml b/java/res/layout/chooser_grid_preview_image.xml
new file mode 100644
index 00000000..96054eb5
--- /dev/null
+++ b/java/res/layout/chooser_grid_preview_image.xml
@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright 2019, The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+-->
+<!-- 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"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:background="?android:attr/colorBackground">
+ <RelativeLayout
+ android:id="@androidprv:id/content_preview_image_area"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_horizontal"
+ android:paddingBottom="@dimen/chooser_view_spacing"
+ android:background="?android:attr/colorBackground">
+
+ <view class="com.android.intentresolver.ChooserActivity$RoundedRectImageView"
+ android:id="@androidprv:id/content_preview_image_1_large"
+ android:layout_width="120dp"
+ android:layout_height="104dp"
+ android:layout_alignParentTop="true"
+ android:adjustViewBounds="true"
+ android:gravity="center"
+ android:scaleType="centerCrop"/>
+
+ <view class="com.android.intentresolver.ChooserActivity$RoundedRectImageView"
+ android:id="@androidprv:id/content_preview_image_2_large"
+ android:visibility="gone"
+ android:layout_width="120dp"
+ android:layout_height="104dp"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@androidprv:id/content_preview_image_1_large"
+ android:layout_marginLeft="10dp"
+ android:adjustViewBounds="true"
+ android:gravity="center"
+ android:scaleType="centerCrop"/>
+
+ <view class="com.android.intentresolver.ChooserActivity$RoundedRectImageView"
+ android:id="@androidprv:id/content_preview_image_2_small"
+ android:visibility="gone"
+ android:layout_width="120dp"
+ android:layout_height="65dp"
+ android:layout_alignParentTop="true"
+ android:layout_toRightOf="@androidprv:id/content_preview_image_1_large"
+ android:layout_marginLeft="10dp"
+ android:adjustViewBounds="true"
+ android:gravity="center"
+ android:scaleType="centerCrop"/>
+
+ <view class="com.android.intentresolver.ChooserActivity$RoundedRectImageView"
+ android:id="@androidprv:id/content_preview_image_3_small"
+ android:visibility="gone"
+ android:layout_width="120dp"
+ android:layout_height="65dp"
+ android:layout_below="@androidprv:id/content_preview_image_2_small"
+ android:layout_toRightOf="@androidprv:id/content_preview_image_1_large"
+ android:layout_marginLeft="10dp"
+ android:layout_marginTop="10dp"
+ android:adjustViewBounds="true"
+ android:gravity="center"
+ android:scaleType="centerCrop"/>
+
+ </RelativeLayout>
+
+ <include
+ android:id="@androidprv:id/chooser_action_row"
+ layout="@layout/chooser_action_row"
+ android:layout_width="@dimen/chooser_preview_width"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/chooser_view_spacing"
+ android:layout_gravity="center"
+ />
+
+</LinearLayout>
+
diff --git a/java/res/layout/chooser_grid_preview_text.xml b/java/res/layout/chooser_grid_preview_text.xml
new file mode 100644
index 00000000..a9ed71b7
--- /dev/null
+++ b/java/res/layout/chooser_grid_preview_text.xml
@@ -0,0 +1,102 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright 2019, The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+-->
+<!-- Layout Option: Text preview, with optional title and thumbnail -->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:id="@androidprv:id/content_preview_text_area"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:background="?android:attr/colorBackground">
+
+ <RelativeLayout
+ android:layout_width="@dimen/chooser_preview_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:orientation="horizontal"
+ android:paddingLeft="@dimen/chooser_edge_margin_normal"
+ android:paddingRight="@dimen/chooser_edge_margin_normal"
+ android:layout_marginBottom="@dimen/chooser_view_spacing"
+ android:id="@androidprv:id/content_preview_text_layout">
+
+ <TextView
+ android:id="@androidprv:id/content_preview_text"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:layout_centerVertical="true"
+ android:ellipsize="end"
+ android:fontFamily="@androidprv:string/config_headlineFontFamily"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textAlignment="gravity"
+ android:textDirection="locale"
+ android:maxLines="2"
+ android:focusable="true"/>
+
+ </RelativeLayout>
+
+ <include
+ android:id="@androidprv:id/chooser_action_row"
+ layout="@layout/chooser_action_row"
+ android:layout_width="@dimen/chooser_preview_width"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="@dimen/chooser_view_spacing"
+ android:layout_gravity="center"
+ />
+
+ <!-- Required sub-layout so we can get the nice rounded corners-->
+ <!-- around this section -->
+ <LinearLayout
+ android:layout_width="@dimen/chooser_preview_width"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:orientation="horizontal"
+ android:layout_marginLeft="@dimen/chooser_edge_margin_normal"
+ android:layout_marginRight="@dimen/chooser_edge_margin_normal"
+ android:layout_marginBottom="@dimen/chooser_view_spacing"
+ android:minHeight="80dp"
+ android:background="@androidprv:drawable/chooser_content_preview_rounded"
+ android:id="@androidprv:id/content_preview_title_layout">
+
+ <view class="com.android.intentresolver.ChooserActivity$RoundedRectImageView"
+ android:id="@androidprv:id/content_preview_thumbnail"
+ android:layout_width="75dp"
+ android:layout_height="75dp"
+ android:layout_marginRight="16dp"
+ android:adjustViewBounds="true"
+ android:layout_gravity="center_vertical"
+ android:gravity="center"
+ android:scaleType="centerCrop"/>
+
+ <TextView
+ android:id="@androidprv:id/content_preview_title"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center_vertical"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAlignment="gravity"
+ android:textDirection="locale"
+ android:textAppearance="@android:style/TextAppearance.DeviceDefault.WindowTitle"
+ android:fontFamily="@androidprv:string/config_headlineFontFamily"/>
+ </LinearLayout>
+</LinearLayout>
+
diff --git a/java/res/layout/chooser_list_per_profile.xml b/java/res/layout/chooser_list_per_profile.xml
new file mode 100644
index 00000000..8d876cdf
--- /dev/null
+++ b/java/res/layout/chooser_list_per_profile.xml
@@ -0,0 +1,33 @@
+<!--
+ ~ Copyright (C) 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <com.android.internal.widget.RecyclerView
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ androidprv:layoutManager="com.android.intentresolver.ChooserGridLayoutManager"
+ android:id="@androidprv:id/resolver_list"
+ android:clipToPadding="false"
+ android:background="?android:attr/colorBackground"
+ android:scrollbars="none"
+ android:elevation="1dp"
+ android:nestedScrollingEnabled="true" />
+
+ <include layout="@layout/resolver_empty_states" />
+</RelativeLayout>
diff --git a/java/res/layout/chooser_profile_row.xml b/java/res/layout/chooser_profile_row.xml
new file mode 100644
index 00000000..855ee9ad
--- /dev/null
+++ b/java/res/layout/chooser_profile_row.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2019, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center">
+ <Button
+ android:id="@androidprv:id/profile_button"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ style="?android:attr/borderlessButtonStyle"
+ android:textAppearance="?android:attr/textAppearanceButton"
+ android:textColor="?android:attr/colorAccent"
+ android:singleLine="true"/>
+</LinearLayout>
+
diff --git a/java/res/layout/chooser_row.xml b/java/res/layout/chooser_row.xml
new file mode 100644
index 00000000..29914a82
--- /dev/null
+++ b/java/res/layout/chooser_row.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2015, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:orientation="horizontal"
+ android:layout_width="match_parent"
+ android:layout_height="100dp"
+ android:gravity="start|top">
+ <TextView
+ android:id="@androidprv:id/chooser_row_text_option"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:layout_gravity="center"
+ android:visibility="gone" />
+</LinearLayout>
+
diff --git a/java/res/layout/chooser_row_direct_share.xml b/java/res/layout/chooser_row_direct_share.xml
new file mode 100644
index 00000000..d7e36eed
--- /dev/null
+++ b/java/res/layout/chooser_row_direct_share.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2019, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="200dp">
+
+</LinearLayout>
+
+
diff --git a/java/res/layout/miniresolver.xml b/java/res/layout/miniresolver.xml
new file mode 100644
index 00000000..ab65aa9b
--- /dev/null
+++ b/java/res/layout/miniresolver.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+<com.android.internal.widget.ResolverDrawerLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:maxWidth="@dimen/resolver_max_width"
+ androidprv:maxCollapsedHeight="@dimen/resolver_max_collapsed_height"
+ androidprv:maxCollapsedHeightSmall="56dp"
+ android:id="@androidprv:id/contentPanel">
+
+ <RelativeLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ androidprv:layout_alwaysShow="true"
+ android:elevation="@dimen/resolver_elevation"
+ android:paddingTop="24dp"
+ android:paddingStart="@dimen/resolver_edge_margin"
+ android:paddingEnd="@dimen/resolver_edge_margin"
+ android:paddingBottom="@dimen/resolver_title_padding_bottom"
+ android:background="@drawable/bottomsheet_background">
+
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:scaleType="fitCenter"
+ />
+
+ <TextView
+ android:id="@androidprv:id/open_cross_profile"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:paddingTop="16dp"
+ android:layout_below="@android:id/icon"
+ android:layout_centerHorizontal="true"
+ android:textSize="24sp"
+ android:lineHeight="32sp"
+ android:gravity="center"
+ android:textColor="?android:textColorPrimary"
+ />
+ </RelativeLayout>
+
+ <LinearLayout
+ android:id="@androidprv:id/button_bar_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ androidprv:layout_alwaysShow="true"
+ android:paddingTop="32dp"
+ android:paddingBottom="@dimen/resolver_button_bar_spacing"
+ android:orientation="vertical"
+ android:background="?android:attr/colorBackground"
+ androidprv:layout_ignoreOffset="true">
+ <RelativeLayout
+ style="?android:attr/buttonBarStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ androidprv:layout_ignoreOffset="true"
+ androidprv:layout_hasNestedScrollIndicator="true"
+ android:gravity="end|center_vertical"
+ android:orientation="horizontal"
+ android:layoutDirection="locale"
+ android:measureWithLargestChild="true"
+ android:paddingHorizontal="16dp"
+ android:paddingBottom="2dp"
+ android:elevation="@dimen/resolver_elevation">
+
+ <Button
+ android:id="@androidprv:id/use_same_profile_browser"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentStart="true"
+ android:maxLines="2"
+ android:background="@drawable/resolver_outlined_button_bg"
+ style="?android:attr/borderlessButtonStyle"
+ android:paddingHorizontal="16dp"
+ android:fontFamily="@androidprv:string/config_headlineFontFamilyMedium"
+ android:textAllCaps="false"
+ android:text="@string/activity_resolver_use_once"
+ />
+
+ <Button
+ android:id="@androidprv:id/button_open"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_alignParentEnd="true"
+ android:maxLines="2"
+ android:paddingHorizontal="16dp"
+ android:background="@drawable/resolver_button_bg"
+ style="?android:attr/borderlessButtonStyle"
+ android:fontFamily="@androidprv:string/config_headlineFontFamilyMedium"
+ android:textAllCaps="false"
+ android:textColor="@androidprv:color/resolver_button_text"
+ android:text="@string/whichViewApplicationLabel"
+ />
+ </RelativeLayout>
+ </LinearLayout>
+</com.android.internal.widget.ResolverDrawerLayout>
diff --git a/java/res/layout/resolve_grid_item.xml b/java/res/layout/resolve_grid_item.xml
new file mode 100644
index 00000000..db6c7dd9
--- /dev/null
+++ b/java/res/layout/resolve_grid_item.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2006, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:minHeight="100dp"
+ android:gravity="center"
+ android:paddingTop="24dp"
+ android:paddingBottom="12dp"
+ android:paddingLeft="12dp"
+ android:paddingRight="12dp"
+ android:focusable="true"
+ android:background="?android:attr/selectableItemBackgroundBorderless">
+
+ <ImageView android:id="@android:id/icon"
+ android:layout_width="@dimen/chooser_icon_size"
+ android:layout_height="@dimen/chooser_icon_size"
+ android:scaleType="fitCenter" />
+
+ <!-- Size manually tuned to match specs -->
+ <Space android:layout_width="1dp"
+ android:layout_height="7dp"/>
+
+ <!-- App name or Direct Share target name, DS set to 2 lines -->
+ <TextView android:id="@android:id/text1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="12sp"
+ android:gravity="top|center_horizontal"
+ android:lines="1"
+ android:ellipsize="end" />
+
+ <!-- Activity name if set, gone for Direct Share targets -->
+ <TextView android:id="@android:id/text2"
+ android:textAppearance="?android:attr/textAppearanceSmall"
+ android:textSize="12sp"
+ android:textColor="?android:attr/textColorSecondary"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:lines="1"
+ android:gravity="top|center_horizontal"
+ android:ellipsize="end"/>
+
+</LinearLayout>
+
diff --git a/java/res/layout/resolve_list_item.xml b/java/res/layout/resolve_list_item.xml
new file mode 100644
index 00000000..4d12b775
--- /dev/null
+++ b/java/res/layout/resolve_list_item.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/* //device/apps/common/res/any/layout/resolve_list_item.xml
+**
+** Copyright 2006, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:orientation="horizontal"
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent"
+ android:minHeight="?android:attr/listPreferredItemHeightSmall"
+ android:background="?android:attr/activatedBackgroundIndicator">
+
+ <!-- Activity icon when presenting dialog
+ Size will be filled in by ResolverActivity -->
+ <ImageView android:id="@android:id/icon"
+ android:layout_width="@dimen/resolver_icon_size"
+ android:layout_height="@dimen/resolver_icon_size"
+ android:layout_gravity="start|center_vertical"
+ android:layout_marginStart="@dimen/resolver_icon_margin"
+ android:layout_marginEnd="@dimen/resolver_icon_margin"
+ android:layout_marginTop="12dp"
+ android:layout_marginBottom="12dp"
+ android:scaleType="fitCenter" />
+
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:gravity="start|center_vertical"
+ android:orientation="vertical"
+ android:paddingEnd="@dimen/resolver_edge_margin"
+ android:layout_height="wrap_content"
+ android:layout_width="wrap_content"
+ android:layout_gravity="start|center_vertical">
+ <!-- Activity name -->
+ <TextView android:id="@android:id/text1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start|center_vertical"
+ android:textColor="?android:attr/textColorPrimary"
+ android:fontFamily="@androidprv:string/config_bodyFontFamily"
+ android:textSize="16sp"
+ android:minLines="1"
+ android:maxLines="1"
+ android:ellipsize="marquee" />
+ <!-- Extended activity info to distinguish between duplicate activity names
+ or provide record w/o permission warnings.
+ -->
+ <TextView android:id="@android:id/text2"
+ android:textColor="?android:attr/textColorSecondary"
+ android:fontFamily="@androidprv:string/config_bodyFontFamily"
+ android:layout_gravity="start|center_vertical"
+ android:textSize="14sp"
+ android:visibility="gone"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minLines="1"
+ android:maxLines="2"
+ android:ellipsize="marquee" />
+ </LinearLayout>
+</LinearLayout>
+
diff --git a/java/res/layout/resolver_different_item_header.xml b/java/res/layout/resolver_different_item_header.xml
new file mode 100644
index 00000000..4f801597
--- /dev/null
+++ b/java/res/layout/resolver_different_item_header.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+ * Copyright 2014, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+-->
+<TextView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ androidprv:layout_alwaysShow="true"
+ android:text="@string/use_a_different_app"
+ android:textColor="?android:attr/textColorPrimary"
+ android:fontFamily="@androidprv:string/config_headlineFontFamilyMedium"
+ android:textSize="16sp"
+ android:gravity="start|center_vertical"
+ android:paddingStart="@dimen/resolver_edge_margin"
+ android:paddingEnd="@dimen/resolver_edge_margin"
+ android:paddingTop="@dimen/resolver_small_margin"
+ android:paddingBottom="@dimen/resolver_edge_margin"
+ android:elevation="1dp" />
diff --git a/java/res/layout/resolver_empty_states.xml b/java/res/layout/resolver_empty_states.xml
new file mode 100644
index 00000000..d77630ee
--- /dev/null
+++ b/java/res/layout/resolver_empty_states.xml
@@ -0,0 +1,91 @@
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:id="@androidprv:id/resolver_empty_state"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:gravity="center_horizontal"
+ android:visibility="gone"
+ android:paddingStart="24dp"
+ android:paddingEnd="24dp">
+ <RelativeLayout
+ android:id="@androidprv:id/resolver_empty_state_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:paddingTop="@dimen/resolver_empty_state_container_padding_top"
+ android:paddingBottom="@dimen/resolver_empty_state_container_padding_bottom"
+ android:gravity="center_horizontal">
+ <TextView
+ android:id="@androidprv:id/resolver_empty_state_title"
+ android:layout_below="@androidprv:id/resolver_empty_state_icon"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:fontFamily="@androidprv:string/config_headlineFontFamilyMedium"
+ android:textColor="?android:attr/textColorPrimary"
+ android:textSize="18sp"
+ android:lineHeight="24sp"
+ android:gravity="center_horizontal"
+ android:layout_centerHorizontal="true" />
+ <TextView
+ android:id="@androidprv:id/resolver_empty_state_subtitle"
+ android:layout_below="@androidprv:id/resolver_empty_state_title"
+ android:layout_marginTop="16dp"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textColor="?android:attr/textColorSecondary"
+ android:textSize="14sp"
+ android:lineHeight="20sp"
+ android:gravity="center_horizontal"
+ android:layout_centerHorizontal="true" />
+ <Button
+ android:id="@androidprv:id/resolver_empty_state_button"
+ android:layout_below="@androidprv:id/resolver_empty_state_subtitle"
+ android:layout_marginTop="16dp"
+ android:text="@string/resolver_switch_on_work"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:padding="12dp"
+ android:fontFamily="@androidprv:string/config_headlineFontFamilyMedium"
+ android:textSize="14sp"
+ android:textColor="?android:attr/textColorPrimary"
+ android:layout_centerHorizontal="true"
+ android:background="@drawable/chooser_action_button_bg"
+ />
+ <ProgressBar
+ android:id="@androidprv:id/resolver_empty_state_progress"
+ style="@android:style/Widget.Material.Light.ProgressBar"
+ android:visibility="gone"
+ android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:indeterminate="true"
+ android:layout_alignTop="@androidprv:id/resolver_empty_state_icon"
+ android:layout_alignBottom="@androidprv:id/resolver_empty_state_button"
+ android:layout_centerHorizontal="true"
+ android:layout_below="@androidprv:id/resolver_empty_state_subtitle"
+ android:indeterminateTint="?android:attr/colorAccent"/>
+ </RelativeLayout>
+ <TextView android:id="@android:id/empty"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/colorBackground"
+ android:text="@string/noApplications"
+ android:padding="@dimen/chooser_edge_margin_normal"
+ android:layout_marginBottom="56dp"
+ android:gravity="center"/>
+</RelativeLayout>
diff --git a/java/res/layout/resolver_list.xml b/java/res/layout/resolver_list.xml
new file mode 100644
index 00000000..179c4073
--- /dev/null
+++ b/java/res/layout/resolver_list.xml
@@ -0,0 +1,172 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright 2012, The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT 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.internal.widget.ResolverDrawerLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:maxWidth="@dimen/resolver_max_width"
+ androidprv:maxCollapsedHeight="@dimen/resolver_max_collapsed_height"
+ androidprv:maxCollapsedHeightSmall="56dp"
+ android:id="@androidprv:id/contentPanel">
+
+ <RelativeLayout
+ android:id="@androidprv:id/title_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ androidprv:layout_alwaysShow="true"
+ android:elevation="@dimen/resolver_elevation"
+ android:paddingTop="@dimen/resolver_small_margin"
+ android:paddingStart="@dimen/resolver_edge_margin"
+ android:paddingEnd="@dimen/resolver_edge_margin"
+ android:paddingBottom="@dimen/resolver_title_padding_bottom"
+ android:background="@drawable/bottomsheet_background">
+
+ <TextView
+ android:id="@androidprv:id/profile_button"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_marginEnd="8dp"
+ android:visibility="gone"
+ style="?android:attr/borderlessButtonStyle"
+ android:textAppearance="?android:attr/textAppearanceButton"
+ android:textColor="?android:attr/colorAccent"
+ android:gravity="center_vertical"
+ android:layout_alignParentTop="true"
+ android:layout_alignParentEnd="true"
+ android:singleLine="true" />
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_below="@androidprv:id/profile_button"
+ android:layout_alignParentStart="true"
+ android:textColor="?android:attr/textColorPrimary"
+ android:fontFamily="@androidprv:string/config_headlineFontFamilyMedium"
+ android:textSize="16sp"
+ android:gravity="start|center_vertical" />
+ </RelativeLayout>
+
+ <View
+ android:id="@androidprv:id/divider"
+ androidprv:layout_alwaysShow="true"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?android:attr/colorBackground"
+ android:foreground="?android:attr/dividerVertical" />
+
+ <FrameLayout
+ android:id="@androidprv:id/stub"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/colorBackground"/>
+
+ <TabHost
+ android:id="@androidprv:id/profile_tabhost"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:accessibilityTraversalAfter="@android:id/title"
+ android:background="?android:attr/colorBackground">
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <!-- horizontal padding = 8dp content padding - 4dp margin that tab buttons have. -->
+ <TabWidget
+ android:id="@android:id/tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:tabStripEnabled="false"
+ android:paddingHorizontal="4dp"
+ android:visibility="gone" />
+ <FrameLayout
+ android:id="@android:id/tabcontent"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <com.android.intentresolver.ResolverViewPager
+ android:id="@androidprv:id/profile_pager"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </FrameLayout>
+ </LinearLayout>
+ </TabHost>
+ <LinearLayout
+ android:id="@androidprv:id/button_bar_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ androidprv:layout_alwaysShow="true"
+ android:orientation="vertical"
+ android:background="?android:attr/colorBackground"
+ androidprv:layout_ignoreOffset="true">
+ <View
+ android:id="@androidprv:id/resolver_button_bar_divider"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?android:attr/colorBackground"
+ android:foreground="?android:attr/dividerVertical" />
+ <LinearLayout
+ android:id="@androidprv:id/button_bar"
+ android:visibility="gone"
+ style="?android:attr/buttonBarStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ androidprv:layout_ignoreOffset="true"
+ androidprv:layout_hasNestedScrollIndicator="true"
+ android:gravity="end|center_vertical"
+ android:orientation="horizontal"
+ android:layoutDirection="locale"
+ android:measureWithLargestChild="true"
+ android:paddingTop="@dimen/resolver_button_bar_spacing"
+ android:paddingBottom="@dimen/resolver_button_bar_spacing"
+ android:paddingStart="@dimen/resolver_edge_margin"
+ android:paddingEnd="@dimen/resolver_small_margin"
+ android:elevation="@dimen/resolver_elevation">
+
+ <Button
+ android:id="@androidprv:id/button_once"
+ android:layout_width="wrap_content"
+ android:layout_gravity="start"
+ android:maxLines="2"
+ style="?android:attr/buttonBarButtonStyle"
+ android:fontFamily="@androidprv:string/config_headlineFontFamilyMedium"
+ android:layout_height="wrap_content"
+ android:textAllCaps="false"
+ android:enabled="false"
+ android:text="@string/activity_resolver_use_once"
+ android:onClick="onButtonClick" />
+
+ <Button
+ android:id="@androidprv:id/button_always"
+ android:layout_width="wrap_content"
+ android:layout_gravity="end"
+ android:maxLines="2"
+ style="?android:attr/buttonBarButtonStyle"
+ android:fontFamily="@androidprv:string/config_headlineFontFamilyMedium"
+ android:textAllCaps="false"
+ android:layout_height="wrap_content"
+ android:enabled="false"
+ android:text="@string/activity_resolver_use_always"
+ android:onClick="onButtonClick" />
+ </LinearLayout>
+ </LinearLayout>
+</com.android.internal.widget.ResolverDrawerLayout>
diff --git a/java/res/layout/resolver_list_per_profile.xml b/java/res/layout/resolver_list_per_profile.xml
new file mode 100644
index 00000000..5752edcd
--- /dev/null
+++ b/java/res/layout/resolver_list_per_profile.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2019 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<RelativeLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <ListView
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:id="@androidprv:id/resolver_list"
+ android:clipToPadding="false"
+ android:background="?android:attr/colorBackground"
+ android:elevation="@dimen/resolver_elevation"
+ android:nestedScrollingEnabled="true"
+ android:scrollbarStyle="outsideOverlay"
+ android:divider="@null"
+ android:footerDividersEnabled="false"
+ android:headerDividersEnabled="false"
+ android:dividerHeight="0dp" />
+
+ <include layout="@layout/resolver_empty_states" />
+</RelativeLayout>
diff --git a/java/res/layout/resolver_list_with_default.xml b/java/res/layout/resolver_list_with_default.xml
new file mode 100644
index 00000000..341c58e7
--- /dev/null
+++ b/java/res/layout/resolver_list_with_default.xml
@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+* Copyright 2014, The Android Open Source Project
+*
+* Licensed under the Apache License, Version 2.0 (the "License");
+* you may not use this file except in compliance with the License.
+* You may obtain a copy of the License at
+*
+* http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+*/
+-->
+<com.android.internal.widget.ResolverDrawerLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:maxWidth="@dimen/resolver_max_width"
+ androidprv:maxCollapsedHeight="@dimen/resolver_max_collapsed_height_with_default"
+ android:id="@androidprv:id/contentPanel">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ androidprv:layout_alwaysShow="true"
+ android:orientation="vertical"
+ android:background="@drawable/bottomsheet_background"
+ android:paddingTop="@dimen/resolver_small_margin"
+ android:elevation="@dimen/resolver_elevation">
+
+ <LinearLayout
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/resolver_edge_margin"
+ android:paddingEnd="@dimen/resolver_edge_margin">
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="@dimen/resolver_icon_size"
+ android:layout_height="@dimen/resolver_icon_size"
+ android:layout_gravity="start|top"
+ android:layout_marginStart="@dimen/resolver_icon_margin"
+ android:src="@drawable/resolver_icon_placeholder"
+ android:scaleType="fitCenter" />
+
+ <TextView
+ android:id="@android:id/title"
+ android:layout_width="0dp"
+ android:layout_weight="1"
+ android:layout_height="wrap_content"
+ android:layout_marginStart="@dimen/resolver_icon_margin"
+ android:textColor="?android:attr/textColorPrimary"
+ android:fontFamily="@androidprv:string/config_headlineFontFamilyMedium"
+ android:textSize="16sp"
+ android:gravity="start|center_vertical"
+ android:paddingEnd="16dp" />
+
+ <LinearLayout
+ android:id="@androidprv:id/profile_button"
+ android:layout_width="wrap_content"
+ android:layout_height="48dp"
+ android:layout_marginTop="4dp"
+ android:layout_marginEnd="4dp"
+ android:paddingStart="8dp"
+ android:paddingEnd="8dp"
+ android:paddingTop="4dp"
+ android:paddingBottom="4dp"
+ android:focusable="true"
+ android:visibility="gone"
+ style="?android:attr/borderlessButtonStyle">
+
+ <ImageView
+ android:id="@android:id/icon"
+ android:layout_width="24dp"
+ android:layout_height="24dp"
+ android:layout_gravity="start|center_vertical"
+ android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:layout_marginTop="12dp"
+ android:layout_marginBottom="12dp"
+ android:scaleType="fitCenter" />
+
+ <TextView
+ android:id="@android:id/text1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="start|center_vertical"
+ android:layout_marginEnd="?android:attr/listPreferredItemPaddingEnd"
+ android:textAppearance="?android:attr/textAppearanceButton"
+ android:textColor="?android:attr/textColorPrimary"
+ android:minLines="1"
+ android:maxLines="1"
+ android:ellipsize="marquee" />
+ </LinearLayout>
+ </LinearLayout>
+
+ <LinearLayout
+ android:id="@androidprv:id/button_bar"
+ android:visibility="gone"
+ style="?android:attr/buttonBarStyle"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ androidprv:layout_alwaysShow="true"
+ android:gravity="end|center_vertical"
+ android:orientation="horizontal"
+ android:layoutDirection="locale"
+ android:measureWithLargestChild="true"
+ android:paddingTop="@dimen/resolver_button_bar_spacing"
+ android:paddingBottom="@dimen/resolver_button_bar_spacing"
+ android:paddingStart="@dimen/resolver_edge_margin"
+ android:paddingEnd="@dimen/resolver_small_margin"
+ android:elevation="@dimen/resolver_elevation">
+
+ <Button
+ android:id="@androidprv:id/button_once"
+ android:layout_width="wrap_content"
+ android:layout_gravity="start"
+ android:maxLines="2"
+ style="?android:attr/buttonBarButtonStyle"
+ android:fontFamily="@androidprv:string/config_headlineFontFamilyMedium"
+ android:layout_height="wrap_content"
+ android:enabled="false"
+ android:textAllCaps="false"
+ android:text="@string/activity_resolver_use_once"
+ android:onClick="onButtonClick" />
+
+ <Button
+ android:id="@androidprv:id/button_always"
+ android:layout_width="wrap_content"
+ android:layout_gravity="end"
+ android:maxLines="2"
+ style="?android:attr/buttonBarButtonStyle"
+ android:fontFamily="@androidprv:string/config_headlineFontFamilyMedium"
+ android:layout_height="wrap_content"
+ android:enabled="false"
+ android:textAllCaps="false"
+ android:text="@string/activity_resolver_use_always"
+ android:onClick="onButtonClick" />
+ </LinearLayout>
+ </LinearLayout>
+
+ <View
+ android:id="@androidprv:id/divider"
+ androidprv:layout_alwaysShow="true"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?android:attr/colorBackground"
+ android:foreground="?android:attr/dividerVertical" />
+
+ <FrameLayout
+ android:id="@androidprv:id/stub"
+ androidprv:layout_alwaysShow="true"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:background="?android:attr/colorBackground"/>
+
+ <TabHost
+ androidprv:layout_alwaysShow="true"
+ android:id="@androidprv:id/profile_tabhost"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_alignParentTop="true"
+ android:layout_centerHorizontal="true"
+ android:background="?android:attr/colorBackground">
+ <LinearLayout
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <TabWidget
+ android:id="@android:id/tabs"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:visibility="gone">
+ </TabWidget>
+ <View
+ android:id="@androidprv:id/resolver_tab_divider"
+ android:visibility="gone"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?android:attr/colorBackground"
+ android:foreground="?android:attr/dividerVertical"/>
+ <FrameLayout
+ android:id="@android:id/tabcontent"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content">
+ <com.android.intentresolver.ResolverViewPager
+ android:id="@androidprv:id/profile_pager"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content" />
+ </FrameLayout>
+ </LinearLayout>
+ </TabHost>
+
+ <View
+ androidprv:layout_alwaysShow="true"
+ android:layout_width="match_parent"
+ android:layout_height="1dp"
+ android:background="?android:attr/colorBackground"
+ android:foreground="?android:attr/dividerVertical" />
+</com.android.internal.widget.ResolverDrawerLayout>
diff --git a/java/res/layout/resolver_profile_tab_button.xml b/java/res/layout/resolver_profile_tab_button.xml
new file mode 100644
index 00000000..95e11cdc
--- /dev/null
+++ b/java/res/layout/resolver_profile_tab_button.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ 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.
+ -->
+
+ <Button
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="0dp"
+ android:layout_height="36dp"
+ android:layout_weight="1"
+ android:layout_marginVertical="6dp"
+ android:layout_marginHorizontal="@dimen/resolver_profile_tab_margin"
+ android:background="@drawable/resolver_profile_tab_bg"
+ android:textColor="@androidprv:color/resolver_profile_tab_text"
+ android:textSize="@dimen/resolver_tab_text_size"
+ android:textAppearance="@android:style/TextAppearance.DeviceDefault.DialogWindowTitle"
+ style="?android:attr/borderlessButtonStyle" />
diff --git a/java/res/values-h480dp/bools.xml b/java/res/values-h480dp/bools.xml
new file mode 100644
index 00000000..7896d9bf
--- /dev/null
+++ b/java/res/values-h480dp/bools.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <bool name="resolver_landscape_phone">false</bool>
+</resources> \ No newline at end of file
diff --git a/java/res/values-h480dp/dimens.xml b/java/res/values-h480dp/dimens.xml
new file mode 100644
index 00000000..9cdc8899
--- /dev/null
+++ b/java/res/values-h480dp/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<!--
+ ~ Copyright (C) 2020 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<resources>
+ <dimen name="resolver_empty_state_container_padding_top">48dp</dimen>
+ <dimen name="resolver_empty_state_container_padding_bottom">48dp</dimen>
+ <dimen name="resolver_title_padding_bottom">@dimen/resolver_edge_margin</dimen>
+ <dimen name="resolver_button_bar_spacing">8dp</dimen>
+</resources> \ No newline at end of file
diff --git a/java/res/values-land/dimens.xml b/java/res/values-land/dimens.xml
new file mode 100644
index 00000000..7e3fb9cb
--- /dev/null
+++ b/java/res/values-land/dimens.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2010, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<resources>
+ <dimen name="chooser_preview_width">412dp</dimen>
+</resources>
diff --git a/java/res/values-night/colors.xml b/java/res/values-night/colors.xml
new file mode 100644
index 00000000..9a4738bd
--- /dev/null
+++ b/java/res/values-night/colors.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2018 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License
+
+ NOTE: You might also want to edit: packages/SystemUI/res/values-night/colors.xml
+ -->
+<resources>
+
+ <color name="chooser_row_divider">@*android:color/list_divider_color_dark</color>
+ <color name="chooser_gradient_background">@*android:color/loading_gradient_background_color_dark</color>
+
+</resources>
diff --git a/java/res/values-sw600dp/dimens.xml b/java/res/values-sw600dp/dimens.xml
new file mode 100644
index 00000000..b397630e
--- /dev/null
+++ b/java/res/values-sw600dp/dimens.xml
@@ -0,0 +1,24 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/* //device/apps/common/assets/res/any/dimens.xml
+**
+** Copyright 2006, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<resources>
+
+ <dimen name="chooser_width">624dp</dimen>
+
+</resources>
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
new file mode 100644
index 00000000..3ec7c2f3
--- /dev/null
+++ b/java/res/values/attrs.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2006 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<!-- Formatting note: terminate all comments with a period, to avoid breaking
+ the documentation output. To suppress comment lines from the documentation
+ output, insert an eat-comment element after the comment lines.
+-->
+
+<resources>
+ <declare-styleable name="ResolverDrawerLayout">
+ <attr name="android:maxWidth" />
+ <attr name="maxCollapsedHeight" format="dimension" />
+ <attr name="maxCollapsedHeightSmall" format="dimension" />
+ <!-- Whether the Drawer should be positioned at the top rather than at the bottom. -->
+ <attr name="showAtTop" format="boolean" />
+ </declare-styleable>
+
+ <declare-styleable name="ResolverDrawerLayout_LayoutParams">
+ <attr name="layout_alwaysShow" format="boolean" />
+ <attr name="layout_ignoreOffset" format="boolean" />
+ <attr name="android:layout_gravity" />
+ <attr name="layout_hasNestedScrollIndicator" format="boolean" />
+ <attr name="layout_maxHeight" format="dimension"/>
+ </declare-styleable>
+</resources>
diff --git a/java/res/values/bools.xml b/java/res/values/bools.xml
new file mode 100644
index 00000000..a84081b6
--- /dev/null
+++ b/java/res/values/bools.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2011 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<resources>
+ <bool name="resolver_landscape_phone">@*android:bool/resolver_landscape_phone</bool>
+</resources>
diff --git a/java/res/values/colors.xml b/java/res/values/colors.xml
new file mode 100644
index 00000000..758e403b
--- /dev/null
+++ b/java/res/values/colors.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/* //device/apps/common/assets/res/any/colors.xml
+**
+** Copyright 2006, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<resources>
+ <color name="chooser_row_divider">@*android:color/list_divider_color_light</color>
+ <color name="chooser_gradient_background">@*android:color/loading_gradient_background_color_light</color>
+</resources>
diff --git a/java/res/values/config.xml b/java/res/values/config.xml
new file mode 100644
index 00000000..f8addc9f
--- /dev/null
+++ b/java/res/values/config.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+** Copyright 2009, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+** http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<!-- These resources are around just to allow their values to be customized
+ for different hardware and product builds. Do not translate.
+
+ NOTE: The naming convention is "config_camelCaseValue". Some legacy
+ entries do not follow the convention, but all new entries should. -->
+
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+ <!-- The duration (in milliseconds) of a short animation. -->
+ <integer name="config_shortAnimTime">@android:integer/config_shortAnimTime</integer>
+
+ <!-- Sharesheet: define a max number of targets per application for new shortcuts-based direct share introduced in Q -->
+ <integer name="config_maxShortcutTargetsPerApp">@*android:integer/config_maxShortcutTargetsPerApp</integer>
+
+ <!-- Component name that accepts ACTION_SEND intents for nearby (proximity-based) sharing.
+ Used by ChooserActivity. -->
+ <string translatable="false" name="config_defaultNearbySharingComponent">@*android:string/config_defaultNearbySharingComponent</string>
+
+ <!-- Chooser image editing activity. Must handle ACTION_EDIT image/png intents.
+ If omitted, image editing will not be offered via Chooser.
+ This name is in the ComponentName flattened format (package/class) [DO NOT TRANSLATE] -->
+ <string name="config_systemImageEditor" translatable="false">@*android:string/config_systemImageEditor</string>
+
+ <integer name="config_chooser_max_targets_per_row">@*android:integer/config_chooser_max_targets_per_row</integer>
+</resources>
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index a0ad1c81..2d6fe816 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -14,8 +14,40 @@
~ limitations under the License.
-->
<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <dimen name="resolver_max_width">480dp</dimen>
+
+ <!-- chooser/resolver (sharesheet) spacing -->
+ <dimen name="chooser_width">412dp</dimen>
+ <dimen name="chooser_corner_radius">28dp</dimen>
+ <dimen name="chooser_row_text_option_translate">25dp</dimen>
+ <dimen name="chooser_view_spacing">18dp</dimen>
+ <dimen name="chooser_edge_margin_thin">16dp</dimen>
+ <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_max_dimen">200dp</dimen>
+ <dimen name="chooser_preview_width">-1px</dimen>
+ <dimen name="chooser_header_scroll_elevation">4dp</dimen>
+ <dimen name="chooser_max_collapsed_height">288dp</dimen>
+ <dimen name="chooser_direct_share_label_placeholder_max_width">72dp</dimen>
<dimen name="chooser_icon_size">56dp</dimen>
<dimen name="chooser_badge_size">22dp</dimen>
<dimen name="resolver_icon_size">32dp</dimen>
+ <dimen name="resolver_button_bar_spacing">0dp</dimen>
<dimen name="resolver_badge_size">18dp</dimen>
-</resources> \ No newline at end of file
+ <dimen name="resolver_icon_margin">8dp</dimen>
+ <dimen name="resolver_small_margin">18dp</dimen>
+ <dimen name="resolver_edge_margin">24dp</dimen>
+ <dimen name="resolver_elevation">1dp</dimen>
+ <dimen name="resolver_max_collapsed_height">192dp</dimen>
+ <dimen name="resolver_max_collapsed_height_with_tabs">268dp</dimen>
+ <dimen name="resolver_max_collapsed_height_with_default">144dp</dimen>
+ <dimen name="resolver_max_collapsed_height_with_default_with_tabs">300dp</dimen>
+ <dimen name="resolver_tab_text_size">14sp</dimen>
+ <dimen name="resolver_title_padding_bottom">0dp</dimen>
+ <dimen name="resolver_empty_state_container_padding_top">48dp</dimen>
+ <dimen name="resolver_empty_state_container_padding_bottom">8dp</dimen>
+ <dimen name="resolver_profile_tab_margin">4dp</dimen>
+
+ <dimen name="chooser_action_button_icon_size">18dp</dimen>
+</resources>
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index 2e570d25..a536d3bc 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -19,4 +19,86 @@
<!-- Title of the IntentResolver application. [CHAR LIMIT=50] -->
<string name="app_label">IntentResolver</string>
+ <!-- Label for a link to a intent resolver dialog to view something -->
+ <string name="whichViewApplicationLabel">Open</string>
+ <!-- Title of the list of alternate options to complete an action shown when the
+ last used option is being displayed separately. -->
+ <string name="use_a_different_app">Use a different app</string>
+ <!-- Text to display when there are no activities found to display in the
+ activity chooser. See the "Select an action" title. -->
+ <string name="noApplications">No apps can perform this action.</string>
+
+ <!-- Title for a button to choose the currently selected activity
+ as the default in the activity resolver. [CHAR LIMIT=25] -->
+ <string name="activity_resolver_use_always">Always</string>
+
+ <!-- Title for a button to choose the currently selected activity
+ from the activity resolver to use just this once. [CHAR LIMIT=25] -->
+ <string name="activity_resolver_use_once">Just once</string>
+
+ <!-- Resolver target actions strings -->
+ <!-- Pin this app to the top of the Sharesheet app list. [CHAR LIMIT=60]-->
+ <string name="pin_specific_target">Pin <xliff:g id="label" example="Tweet">%1$s</xliff:g></string>
+ <!-- Un-pin this app in the Sharesheet, so that it is sorted normally. [CHAR LIMIT=60]-->
+ <string name="unpin_specific_target">Unpin <xliff:g id="label" example="Tweet">%1$s</xliff:g></string>
+
+ <string name="file_count">{count, plural,
+ =1 {{file_name} + # file}
+ other {{file_name} + # files}
+ }
+ </string>
+
+ <!-- ChooserActivity - No direct share targets are available. [CHAR LIMIT=NONE] -->
+ <string name="chooser_no_direct_share_targets">No recommended people to share with</string>
+
+ <!-- ChooserActivity - Alphabetically sorted apps list label. [CHAR LIMIT=NONE] -->
+ <string name="chooser_all_apps_button_label">Apps list</string>
+
+ <!-- Prompt for the USB device resolver dialog with warning text for USB device dialogs. [CHAR LIMIT=200] -->
+ <string name="usb_device_resolve_prompt_warn">This app has not been granted record permission but could capture audio through this USB device.</string>
+
+ <!-- ResolverActivity - profile tabs -->
+ <!-- Label of a tab on a screen. A user can tap this tap to switch to the 'Personal' view (that shows their personal content) if they have a work profile on their device. [CHAR LIMIT=NONE] -->
+ <string name="resolver_personal_tab">Personal</string>
+ <!-- Label of a tab on a screen. A user can tap this tab to switch to the 'Work' view (that shows their work content) if they have a work profile on their device. [CHAR LIMIT=NONE] -->
+ <string name="resolver_work_tab">Work</string>
+
+ <!-- Accessibility label for the personal tab button. [CHAR LIMIT=NONE] -->
+ <string name="resolver_personal_tab_accessibility">Personal view</string>
+ <!-- Accessibility label for the work tab button. [CHAR LIMIT=NONE] -->
+ <string name="resolver_work_tab_accessibility">Work view</string>
+
+ <!-- Title of a screen. This text lets the user know that their IT admin doesn't allow them to share this content across profiles. [CHAR LIMIT=NONE] -->
+ <string name="resolver_cross_profile_blocked">Blocked by your IT admin</string>
+ <!-- Error message. This text is explaining that the user's IT admin doesn't allow this specific content to be shared with apps in the work profile. [CHAR LIMIT=NONE] -->
+ <string name="resolver_cant_share_with_work_apps_explanation">This content can\u2019t be shared with work apps</string>
+
+ <!-- Error message. This message lets the user know that their IT admin doesn't allow them to open this specific content with an app in their work profile. [CHAR LIMIT=NONE] -->
+ <string name="resolver_cant_access_work_apps_explanation">This content can\u2019t be opened with work apps</string>
+
+ <!-- Error message. This text is explaining that the user's IT admin doesn't allow them to share this specific content with apps in their personal profile. [CHAR LIMIT=NONE] -->
+ <string name="resolver_cant_share_with_personal_apps_explanation">This content can\u2019t be shared with personal apps</string>
+
+ <!-- Error message. This message lets the user know that their IT admin doesn't allow them to open this specific content with an app in their personal profile. [CHAR LIMIT=NONE] -->
+ <string name="resolver_cant_access_personal_apps_explanation">This content can\u2019t be opened with personal apps</string>
+
+ <!-- Error message. This text lets the user know that they need to turn on work apps in order to share or open content. There's also a button a user can tap to turn on the apps. [CHAR LIMIT=NONE] -->
+ <string name="resolver_turn_on_work_apps">Work profile is paused</string>
+ <!-- Button text. This button turns on a user's work profile so they can access their work apps and data. [CHAR LIMIT=NONE] -->
+ <string name="resolver_switch_on_work">Tap to turn on</string>
+
+ <!-- Error message. This text lets the user know that their current work apps don't support the specific content. [CHAR LIMIT=NONE] -->
+ <string name="resolver_no_work_apps_available">No work apps</string>
+
+ <!-- Error message. This text lets the user know that their current personal apps don't support the specific content. [CHAR LIMIT=NONE] -->
+ <string name="resolver_no_personal_apps_available">No personal apps</string>
+
+ <!-- Dialog title. User must choose between opening content in a cross-profile app or same-profile browser. [CHAR LIMIT=NONE] -->
+ <string name="miniresolver_open_in_personal">Open <xliff:g id="app" example="YouTube">%s</xliff:g> in your personal profile?</string>
+ <!-- Dialog title. User must choose between opening content in a cross-profile app or same-profile browser. [CHAR LIMIT=NONE] -->
+ <string name="miniresolver_open_in_work">Open <xliff:g id="app" example="YouTube">%s</xliff:g> in your work profile?</string>
+ <!-- Button option. Open the link in the personal browser. [CHAR LIMIT=NONE] -->
+ <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>
</resources>
diff --git a/java/res/values/styles.xml b/java/res/values/styles.xml
index f366cd9a..cbbf406d 100644
--- a/java/res/values/styles.xml
+++ b/java/res/values/styles.xml
@@ -36,14 +36,14 @@
<!-- <item name="listPreferredItemPaddingStart">?attr/dialogPreferredPadding</item>-->
<!-- <item name="listPreferredItemPaddingEnd">?attr/dialogPreferredPadding</item>-->
<item name="android:navigationBarColor">@android:color/transparent</item>
-<!-- <item name="android:iconfactoryIconSize">@dimen/resolver_icon_size</item>-->
-<!-- <item name="android:iconfactoryBadgeSize">@dimen/resolver_badge_size</item>-->
+ <item name="*android:iconfactoryIconSize">@dimen/resolver_icon_size</item>
+ <item name="*android:iconfactoryBadgeSize">@dimen/resolver_badge_size</item>
</style>
<style name="Theme.DeviceDefault.Resolver" parent="Theme.DeviceDefault.ResolverCommon">
<item name="android:windowLightNavigationBar">true</item>
</style>
<style name="Theme.DeviceDefault.Chooser" parent="Theme.DeviceDefault.Resolver">
-<!-- <item name="android:iconfactoryIconSize">@dimen/chooser_icon_size</item>-->
-<!-- <item name="android:iconfactoryBadgeSize">@dimen/chooser_badge_size</item>-->
+ <item name="*android:iconfactoryIconSize">@dimen/chooser_icon_size</item>
+ <item name="*android:iconfactoryBadgeSize">@dimen/chooser_badge_size</item>
</style>
</resources>
diff --git a/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
new file mode 100644
index 00000000..4f6c0bf1
--- /dev/null
+++ b/java/src/com/android/intentresolver/AbstractMultiProfilePagerAdapter.java
@@ -0,0 +1,632 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.intentresolver;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.app.AppGlobals;
+import android.app.admin.DevicePolicyEventLogger;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.IPackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.AsyncTask;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.stats.devicepolicy.DevicePolicyEnums;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.TextView;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.widget.PagerAdapter;
+import com.android.internal.widget.ViewPager;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Skeletal {@link PagerAdapter} implementation of a work or personal profile page for
+ * intent resolution (including share sheet).
+ */
+public abstract class AbstractMultiProfilePagerAdapter extends PagerAdapter {
+
+ private static final String TAG = "AbstractMultiProfilePagerAdapter";
+ static final int PROFILE_PERSONAL = 0;
+ static final int PROFILE_WORK = 1;
+
+ @IntDef({PROFILE_PERSONAL, PROFILE_WORK})
+ @interface Profile {}
+
+ private final Context mContext;
+ private int mCurrentPage;
+ private OnProfileSelectedListener mOnProfileSelectedListener;
+ private OnSwitchOnWorkSelectedListener mOnSwitchOnWorkSelectedListener;
+ private Set<Integer> mLoadedPages;
+ private final UserHandle mPersonalProfileUserHandle;
+ private final UserHandle mWorkProfileUserHandle;
+ private Injector mInjector;
+ private boolean mIsWaitingToEnableWorkProfile;
+
+ AbstractMultiProfilePagerAdapter(Context context, int currentPage,
+ UserHandle personalProfileUserHandle,
+ UserHandle workProfileUserHandle) {
+ mContext = Objects.requireNonNull(context);
+ mCurrentPage = currentPage;
+ mLoadedPages = new HashSet<>();
+ mPersonalProfileUserHandle = personalProfileUserHandle;
+ mWorkProfileUserHandle = workProfileUserHandle;
+ UserManager userManager = context.getSystemService(UserManager.class);
+ mInjector = new Injector() {
+ @Override
+ public boolean hasCrossProfileIntents(List<Intent> intents, int sourceUserId,
+ int targetUserId) {
+ return AbstractMultiProfilePagerAdapter.this
+ .hasCrossProfileIntents(intents, sourceUserId, targetUserId);
+ }
+
+ @Override
+ public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) {
+ return userManager.isQuietModeEnabled(workProfileUserHandle);
+ }
+
+ @Override
+ public void requestQuietModeEnabled(boolean enabled, UserHandle workProfileUserHandle) {
+ AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
+ userManager.requestQuietModeEnabled(enabled, workProfileUserHandle);
+ });
+ mIsWaitingToEnableWorkProfile = true;
+ }
+ };
+ }
+
+ protected void markWorkProfileEnabledBroadcastReceived() {
+ mIsWaitingToEnableWorkProfile = false;
+ }
+
+ protected boolean isWaitingToEnableWorkProfile() {
+ return mIsWaitingToEnableWorkProfile;
+ }
+
+ /**
+ * Overrides the default {@link Injector} for testing purposes.
+ */
+ @VisibleForTesting
+ public void setInjector(Injector injector) {
+ mInjector = injector;
+ }
+
+ protected boolean isQuietModeEnabled(UserHandle workProfileUserHandle) {
+ return mInjector.isQuietModeEnabled(workProfileUserHandle);
+ }
+
+ void setOnProfileSelectedListener(OnProfileSelectedListener listener) {
+ mOnProfileSelectedListener = listener;
+ }
+
+ void setOnSwitchOnWorkSelectedListener(OnSwitchOnWorkSelectedListener listener) {
+ mOnSwitchOnWorkSelectedListener = listener;
+ }
+
+ Context getContext() {
+ return mContext;
+ }
+
+ /**
+ * Sets this instance of this class as {@link ViewPager}'s {@link PagerAdapter} and sets
+ * an {@link ViewPager.OnPageChangeListener} where it keeps track of the currently displayed
+ * page and rebuilds the list.
+ */
+ void setupViewPager(ViewPager viewPager) {
+ viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
+ @Override
+ public void onPageSelected(int position) {
+ mCurrentPage = position;
+ if (!mLoadedPages.contains(position)) {
+ rebuildActiveTab(true);
+ mLoadedPages.add(position);
+ }
+ if (mOnProfileSelectedListener != null) {
+ mOnProfileSelectedListener.onProfileSelected(position);
+ }
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+ if (mOnProfileSelectedListener != null) {
+ mOnProfileSelectedListener.onProfilePageStateChanged(state);
+ }
+ }
+ });
+ viewPager.setAdapter(this);
+ viewPager.setCurrentItem(mCurrentPage);
+ mLoadedPages.add(mCurrentPage);
+ }
+
+ void clearInactiveProfileCache() {
+ if (mLoadedPages.size() == 1) {
+ return;
+ }
+ mLoadedPages.remove(1 - mCurrentPage);
+ }
+
+ @Override
+ public ViewGroup instantiateItem(ViewGroup container, int position) {
+ final ProfileDescriptor profileDescriptor = getItem(position);
+ container.addView(profileDescriptor.rootView);
+ return profileDescriptor.rootView;
+ }
+
+ @Override
+ public void destroyItem(ViewGroup container, int position, Object view) {
+ container.removeView((View) view);
+ }
+
+ @Override
+ public int getCount() {
+ return getItemCount();
+ }
+
+ protected int getCurrentPage() {
+ return mCurrentPage;
+ }
+
+ @VisibleForTesting
+ public UserHandle getCurrentUserHandle() {
+ return getActiveListAdapter().mResolverListController.getUserHandle();
+ }
+
+ @Override
+ public boolean isViewFromObject(View view, Object object) {
+ return view == object;
+ }
+
+ @Override
+ public CharSequence getPageTitle(int position) {
+ return null;
+ }
+
+ /**
+ * Returns the {@link ProfileDescriptor} relevant to the given <code>pageIndex</code>.
+ * <ul>
+ * <li>For a device with only one user, <code>pageIndex</code> value of
+ * <code>0</code> would return the personal profile {@link ProfileDescriptor}.</li>
+ * <li>For a device with a work profile, <code>pageIndex</code> value of <code>0</code> would
+ * return the personal profile {@link ProfileDescriptor}, and <code>pageIndex</code> value of
+ * <code>1</code> would return the work profile {@link ProfileDescriptor}.</li>
+ * </ul>
+ */
+ abstract ProfileDescriptor getItem(int pageIndex);
+
+ /**
+ * Returns the number of {@link ProfileDescriptor} objects.
+ * <p>For a normal consumer device with only one user returns <code>1</code>.
+ * <p>For a device with a work profile returns <code>2</code>.
+ */
+ abstract int getItemCount();
+
+ /**
+ * Performs view-related initialization procedures for the adapter specified
+ * by <code>pageIndex</code>.
+ */
+ abstract void setupListAdapter(int pageIndex);
+
+ /**
+ * Returns the adapter of the list view for the relevant page specified by
+ * <code>pageIndex</code>.
+ * <p>This method is meant to be implemented with an implementation-specific return type
+ * depending on the adapter type.
+ */
+ @VisibleForTesting
+ public abstract Object getAdapterForIndex(int pageIndex);
+
+ /**
+ * Returns the {@link ResolverListAdapter} instance of the profile that represents
+ * <code>userHandle</code>. If there is no such adapter for the specified
+ * <code>userHandle</code>, returns {@code null}.
+ * <p>For example, if there is a work profile on the device with user id 10, calling this method
+ * with <code>UserHandle.of(10)</code> returns the work profile {@link ResolverListAdapter}.
+ */
+ @Nullable
+ abstract ResolverListAdapter getListAdapterForUserHandle(UserHandle userHandle);
+
+ /**
+ * Returns the {@link ResolverListAdapter} instance of the profile that is currently visible
+ * to the user.
+ * <p>For example, if the user is viewing the work tab in the share sheet, this method returns
+ * the work profile {@link ResolverListAdapter}.
+ * @see #getInactiveListAdapter()
+ */
+ @VisibleForTesting
+ public abstract ResolverListAdapter getActiveListAdapter();
+
+ /**
+ * If this is a device with a work profile, returns the {@link ResolverListAdapter} instance
+ * of the profile that is <b><i>not</i></b> currently visible to the user. Otherwise returns
+ * {@code null}.
+ * <p>For example, if the user is viewing the work tab in the share sheet, this method returns
+ * the personal profile {@link ResolverListAdapter}.
+ * @see #getActiveListAdapter()
+ */
+ @VisibleForTesting
+ public abstract @Nullable ResolverListAdapter getInactiveListAdapter();
+
+ public abstract ResolverListAdapter getPersonalListAdapter();
+
+ public abstract @Nullable ResolverListAdapter getWorkListAdapter();
+
+ abstract Object getCurrentRootAdapter();
+
+ abstract ViewGroup getActiveAdapterView();
+
+ abstract @Nullable ViewGroup getInactiveAdapterView();
+
+ abstract String getMetricsCategory();
+
+ /**
+ * Rebuilds the tab that is currently visible to the user.
+ * <p>Returns {@code true} if rebuild has completed.
+ */
+ boolean rebuildActiveTab(boolean doPostProcessing) {
+ Trace.beginSection("MultiProfilePagerAdapter#rebuildActiveTab");
+ boolean result = rebuildTab(getActiveListAdapter(), doPostProcessing);
+ Trace.endSection();
+ return result;
+ }
+
+ /**
+ * Rebuilds the tab that is not currently visible to the user, if such one exists.
+ * <p>Returns {@code true} if rebuild has completed.
+ */
+ boolean rebuildInactiveTab(boolean doPostProcessing) {
+ Trace.beginSection("MultiProfilePagerAdapter#rebuildInactiveTab");
+ if (getItemCount() == 1) {
+ Trace.endSection();
+ return false;
+ }
+ boolean result = rebuildTab(getInactiveListAdapter(), doPostProcessing);
+ Trace.endSection();
+ return result;
+ }
+
+ private int userHandleToPageIndex(UserHandle userHandle) {
+ if (userHandle.equals(getPersonalListAdapter().mResolverListController.getUserHandle())) {
+ return PROFILE_PERSONAL;
+ } else {
+ return PROFILE_WORK;
+ }
+ }
+
+ private boolean rebuildTab(ResolverListAdapter activeListAdapter, boolean doPostProcessing) {
+ if (shouldShowNoCrossProfileIntentsEmptyState(activeListAdapter)) {
+ activeListAdapter.postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true);
+ return false;
+ }
+ return activeListAdapter.rebuildList(doPostProcessing);
+ }
+
+ private boolean shouldShowNoCrossProfileIntentsEmptyState(
+ ResolverListAdapter activeListAdapter) {
+ UserHandle listUserHandle = activeListAdapter.getUserHandle();
+ return UserHandle.myUserId() != listUserHandle.getIdentifier()
+ && allowShowNoCrossProfileIntentsEmptyState()
+ && !mInjector.hasCrossProfileIntents(activeListAdapter.getIntents(),
+ UserHandle.myUserId(), listUserHandle.getIdentifier());
+ }
+
+ boolean allowShowNoCrossProfileIntentsEmptyState() {
+ return true;
+ }
+
+ protected abstract void showWorkProfileOffEmptyState(
+ ResolverListAdapter activeListAdapter, View.OnClickListener listener);
+
+ protected abstract void showNoPersonalToWorkIntentsEmptyState(
+ ResolverListAdapter activeListAdapter);
+
+ protected abstract void showNoPersonalAppsAvailableEmptyState(
+ ResolverListAdapter activeListAdapter);
+
+ protected abstract void showNoWorkAppsAvailableEmptyState(
+ ResolverListAdapter activeListAdapter);
+
+ protected abstract void showNoWorkToPersonalIntentsEmptyState(
+ ResolverListAdapter activeListAdapter);
+
+ /**
+ * The empty state screens are shown according to their priority:
+ * <ol>
+ * <li>(highest priority) cross-profile disabled by policy (handled in
+ * {@link #rebuildTab(ResolverListAdapter, boolean)})</li>
+ * <li>no apps available</li>
+ * <li>(least priority) work is off</li>
+ * </ol>
+ *
+ * The intention is to prevent the user from having to turn
+ * the work profile on if there will not be any apps resolved
+ * anyway.
+ */
+ void showEmptyResolverListEmptyState(ResolverListAdapter listAdapter) {
+ if (maybeShowNoCrossProfileIntentsEmptyState(listAdapter)) {
+ return;
+ }
+ if (maybeShowWorkProfileOffEmptyState(listAdapter)) {
+ return;
+ }
+ maybeShowNoAppsAvailableEmptyState(listAdapter);
+ }
+
+ private boolean maybeShowNoCrossProfileIntentsEmptyState(ResolverListAdapter listAdapter) {
+ if (!shouldShowNoCrossProfileIntentsEmptyState(listAdapter)) {
+ return false;
+ }
+ if (listAdapter.getUserHandle().equals(mPersonalProfileUserHandle)) {
+ DevicePolicyEventLogger.createEvent(
+ DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL)
+ .setStrings(getMetricsCategory())
+ .write();
+ showNoWorkToPersonalIntentsEmptyState(listAdapter);
+ } else {
+ DevicePolicyEventLogger.createEvent(
+ DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_WORK)
+ .setStrings(getMetricsCategory())
+ .write();
+ showNoPersonalToWorkIntentsEmptyState(listAdapter);
+ }
+ return true;
+ }
+
+ /**
+ * Returns {@code true} if the work profile off empty state screen is shown.
+ */
+ private boolean maybeShowWorkProfileOffEmptyState(ResolverListAdapter listAdapter) {
+ UserHandle listUserHandle = listAdapter.getUserHandle();
+ if (!listUserHandle.equals(mWorkProfileUserHandle)
+ || !mInjector.isQuietModeEnabled(mWorkProfileUserHandle)
+ || listAdapter.getCount() == 0) {
+ return false;
+ }
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_EMPTY_STATE_WORK_APPS_DISABLED)
+ .setStrings(getMetricsCategory())
+ .write();
+ showWorkProfileOffEmptyState(listAdapter,
+ v -> {
+ ProfileDescriptor descriptor = getItem(
+ userHandleToPageIndex(listAdapter.getUserHandle()));
+ showSpinner(descriptor.getEmptyStateView());
+ if (mOnSwitchOnWorkSelectedListener != null) {
+ mOnSwitchOnWorkSelectedListener.onSwitchOnWorkSelected();
+ }
+ mInjector.requestQuietModeEnabled(false, mWorkProfileUserHandle);
+ });
+ return true;
+ }
+
+ private void maybeShowNoAppsAvailableEmptyState(ResolverListAdapter listAdapter) {
+ UserHandle listUserHandle = listAdapter.getUserHandle();
+ if (mWorkProfileUserHandle != null
+ && (UserHandle.myUserId() == listUserHandle.getIdentifier()
+ || !hasAppsInOtherProfile(listAdapter))) {
+ DevicePolicyEventLogger.createEvent(
+ DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_APPS_RESOLVED)
+ .setStrings(getMetricsCategory())
+ .setBoolean(/*isPersonalProfile*/ listUserHandle == mPersonalProfileUserHandle)
+ .write();
+ if (listUserHandle == mPersonalProfileUserHandle) {
+ showNoPersonalAppsAvailableEmptyState(listAdapter);
+ } else {
+ showNoWorkAppsAvailableEmptyState(listAdapter);
+ }
+ } else if (mWorkProfileUserHandle == null) {
+ showConsumerUserNoAppsAvailableEmptyState(listAdapter);
+ }
+ }
+
+ protected void showEmptyState(ResolverListAdapter activeListAdapter, String title,
+ String subtitle) {
+ showEmptyState(activeListAdapter, title, subtitle, /* buttonOnClick */ null);
+ }
+
+ protected void showEmptyState(ResolverListAdapter activeListAdapter,
+ String title, String subtitle, View.OnClickListener buttonOnClick) {
+ ProfileDescriptor descriptor = getItem(
+ userHandleToPageIndex(activeListAdapter.getUserHandle()));
+ descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.GONE);
+ ViewGroup emptyStateView = descriptor.getEmptyStateView();
+ resetViewVisibilitiesForWorkProfileEmptyState(emptyStateView);
+ emptyStateView.setVisibility(View.VISIBLE);
+
+ View container = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_container);
+ setupContainerPadding(container);
+
+ TextView titleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title);
+ titleView.setText(title);
+
+ TextView subtitleView = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle);
+ if (subtitle != null) {
+ subtitleView.setVisibility(View.VISIBLE);
+ subtitleView.setText(subtitle);
+ } else {
+ subtitleView.setVisibility(View.GONE);
+ }
+
+ Button button = emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button);
+ button.setVisibility(buttonOnClick != null ? View.VISIBLE : View.GONE);
+ button.setOnClickListener(buttonOnClick);
+
+ activeListAdapter.markTabLoaded();
+ }
+
+ /**
+ * Sets up the padding of the view containing the empty state screens.
+ * <p>This method is meant to be overridden so that subclasses can customize the padding.
+ */
+ protected void setupContainerPadding(View container) {}
+
+ private void showConsumerUserNoAppsAvailableEmptyState(ResolverListAdapter activeListAdapter) {
+ ProfileDescriptor descriptor = getItem(
+ userHandleToPageIndex(activeListAdapter.getUserHandle()));
+ descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.GONE);
+ View emptyStateView = descriptor.getEmptyStateView();
+ resetViewVisibilitiesForConsumerUserEmptyState(emptyStateView);
+ emptyStateView.setVisibility(View.VISIBLE);
+
+ activeListAdapter.markTabLoaded();
+ }
+
+ private boolean isSpinnerShowing(View emptyStateView) {
+ return emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).getVisibility()
+ == View.VISIBLE;
+ }
+
+ private void showSpinner(View emptyStateView) {
+ emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.INVISIBLE);
+ emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.INVISIBLE);
+ emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).setVisibility(View.VISIBLE);
+ emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE);
+ }
+
+ private void resetViewVisibilitiesForWorkProfileEmptyState(View emptyStateView) {
+ emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.VISIBLE);
+ emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle).setVisibility(View.VISIBLE);
+ emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.INVISIBLE);
+ emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).setVisibility(View.GONE);
+ emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.GONE);
+ }
+
+ private void resetViewVisibilitiesForConsumerUserEmptyState(View emptyStateView) {
+ emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_title).setVisibility(View.GONE);
+ emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_subtitle).setVisibility(View.GONE);
+ emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_button).setVisibility(View.GONE);
+ emptyStateView.findViewById(com.android.internal.R.id.resolver_empty_state_progress).setVisibility(View.GONE);
+ emptyStateView.findViewById(com.android.internal.R.id.empty).setVisibility(View.VISIBLE);
+ }
+
+ protected void showListView(ResolverListAdapter activeListAdapter) {
+ ProfileDescriptor descriptor = getItem(
+ userHandleToPageIndex(activeListAdapter.getUserHandle()));
+ descriptor.rootView.findViewById(com.android.internal.R.id.resolver_list).setVisibility(View.VISIBLE);
+ View emptyStateView = descriptor.rootView.findViewById(com.android.internal.R.id.resolver_empty_state);
+ emptyStateView.setVisibility(View.GONE);
+ }
+
+ private boolean hasCrossProfileIntents(List<Intent> intents, int source, int target) {
+ IPackageManager packageManager = AppGlobals.getPackageManager();
+ ContentResolver contentResolver = mContext.getContentResolver();
+ for (Intent intent : intents) {
+ if (IntentForwarderActivity.canForward(intent, source, target, packageManager,
+ contentResolver) != null) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private boolean hasAppsInOtherProfile(ResolverListAdapter adapter) {
+ if (mWorkProfileUserHandle == null) {
+ return false;
+ }
+ List<ResolverActivity.ResolvedComponentInfo> resolversForIntent =
+ adapter.getResolversForUser(UserHandle.of(UserHandle.myUserId()));
+ for (ResolverActivity.ResolvedComponentInfo info : resolversForIntent) {
+ ResolveInfo resolveInfo = info.getResolveInfoAt(0);
+ if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ boolean shouldShowEmptyStateScreen(ResolverListAdapter listAdapter) {
+ int count = listAdapter.getUnfilteredCount();
+ return (count == 0 && listAdapter.getPlaceholderCount() == 0)
+ || (listAdapter.getUserHandle().equals(mWorkProfileUserHandle)
+ && isQuietModeEnabled(mWorkProfileUserHandle));
+ }
+
+ protected class ProfileDescriptor {
+ final ViewGroup rootView;
+ private final ViewGroup mEmptyStateView;
+ ProfileDescriptor(ViewGroup rootView) {
+ this.rootView = rootView;
+ mEmptyStateView = rootView.findViewById(com.android.internal.R.id.resolver_empty_state);
+ }
+
+ protected ViewGroup getEmptyStateView() {
+ return mEmptyStateView;
+ }
+ }
+
+ public interface OnProfileSelectedListener {
+ /**
+ * Callback for when the user changes the active tab from personal to work or vice versa.
+ * <p>This callback is only called when the intent resolver or share sheet shows
+ * the work and personal profiles.
+ * @param profileIndex {@link #PROFILE_PERSONAL} if the personal profile was selected or
+ * {@link #PROFILE_WORK} if the work profile was selected.
+ */
+ void onProfileSelected(int profileIndex);
+
+
+ /**
+ * Callback for when the scroll state changes. Useful for discovering when the user begins
+ * dragging, when the pager is automatically settling to the current page, or when it is
+ * fully stopped/idle.
+ * @param state {@link ViewPager#SCROLL_STATE_IDLE}, {@link ViewPager#SCROLL_STATE_DRAGGING}
+ * or {@link ViewPager#SCROLL_STATE_SETTLING}
+ * @see ViewPager.OnPageChangeListener#onPageScrollStateChanged
+ */
+ void onProfilePageStateChanged(int state);
+ }
+
+ /**
+ * Listener for when the user switches on the work profile from the work tab.
+ */
+ interface OnSwitchOnWorkSelectedListener {
+ /**
+ * Callback for when the user switches on the work profile from the work tab.
+ */
+ void onSwitchOnWorkSelected();
+ }
+
+ /**
+ * Describes an injector to be used for cross profile functionality. Overridable for testing.
+ */
+ @VisibleForTesting
+ public interface Injector {
+ /**
+ * Returns {@code true} if at least one of the provided {@code intents} can be forwarded
+ * from {@code sourceUserId} to {@code targetUserId}.
+ */
+ boolean hasCrossProfileIntents(List<Intent> intents, int sourceUserId, int targetUserId);
+
+ /**
+ * 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);
+ }
+}
diff --git a/java/src/com/android/intentresolver/AbstractResolverComparator.java b/java/src/com/android/intentresolver/AbstractResolverComparator.java
new file mode 100644
index 00000000..6f802876
--- /dev/null
+++ b/java/src/com/android/intentresolver/AbstractResolverComparator.java
@@ -0,0 +1,296 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.usage.UsageStatsManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.BadParcelableException;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * Used to sort resolved activities in {@link ResolverListController}.
+ *
+ * @hide
+ */
+public abstract class AbstractResolverComparator implements Comparator<ResolvedComponentInfo> {
+
+ private static final int NUM_OF_TOP_ANNOTATIONS_TO_USE = 3;
+ private static final boolean DEBUG = true;
+ private static final String TAG = "AbstractResolverComp";
+
+ protected AfterCompute mAfterCompute;
+ protected final PackageManager mPm;
+ protected final UsageStatsManager mUsm;
+ protected String[] mAnnotations;
+ protected String mContentType;
+
+ // True if the current share is a link.
+ private final boolean mHttp;
+
+ // message types
+ static final int RANKER_SERVICE_RESULT = 0;
+ static final int RANKER_RESULT_TIMEOUT = 1;
+
+ // timeout for establishing connections with a ResolverRankerService, collecting features and
+ // predicting ranking scores.
+ private static final int WATCHDOG_TIMEOUT_MILLIS = 500;
+
+ private final Comparator<ResolveInfo> mAzComparator;
+ private ChooserActivityLogger mChooserActivityLogger;
+
+ protected final Handler mHandler = new Handler(Looper.getMainLooper()) {
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case RANKER_SERVICE_RESULT:
+ if (DEBUG) {
+ Log.d(TAG, "RANKER_SERVICE_RESULT");
+ }
+ if (mHandler.hasMessages(RANKER_RESULT_TIMEOUT)) {
+ handleResultMessage(msg);
+ mHandler.removeMessages(RANKER_RESULT_TIMEOUT);
+ afterCompute();
+ }
+ break;
+
+ case RANKER_RESULT_TIMEOUT:
+ if (DEBUG) {
+ Log.d(TAG, "RANKER_RESULT_TIMEOUT; unbinding services");
+ }
+ mHandler.removeMessages(RANKER_SERVICE_RESULT);
+ afterCompute();
+ if (mChooserActivityLogger != null) {
+ mChooserActivityLogger.logSharesheetAppShareRankingTimeout();
+ }
+ break;
+
+ default:
+ super.handleMessage(msg);
+ }
+ }
+ };
+
+ public AbstractResolverComparator(Context context, Intent intent) {
+ String scheme = intent.getScheme();
+ mHttp = "http".equals(scheme) || "https".equals(scheme);
+ mContentType = intent.getType();
+ getContentAnnotations(intent);
+ mPm = context.getPackageManager();
+ mUsm = (UsageStatsManager) context.getSystemService(Context.USAGE_STATS_SERVICE);
+ mAzComparator = new AzInfoComparator(context);
+ }
+
+ // get annotations of content from intent.
+ private void getContentAnnotations(Intent intent) {
+ try {
+ ArrayList<String> annotations = intent.getStringArrayListExtra(
+ Intent.EXTRA_CONTENT_ANNOTATIONS);
+ if (annotations != null) {
+ int size = annotations.size();
+ if (size > NUM_OF_TOP_ANNOTATIONS_TO_USE) {
+ size = NUM_OF_TOP_ANNOTATIONS_TO_USE;
+ }
+ mAnnotations = new String[size];
+ for (int i = 0; i < size; i++) {
+ mAnnotations[i] = annotations.get(i);
+ }
+ }
+ } catch (BadParcelableException e) {
+ Log.i(TAG, "Couldn't unparcel intent annotations. Ignoring.");
+ mAnnotations = new String[0];
+ }
+ }
+
+ /**
+ * Callback to be called when {@link #compute(List)} finishes. This signals to stop waiting.
+ */
+ interface AfterCompute {
+
+ void afterCompute();
+ }
+
+ void setCallBack(AfterCompute afterCompute) {
+ mAfterCompute = afterCompute;
+ }
+
+ void setChooserActivityLogger(ChooserActivityLogger chooserActivityLogger) {
+ mChooserActivityLogger = chooserActivityLogger;
+ }
+
+ ChooserActivityLogger getChooserActivityLogger() {
+ return mChooserActivityLogger;
+ }
+
+ protected final void afterCompute() {
+ final AfterCompute afterCompute = mAfterCompute;
+ if (afterCompute != null) {
+ afterCompute.afterCompute();
+ }
+ }
+
+ @Override
+ public final int compare(ResolvedComponentInfo lhsp, ResolvedComponentInfo rhsp) {
+ final ResolveInfo lhs = lhsp.getResolveInfoAt(0);
+ final ResolveInfo rhs = rhsp.getResolveInfoAt(0);
+
+ final boolean lFixedAtTop = lhsp.isFixedAtTop();
+ final boolean rFixedAtTop = rhsp.isFixedAtTop();
+ if (lFixedAtTop && !rFixedAtTop) return -1;
+ if (!lFixedAtTop && rFixedAtTop) return 1;
+
+ // We want to put the one targeted to another user at the end of the dialog.
+ if (lhs.targetUserId != UserHandle.USER_CURRENT) {
+ return rhs.targetUserId != UserHandle.USER_CURRENT ? 0 : 1;
+ }
+ if (rhs.targetUserId != UserHandle.USER_CURRENT) {
+ return -1;
+ }
+
+ if (mHttp) {
+ final boolean lhsSpecific = ResolverActivity.isSpecificUriMatch(lhs.match);
+ final boolean rhsSpecific = ResolverActivity.isSpecificUriMatch(rhs.match);
+ if (lhsSpecific != rhsSpecific) {
+ return lhsSpecific ? -1 : 1;
+ }
+ }
+
+ final boolean lPinned = lhsp.isPinned();
+ final boolean rPinned = rhsp.isPinned();
+
+ // Pinned items always receive priority.
+ if (lPinned && !rPinned) {
+ return -1;
+ } else if (!lPinned && rPinned) {
+ return 1;
+ } else if (lPinned && rPinned) {
+ // If both items are pinned, resolve the tie alphabetically.
+ return mAzComparator.compare(lhsp.getResolveInfoAt(0), rhsp.getResolveInfoAt(0));
+ }
+
+ return compare(lhs, rhs);
+ }
+
+ /**
+ * Delegated to when used as a {@link Comparator<ResolvedComponentInfo>} if there is not a
+ * special case. The {@link ResolveInfo ResolveInfos} are the first {@link ResolveInfo} in
+ * {@link ResolvedComponentInfo#getResolveInfoAt(int)} from the parameters of {@link
+ * #compare(ResolvedComponentInfo, ResolvedComponentInfo)}
+ */
+ abstract int compare(ResolveInfo lhs, ResolveInfo rhs);
+
+ /**
+ * Computes features for each target. This will be called before calls to {@link
+ * #getScore(ComponentName)} or {@link #compare(Object, Object)}, in order to prepare the
+ * comparator for those calls. Note that {@link #getScore(ComponentName)} uses {@link
+ * ComponentName}, so the implementation will have to be prepared to identify a {@link
+ * ResolvedComponentInfo} by {@link ComponentName}. {@link #beforeCompute()} will be called
+ * before doing any computing.
+ */
+ final void compute(List<ResolvedComponentInfo> targets) {
+ beforeCompute();
+ doCompute(targets);
+ }
+
+ /** Implementation of compute called after {@link #beforeCompute()}. */
+ abstract void doCompute(List<ResolvedComponentInfo> targets);
+
+ /**
+ * Returns the score that was calculated for the corresponding {@link ResolvedComponentInfo}
+ * when {@link #compute(List)} was called before this.
+ */
+ abstract float getScore(ComponentName name);
+
+ /** Handles result message sent to mHandler. */
+ abstract void handleResultMessage(Message message);
+
+ /**
+ * Reports to UsageStats what was chosen.
+ */
+ final void updateChooserCounts(String packageName, int userId, String action) {
+ if (mUsm != null) {
+ mUsm.reportChooserSelection(packageName, userId, mContentType, mAnnotations, action);
+ }
+ }
+
+ /**
+ * Updates the model used to rank the componentNames.
+ *
+ * <p>Default implementation does nothing, as we could have simple model that does not train
+ * online.
+ *
+ * @param componentName the component that the user clicked
+ */
+ void updateModel(ComponentName componentName) {
+ }
+
+ /** Called before {@link #doCompute(List)}. Sets up 500ms timeout. */
+ void beforeCompute() {
+ if (DEBUG) Log.d(TAG, "Setting watchdog timer for " + WATCHDOG_TIMEOUT_MILLIS + "ms");
+ if (mHandler == null) {
+ Log.d(TAG, "Error: Handler is Null; Needs to be initialized.");
+ return;
+ }
+ mHandler.sendEmptyMessageDelayed(RANKER_RESULT_TIMEOUT, WATCHDOG_TIMEOUT_MILLIS);
+ }
+
+ /**
+ * Called when the {@link ResolverActivity} is destroyed. This calls {@link #afterCompute()}. If
+ * this call needs to happen at a different time during destroy, the method should be
+ * overridden.
+ */
+ void destroy() {
+ mHandler.removeMessages(RANKER_SERVICE_RESULT);
+ mHandler.removeMessages(RANKER_RESULT_TIMEOUT);
+ afterCompute();
+ mAfterCompute = null;
+ }
+
+ /**
+ * Sort intents alphabetically based on package name.
+ */
+ class AzInfoComparator implements Comparator<ResolveInfo> {
+ Collator mCollator;
+ AzInfoComparator(Context context) {
+ mCollator = Collator.getInstance(context.getResources().getConfiguration().locale);
+ }
+
+ @Override
+ public int compare(ResolveInfo lhsp, ResolveInfo rhsp) {
+ if (lhsp == null) {
+ return -1;
+ } else if (rhsp == null) {
+ return 1;
+ }
+ return mCollator.compare(lhsp.activityInfo.packageName, rhsp.activityInfo.packageName);
+ }
+ }
+
+}
diff --git a/java/src/com/android/intentresolver/AppPredictionServiceResolverComparator.java b/java/src/com/android/intentresolver/AppPredictionServiceResolverComparator.java
new file mode 100644
index 00000000..9b9fc1c0
--- /dev/null
+++ b/java/src/com/android/intentresolver/AppPredictionServiceResolverComparator.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import static android.app.prediction.AppTargetEvent.ACTION_LAUNCH;
+
+import android.annotation.Nullable;
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.app.prediction.AppTargetId;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.os.Message;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executors;
+
+/**
+ * Uses an {@link AppPredictor} to sort Resolver targets. If the AppPredictionService appears to be
+ * disabled by returning an empty sorted target list, {@link AppPredictionServiceResolverComparator}
+ * will fallback to using a {@link ResolverRankerServiceResolverComparator}.
+ */
+class AppPredictionServiceResolverComparator extends AbstractResolverComparator {
+
+ private static final String TAG = "APSResolverComparator";
+
+ private final AppPredictor mAppPredictor;
+ private final Context mContext;
+ private final Map<ComponentName, Integer> mTargetRanks = new HashMap<>();
+ private final Map<ComponentName, Integer> mTargetScores = new HashMap<>();
+ private final UserHandle mUser;
+ private final Intent mIntent;
+ private final String mReferrerPackage;
+ // If this is non-null (and this is not destroyed), it means APS is disabled and we should fall
+ // back to using the ResolverRankerService.
+ // TODO: responsibility for this fallback behavior can live outside of the AppPrediction client.
+ private ResolverRankerServiceResolverComparator mResolverRankerService;
+ private AppPredictionServiceComparatorModel mComparatorModel;
+
+ AppPredictionServiceResolverComparator(
+ Context context,
+ Intent intent,
+ String referrerPackage,
+ AppPredictor appPredictor,
+ UserHandle user,
+ ChooserActivityLogger chooserActivityLogger) {
+ super(context, intent);
+ mContext = context;
+ mIntent = intent;
+ mAppPredictor = appPredictor;
+ mUser = user;
+ mReferrerPackage = referrerPackage;
+ setChooserActivityLogger(chooserActivityLogger);
+ mComparatorModel = buildUpdatedModel();
+ }
+
+ @Override
+ int compare(ResolveInfo lhs, ResolveInfo rhs) {
+ return mComparatorModel.getComparator().compare(lhs, rhs);
+ }
+
+ @Override
+ void doCompute(List<ResolvedComponentInfo> targets) {
+ if (targets.isEmpty()) {
+ mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT);
+ return;
+ }
+ List<AppTarget> appTargets = new ArrayList<>();
+ for (ResolvedComponentInfo target : targets) {
+ appTargets.add(
+ new AppTarget.Builder(
+ new AppTargetId(target.name.flattenToString()),
+ target.name.getPackageName(),
+ mUser)
+ .setClassName(target.name.getClassName())
+ .build());
+ }
+ mAppPredictor.sortTargets(appTargets, Executors.newSingleThreadExecutor(),
+ sortedAppTargets -> {
+ if (sortedAppTargets.isEmpty()) {
+ Log.i(TAG, "AppPredictionService disabled. Using resolver.");
+ // APS for chooser is disabled. Fallback to resolver.
+ mResolverRankerService =
+ new ResolverRankerServiceResolverComparator(
+ mContext, mIntent, mReferrerPackage,
+ () -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT),
+ getChooserActivityLogger());
+ mComparatorModel = buildUpdatedModel();
+ mResolverRankerService.compute(targets);
+ } else {
+ Log.i(TAG, "AppPredictionService response received");
+ // Skip sending to Handler which takes extra time to dispatch messages.
+ handleResult(sortedAppTargets);
+ }
+ }
+ );
+ }
+
+ @Override
+ void handleResultMessage(Message msg) {
+ // Null value is okay if we have defaulted to the ResolverRankerService.
+ if (msg.what == RANKER_SERVICE_RESULT && msg.obj != null) {
+ final List<AppTarget> sortedAppTargets = (List<AppTarget>) msg.obj;
+ handleSortedAppTargets(sortedAppTargets);
+ } else if (msg.obj == null && mResolverRankerService == null) {
+ Log.e(TAG, "Unexpected null result");
+ }
+ }
+
+ private void handleResult(List<AppTarget> sortedAppTargets) {
+ if (mHandler.hasMessages(RANKER_RESULT_TIMEOUT)) {
+ handleSortedAppTargets(sortedAppTargets);
+ mHandler.removeMessages(RANKER_RESULT_TIMEOUT);
+ afterCompute();
+ }
+ }
+
+ private void handleSortedAppTargets(List<AppTarget> sortedAppTargets) {
+ if (checkAppTargetRankValid(sortedAppTargets)) {
+ sortedAppTargets.forEach(target -> mTargetScores.put(
+ new ComponentName(target.getPackageName(), target.getClassName()),
+ target.getRank()));
+ }
+ for (int i = 0; i < sortedAppTargets.size(); i++) {
+ ComponentName componentName = new ComponentName(
+ sortedAppTargets.get(i).getPackageName(),
+ sortedAppTargets.get(i).getClassName());
+ mTargetRanks.put(componentName, i);
+ Log.i(TAG, "handleSortedAppTargets, sortedAppTargets #" + i + ": " + componentName);
+ }
+ mComparatorModel = buildUpdatedModel();
+ }
+
+ private boolean checkAppTargetRankValid(List<AppTarget> sortedAppTargets) {
+ for (AppTarget target : sortedAppTargets) {
+ if (target.getRank() != 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ float getScore(ComponentName name) {
+ return mComparatorModel.getScore(name);
+ }
+
+ @Override
+ void updateModel(ComponentName componentName) {
+ mComparatorModel.notifyOnTargetSelected(componentName);
+ }
+
+ @Override
+ void destroy() {
+ if (mResolverRankerService != null) {
+ mResolverRankerService.destroy();
+ mResolverRankerService = null;
+ mComparatorModel = buildUpdatedModel();
+ }
+ }
+
+ /**
+ * Re-construct an {@code AppPredictionServiceComparatorModel} to replace the current model
+ * instance (if any) using the up-to-date {@code AppPredictionServiceResolverComparator} ivar
+ * values.
+ *
+ * TODO: each time we replace the model instance, we're either updating the model to use
+ * adjusted data (which is appropriate), or we're providing a (late) value for one of our ivars
+ * that wasn't available the last time the model was updated. For those latter cases, we should
+ * just avoid creating the model altogether until we have all the prerequisites we'll need. Then
+ * we can probably simplify the logic in {@code AppPredictionServiceComparatorModel} since we
+ * won't need to handle edge cases when the model data isn't fully prepared.
+ * (In some cases, these kinds of "updates" might interleave -- e.g., we might have finished
+ * initializing the first time and now want to adjust some data, but still need to wait for
+ * changes to propagate to the other ivars before rebuilding the model.)
+ */
+ private AppPredictionServiceComparatorModel buildUpdatedModel() {
+ return new AppPredictionServiceComparatorModel(
+ mAppPredictor, mResolverRankerService, mUser, mTargetRanks);
+ }
+
+ // TODO: Finish separating behaviors of AbstractResolverComparator, then (probably) make this a
+ // standalone class once clients are written in terms of ResolverComparatorModel.
+ static class AppPredictionServiceComparatorModel implements ResolverComparatorModel {
+ private final AppPredictor mAppPredictor;
+ private final ResolverRankerServiceResolverComparator mResolverRankerService;
+ private final UserHandle mUser;
+ private final Map<ComponentName, Integer> mTargetRanks; // Treat as immutable.
+
+ AppPredictionServiceComparatorModel(
+ AppPredictor appPredictor,
+ @Nullable ResolverRankerServiceResolverComparator resolverRankerService,
+ UserHandle user,
+ Map<ComponentName, Integer> targetRanks) {
+ mAppPredictor = appPredictor;
+ mResolverRankerService = resolverRankerService;
+ mUser = user;
+ mTargetRanks = targetRanks;
+ }
+
+ @Override
+ public Comparator<ResolveInfo> getComparator() {
+ return (lhs, rhs) -> {
+ if (mResolverRankerService != null) {
+ return mResolverRankerService.compare(lhs, rhs);
+ }
+ Integer lhsRank = mTargetRanks.get(new ComponentName(lhs.activityInfo.packageName,
+ lhs.activityInfo.name));
+ Integer rhsRank = mTargetRanks.get(new ComponentName(rhs.activityInfo.packageName,
+ rhs.activityInfo.name));
+ if (lhsRank == null && rhsRank == null) {
+ return 0;
+ } else if (lhsRank == null) {
+ return -1;
+ } else if (rhsRank == null) {
+ return 1;
+ }
+ return lhsRank - rhsRank;
+ };
+ }
+
+ @Override
+ public float getScore(ComponentName name) {
+ if (mResolverRankerService != null) {
+ return mResolverRankerService.getScore(name);
+ }
+ Integer rank = mTargetRanks.get(name);
+ if (rank == null) {
+ Log.w(TAG, "Score requested for unknown component. Did you call compute yet?");
+ return 0f;
+ }
+ int consecutiveSumOfRanks = (mTargetRanks.size() - 1) * (mTargetRanks.size()) / 2;
+ return 1.0f - (((float) rank) / consecutiveSumOfRanks);
+ }
+
+ @Override
+ public void notifyOnTargetSelected(ComponentName componentName) {
+ if (mResolverRankerService != null) {
+ mResolverRankerService.updateModel(componentName);
+ return;
+ }
+ mAppPredictor.notifyAppTargetEvent(
+ new AppTargetEvent.Builder(
+ new AppTarget.Builder(
+ new AppTargetId(componentName.toString()),
+ componentName.getPackageName(), mUser)
+ .setClassName(componentName.getClassName()).build(),
+ ACTION_LAUNCH).build());
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 4c96f7a7..14d77427 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * 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.
@@ -16,27 +16,4126 @@
package com.android.intentresolver;
+import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.ActivityOptions;
+import android.app.SharedElementCallback;
+import android.app.prediction.AppPredictionContext;
+import android.app.prediction.AppPredictionManager;
+import android.app.prediction.AppPredictor;
+import android.app.prediction.AppTarget;
+import android.app.prediction.AppTargetEvent;
+import android.app.prediction.AppTargetId;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.ComponentName;
+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.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Insets;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.drawable.AnimatedVectorDrawable;
+import android.graphics.drawable.Drawable;
+import android.metrics.LogMaker;
+import android.net.Uri;
+import android.os.AsyncTask;
import android.os.Bundle;
-import android.os.StrictMode;
+import android.os.Environment;
+import android.os.Handler;
+import android.os.Message;
+import android.os.Parcelable;
+import android.os.PatternMatcher;
+import android.os.ResultReceiver;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.os.storage.StorageManager;
+import android.provider.DeviceConfig;
+import android.provider.DocumentsContract;
+import android.provider.Downloads;
+import android.provider.OpenableColumns;
+import android.provider.Settings;
+import android.service.chooser.ChooserTarget;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.HashedStringCache;
+import android.util.Log;
+import android.util.PluralsMessageFormatter;
+import android.util.Size;
+import android.util.Slog;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewTreeObserver;
+import android.view.WindowInsets;
+import android.view.animation.AccelerateInterpolator;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.LinearInterpolator;
+import android.widget.Button;
+import android.widget.ImageView;
+import android.widget.Space;
+import android.widget.TextView;
+
+import com.android.intentresolver.ResolverListAdapter.ActivityInfoPresentationGetter;
+import com.android.intentresolver.ResolverListAdapter.ViewHolder;
+import com.android.intentresolver.chooser.ChooserTargetInfo;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
+import com.android.intentresolver.chooser.NotSelectableTargetInfo;
+import com.android.intentresolver.chooser.SelectableTargetInfo;
+import com.android.intentresolver.chooser.SelectableTargetInfo.SelectableTargetInfoCommunicator;
+import com.android.intentresolver.chooser.TargetInfo;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.widget.GridLayoutManager;
+import com.android.internal.widget.RecyclerView;
+import com.android.internal.widget.ResolverDrawerLayout;
+import com.android.internal.widget.ViewPager;
-import androidx.annotation.Nullable;
+import com.google.android.collect.Lists;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.URISyntaxException;
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.function.Supplier;
/**
- * Activity for selecting which application ought to handle an ACTION_SEND intent.
+ * The Chooser Activity handles intent resolution specifically for sharing intents -
+ * for example, those generated by @see android.content.Intent#createChooser(Intent, CharSequence).
+ *
*/
-public class ChooserActivity extends com.android.internal.app.ChooserActivity {
+public class ChooserActivity extends ResolverActivity implements
+ ChooserListAdapter.ChooserListCommunicator,
+ SelectableTargetInfoCommunicator {
+ private static final String TAG = "ChooserActivity";
+
+ private AppPredictor mPersonalAppPredictor;
+ private AppPredictor mWorkAppPredictor;
+ private boolean mShouldDisplayLandscape;
+
+ @UnsupportedAppUsage
+ public ChooserActivity() {
+ }
+ /**
+ * Boolean extra to change the following behavior: Normally, ChooserActivity finishes itself
+ * in onStop when launched in a new task. If this extra is set to true, we do not finish
+ * ourselves when onStop gets called.
+ */
+ public static final String EXTRA_PRIVATE_RETAIN_IN_ON_STOP
+ = "com.android.internal.app.ChooserActivity.EXTRA_PRIVATE_RETAIN_IN_ON_STOP";
+
+
+ /**
+ * Transition name for the first image preview.
+ * To be used for shared element transition into this activity.
+ * @hide
+ */
+ public static final String FIRST_IMAGE_PREVIEW_TRANSITION_NAME = "screenshot_preview_image";
+
+ private static final String PREF_NUM_SHEET_EXPANSIONS = "pref_num_sheet_expansions";
+
+ 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 boolean DEBUG = true;
+
+ private static final boolean USE_PREDICTION_MANAGER_FOR_SHARE_ACTIVITIES = true;
+ // TODO(b/123088566) Share these in a better way.
+ private static final String APP_PREDICTION_SHARE_UI_SURFACE = "share";
+ public static final String LAUNCH_LOCATION_DIRECT_SHARE = "direct_share";
+ public static final String CHOOSER_TARGET = "chooser_target";
+ private static final String SHORTCUT_TARGET = "shortcut_target";
+ private static final int APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT = 20;
+ public static final String APP_PREDICTION_INTENT_FILTER_KEY = "intent_filter";
+ private static final String SHARED_TEXT_KEY = "shared_text";
+
+ private static final String PLURALS_COUNT = "count";
+ private static final String PLURALS_FILE_NAME = "file_name";
+
+ private static final String IMAGE_EDITOR_SHARED_ELEMENT = "screenshot_preview_image";
+
+ private boolean mIsAppPredictorComponentAvailable;
+ private Map<ChooserTarget, AppTarget> mDirectShareAppTargetCache;
+ private Map<ChooserTarget, ShortcutInfo> mDirectShareShortcutInfoCache;
+
+ public static final int TARGET_TYPE_DEFAULT = 0;
+ public static final int TARGET_TYPE_CHOOSER_TARGET = 1;
+ public static final int TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER = 2;
+ public static final int TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE = 3;
+
+ public static final int SELECTION_TYPE_SERVICE = 1;
+ public static final int SELECTION_TYPE_APP = 2;
+ public static final int SELECTION_TYPE_STANDARD = 3;
+ public static final int SELECTION_TYPE_COPY = 4;
+ public static final int SELECTION_TYPE_NEARBY = 5;
+ public static final int SELECTION_TYPE_EDIT = 6;
+
+ private static final int SCROLL_STATUS_IDLE = 0;
+ private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1;
+ private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2;
+
+ // statsd logger wrapper
+ protected ChooserActivityLogger mChooserActivityLogger;
+
+ @IntDef(flag = false, prefix = { "TARGET_TYPE_" }, value = {
+ TARGET_TYPE_DEFAULT,
+ TARGET_TYPE_CHOOSER_TARGET,
+ TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER,
+ TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ShareTargetType {}
+
+ /**
+ * The transition time between placeholders for direct share to a message
+ * indicating that non are available.
+ */
+ private static final int NO_DIRECT_SHARE_ANIM_IN_MILLIS = 200;
+
+ private static final float DIRECT_SHARE_EXPANSION_RATE = 0.78f;
+
+ private static final int DEFAULT_SALT_EXPIRATION_DAYS = 7;
+ private int mMaxHashSaltDays = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.HASH_SALT_MAX_DAYS,
+ DEFAULT_SALT_EXPIRATION_DAYS);
+
+ private static final boolean DEFAULT_IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP = false;
+ private boolean mIsNearbyShareFirstTargetInRankedApp =
+ DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP,
+ DEFAULT_IS_NEARBY_SHARE_FIRST_TARGET_IN_RANKED_APP);
+
+ private static final int DEFAULT_LIST_VIEW_UPDATE_DELAY_MS = 0;
+
+ private static final int URI_PERMISSION_INTENT_FLAGS = Intent.FLAG_GRANT_READ_URI_PERMISSION
+ | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
+ | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION;
+
+ @VisibleForTesting
+ int mListViewUpdateDelayMs = DeviceConfig.getInt(DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.SHARESHEET_LIST_VIEW_UPDATE_DELAY,
+ DEFAULT_LIST_VIEW_UPDATE_DELAY_MS);
+
+ private Bundle mReplacementExtras;
+ private IntentSender mChosenComponentSender;
+ private IntentSender mRefinementIntentSender;
+ private RefinementResultReceiver mRefinementResultReceiver;
+ private ChooserTarget[] mCallerChooserTargets;
+ private ComponentName[] mFilteredComponentNames;
+
+ private Intent mReferrerFillInIntent;
+
+ private long mChooserShownTime;
+ protected boolean mIsSuccessfullySelected;
+
+ private long mQueriedSharingShortcutsTimeMs;
+
+ private int mCurrAvailableWidth = 0;
+ private Insets mLastAppliedInsets = null;
+ private int mLastNumberOfChildren = -1;
+ private int mMaxTargetsPerRow = 1;
+
+ private static final String TARGET_DETAILS_FRAGMENT_TAG = "targetDetailsFragment";
+
+ private static final int MAX_LOG_RANK_POSITION = 12;
+
+ private static final int MAX_EXTRA_INITIAL_INTENTS = 2;
+ private static final int MAX_EXTRA_CHOOSER_TARGETS = 2;
+
+ private SharedPreferences mPinnedSharedPrefs;
+ private static final String PINNED_SHARED_PREFS_NAME = "chooser_pin_settings";
+
+ @Retention(SOURCE)
+ @IntDef({CONTENT_PREVIEW_FILE, CONTENT_PREVIEW_IMAGE, CONTENT_PREVIEW_TEXT})
+ private @interface ContentPreviewType {
+ }
+
+ // Starting at 1 since 0 is considered "undefined" for some of the database transformations
+ // of tron logs.
+ protected static final int CONTENT_PREVIEW_IMAGE = 1;
+ protected static final int CONTENT_PREVIEW_FILE = 2;
+ protected static final int CONTENT_PREVIEW_TEXT = 3;
+ protected MetricsLogger mMetricsLogger;
+
+ private ContentPreviewCoordinator mPreviewCoord;
+ private int mScrollStatus = SCROLL_STATUS_IDLE;
+
+ @VisibleForTesting
+ protected ChooserMultiProfilePagerAdapter mChooserMultiProfilePagerAdapter;
+ private final EnterTransitionAnimationDelegate mEnterTransitionAnimationDelegate =
+ new EnterTransitionAnimationDelegate();
+
+ private boolean mRemoveSharedElements = false;
+
+ private View mContentView = null;
+
+ private class ContentPreviewCoordinator {
+ private static final int IMAGE_FADE_IN_MILLIS = 150;
+ private static final int IMAGE_LOAD_TIMEOUT = 1;
+ private static final int IMAGE_LOAD_INTO_VIEW = 2;
+
+ private final int mImageLoadTimeoutMillis =
+ getResources().getInteger(R.integer.config_shortAnimTime);
+
+ private final View mParentView;
+ private boolean mHideParentOnFail;
+ private boolean mAtLeastOneLoaded = false;
+
+ class LoadUriTask {
+ public final Uri mUri;
+ public final int mImageResourceId;
+ public final int mExtraCount;
+ public final Bitmap mBmp;
+
+ LoadUriTask(int imageResourceId, Uri uri, int extraCount, Bitmap bmp) {
+ this.mImageResourceId = imageResourceId;
+ this.mUri = uri;
+ this.mExtraCount = extraCount;
+ this.mBmp = bmp;
+ }
+ }
+
+ // If at least one image loads within the timeout period, allow other
+ // loads to continue. Otherwise terminate and optionally hide
+ // the parent area
+ private final Handler mHandler = new Handler() {
+ @Override
+ public void handleMessage(Message msg) {
+ switch (msg.what) {
+ case IMAGE_LOAD_TIMEOUT:
+ maybeHideContentPreview();
+ break;
+
+ case IMAGE_LOAD_INTO_VIEW:
+ if (isFinishing()) break;
+
+ LoadUriTask task = (LoadUriTask) msg.obj;
+ RoundedRectImageView imageView = mParentView.findViewById(
+ task.mImageResourceId);
+ if (task.mBmp == null) {
+ imageView.setVisibility(View.GONE);
+ maybeHideContentPreview();
+ return;
+ }
+
+ mAtLeastOneLoaded = true;
+ imageView.setVisibility(View.VISIBLE);
+ imageView.setAlpha(0.0f);
+ imageView.setImageBitmap(task.mBmp);
+
+ ValueAnimator fadeAnim = ObjectAnimator.ofFloat(imageView, "alpha", 0.0f,
+ 1.0f);
+ fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
+ fadeAnim.setDuration(IMAGE_FADE_IN_MILLIS);
+ fadeAnim.start();
+
+ if (task.mExtraCount > 0) {
+ imageView.setExtraImageCount(task.mExtraCount);
+ }
+
+ setupPreDrawForSharedElementTransition(imageView);
+ }
+ }
+ };
+
+ private void setupPreDrawForSharedElementTransition(View v) {
+ v.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
+ @Override
+ public boolean onPreDraw() {
+ v.getViewTreeObserver().removeOnPreDrawListener(this);
+
+ if (!mRemoveSharedElements && isActivityTransitionRunning()) {
+ // Disable the window animations as it interferes with the
+ // transition animation.
+ getWindow().setWindowAnimations(0);
+ }
+ mEnterTransitionAnimationDelegate.markImagePreviewReady();
+ return true;
+ }
+ });
+ }
+
+ ContentPreviewCoordinator(View parentView, boolean hideParentOnFail) {
+ super();
+
+ this.mParentView = parentView;
+ this.mHideParentOnFail = hideParentOnFail;
+ }
+
+ private void loadUriIntoView(final int imageResourceId, final Uri uri,
+ final int extraImages) {
+ mHandler.sendEmptyMessageDelayed(IMAGE_LOAD_TIMEOUT, mImageLoadTimeoutMillis);
+
+ AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
+ int size = getResources().getDimensionPixelSize(
+ R.dimen.chooser_preview_image_max_dimen);
+ final Bitmap bmp = loadThumbnail(uri, new Size(size, size));
+ final Message msg = Message.obtain();
+ msg.what = IMAGE_LOAD_INTO_VIEW;
+ msg.obj = new LoadUriTask(imageResourceId, uri, extraImages, bmp);
+ mHandler.sendMessage(msg);
+ });
+ }
+
+ private void cancelLoads() {
+ mHandler.removeMessages(IMAGE_LOAD_INTO_VIEW);
+ mHandler.removeMessages(IMAGE_LOAD_TIMEOUT);
+ }
+
+ private void maybeHideContentPreview() {
+ if (!mAtLeastOneLoaded) {
+ if (mHideParentOnFail) {
+ Log.i(TAG, "Hiding image preview area. Timed out waiting for preview to load"
+ + " within " + mImageLoadTimeoutMillis + "ms.");
+ collapseParentView();
+ if (shouldShowTabs()) {
+ hideStickyContentPreview();
+ } else if (mChooserMultiProfilePagerAdapter.getCurrentRootAdapter() != null) {
+ mChooserMultiProfilePagerAdapter.getCurrentRootAdapter()
+ .hideContentPreview();
+ }
+ mHideParentOnFail = false;
+ }
+ mRemoveSharedElements = true;
+ mEnterTransitionAnimationDelegate.markImagePreviewReady();
+ }
+ }
+
+ private void collapseParentView() {
+ // This will effectively hide the content preview row by forcing the height
+ // to zero. It is faster than forcing a relayout of the listview
+ final View v = mParentView;
+ int widthSpec = MeasureSpec.makeMeasureSpec(v.getWidth(), MeasureSpec.EXACTLY);
+ int heightSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.EXACTLY);
+ v.measure(widthSpec, heightSpec);
+ v.getLayoutParams().height = 0;
+ v.layout(v.getLeft(), v.getTop(), v.getRight(), v.getTop());
+ v.invalidate();
+ }
+ }
+
+ private final ChooserHandler mChooserHandler = new ChooserHandler();
+
+ private class ChooserHandler extends Handler {
+ private static final int LIST_VIEW_UPDATE_MESSAGE = 6;
+ private static final int SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS = 7;
+
+ private void removeAllMessages() {
+ removeMessages(LIST_VIEW_UPDATE_MESSAGE);
+ removeMessages(SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS);
+ }
+
+ @Override
+ public void handleMessage(Message msg) {
+ if (mChooserMultiProfilePagerAdapter.getActiveListAdapter() == null || isDestroyed()) {
+ return;
+ }
+
+ switch (msg.what) {
+ case LIST_VIEW_UPDATE_MESSAGE:
+ if (DEBUG) {
+ Log.d(TAG, "LIST_VIEW_UPDATE_MESSAGE; ");
+ }
+
+ UserHandle userHandle = (UserHandle) msg.obj;
+ mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(userHandle)
+ .refreshListView();
+ break;
+
+ case SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS:
+ if (DEBUG) Log.d(TAG, "SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS");
+ final ServiceResultInfo[] resultInfos = (ServiceResultInfo[]) msg.obj;
+ for (ServiceResultInfo resultInfo : resultInfos) {
+ if (resultInfo.resultTargets != null) {
+ ChooserListAdapter adapterForUserHandle =
+ mChooserMultiProfilePagerAdapter.getListAdapterForUserHandle(
+ resultInfo.userHandle);
+ if (adapterForUserHandle != null) {
+ adapterForUserHandle.addServiceResults(
+ resultInfo.originalTarget,
+ resultInfo.resultTargets, msg.arg1,
+ mDirectShareShortcutInfoCache);
+ }
+ }
+ }
+
+ logDirectShareTargetReceived(
+ MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER);
+ sendVoiceChoicesIfNeeded();
+ getChooserActivityLogger().logSharesheetDirectLoadComplete();
+
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter()
+ .completeServiceTargetLoading();
+ break;
+
+ default:
+ super.handleMessage(msg);
+ }
+ }
+ };
@Override
- public void startActivityAsCaller(Intent intent, @Nullable Bundle options,
- boolean ignoreTargetSecurity, int userId) {
- // We're dispatching intents that might be coming from legacy apps, so
- // (as in com.android.internal.app.ResolverActivity) exempt ourselves from death.
- StrictMode.disableDeathOnFileUriExposure();
+ protected void onCreate(Bundle savedInstanceState) {
+ final long intentReceivedTime = System.currentTimeMillis();
+ mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
+
+ getChooserActivityLogger().logSharesheetTriggered();
+ // This is the only place this value is being set. Effectively final.
+ mIsAppPredictorComponentAvailable = isAppPredictionServiceAvailable();
+
+ mIsSuccessfullySelected = false;
+ Intent intent = getIntent();
+ Parcelable targetParcelable = intent.getParcelableExtra(Intent.EXTRA_INTENT);
+ if (targetParcelable instanceof Uri) {
+ try {
+ targetParcelable = Intent.parseUri(targetParcelable.toString(),
+ Intent.URI_INTENT_SCHEME);
+ } catch (URISyntaxException ex) {
+ // doesn't parse as an intent; let the next test fail and error out
+ }
+ }
+
+ if (!(targetParcelable instanceof Intent)) {
+ Log.w("ChooserActivity", "Target is not an intent: " + targetParcelable);
+ finish();
+ super.onCreate(null);
+ return;
+ }
+ Intent target = (Intent) targetParcelable;
+ if (target != null) {
+ modifyTargetIntent(target);
+ }
+ Parcelable[] targetsParcelable
+ = intent.getParcelableArrayExtra(Intent.EXTRA_ALTERNATE_INTENTS);
+ if (targetsParcelable != null) {
+ final boolean offset = target == null;
+ Intent[] additionalTargets =
+ new Intent[offset ? targetsParcelable.length - 1 : targetsParcelable.length];
+ for (int i = 0; i < targetsParcelable.length; i++) {
+ if (!(targetsParcelable[i] instanceof Intent)) {
+ Log.w(TAG, "EXTRA_ALTERNATE_INTENTS array entry #" + i + " is not an Intent: "
+ + targetsParcelable[i]);
+ finish();
+ super.onCreate(null);
+ return;
+ }
+ final Intent additionalTarget = (Intent) targetsParcelable[i];
+ if (i == 0 && target == null) {
+ target = additionalTarget;
+ modifyTargetIntent(target);
+ } else {
+ additionalTargets[offset ? i - 1 : i] = additionalTarget;
+ modifyTargetIntent(additionalTarget);
+ }
+ }
+ setAdditionalTargets(additionalTargets);
+ }
+
+ mReplacementExtras = intent.getBundleExtra(Intent.EXTRA_REPLACEMENT_EXTRAS);
+
+ // Do not allow the title to be changed when sharing content
+ CharSequence title = null;
+ if (target != null) {
+ if (!isSendAction(target)) {
+ title = intent.getCharSequenceExtra(Intent.EXTRA_TITLE);
+ } else {
+ Log.w(TAG, "Ignoring intent's EXTRA_TITLE, deprecated in P. You may wish to set a"
+ + " preview title by using EXTRA_TITLE property of the wrapped"
+ + " EXTRA_INTENT.");
+ }
+ }
+
+ int defaultTitleRes = 0;
+ if (title == null) {
+ defaultTitleRes = com.android.internal.R.string.chooseActivity;
+ }
+
+ Parcelable[] pa = intent.getParcelableArrayExtra(Intent.EXTRA_INITIAL_INTENTS);
+ Intent[] initialIntents = null;
+ if (pa != null) {
+ int count = Math.min(pa.length, MAX_EXTRA_INITIAL_INTENTS);
+ initialIntents = new Intent[count];
+ for (int i = 0; i < count; i++) {
+ if (!(pa[i] instanceof Intent)) {
+ Log.w(TAG, "Initial intent #" + i + " not an Intent: " + pa[i]);
+ finish();
+ super.onCreate(null);
+ return;
+ }
+ final Intent in = (Intent) pa[i];
+ modifyTargetIntent(in);
+ initialIntents[i] = in;
+ }
+ }
+
+ mReferrerFillInIntent = new Intent().putExtra(Intent.EXTRA_REFERRER, getReferrer());
+
+ mChosenComponentSender = intent.getParcelableExtra(
+ Intent.EXTRA_CHOSEN_COMPONENT_INTENT_SENDER);
+ mRefinementIntentSender = intent.getParcelableExtra(
+ Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER);
+ setSafeForwardingMode(true);
+
+ mPinnedSharedPrefs = getPinnedSharedPrefs(this);
+
+ pa = intent.getParcelableArrayExtra(Intent.EXTRA_EXCLUDE_COMPONENTS);
+
+
+ // Exclude out Nearby from main list if chip is present, to avoid duplication
+ ComponentName nearbySharingComponent = getNearbySharingComponent();
+ boolean shouldFilterNearby = !shouldNearbyShareBeFirstInRankedRow()
+ && nearbySharingComponent != null;
+
+ if (pa != null) {
+ ComponentName[] names = new ComponentName[pa.length + (shouldFilterNearby ? 1 : 0)];
+ for (int i = 0; i < pa.length; i++) {
+ if (!(pa[i] instanceof ComponentName)) {
+ Log.w(TAG, "Filtered component #" + i + " not a ComponentName: " + pa[i]);
+ names = null;
+ break;
+ }
+ names[i] = (ComponentName) pa[i];
+ }
+ if (shouldFilterNearby) {
+ names[names.length - 1] = nearbySharingComponent;
+ }
+
+ mFilteredComponentNames = names;
+ } else if (shouldFilterNearby) {
+ mFilteredComponentNames = new ComponentName[1];
+ mFilteredComponentNames[0] = nearbySharingComponent;
+ }
+
+ pa = intent.getParcelableArrayExtra(Intent.EXTRA_CHOOSER_TARGETS);
+ if (pa != null) {
+ int count = Math.min(pa.length, MAX_EXTRA_CHOOSER_TARGETS);
+ ChooserTarget[] targets = new ChooserTarget[count];
+ for (int i = 0; i < count; i++) {
+ if (!(pa[i] instanceof ChooserTarget)) {
+ Log.w(TAG, "Chooser target #" + i + " not a ChooserTarget: " + pa[i]);
+ targets = null;
+ break;
+ }
+ targets[i] = (ChooserTarget) pa[i];
+ }
+ mCallerChooserTargets = targets;
+ }
+
+ mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
+ mShouldDisplayLandscape =
+ shouldDisplayLandscape(getResources().getConfiguration().orientation);
+ setRetainInOnStop(intent.getBooleanExtra(EXTRA_PRIVATE_RETAIN_IN_ON_STOP, false));
+ super.onCreate(savedInstanceState, target, title, defaultTitleRes, initialIntents,
+ null, false);
+
+ mChooserShownTime = System.currentTimeMillis();
+ final long systemCost = mChooserShownTime - intentReceivedTime;
+
+ getMetricsLogger().write(new LogMaker(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN)
+ .setSubtype(isWorkProfile() ? MetricsEvent.MANAGED_PROFILE :
+ MetricsEvent.PARENT_PROFILE)
+ .addTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE, target.getType())
+ .addTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS, systemCost));
+
+ if (mResolverDrawerLayout != null) {
+ mResolverDrawerLayout.addOnLayoutChangeListener(this::handleLayoutChange);
+
+ // expand/shrink direct share 4 -> 8 viewgroup
+ if (isSendAction(target)) {
+ mResolverDrawerLayout.setOnScrollChangeListener(this::handleScroll);
+ }
+
+ mResolverDrawerLayout.setOnCollapsedChangedListener(
+ new ResolverDrawerLayout.OnCollapsedChangedListener() {
+
+ // Only consider one expansion per activity creation
+ private boolean mWrittenOnce = false;
+
+ @Override
+ public void onCollapsedChanged(boolean isCollapsed) {
+ if (!isCollapsed && !mWrittenOnce) {
+ incrementNumSheetExpansions();
+ mWrittenOnce = true;
+ }
+ getChooserActivityLogger()
+ .logSharesheetExpansionChanged(isCollapsed);
+ }
+ });
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "System Time Cost is " + systemCost);
+ }
+
+ getChooserActivityLogger().logShareStarted(
+ FrameworkStatsLog.SHARESHEET_STARTED,
+ getReferrerPackageName(),
+ target.getType(),
+ mCallerChooserTargets == null ? 0 : mCallerChooserTargets.length,
+ initialIntents == null ? 0 : initialIntents.length,
+ isWorkProfile(),
+ findPreferredContentPreview(getTargetIntent(), getContentResolver()),
+ target.getAction()
+ );
+ mDirectShareShortcutInfoCache = new HashMap<>();
+
+ setEnterSharedElementCallback(new SharedElementCallback() {
+ @Override
+ public void onMapSharedElements(List<String> names, Map<String, View> sharedElements) {
+ if (mRemoveSharedElements) {
+ names.remove(FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
+ sharedElements.remove(FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
+ }
+ super.onMapSharedElements(names, sharedElements);
+ mRemoveSharedElements = false;
+ }
+ });
+ mEnterTransitionAnimationDelegate.postponeTransition();
+ }
+
+ @Override
+ protected int appliedThemeResId() {
+ return R.style.Theme_DeviceDefault_Chooser;
+ }
+
+ private AppPredictor setupAppPredictorForUser(UserHandle userHandle,
+ AppPredictor.Callback appPredictorCallback) {
+ AppPredictor appPredictor = getAppPredictorForDirectShareIfEnabled(userHandle);
+ if (appPredictor == null) {
+ return null;
+ }
+ mDirectShareAppTargetCache = new HashMap<>();
+ appPredictor.registerPredictionUpdates(this.getMainExecutor(), appPredictorCallback);
+ return appPredictor;
+ }
+
+ private AppPredictor.Callback createAppPredictorCallback(
+ ChooserListAdapter chooserListAdapter) {
+ return resultList -> {
+ if (isFinishing() || isDestroyed()) {
+ return;
+ }
+ if (chooserListAdapter.getCount() == 0) {
+ return;
+ }
+ if (resultList.isEmpty()
+ && shouldQueryShortcutManager(chooserListAdapter.getUserHandle())) {
+ // APS may be disabled, so try querying targets ourselves.
+ queryDirectShareTargets(chooserListAdapter, true);
+ return;
+ }
+ final List<ShortcutManager.ShareShortcutInfo> shareShortcutInfos =
+ new ArrayList<>();
+
+ List<AppTarget> shortcutResults = new ArrayList<>();
+ for (AppTarget appTarget : resultList) {
+ if (appTarget.getShortcutInfo() == null) {
+ continue;
+ }
+ shortcutResults.add(appTarget);
+ }
+ resultList = shortcutResults;
+ for (AppTarget appTarget : resultList) {
+ shareShortcutInfos.add(new ShortcutManager.ShareShortcutInfo(
+ appTarget.getShortcutInfo(),
+ new ComponentName(
+ appTarget.getPackageName(), appTarget.getClassName())));
+ }
+ sendShareShortcutInfoList(shareShortcutInfos, chooserListAdapter, resultList,
+ chooserListAdapter.getUserHandle());
+ };
+ }
+
+ static SharedPreferences getPinnedSharedPrefs(Context context) {
+ // The code below is because in the android:ui process, no one can hear you scream.
+ // The package info in the context isn't initialized in the way it is for normal apps,
+ // so the standard, name-based context.getSharedPreferences doesn't work. Instead, we
+ // build the path manually below using the same policy that appears in ContextImpl.
+ // This fails silently under the hood if there's a problem, so if we find ourselves in
+ // the case where we don't have access to credential encrypted storage we just won't
+ // have our pinned target info.
+ final File prefsFile = new File(new File(
+ Environment.getDataUserCePackageDirectory(StorageManager.UUID_PRIVATE_INTERNAL,
+ context.getUserId(), context.getPackageName()),
+ "shared_prefs"),
+ PINNED_SHARED_PREFS_NAME + ".xml");
+ return context.getSharedPreferences(prefsFile, MODE_PRIVATE);
+ }
+
+ @Override
+ protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter(
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed) {
+ if (shouldShowTabs()) {
+ mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForTwoProfiles(
+ initialIntents, rList, filterLastUsed);
+ } else {
+ mChooserMultiProfilePagerAdapter = createChooserMultiProfilePagerAdapterForOneProfile(
+ initialIntents, rList, filterLastUsed);
+ }
+ return mChooserMultiProfilePagerAdapter;
+ }
+
+ private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForOneProfile(
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed) {
+ ChooserGridAdapter adapter = createChooserGridAdapter(
+ /* context */ this,
+ /* payloadIntents */ mIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ /* userHandle */ UserHandle.of(UserHandle.myUserId()));
+ return new ChooserMultiProfilePagerAdapter(
+ /* context */ this,
+ adapter,
+ getPersonalProfileUserHandle(),
+ /* workProfileUserHandle= */ null,
+ isSendAction(getTargetIntent()), mMaxTargetsPerRow);
+ }
+
+ private ChooserMultiProfilePagerAdapter createChooserMultiProfilePagerAdapterForTwoProfiles(
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed) {
+ int selectedProfile = findSelectedProfile();
+ ChooserGridAdapter personalAdapter = createChooserGridAdapter(
+ /* context */ this,
+ /* payloadIntents */ mIntents,
+ selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
+ rList,
+ filterLastUsed,
+ /* userHandle */ getPersonalProfileUserHandle());
+ ChooserGridAdapter workAdapter = createChooserGridAdapter(
+ /* context */ this,
+ /* payloadIntents */ mIntents,
+ selectedProfile == PROFILE_WORK ? initialIntents : null,
+ rList,
+ filterLastUsed,
+ /* userHandle */ getWorkProfileUserHandle());
+ return new ChooserMultiProfilePagerAdapter(
+ /* context */ this,
+ personalAdapter,
+ workAdapter,
+ selectedProfile,
+ getPersonalProfileUserHandle(),
+ getWorkProfileUserHandle(),
+ isSendAction(getTargetIntent()), mMaxTargetsPerRow);
+ }
+
+ private int findSelectedProfile() {
+ int selectedProfile = getSelectedProfileExtra();
+ if (selectedProfile == -1) {
+ selectedProfile = getProfileForUser(getUser());
+ }
+ return selectedProfile;
+ }
+
+ @Override
+ protected boolean postRebuildList(boolean rebuildCompleted) {
+ updateStickyContentPreview();
+ if (shouldShowStickyContentPreview()
+ || mChooserMultiProfilePagerAdapter
+ .getCurrentRootAdapter().getSystemRowCount() != 0) {
+ logActionShareWithPreview();
+ }
+ return postRebuildListInternal(rebuildCompleted);
+ }
+
+ /**
+ * Returns true if app prediction service is defined and the component exists on device.
+ */
+ private boolean isAppPredictionServiceAvailable() {
+ return getPackageManager().getAppPredictionServicePackageName() != null;
+ }
+
+ /**
+ * Check if the profile currently used is a work profile.
+ * @return true if it is work profile, false if it is parent profile (or no work profile is
+ * set up)
+ */
+ protected boolean isWorkProfile() {
+ return getSystemService(UserManager.class)
+ .getUserInfo(UserHandle.myUserId()).isManagedProfile();
+ }
+
+ @Override
+ protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) {
+ return new PackageMonitor() {
+ @Override
+ public void onSomePackagesChanged() {
+ handlePackagesChanged(listAdapter);
+ }
+ };
+ }
+
+ /**
+ * Update UI to reflect changes in data.
+ */
+ public void handlePackagesChanged() {
+ handlePackagesChanged(/* listAdapter */ null);
+ }
+
+ /**
+ * Update UI to reflect changes in data.
+ * <p>If {@code listAdapter} is {@code null}, both profile list adapters are updated if
+ * available.
+ */
+ private void handlePackagesChanged(@Nullable ResolverListAdapter listAdapter) {
+ // Refresh pinned items
+ mPinnedSharedPrefs = getPinnedSharedPrefs(this);
+ if (listAdapter == null) {
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
+ if (mChooserMultiProfilePagerAdapter.getCount() > 1) {
+ mChooserMultiProfilePagerAdapter.getInactiveListAdapter().handlePackagesChanged();
+ }
+ } else {
+ listAdapter.handlePackagesChanged();
+ }
+ updateProfileViewButton();
+ }
+
+ private void onCopyButtonClicked(View v) {
+ 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());
+
+ // Log share completion via copy
+ LogMaker targetLogMaker = new LogMaker(
+ MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SYSTEM_TARGET).setSubtype(1);
+ getMetricsLogger().write(targetLogMaker);
+ getChooserActivityLogger().logShareTargetSelected(
+ SELECTION_TYPE_COPY,
+ "",
+ -1,
+ false);
+
+ setResult(RESULT_OK);
+ finish();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ Log.d(TAG, "onResume: " + getComponentName().flattenToShortString());
+ maybeCancelFinishAnimation();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ if (viewPager.isLayoutRtl()) {
+ mMultiProfilePagerAdapter.setupViewPager(viewPager);
+ }
+
+ mShouldDisplayLandscape = shouldDisplayLandscape(newConfig.orientation);
+ mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
+ mChooserMultiProfilePagerAdapter.setMaxTargetsPerRow(mMaxTargetsPerRow);
+ adjustPreviewWidth(newConfig.orientation, null);
+ updateStickyContentPreview();
+ updateTabPadding();
+ }
+
+ private boolean shouldDisplayLandscape(int orientation) {
+ // Sharesheet fixes the # of items per row and therefore can not correctly lay out
+ // when in the restricted size of multi-window mode. In the future, would be nice
+ // to use minimum dp size requirements instead
+ return orientation == Configuration.ORIENTATION_LANDSCAPE && !isInMultiWindowMode();
+ }
+
+ private void adjustPreviewWidth(int orientation, View parent) {
+ int width = -1;
+ if (mShouldDisplayLandscape) {
+ width = getResources().getDimensionPixelSize(R.dimen.chooser_preview_width);
+ }
+
+ parent = parent == null ? getWindow().getDecorView() : parent;
+
+ updateLayoutWidth(com.android.internal.R.id.content_preview_text_layout, width, parent);
+ updateLayoutWidth(com.android.internal.R.id.content_preview_title_layout, width, parent);
+ updateLayoutWidth(com.android.internal.R.id.content_preview_file_layout, width, parent);
+ }
+
+ private void updateTabPadding() {
+ if (shouldShowTabs()) {
+ View tabs = findViewById(com.android.internal.R.id.tabs);
+ float iconSize = getResources().getDimension(R.dimen.chooser_icon_size);
+ // The entire width consists of icons or padding. Divide the item padding in half to get
+ // paddingHorizontal.
+ float padding = (tabs.getWidth() - mMaxTargetsPerRow * iconSize)
+ / mMaxTargetsPerRow / 2;
+ // Subtract the margin the buttons already have.
+ padding -= getResources().getDimension(R.dimen.resolver_profile_tab_margin);
+ tabs.setPadding((int) padding, 0, (int) padding, 0);
+ }
+ }
+
+ private void updateLayoutWidth(int layoutResourceId, int width, View parent) {
+ View view = parent.findViewById(layoutResourceId);
+ if (view != null && view.getLayoutParams() != null) {
+ LayoutParams params = view.getLayoutParams();
+ params.width = width;
+ view.setLayoutParams(params);
+ }
+ }
+
+ private ViewGroup createContentPreviewView(ViewGroup parent) {
+ Intent targetIntent = getTargetIntent();
+ int previewType = findPreferredContentPreview(targetIntent, getContentResolver());
+ return displayContentPreview(previewType, targetIntent, getLayoutInflater(), parent);
+ }
+
+ @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);
+ 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 = new DisplayResolveInfo(
+ originalIntent, ri, getString(com.android.internal.R.string.screenshot_edit), "", resolveIntent, null);
+ dri.setDisplayIcon(getDrawable(com.android.internal.R.drawable.ic_screenshot_edit));
+ 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 = new DisplayResolveInfo(
+ originalIntent, ri, name, "", resolveIntent, null);
+ dri.setDisplayIcon(icon);
+ return dri;
+ }
+
+ private Button createActionButton(Drawable icon, CharSequence title, View.OnClickListener r) {
+ Button b = (Button) LayoutInflater.from(this).inflate(R.layout.chooser_action_button, null);
+ if (icon != null) {
+ final int size = getResources()
+ .getDimensionPixelSize(R.dimen.chooser_action_button_icon_size);
+ icon.setBounds(0, 0, size, size);
+ b.setCompoundDrawablesRelative(icon, null, null, null);
+ }
+ b.setText(title);
+ b.setOnClickListener(r);
+ return b;
+ }
+
+ private Button createCopyButton() {
+ final Button b = createActionButton(
+ getDrawable(com.android.internal.R.drawable.ic_menu_copy_material),
+ getString(com.android.internal.R.string.copy), this::onCopyButtonClicked);
+ b.setId(com.android.internal.R.id.chooser_copy_button);
+ return b;
+ }
+
+ private @Nullable Button createNearbyButton(Intent originalIntent) {
+ final TargetInfo ti = getNearbySharingTarget(originalIntent);
+ if (ti == null) return null;
+
+ final Button b = createActionButton(
+ ti.getDisplayIcon(this),
+ ti.getDisplayLabel(),
+ (View unused) -> {
+ // Log share completion via nearby
+ getChooserActivityLogger().logShareTargetSelected(
+ SELECTION_TYPE_NEARBY,
+ "",
+ -1,
+ false);
+ // Action bar is user-independent, always start as primary
+ safelyStartActivityAsUser(ti, getPersonalProfileUserHandle());
+ finish();
+ }
+ );
+ b.setId(com.android.internal.R.id.chooser_nearby_button);
+ return b;
+ }
+
+ private @Nullable Button createEditButton(Intent originalIntent) {
+ final TargetInfo ti = getEditSharingTarget(originalIntent);
+ if (ti == null) return null;
+
+ final Button b = createActionButton(
+ ti.getDisplayIcon(this),
+ ti.getDisplayLabel(),
+ (View unused) -> {
+ // Log share completion via edit
+ getChooserActivityLogger().logShareTargetSelected(
+ SELECTION_TYPE_EDIT,
+ "",
+ -1,
+ false);
+ 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();
+ }
+ }
+ );
+ b.setId(com.android.internal.R.id.chooser_edit_button);
+ return b;
+ }
+
+ @Nullable
+ private View getFirstVisibleImgPreviewView() {
+ View firstImage = findViewById(com.android.internal.R.id.content_preview_image_1_large);
+ return firstImage != null && firstImage.isVisibleToUser() ? firstImage : null;
+ }
+
+ private void addActionButton(ViewGroup parent, Button b) {
+ if (b == null) return;
+ final ViewGroup.MarginLayoutParams lp = new ViewGroup.MarginLayoutParams(
+ LayoutParams.WRAP_CONTENT,
+ LayoutParams.WRAP_CONTENT
+ );
+ final int gap = getResources().getDimensionPixelSize(R.dimen.resolver_icon_margin) / 2;
+ lp.setMarginsRelative(gap, 0, gap, 0);
+ parent.addView(b, lp);
+ }
+
+ private ViewGroup displayContentPreview(@ContentPreviewType int previewType,
+ Intent targetIntent, LayoutInflater layoutInflater, ViewGroup parent) {
+ ViewGroup layout = null;
+
+ switch (previewType) {
+ case CONTENT_PREVIEW_TEXT:
+ layout = displayTextContentPreview(targetIntent, layoutInflater, parent);
+ break;
+ case CONTENT_PREVIEW_IMAGE:
+ layout = displayImageContentPreview(targetIntent, layoutInflater, parent);
+ break;
+ case CONTENT_PREVIEW_FILE:
+ layout = displayFileContentPreview(targetIntent, layoutInflater, parent);
+ break;
+ default:
+ Log.e(TAG, "Unexpected content preview type: " + previewType);
+ }
+
+ if (layout != null) {
+ adjustPreviewWidth(getResources().getConfiguration().orientation, layout);
+ }
+ if (previewType != CONTENT_PREVIEW_IMAGE) {
+ mEnterTransitionAnimationDelegate.markImagePreviewReady();
+ }
+
+ return layout;
+ }
+
+ private ViewGroup displayTextContentPreview(Intent targetIntent, LayoutInflater layoutInflater,
+ ViewGroup parent) {
+ ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+ R.layout.chooser_grid_preview_text, parent, false);
+
+ final ViewGroup actionRow =
+ (ViewGroup) contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
+ addActionButton(actionRow, createCopyButton());
+ if (shouldNearbyShareBeIncludedAsActionButton()) {
+ addActionButton(actionRow, createNearbyButton(targetIntent));
+ }
+
+ CharSequence sharingText = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT);
+ if (sharingText == null) {
+ contentPreviewLayout.findViewById(com.android.internal.R.id.content_preview_text_layout).setVisibility(
+ View.GONE);
+ } else {
+ TextView textView = contentPreviewLayout.findViewById(com.android.internal.R.id.content_preview_text);
+ textView.setText(sharingText);
+ }
+
+ String previewTitle = targetIntent.getStringExtra(Intent.EXTRA_TITLE);
+ if (TextUtils.isEmpty(previewTitle)) {
+ contentPreviewLayout.findViewById(com.android.internal.R.id.content_preview_title_layout).setVisibility(
+ View.GONE);
+ } else {
+ TextView previewTitleView = contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_title);
+ previewTitleView.setText(previewTitle);
+
+ ClipData previewData = targetIntent.getClipData();
+ Uri previewThumbnail = null;
+ if (previewData != null) {
+ if (previewData.getItemCount() > 0) {
+ ClipData.Item previewDataItem = previewData.getItemAt(0);
+ previewThumbnail = previewDataItem.getUri();
+ }
+ }
+
+ ImageView previewThumbnailView = contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_thumbnail);
+ if (previewThumbnail == null) {
+ previewThumbnailView.setVisibility(View.GONE);
+ } else {
+ mPreviewCoord = new ContentPreviewCoordinator(contentPreviewLayout, false);
+ mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_thumbnail, previewThumbnail, 0);
+ }
+ }
+
+ return contentPreviewLayout;
+ }
+
+ private ViewGroup displayImageContentPreview(Intent targetIntent, LayoutInflater layoutInflater,
+ ViewGroup parent) {
+ ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+ R.layout.chooser_grid_preview_image, parent, false);
+ ViewGroup imagePreview = contentPreviewLayout.findViewById(com.android.internal.R.id.content_preview_image_area);
+
+ final ViewGroup actionRow =
+ (ViewGroup) contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
+ //TODO: addActionButton(actionRow, createCopyButton());
+ if (shouldNearbyShareBeIncludedAsActionButton()) {
+ addActionButton(actionRow, createNearbyButton(targetIntent));
+ }
+ addActionButton(actionRow, createEditButton(targetIntent));
+
+ mPreviewCoord = new ContentPreviewCoordinator(contentPreviewLayout, false);
+
+ String action = targetIntent.getAction();
+ if (Intent.ACTION_SEND.equals(action)) {
+ Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+ imagePreview.findViewById(com.android.internal.R.id.content_preview_image_1_large)
+ .setTransitionName(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
+ mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_image_1_large, uri, 0);
+ } else {
+ ContentResolver resolver = getContentResolver();
+
+ List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ List<Uri> imageUris = new ArrayList<>();
+ for (Uri uri : uris) {
+ if (isImageType(resolver.getType(uri))) {
+ imageUris.add(uri);
+ }
+ }
+
+ if (imageUris.size() == 0) {
+ Log.i(TAG, "Attempted to display image preview area with zero"
+ + " available images detected in EXTRA_STREAM list");
+ imagePreview.setVisibility(View.GONE);
+ return contentPreviewLayout;
+ }
+
+ imagePreview.findViewById(com.android.internal.R.id.content_preview_image_1_large)
+ .setTransitionName(ChooserActivity.FIRST_IMAGE_PREVIEW_TRANSITION_NAME);
+ mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_image_1_large, imageUris.get(0), 0);
+
+ if (imageUris.size() == 2) {
+ mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_image_2_large,
+ imageUris.get(1), 0);
+ } else if (imageUris.size() > 2) {
+ mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_image_2_small,
+ imageUris.get(1), 0);
+ mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_image_3_small,
+ imageUris.get(2), imageUris.size() - 3);
+ }
+ }
+
+ return contentPreviewLayout;
+ }
+
+ private static class FileInfo {
+ public final String name;
+ public final boolean hasThumbnail;
+
+ FileInfo(String name, boolean hasThumbnail) {
+ this.name = name;
+ this.hasThumbnail = hasThumbnail;
+ }
+ }
+
+ /**
+ * Wrapping the ContentResolver call to expose for easier mocking,
+ * and to avoid mocking Android core classes.
+ */
+ @VisibleForTesting
+ public Cursor queryResolver(ContentResolver resolver, Uri uri) {
+ return resolver.query(uri, null, null, null, null);
+ }
+
+ private FileInfo extractFileInfo(Uri uri, ContentResolver resolver) {
+ String fileName = null;
+ boolean hasThumbnail = false;
+
+ try (Cursor cursor = queryResolver(resolver, uri)) {
+ if (cursor != null && cursor.getCount() > 0) {
+ int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
+ int titleIndex = cursor.getColumnIndex(Downloads.Impl.COLUMN_TITLE);
+ int flagsIndex = cursor.getColumnIndex(DocumentsContract.Document.COLUMN_FLAGS);
+
+ cursor.moveToFirst();
+ if (nameIndex != -1) {
+ fileName = cursor.getString(nameIndex);
+ } else if (titleIndex != -1) {
+ fileName = cursor.getString(titleIndex);
+ }
+
+ if (flagsIndex != -1) {
+ hasThumbnail = (cursor.getInt(flagsIndex)
+ & DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL) != 0;
+ }
+ }
+ } catch (SecurityException | NullPointerException e) {
+ logContentPreviewWarning(uri);
+ }
+
+ if (TextUtils.isEmpty(fileName)) {
+ fileName = uri.getPath();
+ int index = fileName.lastIndexOf('/');
+ if (index != -1) {
+ fileName = fileName.substring(index + 1);
+ }
+ }
+
+ return new FileInfo(fileName, hasThumbnail);
+ }
+
+ private void logContentPreviewWarning(Uri uri) {
+ // The ContentResolver already logs the exception. Log something more informative.
+ Log.w(TAG, "Could not load (" + uri.toString() + ") thumbnail/name for preview. If "
+ + "desired, consider using Intent#createChooser to launch the ChooserActivity, "
+ + "and set your Intent's clipData and flags in accordance with that method's "
+ + "documentation");
+ }
+
+ private ViewGroup displayFileContentPreview(Intent targetIntent, LayoutInflater layoutInflater,
+ ViewGroup parent) {
+
+ ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate(
+ R.layout.chooser_grid_preview_file, parent, false);
+
+ final ViewGroup actionRow =
+ (ViewGroup) contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row);
+ //TODO(b/120417119): addActionButton(actionRow, createCopyButton());
+ if (shouldNearbyShareBeIncludedAsActionButton()) {
+ addActionButton(actionRow, createNearbyButton(targetIntent));
+ }
+
+ String action = targetIntent.getAction();
+ if (Intent.ACTION_SEND.equals(action)) {
+ Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+ loadFileUriIntoView(uri, contentPreviewLayout);
+ } else {
+ List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ int uriCount = uris.size();
+
+ if (uriCount == 0) {
+ contentPreviewLayout.setVisibility(View.GONE);
+ Log.i(TAG,
+ "Appears to be no uris available in EXTRA_STREAM, removing "
+ + "preview area");
+ return contentPreviewLayout;
+ } else if (uriCount == 1) {
+ loadFileUriIntoView(uris.get(0), contentPreviewLayout);
+ } else {
+ FileInfo fileInfo = extractFileInfo(uris.get(0), getContentResolver());
+ int remUriCount = uriCount - 1;
+ Map<String, Object> arguments = new HashMap<>();
+ arguments.put(PLURALS_COUNT, remUriCount);
+ arguments.put(PLURALS_FILE_NAME, fileInfo.name);
+ String fileName = PluralsMessageFormatter.format(
+ getResources(),
+ arguments,
+ R.string.file_count);
+
+ TextView fileNameView = contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_filename);
+ fileNameView.setText(fileName);
+
+ View thumbnailView = contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_file_thumbnail);
+ thumbnailView.setVisibility(View.GONE);
+
+ ImageView fileIconView = contentPreviewLayout.findViewById(
+ com.android.internal.R.id.content_preview_file_icon);
+ fileIconView.setVisibility(View.VISIBLE);
+ fileIconView.setImageResource(R.drawable.ic_file_copy);
+ }
+ }
+
+ return contentPreviewLayout;
+ }
+
+ private void loadFileUriIntoView(final Uri uri, final View parent) {
+ FileInfo fileInfo = extractFileInfo(uri, getContentResolver());
+
+ TextView fileNameView = parent.findViewById(com.android.internal.R.id.content_preview_filename);
+ fileNameView.setText(fileInfo.name);
+
+ if (fileInfo.hasThumbnail) {
+ mPreviewCoord = new ContentPreviewCoordinator(parent, false);
+ mPreviewCoord.loadUriIntoView(com.android.internal.R.id.content_preview_file_thumbnail, uri, 0);
+ } else {
+ View thumbnailView = parent.findViewById(com.android.internal.R.id.content_preview_file_thumbnail);
+ thumbnailView.setVisibility(View.GONE);
+
+ ImageView fileIconView = parent.findViewById(com.android.internal.R.id.content_preview_file_icon);
+ fileIconView.setVisibility(View.VISIBLE);
+ fileIconView.setImageResource(R.drawable.chooser_file_generic);
+ }
+ }
+
+ @VisibleForTesting
+ protected boolean isImageType(String mimeType) {
+ return mimeType != null && mimeType.startsWith("image/");
+ }
+
+ @ContentPreviewType
+ private int findPreferredContentPreview(Uri uri, ContentResolver resolver) {
+ if (uri == null) {
+ return CONTENT_PREVIEW_TEXT;
+ }
+
+ String mimeType = resolver.getType(uri);
+ return isImageType(mimeType) ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE;
+ }
+
+ /**
+ * In {@link android.content.Intent#getType}, the app may specify a very general
+ * mime-type that broadly covers all data being shared, such as {@literal *}/*
+ * when sending an image and text. We therefore should inspect each item for the
+ * the preferred type, in order of IMAGE, FILE, TEXT.
+ */
+ @ContentPreviewType
+ private int findPreferredContentPreview(Intent targetIntent, ContentResolver resolver) {
+ String action = targetIntent.getAction();
+ if (Intent.ACTION_SEND.equals(action)) {
+ Uri uri = targetIntent.getParcelableExtra(Intent.EXTRA_STREAM);
+ return findPreferredContentPreview(uri, resolver);
+ } else if (Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+ List<Uri> uris = targetIntent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
+ if (uris == null || uris.isEmpty()) {
+ return CONTENT_PREVIEW_TEXT;
+ }
+
+ for (Uri uri : uris) {
+ // Defaulting to file preview when there are mixed image/file types is
+ // preferable, as it shows the user the correct number of items being shared
+ if (findPreferredContentPreview(uri, resolver) == CONTENT_PREVIEW_FILE) {
+ return CONTENT_PREVIEW_FILE;
+ }
+ }
+
+ return CONTENT_PREVIEW_IMAGE;
+ }
+
+ return CONTENT_PREVIEW_TEXT;
+ }
+
+ private int getNumSheetExpansions() {
+ return getPreferences(Context.MODE_PRIVATE).getInt(PREF_NUM_SHEET_EXPANSIONS, 0);
+ }
+
+ private void incrementNumSheetExpansions() {
+ getPreferences(Context.MODE_PRIVATE).edit().putInt(PREF_NUM_SHEET_EXPANSIONS,
+ getNumSheetExpansions() + 1).apply();
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (maybeCancelFinishAnimation()) {
+ finish();
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ if (isFinishing()) {
+ mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET);
+ }
+
+ if (mRefinementResultReceiver != null) {
+ mRefinementResultReceiver.destroy();
+ mRefinementResultReceiver = null;
+ }
+ mChooserHandler.removeAllMessages();
+
+ if (mPreviewCoord != null) mPreviewCoord.cancelLoads();
+
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter().destroyAppPredictor();
+ if (mChooserMultiProfilePagerAdapter.getInactiveListAdapter() != null) {
+ mChooserMultiProfilePagerAdapter.getInactiveListAdapter().destroyAppPredictor();
+ }
+ mPersonalAppPredictor = null;
+ mWorkAppPredictor = null;
+ }
+
+ @Override // ResolverListCommunicator
+ public Intent getReplacementIntent(ActivityInfo aInfo, Intent defIntent) {
+ Intent result = defIntent;
+ if (mReplacementExtras != null) {
+ final Bundle replExtras = mReplacementExtras.getBundle(aInfo.packageName);
+ if (replExtras != null) {
+ result = new Intent(defIntent);
+ result.putExtras(replExtras);
+ }
+ }
+ if (aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_PARENT)
+ || aInfo.name.equals(IntentForwarderActivity.FORWARD_INTENT_TO_MANAGED_PROFILE)) {
+ result = Intent.createChooser(result,
+ getIntent().getCharSequenceExtra(Intent.EXTRA_TITLE));
+
+ // Don't auto-launch single intents if the intent is being forwarded. This is done
+ // because automatically launching a resolving application as a response to the user
+ // action of switching accounts is pretty unexpected.
+ result.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false);
+ }
+ return result;
+ }
+
+ @Override
+ public void onActivityStarted(TargetInfo cti) {
+ if (mChosenComponentSender != null) {
+ final ComponentName target = cti.getResolvedComponentName();
+ if (target != null) {
+ final Intent fillIn = new Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, target);
+ try {
+ mChosenComponentSender.sendIntent(this, Activity.RESULT_OK, fillIn, null, null);
+ } catch (IntentSender.SendIntentException e) {
+ Slog.e(TAG, "Unable to launch supplied IntentSender to report "
+ + "the chosen component: " + e);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void addUseDifferentAppLabelIfNecessary(ResolverListAdapter adapter) {
+ if (mCallerChooserTargets != null && mCallerChooserTargets.length > 0) {
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter().addServiceResults(
+ /* origTarget */ null,
+ Lists.newArrayList(mCallerChooserTargets),
+ TARGET_TYPE_DEFAULT,
+ /* directShareShortcutInfoCache */ null);
+ }
+ }
+
+ @Override
+ public int getLayoutResource() {
+ return R.layout.chooser_grid;
+ }
+
+ @Override // ResolverListCommunicator
+ public boolean shouldGetActivityMetadata() {
+ return true;
+ }
+
+ @Override
+ public boolean shouldAutoLaunchSingleChoice(TargetInfo target) {
+ // Note that this is only safe because the Intent handled by the ChooserActivity is
+ // guaranteed to contain no extras unknown to the local ClassLoader. That is why this
+ // method can not be replaced in the ResolverActivity whole hog.
+ if (!super.shouldAutoLaunchSingleChoice(target)) {
+ return false;
+ }
+
+ return getIntent().getBooleanExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, true);
+ }
+
+ private void showTargetDetails(TargetInfo targetInfo) {
+ if (targetInfo == null) return;
+
+ ArrayList<DisplayResolveInfo> targetList;
+ ChooserTargetActionsDialogFragment fragment = new ChooserTargetActionsDialogFragment();
+ Bundle bundle = new Bundle();
+
+ if (targetInfo instanceof SelectableTargetInfo) {
+ SelectableTargetInfo selectableTargetInfo = (SelectableTargetInfo) targetInfo;
+ if (selectableTargetInfo.getDisplayResolveInfo() == null
+ || selectableTargetInfo.getChooserTarget() == null) {
+ Log.e(TAG, "displayResolveInfo or chooserTarget in selectableTargetInfo are null");
+ return;
+ }
+ targetList = new ArrayList<>();
+ targetList.add(selectableTargetInfo.getDisplayResolveInfo());
+ bundle.putString(ChooserTargetActionsDialogFragment.SHORTCUT_ID_KEY,
+ selectableTargetInfo.getChooserTarget().getIntentExtras().getString(
+ Intent.EXTRA_SHORTCUT_ID));
+ bundle.putBoolean(ChooserTargetActionsDialogFragment.IS_SHORTCUT_PINNED_KEY,
+ selectableTargetInfo.isPinned());
+ bundle.putParcelable(ChooserTargetActionsDialogFragment.INTENT_FILTER_KEY,
+ getTargetIntentFilter());
+ if (selectableTargetInfo.getDisplayLabel() != null) {
+ bundle.putString(ChooserTargetActionsDialogFragment.SHORTCUT_TITLE_KEY,
+ selectableTargetInfo.getDisplayLabel().toString());
+ }
+ } else if (targetInfo instanceof MultiDisplayResolveInfo) {
+ // For multiple targets, include info on all targets
+ MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo;
+ targetList = mti.getTargets();
+ } else {
+ targetList = new ArrayList<DisplayResolveInfo>();
+ targetList.add((DisplayResolveInfo) targetInfo);
+ }
+ bundle.putParcelable(ChooserTargetActionsDialogFragment.USER_HANDLE_KEY,
+ mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
+ bundle.putParcelableArrayList(ChooserTargetActionsDialogFragment.TARGET_INFOS_KEY,
+ targetList);
+ fragment.setArguments(bundle);
+
+ fragment.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG);
+ }
+
+ private void modifyTargetIntent(Intent in) {
+ if (isSendAction(in)) {
+ in.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT |
+ Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+ }
+ }
+
+ @Override
+ protected boolean onTargetSelected(TargetInfo target, boolean alwaysCheck) {
+ if (mRefinementIntentSender != 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 {
+ mRefinementIntentSender.sendIntent(this, 0, fillIn, null, null);
+ return false;
+ } catch (SendIntentException e) {
+ Log.e(TAG, "Refinement IntentSender failed to send", e);
+ }
+ }
+ }
+ updateModelAndChooserCounts(target);
+ return super.onTargetSelected(target, alwaysCheck);
+ }
+
+ @Override
+ public void startSelected(int which, boolean always, boolean filtered) {
+ ChooserListAdapter currentListAdapter =
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter();
+ TargetInfo targetInfo = currentListAdapter
+ .targetInfoForPosition(which, filtered);
+ if (targetInfo != null && targetInfo instanceof NotSelectableTargetInfo) {
+ return;
+ }
+
+ final long selectionCost = System.currentTimeMillis() - mChooserShownTime;
+
+ if (targetInfo instanceof MultiDisplayResolveInfo) {
+ MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo;
+ if (!mti.hasSelected()) {
+ ChooserStackedAppDialogFragment f = new ChooserStackedAppDialogFragment();
+ Bundle b = new Bundle();
+ b.putParcelable(ChooserTargetActionsDialogFragment.USER_HANDLE_KEY,
+ mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
+ b.putObject(ChooserStackedAppDialogFragment.MULTI_DRI_KEY,
+ mti);
+ b.putInt(ChooserStackedAppDialogFragment.WHICH_KEY, which);
+ f.setArguments(b);
+
+ f.show(getFragmentManager(), TARGET_DETAILS_FRAGMENT_TAG);
+ return;
+ }
+ }
+
+ super.startSelected(which, always, filtered);
+
+ if (currentListAdapter.getCount() > 0) {
+ // Log the index of which type of target the user picked.
+ // Lower values mean the ranking was better.
+ int cat = 0;
+ int value = which;
+ int directTargetAlsoRanked = -1;
+ int numCallerProvided = 0;
+ HashedStringCache.HashResult directTargetHashed = null;
+ switch (currentListAdapter.getPositionTargetType(which)) {
+ case ChooserListAdapter.TARGET_SERVICE:
+ cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET;
+ // Log the package name + target name to answer the question if most users
+ // share to mostly the same person or to a bunch of different people.
+ ChooserTarget target = currentListAdapter.getChooserTargetForValue(value);
+ directTargetHashed = HashedStringCache.getInstance().hashString(
+ this,
+ TAG,
+ target.getComponentName().getPackageName()
+ + target.getTitle().toString(),
+ mMaxHashSaltDays);
+ SelectableTargetInfo selectableTargetInfo = (SelectableTargetInfo) targetInfo;
+ directTargetAlsoRanked = getRankedPosition(selectableTargetInfo);
+
+ if (mCallerChooserTargets != null) {
+ numCallerProvided = mCallerChooserTargets.length;
+ }
+ getChooserActivityLogger().logShareTargetSelected(
+ SELECTION_TYPE_SERVICE,
+ targetInfo.getResolveInfo().activityInfo.processName,
+ value,
+ selectableTargetInfo.isPinned()
+ );
+ break;
+ case ChooserListAdapter.TARGET_CALLER:
+ case ChooserListAdapter.TARGET_STANDARD:
+ cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET;
+ value -= currentListAdapter.getSurfacedTargetInfo().size();
+ numCallerProvided = currentListAdapter.getCallerTargetCount();
+ getChooserActivityLogger().logShareTargetSelected(
+ SELECTION_TYPE_APP,
+ targetInfo.getResolveInfo().activityInfo.processName,
+ value,
+ targetInfo.isPinned()
+ );
+ break;
+ case ChooserListAdapter.TARGET_STANDARD_AZ:
+ // A-Z targets are unranked standard targets; we use -1 to mark that they
+ // are from the alphabetical pool.
+ value = -1;
+ cat = MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET;
+ getChooserActivityLogger().logShareTargetSelected(
+ SELECTION_TYPE_STANDARD,
+ targetInfo.getResolveInfo().activityInfo.processName,
+ value,
+ false
+ );
+ break;
+ }
+
+ if (cat != 0) {
+ LogMaker targetLogMaker = new LogMaker(cat).setSubtype(value);
+ if (directTargetHashed != null) {
+ targetLogMaker.addTaggedData(
+ MetricsEvent.FIELD_HASHED_TARGET_NAME, directTargetHashed.hashedString);
+ targetLogMaker.addTaggedData(
+ MetricsEvent.FIELD_HASHED_TARGET_SALT_GEN,
+ directTargetHashed.saltGeneration);
+ targetLogMaker.addTaggedData(MetricsEvent.FIELD_RANKED_POSITION,
+ directTargetAlsoRanked);
+ }
+ targetLogMaker.addTaggedData(MetricsEvent.FIELD_IS_CATEGORY_USED,
+ numCallerProvided);
+ getMetricsLogger().write(targetLogMaker);
+ }
+
+ if (mIsSuccessfullySelected) {
+ if (DEBUG) {
+ Log.d(TAG, "User Selection Time Cost is " + selectionCost);
+ Log.d(TAG, "position of selected app/service/caller is " +
+ Integer.toString(value));
+ }
+ MetricsLogger.histogram(null, "user_selection_cost_for_smart_sharing",
+ (int) selectionCost);
+ MetricsLogger.histogram(null, "app_position_for_smart_sharing", value);
+ }
+ }
+ }
+
+ private int getRankedPosition(SelectableTargetInfo targetInfo) {
+ String targetPackageName =
+ targetInfo.getChooserTarget().getComponentName().getPackageName();
+ ChooserListAdapter currentListAdapter =
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter();
+ int maxRankedResults = Math.min(currentListAdapter.mDisplayList.size(),
+ MAX_LOG_RANK_POSITION);
+
+ for (int i = 0; i < maxRankedResults; i++) {
+ if (currentListAdapter.mDisplayList.get(i)
+ .getResolveInfo().activityInfo.packageName.equals(targetPackageName)) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ @Override
+ protected boolean shouldAddFooterView() {
+ // To accommodate for window insets
+ return true;
+ }
+
+ @Override
+ protected void applyFooterView(int height) {
+ int count = mChooserMultiProfilePagerAdapter.getItemCount();
+
+ for (int i = 0; i < count; i++) {
+ mChooserMultiProfilePagerAdapter.getAdapterForIndex(i).setFooterHeight(height);
+ }
+ }
+
+ private IntentFilter getTargetIntentFilter() {
+ try {
+ final Intent intent = getTargetIntent();
+ 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;
+ }
+ }
+
+ @VisibleForTesting
+ protected void queryDirectShareTargets(
+ ChooserListAdapter adapter, boolean skipAppPredictionService) {
+ mQueriedSharingShortcutsTimeMs = System.currentTimeMillis();
+ UserHandle userHandle = adapter.getUserHandle();
+ if (!skipAppPredictionService) {
+ AppPredictor appPredictor = getAppPredictorForDirectShareIfEnabled(userHandle);
+ if (appPredictor != null) {
+ appPredictor.requestPredictionUpdate();
+ return;
+ }
+ }
+ // Default to just querying ShortcutManager if AppPredictor not present.
+ final IntentFilter filter = getTargetIntentFilter();
+ if (filter == null) {
+ return;
+ }
+
+ AsyncTask.execute(() -> {
+ Context selectedProfileContext = createContextAsUser(userHandle, 0 /* flags */);
+ ShortcutManager sm = (ShortcutManager) selectedProfileContext
+ .getSystemService(Context.SHORTCUT_SERVICE);
+ List<ShortcutManager.ShareShortcutInfo> resultList = sm.getShareTargets(filter);
+ sendShareShortcutInfoList(resultList, adapter, null, userHandle);
+ });
+ }
+
+ /**
+ * Returns {@code false} if {@code userHandle} is the work profile and it's either
+ * in quiet mode or not running.
+ */
+ private boolean shouldQueryShortcutManager(UserHandle userHandle) {
+ if (!shouldShowTabs()) {
+ return true;
+ }
+ if (!getWorkProfileUserHandle().equals(userHandle)) {
+ return true;
+ }
+ if (!isUserRunning(userHandle)) {
+ return false;
+ }
+ if (!isUserUnlocked(userHandle)) {
+ return false;
+ }
+ if (isQuietModeEnabled(userHandle)) {
+ return false;
+ }
+ return true;
+ }
+
+ private void sendShareShortcutInfoList(
+ List<ShortcutManager.ShareShortcutInfo> resultList,
+ ChooserListAdapter chooserListAdapter,
+ @Nullable List<AppTarget> appTargets, UserHandle userHandle) {
+ if (appTargets != null && appTargets.size() != resultList.size()) {
+ throw new RuntimeException("resultList and appTargets must have the same size."
+ + " resultList.size()=" + resultList.size()
+ + " appTargets.size()=" + appTargets.size());
+ }
+ Context selectedProfileContext = createContextAsUser(userHandle, 0 /* flags */);
+ for (int i = resultList.size() - 1; i >= 0; i--) {
+ final String packageName = resultList.get(i).getTargetComponent().getPackageName();
+ if (!isPackageEnabled(selectedProfileContext, packageName)) {
+ resultList.remove(i);
+ if (appTargets != null) {
+ appTargets.remove(i);
+ }
+ }
+ }
+
+ // If |appTargets| is not null, results are from AppPredictionService and already sorted.
+ final int shortcutType = (appTargets == null ? TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER :
+ TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
+
+ // Match ShareShortcutInfos with DisplayResolveInfos to be able to use the old code path
+ // for direct share targets. After ShareSheet is refactored we should use the
+ // ShareShortcutInfos directly.
+ List<ServiceResultInfo> resultRecords = new ArrayList<>();
+ for (int i = 0; i < chooserListAdapter.getDisplayResolveInfoCount(); i++) {
+ DisplayResolveInfo displayResolveInfo = chooserListAdapter.getDisplayResolveInfo(i);
+ List<ShortcutManager.ShareShortcutInfo> matchingShortcuts =
+ filterShortcutsByTargetComponentName(
+ resultList, displayResolveInfo.getResolvedComponentName());
+ if (matchingShortcuts.isEmpty()) {
+ continue;
+ }
+ List<ChooserTarget> chooserTargets = convertToChooserTarget(
+ matchingShortcuts, resultList, appTargets, shortcutType);
+
+ ServiceResultInfo resultRecord = new ServiceResultInfo(
+ displayResolveInfo, chooserTargets, userHandle);
+ resultRecords.add(resultRecord);
+ }
+
+ sendShortcutManagerShareTargetResults(
+ shortcutType, resultRecords.toArray(new ServiceResultInfo[0]));
+ }
+
+ private List<ShortcutManager.ShareShortcutInfo> filterShortcutsByTargetComponentName(
+ List<ShortcutManager.ShareShortcutInfo> allShortcuts, ComponentName requiredTarget) {
+ List<ShortcutManager.ShareShortcutInfo> matchingShortcuts = new ArrayList<>();
+ for (ShortcutManager.ShareShortcutInfo shortcut : allShortcuts) {
+ if (requiredTarget.equals(shortcut.getTargetComponent())) {
+ matchingShortcuts.add(shortcut);
+ }
+ }
+ return matchingShortcuts;
+ }
+
+ private void sendShortcutManagerShareTargetResults(
+ int shortcutType, ServiceResultInfo[] results) {
+ final Message msg = Message.obtain();
+ msg.what = ChooserHandler.SHORTCUT_MANAGER_ALL_SHARE_TARGET_RESULTS;
+ msg.obj = results;
+ msg.arg1 = shortcutType;
+ mChooserHandler.sendMessage(msg);
+ }
+
+ private boolean isPackageEnabled(Context context, String packageName) {
+ if (TextUtils.isEmpty(packageName)) {
+ return false;
+ }
+ ApplicationInfo appInfo;
try {
- super.startActivityAsCaller(intent, options, ignoreTargetSecurity, userId);
- } finally {
- StrictMode.enableDeathOnFileUriExposure();
+ appInfo = context.getPackageManager().getApplicationInfo(packageName, 0);
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+
+ if (appInfo != null && appInfo.enabled
+ && (appInfo.flags & ApplicationInfo.FLAG_SUSPENDED) == 0) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Converts a list of ShareShortcutInfos to ChooserTargets.
+ * @param matchingShortcuts List of shortcuts, all from the same package, that match the current
+ * share intent filter.
+ * @param allShortcuts List of all the shortcuts from all the packages on the device that are
+ * returned for the current sharing action.
+ * @param allAppTargets List of AppTargets. Null if the results are not from prediction service.
+ * @param shortcutType One of the values TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER or
+ * TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE
+ * @return A list of ChooserTargets sorted by score in descending order.
+ */
+ @VisibleForTesting
+ @NonNull
+ public List<ChooserTarget> convertToChooserTarget(
+ @NonNull List<ShortcutManager.ShareShortcutInfo> matchingShortcuts,
+ @NonNull List<ShortcutManager.ShareShortcutInfo> allShortcuts,
+ @Nullable List<AppTarget> allAppTargets, @ShareTargetType int shortcutType) {
+ // A set of distinct scores for the matched shortcuts. We use index of a rank in the sorted
+ // list instead of the actual rank value when converting a rank to a score.
+ List<Integer> scoreList = new ArrayList<>();
+ if (shortcutType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER) {
+ for (int i = 0; i < matchingShortcuts.size(); i++) {
+ int shortcutRank = matchingShortcuts.get(i).getShortcutInfo().getRank();
+ if (!scoreList.contains(shortcutRank)) {
+ scoreList.add(shortcutRank);
+ }
+ }
+ Collections.sort(scoreList);
+ }
+
+ List<ChooserTarget> chooserTargetList = new ArrayList<>(matchingShortcuts.size());
+ for (int i = 0; i < matchingShortcuts.size(); i++) {
+ ShortcutInfo shortcutInfo = matchingShortcuts.get(i).getShortcutInfo();
+ int indexInAllShortcuts = allShortcuts.indexOf(matchingShortcuts.get(i));
+
+ float score;
+ if (shortcutType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) {
+ // Incoming results are ordered. Create a score based on index in the original list.
+ score = Math.max(1.0f - (0.01f * indexInAllShortcuts), 0.0f);
+ } else {
+ // Create a score based on the rank of the shortcut.
+ int rankIndex = scoreList.indexOf(shortcutInfo.getRank());
+ score = Math.max(1.0f - (0.01f * rankIndex), 0.0f);
+ }
+
+ Bundle extras = new Bundle();
+ extras.putString(Intent.EXTRA_SHORTCUT_ID, shortcutInfo.getId());
+
+ ChooserTarget chooserTarget = new ChooserTarget(
+ shortcutInfo.getLabel(),
+ null, // Icon will be loaded later if this target is selected to be shown.
+ score, matchingShortcuts.get(i).getTargetComponent().clone(), extras);
+
+ chooserTargetList.add(chooserTarget);
+ if (mDirectShareAppTargetCache != null && allAppTargets != null) {
+ mDirectShareAppTargetCache.put(chooserTarget,
+ allAppTargets.get(indexInAllShortcuts));
+ }
+ if (mDirectShareShortcutInfoCache != null) {
+ mDirectShareShortcutInfoCache.put(chooserTarget, shortcutInfo);
+ }
+ }
+ // Sort ChooserTargets by score in descending order
+ Comparator<ChooserTarget> byScore =
+ (ChooserTarget a, ChooserTarget b) -> -Float.compare(a.getScore(), b.getScore());
+ Collections.sort(chooserTargetList, byScore);
+ return chooserTargetList;
+ }
+
+ private void logDirectShareTargetReceived(int logCategory) {
+ final int apiLatency = (int) (System.currentTimeMillis() - mQueriedSharingShortcutsTimeMs);
+ getMetricsLogger().write(new LogMaker(logCategory).setSubtype(apiLatency));
+ }
+
+ void updateModelAndChooserCounts(TargetInfo info) {
+ if (info != null && info instanceof MultiDisplayResolveInfo) {
+ info = ((MultiDisplayResolveInfo) info).getSelectedTarget();
}
+ if (info != null) {
+ sendClickToAppPredictor(info);
+ final ResolveInfo ri = info.getResolveInfo();
+ Intent targetIntent = getTargetIntent();
+ if (ri != null && ri.activityInfo != null && targetIntent != null) {
+ ChooserListAdapter currentListAdapter =
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter();
+ if (currentListAdapter != null) {
+ sendImpressionToAppPredictor(info, currentListAdapter);
+ currentListAdapter.updateModel(info.getResolvedComponentName());
+ currentListAdapter.updateChooserCounts(ri.activityInfo.packageName,
+ targetIntent.getAction());
+ }
+ if (DEBUG) {
+ Log.d(TAG, "ResolveInfo Package is " + ri.activityInfo.packageName);
+ Log.d(TAG, "Action to be updated is " + targetIntent.getAction());
+ }
+ } else if (DEBUG) {
+ Log.d(TAG, "Can not log Chooser Counts of null ResovleInfo");
+ }
+ }
+ mIsSuccessfullySelected = true;
+ }
+
+ private void sendImpressionToAppPredictor(TargetInfo targetInfo, ChooserListAdapter adapter) {
+ AppPredictor directShareAppPredictor = getAppPredictorForDirectShareIfEnabled(
+ mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
+ if (directShareAppPredictor == null) {
+ return;
+ }
+ // Send DS target impression info to AppPredictor, only when user chooses app share.
+ if (targetInfo instanceof ChooserTargetInfo) {
+ return;
+ }
+ List<ChooserTargetInfo> surfacedTargetInfo = adapter.getSurfacedTargetInfo();
+ List<AppTargetId> targetIds = new ArrayList<>();
+ for (ChooserTargetInfo chooserTargetInfo : surfacedTargetInfo) {
+ ChooserTarget chooserTarget = chooserTargetInfo.getChooserTarget();
+ ComponentName componentName = chooserTarget.getComponentName();
+ if (mDirectShareShortcutInfoCache.containsKey(chooserTarget)) {
+ String shortcutId = mDirectShareShortcutInfoCache.get(chooserTarget).getId();
+ targetIds.add(new AppTargetId(
+ String.format("%s/%s/%s", shortcutId, componentName.flattenToString(),
+ SHORTCUT_TARGET)));
+ }
+ }
+ directShareAppPredictor.notifyLaunchLocationShown(LAUNCH_LOCATION_DIRECT_SHARE, targetIds);
+ }
+
+ private void sendClickToAppPredictor(TargetInfo targetInfo) {
+ AppPredictor directShareAppPredictor = getAppPredictorForDirectShareIfEnabled(
+ mChooserMultiProfilePagerAdapter.getCurrentUserHandle());
+ if (directShareAppPredictor == null) {
+ return;
+ }
+ if (!(targetInfo instanceof ChooserTargetInfo)) {
+ return;
+ }
+ ChooserTarget chooserTarget = ((ChooserTargetInfo) targetInfo).getChooserTarget();
+ AppTarget appTarget = null;
+ if (mDirectShareAppTargetCache != null) {
+ appTarget = mDirectShareAppTargetCache.get(chooserTarget);
+ }
+ // This is a direct share click that was provided by the APS
+ if (appTarget != null) {
+ directShareAppPredictor.notifyAppTargetEvent(
+ new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH)
+ .setLaunchLocation(LAUNCH_LOCATION_DIRECT_SHARE)
+ .build());
+ }
+ }
+
+ @Nullable
+ private AppPredictor createAppPredictor(UserHandle userHandle) {
+ if (!mIsAppPredictorComponentAvailable) {
+ return null;
+ }
+
+ if (getPersonalProfileUserHandle().equals(userHandle)) {
+ if (mPersonalAppPredictor != null) {
+ return mPersonalAppPredictor;
+ }
+ } else {
+ if (mWorkAppPredictor != null) {
+ return mWorkAppPredictor;
+ }
+ }
+
+ // TODO(b/148230574): Currently AppPredictor fetches only the same-profile app targets.
+ // Make AppPredictor work cross-profile.
+ Context contextAsUser = createContextAsUser(userHandle, 0 /* flags */);
+ final IntentFilter filter = getTargetIntentFilter();
+ Bundle extras = new Bundle();
+ extras.putParcelable(APP_PREDICTION_INTENT_FILTER_KEY, filter);
+ populateTextContent(extras);
+ AppPredictionContext appPredictionContext = new AppPredictionContext.Builder(contextAsUser)
+ .setUiSurface(APP_PREDICTION_SHARE_UI_SURFACE)
+ .setPredictedTargetCount(APP_PREDICTION_SHARE_TARGET_QUERY_PACKAGE_LIMIT)
+ .setExtras(extras)
+ .build();
+ AppPredictionManager appPredictionManager =
+ contextAsUser
+ .getSystemService(AppPredictionManager.class);
+ AppPredictor appPredictionSession = appPredictionManager.createAppPredictionSession(
+ appPredictionContext);
+ if (getPersonalProfileUserHandle().equals(userHandle)) {
+ mPersonalAppPredictor = appPredictionSession;
+ } else {
+ mWorkAppPredictor = appPredictionSession;
+ }
+ return appPredictionSession;
+ }
+
+ private void populateTextContent(Bundle extras) {
+ final Intent intent = getTargetIntent();
+ String sharedText = intent.getStringExtra(Intent.EXTRA_TEXT);
+ extras.putString(SHARED_TEXT_KEY, sharedText);
+ }
+
+ /**
+ * This will return an app predictor if it is enabled for direct share sorting
+ * and if one exists. Otherwise, it returns null.
+ * @param userHandle
+ */
+ @Nullable
+ private AppPredictor getAppPredictorForDirectShareIfEnabled(UserHandle userHandle) {
+ return ChooserFlags.USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS
+ && !ActivityManager.isLowRamDeviceStatic() ? createAppPredictor(userHandle) : null;
+ }
+
+ /**
+ * This will return an app predictor if it is enabled for share activity sorting
+ * and if one exists. Otherwise, it returns null.
+ */
+ @Nullable
+ private AppPredictor getAppPredictorForShareActivitiesIfEnabled(UserHandle userHandle) {
+ return USE_PREDICTION_MANAGER_FOR_SHARE_ACTIVITIES ? createAppPredictor(userHandle) : null;
+ }
+
+ 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.
+ */
+ static class AzInfoComparator implements Comparator<DisplayResolveInfo> {
+ Collator mCollator;
+ AzInfoComparator(Context context) {
+ mCollator = Collator.getInstance(context.getResources().getConfiguration().locale);
+ }
+
+ @Override
+ public int compare(
+ DisplayResolveInfo lhsp, DisplayResolveInfo rhsp) {
+ return mCollator.compare(lhsp.getDisplayLabel(), rhsp.getDisplayLabel());
+ }
+ }
+
+ protected MetricsLogger getMetricsLogger() {
+ if (mMetricsLogger == null) {
+ mMetricsLogger = new MetricsLogger();
+ }
+ return mMetricsLogger;
+ }
+
+ protected ChooserActivityLogger getChooserActivityLogger() {
+ if (mChooserActivityLogger == null) {
+ mChooserActivityLogger = new ChooserActivityLoggerImpl();
+ }
+ return mChooserActivityLogger;
+ }
+
+ public class ChooserListController extends ResolverListController {
+ public ChooserListController(Context context,
+ PackageManager pm,
+ Intent targetIntent,
+ String referrerPackageName,
+ int launchedFromUid,
+ UserHandle userId,
+ AbstractResolverComparator resolverComparator) {
+ super(context, pm, targetIntent, referrerPackageName, launchedFromUid, userId,
+ resolverComparator);
+ }
+
+ @Override
+ boolean isComponentFiltered(ComponentName name) {
+ if (mFilteredComponentNames == null) {
+ return false;
+ }
+ for (ComponentName filteredComponentName : mFilteredComponentNames) {
+ if (name.equals(filteredComponentName)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean isComponentPinned(ComponentName name) {
+ return mPinnedSharedPrefs.getBoolean(name.flattenToString(), false);
+ }
+
+ @Override
+ public boolean isFixedAtTop(ComponentName name) {
+ return name != null && name.equals(getNearbySharingComponent())
+ && shouldNearbyShareBeFirstInRankedRow();
+ }
+ }
+
+ @VisibleForTesting
+ public ChooserGridAdapter createChooserGridAdapter(Context context,
+ List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList,
+ boolean filterLastUsed, UserHandle userHandle) {
+ ChooserListAdapter chooserListAdapter = createChooserListAdapter(context, payloadIntents,
+ initialIntents, rList, filterLastUsed,
+ createListController(userHandle));
+ AppPredictor.Callback appPredictorCallback = createAppPredictorCallback(chooserListAdapter);
+ AppPredictor appPredictor = setupAppPredictorForUser(userHandle, appPredictorCallback);
+ chooserListAdapter.setAppPredictor(appPredictor);
+ chooserListAdapter.setAppPredictorCallback(appPredictorCallback);
+ return new ChooserGridAdapter(chooserListAdapter);
+ }
+
+ @VisibleForTesting
+ public ChooserListAdapter createChooserListAdapter(Context context,
+ List<Intent> payloadIntents, Intent[] initialIntents, List<ResolveInfo> rList,
+ boolean filterLastUsed, ResolverListController resolverListController) {
+ return new ChooserListAdapter(context, payloadIntents, initialIntents, rList,
+ filterLastUsed, resolverListController, this,
+ this, context.getPackageManager(),
+ getChooserActivityLogger());
+ }
+
+ @VisibleForTesting
+ protected ResolverListController createListController(UserHandle userHandle) {
+ AppPredictor appPredictor = getAppPredictorForShareActivitiesIfEnabled(userHandle);
+ AbstractResolverComparator resolverComparator;
+ if (appPredictor != null) {
+ resolverComparator = new AppPredictionServiceResolverComparator(this, getTargetIntent(),
+ getReferrerPackageName(), appPredictor, userHandle, getChooserActivityLogger());
+ } else {
+ resolverComparator =
+ new ResolverRankerServiceResolverComparator(this, getTargetIntent(),
+ getReferrerPackageName(), null, getChooserActivityLogger());
+ }
+
+ return new ChooserListController(
+ this,
+ mPm,
+ getTargetIntent(),
+ getReferrerPackageName(),
+ mLaunchedFromUid,
+ userHandle,
+ resolverComparator);
+ }
+
+ @VisibleForTesting
+ protected Bitmap loadThumbnail(Uri uri, Size size) {
+ if (uri == null || size == null) {
+ return null;
+ }
+
+ try {
+ return getContentResolver().loadThumbnail(uri, size, null);
+ } catch (IOException | NullPointerException | SecurityException ex) {
+ logContentPreviewWarning(uri);
+ }
+ return null;
+ }
+
+ static final class PlaceHolderTargetInfo extends NotSelectableTargetInfo {
+ public Drawable getDisplayIcon(Context context) {
+ AnimatedVectorDrawable avd = (AnimatedVectorDrawable)
+ context.getDrawable(R.drawable.chooser_direct_share_icon_placeholder);
+ avd.start(); // Start animation after generation
+ return avd;
+ }
+ }
+
+ protected static final class EmptyTargetInfo extends NotSelectableTargetInfo {
+ public EmptyTargetInfo() {}
+
+ public Drawable getDisplayIcon(Context context) {
+ return null;
+ }
+ }
+
+ private void handleScroll(View view, int x, int y, int oldx, int oldy) {
+ if (mChooserMultiProfilePagerAdapter.getCurrentRootAdapter() != null) {
+ mChooserMultiProfilePagerAdapter.getCurrentRootAdapter().handleScroll(view, y, oldy);
+ }
+ }
+
+ /*
+ * Need to dynamically adjust how many icons can fit per row before we add them,
+ * which also means setting the correct offset to initially show the content
+ * preview area + 2 rows of targets
+ */
+ private void handleLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+ int oldTop, int oldRight, int oldBottom) {
+ if (mChooserMultiProfilePagerAdapter == null) {
+ return;
+ }
+ RecyclerView recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
+ ChooserGridAdapter gridAdapter = mChooserMultiProfilePagerAdapter.getCurrentRootAdapter();
+ // Skip height calculation if recycler view was scrolled to prevent it inaccurately
+ // calculating the height, as the logic below does not account for the scrolled offset.
+ if (gridAdapter == null || recyclerView == null
+ || recyclerView.computeVerticalScrollOffset() != 0) {
+ return;
+ }
+
+ final int availableWidth = right - left - v.getPaddingLeft() - v.getPaddingRight();
+ boolean isLayoutUpdated = gridAdapter.consumeLayoutRequest()
+ || gridAdapter.calculateChooserTargetWidth(availableWidth)
+ || recyclerView.getAdapter() == null
+ || availableWidth != mCurrAvailableWidth;
+
+ boolean insetsChanged = !Objects.equals(mLastAppliedInsets, mSystemWindowInsets);
+
+ if (isLayoutUpdated
+ || insetsChanged
+ || mLastNumberOfChildren != recyclerView.getChildCount()) {
+ mCurrAvailableWidth = availableWidth;
+ if (isLayoutUpdated) {
+ // It is very important we call setAdapter from here. Otherwise in some cases
+ // the resolver list doesn't get populated, such as b/150922090, b/150918223
+ // and b/150936654
+ recyclerView.setAdapter(gridAdapter);
+ ((GridLayoutManager) recyclerView.getLayoutManager()).setSpanCount(
+ mMaxTargetsPerRow);
+
+ updateTabPadding();
+ }
+
+ UserHandle currentUserHandle = mChooserMultiProfilePagerAdapter.getCurrentUserHandle();
+ int currentProfile = getProfileForUser(currentUserHandle);
+ int initialProfile = findSelectedProfile();
+ if (currentProfile != initialProfile) {
+ return;
+ }
+
+ if (mLastNumberOfChildren == recyclerView.getChildCount() && !insetsChanged) {
+ return;
+ }
+
+ getMainThreadHandler().post(() -> {
+ if (mResolverDrawerLayout == null || gridAdapter == null) {
+ return;
+ }
+ int offset = calculateDrawerOffset(top, bottom, recyclerView, gridAdapter);
+ mResolverDrawerLayout.setCollapsibleHeightReserved(offset);
+ mEnterTransitionAnimationDelegate.markOffsetCalculated();
+ mLastAppliedInsets = mSystemWindowInsets;
+ });
+ }
+ }
+
+ private int calculateDrawerOffset(
+ int top, int bottom, RecyclerView recyclerView, ChooserGridAdapter gridAdapter) {
+
+ final int bottomInset = mSystemWindowInsets != null
+ ? mSystemWindowInsets.bottom : 0;
+ int offset = bottomInset;
+ int rowsToShow = gridAdapter.getSystemRowCount()
+ + gridAdapter.getProfileRowCount()
+ + gridAdapter.getServiceTargetRowCount()
+ + gridAdapter.getCallerAndRankedTargetRowCount();
+
+ // then this is most likely not a SEND_* action, so check
+ // the app target count
+ if (rowsToShow == 0) {
+ rowsToShow = gridAdapter.getRowCount();
+ }
+
+ // still zero? then use a default height and leave, which
+ // can happen when there are no targets to show
+ if (rowsToShow == 0 && !shouldShowStickyContentPreview()) {
+ offset += getResources().getDimensionPixelSize(
+ R.dimen.chooser_max_collapsed_height);
+ return offset;
+ }
+
+ View stickyContentPreview = findViewById(com.android.internal.R.id.content_preview_container);
+ if (shouldShowStickyContentPreview() && isStickyContentPreviewShowing()) {
+ offset += stickyContentPreview.getHeight();
+ }
+
+ if (shouldShowTabs()) {
+ offset += findViewById(com.android.internal.R.id.tabs).getHeight();
+ }
+
+ if (recyclerView.getVisibility() == View.VISIBLE) {
+ int directShareHeight = 0;
+ rowsToShow = Math.min(4, rowsToShow);
+ boolean shouldShowExtraRow = shouldShowExtraRow(rowsToShow);
+ mLastNumberOfChildren = recyclerView.getChildCount();
+ for (int i = 0, childCount = recyclerView.getChildCount();
+ i < childCount && rowsToShow > 0; i++) {
+ View child = recyclerView.getChildAt(i);
+ if (((GridLayoutManager.LayoutParams)
+ child.getLayoutParams()).getSpanIndex() != 0) {
+ continue;
+ }
+ int height = child.getHeight();
+ offset += height;
+ if (shouldShowExtraRow) {
+ offset += height;
+ }
+
+ if (gridAdapter.getTargetType(
+ recyclerView.getChildAdapterPosition(child))
+ == ChooserListAdapter.TARGET_SERVICE) {
+ directShareHeight = height;
+ }
+ rowsToShow--;
+ }
+
+ boolean isExpandable = getResources().getConfiguration().orientation
+ == Configuration.ORIENTATION_PORTRAIT && !isInMultiWindowMode();
+ if (directShareHeight != 0 && isSendAction(getTargetIntent())
+ && isExpandable) {
+ // make sure to leave room for direct share 4->8 expansion
+ int requiredExpansionHeight =
+ (int) (directShareHeight / DIRECT_SHARE_EXPANSION_RATE);
+ int topInset = mSystemWindowInsets != null ? mSystemWindowInsets.top : 0;
+ int minHeight = bottom - top - mResolverDrawerLayout.getAlwaysShowHeight()
+ - requiredExpansionHeight - topInset - bottomInset;
+
+ offset = Math.min(offset, minHeight);
+ }
+ } else {
+ ViewGroup currentEmptyStateView = getActiveEmptyStateView();
+ if (currentEmptyStateView.getVisibility() == View.VISIBLE) {
+ offset += currentEmptyStateView.getHeight();
+ }
+ }
+
+ return Math.min(offset, bottom - top);
+ }
+
+ /**
+ * If we have a tabbed view and are showing 1 row in the current profile and an empty
+ * state screen in the other profile, to prevent cropping of the empty state screen we show
+ * a second row in the current profile.
+ */
+ private boolean shouldShowExtraRow(int rowsToShow) {
+ return shouldShowTabs()
+ && rowsToShow == 1
+ && mChooserMultiProfilePagerAdapter.shouldShowEmptyStateScreen(
+ mChooserMultiProfilePagerAdapter.getInactiveListAdapter());
+ }
+
+ /**
+ * Returns {@link #PROFILE_PERSONAL}, {@link #PROFILE_WORK}, or -1 if the given user handle
+ * does not match either the personal or work user handle.
+ **/
+ private int getProfileForUser(UserHandle currentUserHandle) {
+ if (currentUserHandle.equals(getPersonalProfileUserHandle())) {
+ return PROFILE_PERSONAL;
+ } else if (currentUserHandle.equals(getWorkProfileUserHandle())) {
+ return PROFILE_WORK;
+ }
+ Log.e(TAG, "User " + currentUserHandle + " does not belong to a personal or work profile.");
+ return -1;
+ }
+
+ private ViewGroup getActiveEmptyStateView() {
+ int currentPage = mChooserMultiProfilePagerAdapter.getCurrentPage();
+ return mChooserMultiProfilePagerAdapter.getItem(currentPage).getEmptyStateView();
+ }
+
+ static class BaseChooserTargetComparator implements Comparator<ChooserTarget> {
+ @Override
+ public int compare(ChooserTarget lhs, ChooserTarget rhs) {
+ // Descending order
+ return (int) Math.signum(rhs.getScore() - lhs.getScore());
+ }
+ }
+
+ @Override // ResolverListCommunicator
+ public void onHandlePackagesChanged(ResolverListAdapter listAdapter) {
+ mChooserMultiProfilePagerAdapter.getActiveListAdapter().notifyDataSetChanged();
+ super.onHandlePackagesChanged(listAdapter);
+ }
+
+ @Override // SelectableTargetInfoCommunicator
+ public ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo info) {
+ return mChooserMultiProfilePagerAdapter.getActiveListAdapter().makePresentationGetter(info);
+ }
+
+ @Override // SelectableTargetInfoCommunicator
+ public Intent getReferrerFillInIntent() {
+ return mReferrerFillInIntent;
+ }
+
+ @Override // ChooserListCommunicator
+ public int getMaxRankedTargets() {
+ return mMaxTargetsPerRow;
+ }
+
+ @Override // ChooserListCommunicator
+ public void sendListViewUpdateMessage(UserHandle userHandle) {
+ Message msg = Message.obtain();
+ msg.what = ChooserHandler.LIST_VIEW_UPDATE_MESSAGE;
+ msg.obj = userHandle;
+ mChooserHandler.sendMessageDelayed(msg, mListViewUpdateDelayMs);
+ }
+
+ @Override
+ public void onListRebuilt(ResolverListAdapter listAdapter, boolean rebuildComplete) {
+ setupScrollListener();
+ maybeSetupGlobalLayoutListener();
+
+ ChooserListAdapter chooserListAdapter = (ChooserListAdapter) listAdapter;
+ if (chooserListAdapter.getUserHandle()
+ .equals(mChooserMultiProfilePagerAdapter.getCurrentUserHandle())) {
+ mChooserMultiProfilePagerAdapter.getActiveAdapterView()
+ .setAdapter(mChooserMultiProfilePagerAdapter.getCurrentRootAdapter());
+ mChooserMultiProfilePagerAdapter
+ .setupListAdapter(mChooserMultiProfilePagerAdapter.getCurrentPage());
+ }
+
+ if (chooserListAdapter.mDisplayList == null
+ || chooserListAdapter.mDisplayList.isEmpty()) {
+ chooserListAdapter.notifyDataSetChanged();
+ } else {
+ chooserListAdapter.updateAlphabeticalList();
+ }
+
+ if (rebuildComplete) {
+ getChooserActivityLogger().logSharesheetAppLoadComplete();
+ maybeQueryAdditionalPostProcessingTargets(chooserListAdapter);
+ mLatencyTracker.onActionEnd(ACTION_LOAD_SHARE_SHEET);
+ }
+ }
+
+ private void maybeQueryAdditionalPostProcessingTargets(ChooserListAdapter chooserListAdapter) {
+ // don't support direct share on low ram devices
+ if (ActivityManager.isLowRamDeviceStatic()) {
+ return;
+ }
+
+ // no need to query direct share for work profile when its locked or disabled
+ if (!shouldQueryShortcutManager(chooserListAdapter.getUserHandle())) {
+ return;
+ }
+
+ if (ChooserFlags.USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS) {
+ if (DEBUG) {
+ Log.d(TAG, "querying direct share targets from ShortcutManager");
+ }
+
+ queryDirectShareTargets(chooserListAdapter, false);
+ }
+ }
+
+ @VisibleForTesting
+ protected boolean isUserRunning(UserHandle userHandle) {
+ UserManager userManager = getSystemService(UserManager.class);
+ return userManager.isUserRunning(userHandle);
+ }
+
+ @VisibleForTesting
+ protected boolean isUserUnlocked(UserHandle userHandle) {
+ UserManager userManager = getSystemService(UserManager.class);
+ return userManager.isUserUnlocked(userHandle);
+ }
+
+ @VisibleForTesting
+ protected boolean isQuietModeEnabled(UserHandle userHandle) {
+ UserManager userManager = getSystemService(UserManager.class);
+ return userManager.isQuietModeEnabled(userHandle);
+ }
+
+ private void setupScrollListener() {
+ if (mResolverDrawerLayout == null) {
+ return;
+ }
+ int elevatedViewResId = shouldShowTabs() ? com.android.internal.R.id.tabs : com.android.internal.R.id.chooser_header;
+ final View elevatedView = mResolverDrawerLayout.findViewById(elevatedViewResId);
+ final float defaultElevation = elevatedView.getElevation();
+ final float chooserHeaderScrollElevation =
+ getResources().getDimensionPixelSize(R.dimen.chooser_header_scroll_elevation);
+ mChooserMultiProfilePagerAdapter.getActiveAdapterView().addOnScrollListener(
+ new RecyclerView.OnScrollListener() {
+ public void onScrollStateChanged(RecyclerView view, int scrollState) {
+ if (scrollState == RecyclerView.SCROLL_STATE_IDLE) {
+ if (mScrollStatus == SCROLL_STATUS_SCROLLING_VERTICAL) {
+ mScrollStatus = SCROLL_STATUS_IDLE;
+ setHorizontalScrollingEnabled(true);
+ }
+ } else if (scrollState == RecyclerView.SCROLL_STATE_DRAGGING) {
+ if (mScrollStatus == SCROLL_STATUS_IDLE) {
+ mScrollStatus = SCROLL_STATUS_SCROLLING_VERTICAL;
+ setHorizontalScrollingEnabled(false);
+ }
+ }
+ }
+
+ public void onScrolled(RecyclerView view, int dx, int dy) {
+ if (view.getChildCount() > 0) {
+ View child = view.getLayoutManager().findViewByPosition(0);
+ if (child == null || child.getTop() < 0) {
+ elevatedView.setElevation(chooserHeaderScrollElevation);
+ return;
+ }
+ }
+
+ elevatedView.setElevation(defaultElevation);
+ }
+ });
+ }
+
+ private void maybeSetupGlobalLayoutListener() {
+ if (shouldShowTabs()) {
+ return;
+ }
+ final View recyclerView = mChooserMultiProfilePagerAdapter.getActiveAdapterView();
+ recyclerView.getViewTreeObserver()
+ .addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
+ @Override
+ public void onGlobalLayout() {
+ // Fixes an issue were the accessibility border disappears on list creation.
+ recyclerView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
+ final TextView titleView = findViewById(com.android.internal.R.id.title);
+ if (titleView != null) {
+ titleView.setFocusable(true);
+ titleView.setFocusableInTouchMode(true);
+ titleView.requestFocus();
+ titleView.requestAccessibilityFocus();
+ }
+ }
+ });
+ }
+
+ @Override // ChooserListCommunicator
+ public boolean isSendAction(Intent targetIntent) {
+ if (targetIntent == null) {
+ return false;
+ }
+
+ String action = targetIntent.getAction();
+ if (action == null) {
+ return false;
+ }
+
+ if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * The sticky content preview is shown only when we have a tabbed view. It's shown above
+ * the tabs so it is not part of the scrollable list. If we are not in tabbed view,
+ * we instead show the content preview as a regular list item.
+ */
+ private boolean shouldShowStickyContentPreview() {
+ return shouldShowStickyContentPreviewNoOrientationCheck()
+ && !getResources().getBoolean(R.bool.resolver_landscape_phone);
+ }
+
+ private boolean shouldShowStickyContentPreviewNoOrientationCheck() {
+ return shouldShowTabs()
+ && mMultiProfilePagerAdapter.getListAdapterForUserHandle(
+ UserHandle.of(UserHandle.myUserId())).getCount() > 0
+ && isSendAction(getTargetIntent());
+ }
+
+ private void updateStickyContentPreview() {
+ if (shouldShowStickyContentPreviewNoOrientationCheck()) {
+ // The sticky content preview is only shown when we show the work and personal tabs.
+ // We don't show it in landscape as otherwise there is no room for scrolling.
+ // If the sticky content preview will be shown at some point with orientation change,
+ // then always preload it to avoid subsequent resizing of the share sheet.
+ ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
+ if (contentPreviewContainer.getChildCount() == 0) {
+ ViewGroup contentPreviewView = createContentPreviewView(contentPreviewContainer);
+ contentPreviewContainer.addView(contentPreviewView);
+ }
+ }
+ if (shouldShowStickyContentPreview()) {
+ showStickyContentPreview();
+ } else {
+ hideStickyContentPreview();
+ }
+ }
+
+ private void showStickyContentPreview() {
+ if (isStickyContentPreviewShowing()) {
+ return;
+ }
+ ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
+ contentPreviewContainer.setVisibility(View.VISIBLE);
+ }
+
+ private boolean isStickyContentPreviewShowing() {
+ ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
+ return contentPreviewContainer.getVisibility() == View.VISIBLE;
+ }
+
+ private void hideStickyContentPreview() {
+ if (!isStickyContentPreviewShowing()) {
+ return;
+ }
+ ViewGroup contentPreviewContainer = findViewById(com.android.internal.R.id.content_preview_container);
+ contentPreviewContainer.setVisibility(View.GONE);
+ }
+
+ private void logActionShareWithPreview() {
+ Intent targetIntent = getTargetIntent();
+ int previewType = findPreferredContentPreview(targetIntent, getContentResolver());
+ getMetricsLogger().write(new LogMaker(MetricsEvent.ACTION_SHARE_WITH_PREVIEW)
+ .setSubtype(previewType));
+ }
+
+ private void startFinishAnimation() {
+ View rootView = findRootView();
+ rootView.startAnimation(new FinishAnimation(this, rootView));
+ }
+
+ private boolean maybeCancelFinishAnimation() {
+ View rootView = findRootView();
+ Animation animation = rootView.getAnimation();
+ if (animation instanceof FinishAnimation) {
+ boolean hasEnded = animation.hasEnded();
+ animation.cancel();
+ rootView.clearAnimation();
+ return !hasEnded;
+ }
+ return false;
+ }
+
+ private View findRootView() {
+ if (mContentView == null) {
+ mContentView = findViewById(android.R.id.content);
+ }
+ return mContentView;
+ }
+
+ abstract static class ViewHolderBase extends RecyclerView.ViewHolder {
+ private int mViewType;
+
+ ViewHolderBase(View itemView, int viewType) {
+ super(itemView);
+ this.mViewType = viewType;
+ }
+
+ int getViewType() {
+ return mViewType;
+ }
+ }
+
+ /**
+ * Used to bind types of individual item including
+ * {@link ChooserGridAdapter#VIEW_TYPE_NORMAL},
+ * {@link ChooserGridAdapter#VIEW_TYPE_CONTENT_PREVIEW},
+ * {@link ChooserGridAdapter#VIEW_TYPE_PROFILE},
+ * and {@link ChooserGridAdapter#VIEW_TYPE_AZ_LABEL}.
+ */
+ final class ItemViewHolder extends ViewHolderBase {
+ ResolverListAdapter.ViewHolder mWrappedViewHolder;
+ int mListPosition = ChooserListAdapter.NO_POSITION;
+
+ ItemViewHolder(View itemView, boolean isClickable, int viewType) {
+ super(itemView, viewType);
+ mWrappedViewHolder = new ResolverListAdapter.ViewHolder(itemView);
+ if (isClickable) {
+ itemView.setOnClickListener(v -> startSelected(mListPosition,
+ false/* always */, true/* filterd */));
+
+ itemView.setOnLongClickListener(v -> {
+ final TargetInfo ti = mChooserMultiProfilePagerAdapter.getActiveListAdapter()
+ .targetInfoForPosition(mListPosition, /* filtered */ true);
+
+ // This should always be the case for ItemViewHolder, check for validity
+ if (ti instanceof DisplayResolveInfo && shouldShowTargetDetails(ti)) {
+ showTargetDetails((DisplayResolveInfo) ti);
+ }
+ return true;
+ });
+ }
+ }
+ }
+
+ private boolean shouldShowTargetDetails(TargetInfo ti) {
+ ComponentName nearbyShare = getNearbySharingComponent();
+ // Suppress target details for nearby share to hide pin/unpin action
+ boolean isNearbyShare = nearbyShare != null && nearbyShare.equals(
+ ti.getResolvedComponentName()) && shouldNearbyShareBeFirstInRankedRow();
+ return ti instanceof SelectableTargetInfo
+ || (ti instanceof DisplayResolveInfo && !isNearbyShare);
+ }
+
+ /**
+ * Add a footer to the list, to support scrolling behavior below the navbar.
+ */
+ static final class FooterViewHolder extends ViewHolderBase {
+ FooterViewHolder(View itemView, int viewType) {
+ super(itemView, viewType);
+ }
+ }
+
+ /**
+ * Intentionally override the {@link ResolverActivity} implementation as we only need that
+ * implementation for the intent resolver case.
+ */
+ @Override
+ public void onButtonClick(View v) {}
+
+ /**
+ * Intentionally override the {@link ResolverActivity} implementation as we only need that
+ * implementation for the intent resolver case.
+ */
+ @Override
+ protected void resetButtonBar() {}
+
+ @Override
+ protected String getMetricsCategory() {
+ return METRICS_CATEGORY_CHOOSER;
+ }
+
+ @Override
+ protected void onProfileTabSelected() {
+ ChooserGridAdapter currentRootAdapter =
+ mChooserMultiProfilePagerAdapter.getCurrentRootAdapter();
+ currentRootAdapter.updateDirectShareExpansion();
+ // This fixes an edge case where after performing a variety of gestures, vertical scrolling
+ // ends up disabled. That's because at some point the old tab's vertical scrolling is
+ // disabled and the new tab's is enabled. For context, see b/159997845
+ setVerticalScrollEnabled(true);
+ if (mResolverDrawerLayout != null) {
+ mResolverDrawerLayout.scrollNestedScrollableChildBackToTop();
+ }
+ }
+
+ @Override
+ protected WindowInsets onApplyWindowInsets(View v, WindowInsets insets) {
+ if (shouldShowTabs()) {
+ mChooserMultiProfilePagerAdapter
+ .setEmptyStateBottomOffset(insets.getSystemWindowInsetBottom());
+ mChooserMultiProfilePagerAdapter.setupContainerPadding(
+ getActiveEmptyStateView().findViewById(com.android.internal.R.id.resolver_empty_state_container));
+ }
+
+ WindowInsets result = super.onApplyWindowInsets(v, insets);
+ if (mResolverDrawerLayout != null) {
+ mResolverDrawerLayout.requestLayout();
+ }
+ return result;
+ }
+
+ private void setHorizontalScrollingEnabled(boolean enabled) {
+ ResolverViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ viewPager.setSwipingEnabled(enabled);
+ }
+
+ private void setVerticalScrollEnabled(boolean enabled) {
+ ChooserGridLayoutManager layoutManager =
+ (ChooserGridLayoutManager) mChooserMultiProfilePagerAdapter.getActiveAdapterView()
+ .getLayoutManager();
+ layoutManager.setVerticalScrollEnabled(enabled);
+ }
+
+ @Override
+ void onHorizontalSwipeStateChanged(int state) {
+ if (state == ViewPager.SCROLL_STATE_DRAGGING) {
+ if (mScrollStatus == SCROLL_STATUS_IDLE) {
+ mScrollStatus = SCROLL_STATUS_SCROLLING_HORIZONTAL;
+ setVerticalScrollEnabled(false);
+ }
+ } else if (state == ViewPager.SCROLL_STATE_IDLE) {
+ if (mScrollStatus == SCROLL_STATUS_SCROLLING_HORIZONTAL) {
+ mScrollStatus = SCROLL_STATUS_IDLE;
+ setVerticalScrollEnabled(true);
+ }
+ }
+ }
+
+ /**
+ * Adapter for all types of items and targets in ShareSheet.
+ * Note that ranked sections like Direct Share - while appearing grid-like - are handled on the
+ * row level by this adapter but not on the item level. Individual targets within the row are
+ * handled by {@link ChooserListAdapter}
+ */
+ @VisibleForTesting
+ public final class ChooserGridAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
+ private ChooserListAdapter mChooserListAdapter;
+ private final LayoutInflater mLayoutInflater;
+
+ private DirectShareViewHolder mDirectShareViewHolder;
+ private int mChooserTargetWidth = 0;
+ private boolean mShowAzLabelIfPoss;
+ private boolean mLayoutRequested = false;
+
+ private int mFooterHeight = 0;
+
+ private static final int VIEW_TYPE_DIRECT_SHARE = 0;
+ private static final int VIEW_TYPE_NORMAL = 1;
+ private static final int VIEW_TYPE_CONTENT_PREVIEW = 2;
+ private static final int VIEW_TYPE_PROFILE = 3;
+ private static final int VIEW_TYPE_AZ_LABEL = 4;
+ private static final int VIEW_TYPE_CALLER_AND_RANK = 5;
+ private static final int VIEW_TYPE_FOOTER = 6;
+
+ private static final int NUM_EXPANSIONS_TO_HIDE_AZ_LABEL = 20;
+
+ ChooserGridAdapter(ChooserListAdapter wrappedAdapter) {
+ super();
+ mChooserListAdapter = wrappedAdapter;
+ mLayoutInflater = LayoutInflater.from(ChooserActivity.this);
+
+ mShowAzLabelIfPoss = getNumSheetExpansions() < NUM_EXPANSIONS_TO_HIDE_AZ_LABEL;
+
+ wrappedAdapter.registerDataSetObserver(new DataSetObserver() {
+ @Override
+ public void onChanged() {
+ super.onChanged();
+ notifyDataSetChanged();
+ }
+
+ @Override
+ public void onInvalidated() {
+ super.onInvalidated();
+ notifyDataSetChanged();
+ }
+ });
+ }
+
+ public void setFooterHeight(int height) {
+ mFooterHeight = height;
+ }
+
+ /**
+ * Calculate the chooser target width to maximize space per item
+ *
+ * @param width The new row width to use for recalculation
+ * @return true if the view width has changed
+ */
+ public boolean calculateChooserTargetWidth(int width) {
+ if (width == 0) {
+ return false;
+ }
+
+ // Limit width to the maximum width of the chooser activity
+ int maxWidth = getResources().getDimensionPixelSize(R.dimen.chooser_width);
+ width = Math.min(maxWidth, width);
+
+ int newWidth = width / mMaxTargetsPerRow;
+ if (newWidth != mChooserTargetWidth) {
+ mChooserTargetWidth = newWidth;
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Hides the list item content preview.
+ * <p>Not to be confused with the sticky content preview which is above the
+ * personal and work tabs.
+ */
+ public void hideContentPreview() {
+ mLayoutRequested = true;
+ notifyDataSetChanged();
+ }
+
+ public boolean consumeLayoutRequest() {
+ boolean oldValue = mLayoutRequested;
+ mLayoutRequested = false;
+ return oldValue;
+ }
+
+ public int getRowCount() {
+ return (int) (
+ getSystemRowCount()
+ + getProfileRowCount()
+ + getServiceTargetRowCount()
+ + getCallerAndRankedTargetRowCount()
+ + getAzLabelRowCount()
+ + Math.ceil(
+ (float) mChooserListAdapter.getAlphaTargetCount()
+ / mMaxTargetsPerRow)
+ );
+ }
+
+ /**
+ * Whether the "system" row of targets is displayed.
+ * This area includes the content preview (if present) and action row.
+ */
+ public int getSystemRowCount() {
+ // For the tabbed case we show the sticky content preview above the tabs,
+ // please refer to shouldShowStickyContentPreview
+ if (shouldShowTabs()) {
+ return 0;
+ }
+
+ if (!isSendAction(getTargetIntent())) {
+ return 0;
+ }
+
+ if (mChooserListAdapter == null || mChooserListAdapter.getCount() == 0) {
+ return 0;
+ }
+
+ return 1;
+ }
+
+ public int getProfileRowCount() {
+ if (shouldShowTabs()) {
+ return 0;
+ }
+ return mChooserListAdapter.getOtherProfile() == null ? 0 : 1;
+ }
+
+ public int getFooterRowCount() {
+ return 1;
+ }
+
+ public int getCallerAndRankedTargetRowCount() {
+ return (int) Math.ceil(
+ ((float) mChooserListAdapter.getCallerTargetCount()
+ + mChooserListAdapter.getRankedTargetCount()) / mMaxTargetsPerRow);
+ }
+
+ // There can be at most one row in the listview, that is internally
+ // a ViewGroup with 2 rows
+ public int getServiceTargetRowCount() {
+ if (isSendAction(getTargetIntent())
+ && !ActivityManager.isLowRamDeviceStatic()) {
+ return 1;
+ }
+ return 0;
+ }
+
+ public int getAzLabelRowCount() {
+ // Only show a label if the a-z list is showing
+ return (mShowAzLabelIfPoss && mChooserListAdapter.getAlphaTargetCount() > 0) ? 1 : 0;
+ }
+
+ @Override
+ public int getItemCount() {
+ return (int) (
+ getSystemRowCount()
+ + getProfileRowCount()
+ + getServiceTargetRowCount()
+ + getCallerAndRankedTargetRowCount()
+ + getAzLabelRowCount()
+ + mChooserListAdapter.getAlphaTargetCount()
+ + getFooterRowCount()
+ );
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ switch (viewType) {
+ case VIEW_TYPE_CONTENT_PREVIEW:
+ return new ItemViewHolder(createContentPreviewView(parent), false, viewType);
+ case VIEW_TYPE_PROFILE:
+ return new ItemViewHolder(createProfileView(parent), false, viewType);
+ case VIEW_TYPE_AZ_LABEL:
+ return new ItemViewHolder(createAzLabelView(parent), false, viewType);
+ case VIEW_TYPE_NORMAL:
+ return new ItemViewHolder(
+ mChooserListAdapter.createView(parent), true, viewType);
+ case VIEW_TYPE_DIRECT_SHARE:
+ case VIEW_TYPE_CALLER_AND_RANK:
+ return createItemGroupViewHolder(viewType, parent);
+ case VIEW_TYPE_FOOTER:
+ Space sp = new Space(parent.getContext());
+ sp.setLayoutParams(new RecyclerView.LayoutParams(
+ LayoutParams.MATCH_PARENT, mFooterHeight));
+ return new FooterViewHolder(sp, viewType);
+ default:
+ // Since we catch all possible viewTypes above, no chance this is being called.
+ return null;
+ }
+ }
+
+ @Override
+ public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
+ int viewType = ((ViewHolderBase) holder).getViewType();
+ switch (viewType) {
+ case VIEW_TYPE_DIRECT_SHARE:
+ case VIEW_TYPE_CALLER_AND_RANK:
+ bindItemGroupViewHolder(position, (ItemGroupViewHolder) holder);
+ break;
+ case VIEW_TYPE_NORMAL:
+ bindItemViewHolder(position, (ItemViewHolder) holder);
+ break;
+ default:
+ }
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ int count;
+
+ int countSum = (count = getSystemRowCount());
+ if (count > 0 && position < countSum) return VIEW_TYPE_CONTENT_PREVIEW;
+
+ countSum += (count = getProfileRowCount());
+ if (count > 0 && position < countSum) return VIEW_TYPE_PROFILE;
+
+ countSum += (count = getServiceTargetRowCount());
+ if (count > 0 && position < countSum) return VIEW_TYPE_DIRECT_SHARE;
+
+ countSum += (count = getCallerAndRankedTargetRowCount());
+ if (count > 0 && position < countSum) return VIEW_TYPE_CALLER_AND_RANK;
+
+ countSum += (count = getAzLabelRowCount());
+ if (count > 0 && position < countSum) return VIEW_TYPE_AZ_LABEL;
+
+ if (position == getItemCount() - 1) return VIEW_TYPE_FOOTER;
+
+ return VIEW_TYPE_NORMAL;
+ }
+
+ public int getTargetType(int position) {
+ return mChooserListAdapter.getPositionTargetType(getListPosition(position));
+ }
+
+ private View createProfileView(ViewGroup parent) {
+ View profileRow = mLayoutInflater.inflate(R.layout.chooser_profile_row, parent, false);
+ mProfileView = profileRow.findViewById(com.android.internal.R.id.profile_button);
+ mProfileView.setOnClickListener(ChooserActivity.this::onProfileClick);
+ updateProfileViewButton();
+ return profileRow;
+ }
+
+ private View createAzLabelView(ViewGroup parent) {
+ return mLayoutInflater.inflate(R.layout.chooser_az_label_row, parent, false);
+ }
+
+ private ItemGroupViewHolder loadViewsIntoGroup(ItemGroupViewHolder holder) {
+ final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ final int exactSpec = MeasureSpec.makeMeasureSpec(mChooserTargetWidth,
+ MeasureSpec.EXACTLY);
+ int columnCount = holder.getColumnCount();
+
+ final boolean isDirectShare = holder instanceof DirectShareViewHolder;
+
+ for (int i = 0; i < columnCount; i++) {
+ final View v = mChooserListAdapter.createView(holder.getRowByIndex(i));
+ final int column = i;
+ v.setOnClickListener(new OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ startSelected(holder.getItemIndex(column), false, true);
+ }
+ });
+
+ // Show menu for both direct share and app share targets after long click.
+ v.setOnLongClickListener(v1 -> {
+ TargetInfo ti = mChooserListAdapter.targetInfoForPosition(
+ holder.getItemIndex(column), true);
+ if (shouldShowTargetDetails(ti)) {
+ showTargetDetails(ti);
+ }
+ return true;
+ });
+
+ holder.addView(i, v);
+
+ // Force Direct Share to be 2 lines and auto-wrap to second line via hoz scroll =
+ // false. TextView#setHorizontallyScrolling must be reset after #setLines. Must be
+ // done before measuring.
+ if (isDirectShare) {
+ final ViewHolder vh = (ViewHolder) v.getTag();
+ vh.text.setLines(2);
+ vh.text.setHorizontallyScrolling(false);
+ vh.text2.setVisibility(View.GONE);
+ }
+
+ // Force height to be a given so we don't have visual disruption during scaling.
+ v.measure(exactSpec, spec);
+ setViewBounds(v, v.getMeasuredWidth(), v.getMeasuredHeight());
+ }
+
+ final ViewGroup viewGroup = holder.getViewGroup();
+
+ // Pre-measure and fix height so we can scale later.
+ holder.measure();
+ setViewBounds(viewGroup, LayoutParams.MATCH_PARENT, holder.getMeasuredRowHeight());
+
+ if (isDirectShare) {
+ DirectShareViewHolder dsvh = (DirectShareViewHolder) holder;
+ setViewBounds(dsvh.getRow(0), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight());
+ setViewBounds(dsvh.getRow(1), LayoutParams.MATCH_PARENT, dsvh.getMinRowHeight());
+ }
+
+ viewGroup.setTag(holder);
+ return holder;
+ }
+
+ private void setViewBounds(View view, int widthPx, int heightPx) {
+ LayoutParams lp = view.getLayoutParams();
+ if (lp == null) {
+ lp = new LayoutParams(widthPx, heightPx);
+ view.setLayoutParams(lp);
+ } else {
+ lp.height = heightPx;
+ lp.width = widthPx;
+ }
+ }
+
+ ItemGroupViewHolder createItemGroupViewHolder(int viewType, ViewGroup parent) {
+ if (viewType == VIEW_TYPE_DIRECT_SHARE) {
+ ViewGroup parentGroup = (ViewGroup) mLayoutInflater.inflate(
+ R.layout.chooser_row_direct_share, parent, false);
+ ViewGroup row1 = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row,
+ parentGroup, false);
+ ViewGroup row2 = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row,
+ parentGroup, false);
+ parentGroup.addView(row1);
+ parentGroup.addView(row2);
+
+ mDirectShareViewHolder = new DirectShareViewHolder(parentGroup,
+ Lists.newArrayList(row1, row2), mMaxTargetsPerRow, viewType,
+ mChooserMultiProfilePagerAdapter::getActiveListAdapter);
+ loadViewsIntoGroup(mDirectShareViewHolder);
+
+ return mDirectShareViewHolder;
+ } else {
+ ViewGroup row = (ViewGroup) mLayoutInflater.inflate(R.layout.chooser_row, parent,
+ false);
+ ItemGroupViewHolder holder =
+ new SingleRowViewHolder(row, mMaxTargetsPerRow, viewType);
+ loadViewsIntoGroup(holder);
+
+ return holder;
+ }
+ }
+
+ /**
+ * Need to merge CALLER + ranked STANDARD into a single row and prevent a separator from
+ * showing on top of the AZ list if the AZ label is visible. All other types are placed into
+ * their own row as determined by their target type, and dividers are added in the list to
+ * separate each type.
+ */
+ int getRowType(int rowPosition) {
+ // Merge caller and ranked standard into a single row
+ int positionType = mChooserListAdapter.getPositionTargetType(rowPosition);
+ if (positionType == ChooserListAdapter.TARGET_CALLER) {
+ return ChooserListAdapter.TARGET_STANDARD;
+ }
+
+ // If an the A-Z label is shown, prevent a separator from appearing by making the A-Z
+ // row type the same as the suggestion row type
+ if (getAzLabelRowCount() > 0 && positionType == ChooserListAdapter.TARGET_STANDARD_AZ) {
+ return ChooserListAdapter.TARGET_STANDARD;
+ }
+
+ return positionType;
+ }
+
+ void bindItemViewHolder(int position, ItemViewHolder holder) {
+ View v = holder.itemView;
+ int listPosition = getListPosition(position);
+ holder.mListPosition = listPosition;
+ mChooserListAdapter.bindView(listPosition, v);
+ }
+
+ void bindItemGroupViewHolder(int position, ItemGroupViewHolder holder) {
+ final ViewGroup viewGroup = (ViewGroup) holder.itemView;
+ int start = getListPosition(position);
+ int startType = getRowType(start);
+
+ int columnCount = holder.getColumnCount();
+ int end = start + columnCount - 1;
+ while (getRowType(end) != startType && end >= start) {
+ end--;
+ }
+
+ if (end == start && mChooserListAdapter.getItem(start) instanceof EmptyTargetInfo) {
+ final TextView textView = viewGroup.findViewById(com.android.internal.R.id.chooser_row_text_option);
+
+ if (textView.getVisibility() != View.VISIBLE) {
+ textView.setAlpha(0.0f);
+ textView.setVisibility(View.VISIBLE);
+ textView.setText(R.string.chooser_no_direct_share_targets);
+
+ ValueAnimator fadeAnim = ObjectAnimator.ofFloat(textView, "alpha", 0.0f, 1.0f);
+ fadeAnim.setInterpolator(new DecelerateInterpolator(1.0f));
+
+ float translationInPx = getResources().getDimensionPixelSize(
+ R.dimen.chooser_row_text_option_translate);
+ textView.setTranslationY(translationInPx);
+ ValueAnimator translateAnim = ObjectAnimator.ofFloat(textView, "translationY",
+ 0.0f);
+ translateAnim.setInterpolator(new DecelerateInterpolator(1.0f));
+
+ AnimatorSet animSet = new AnimatorSet();
+ animSet.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
+ animSet.setStartDelay(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
+ animSet.playTogether(fadeAnim, translateAnim);
+ animSet.start();
+ }
+ }
+
+ for (int i = 0; i < columnCount; i++) {
+ final View v = holder.getView(i);
+
+ if (start + i <= end) {
+ holder.setViewVisibility(i, View.VISIBLE);
+ holder.setItemIndex(i, start + i);
+ mChooserListAdapter.bindView(holder.getItemIndex(i), v);
+ } else {
+ holder.setViewVisibility(i, View.INVISIBLE);
+ }
+ }
+ }
+
+ int getListPosition(int position) {
+ position -= getSystemRowCount() + getProfileRowCount();
+
+ final int serviceCount = mChooserListAdapter.getServiceTargetCount();
+ final int serviceRows = (int) Math.ceil((float) serviceCount / getMaxRankedTargets());
+ if (position < serviceRows) {
+ return position * mMaxTargetsPerRow;
+ }
+
+ position -= serviceRows;
+
+ final int callerAndRankedCount = mChooserListAdapter.getCallerTargetCount()
+ + mChooserListAdapter.getRankedTargetCount();
+ final int callerAndRankedRows = getCallerAndRankedTargetRowCount();
+ if (position < callerAndRankedRows) {
+ return serviceCount + position * mMaxTargetsPerRow;
+ }
+
+ position -= getAzLabelRowCount() + callerAndRankedRows;
+
+ return callerAndRankedCount + serviceCount + position;
+ }
+
+ public void handleScroll(View v, int y, int oldy) {
+ boolean canExpandDirectShare = canExpandDirectShare();
+ if (mDirectShareViewHolder != null && canExpandDirectShare) {
+ mDirectShareViewHolder.handleScroll(
+ mChooserMultiProfilePagerAdapter.getActiveAdapterView(), y, oldy,
+ mMaxTargetsPerRow);
+ }
+ }
+
+ /**
+ * Only expand direct share area if there is a minimum number of targets.
+ */
+ private boolean canExpandDirectShare() {
+ // Do not enable until we have confirmed more apps are using sharing shortcuts
+ // Check git history for enablement logic
+ return false;
+ }
+
+ public ChooserListAdapter getListAdapter() {
+ return mChooserListAdapter;
+ }
+
+ boolean shouldCellSpan(int position) {
+ return getItemViewType(position) == VIEW_TYPE_NORMAL;
+ }
+
+ void updateDirectShareExpansion() {
+ if (mDirectShareViewHolder == null || !canExpandDirectShare()) {
+ return;
+ }
+ RecyclerView activeAdapterView =
+ mChooserMultiProfilePagerAdapter.getActiveAdapterView();
+ if (mResolverDrawerLayout.isCollapsed()) {
+ mDirectShareViewHolder.collapse(activeAdapterView);
+ } else {
+ mDirectShareViewHolder.expand(activeAdapterView);
+ }
+ }
+ }
+
+ /**
+ * Used to bind types for group of items including:
+ * {@link ChooserGridAdapter#VIEW_TYPE_DIRECT_SHARE},
+ * and {@link ChooserGridAdapter#VIEW_TYPE_CALLER_AND_RANK}.
+ */
+ abstract static class ItemGroupViewHolder extends ViewHolderBase {
+ protected int mMeasuredRowHeight;
+ private int[] mItemIndices;
+ protected final View[] mCells;
+ private final int mColumnCount;
+
+ ItemGroupViewHolder(int cellCount, View itemView, int viewType) {
+ super(itemView, viewType);
+ this.mCells = new View[cellCount];
+ this.mItemIndices = new int[cellCount];
+ this.mColumnCount = cellCount;
+ }
+
+ abstract ViewGroup addView(int index, View v);
+
+ abstract ViewGroup getViewGroup();
+
+ abstract ViewGroup getRowByIndex(int index);
+
+ abstract ViewGroup getRow(int rowNumber);
+
+ abstract void setViewVisibility(int i, int visibility);
+
+ public int getColumnCount() {
+ return mColumnCount;
+ }
+
+ public void measure() {
+ final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ getViewGroup().measure(spec, spec);
+ mMeasuredRowHeight = getViewGroup().getMeasuredHeight();
+ }
+
+ public int getMeasuredRowHeight() {
+ return mMeasuredRowHeight;
+ }
+
+ public void setItemIndex(int itemIndex, int listIndex) {
+ mItemIndices[itemIndex] = listIndex;
+ }
+
+ public int getItemIndex(int itemIndex) {
+ return mItemIndices[itemIndex];
+ }
+
+ public View getView(int index) {
+ return mCells[index];
+ }
+ }
+
+ static class SingleRowViewHolder extends ItemGroupViewHolder {
+ private final ViewGroup mRow;
+
+ SingleRowViewHolder(ViewGroup row, int cellCount, int viewType) {
+ super(cellCount, row, viewType);
+
+ this.mRow = row;
+ }
+
+ public ViewGroup getViewGroup() {
+ return mRow;
+ }
+
+ public ViewGroup getRowByIndex(int index) {
+ return mRow;
+ }
+
+ public ViewGroup getRow(int rowNumber) {
+ if (rowNumber == 0) return mRow;
+ return null;
+ }
+
+ public ViewGroup addView(int index, View v) {
+ mRow.addView(v);
+ mCells[index] = v;
+
+ return mRow;
+ }
+
+ public void setViewVisibility(int i, int visibility) {
+ getView(i).setVisibility(visibility);
+ }
+ }
+
+ static class DirectShareViewHolder extends ItemGroupViewHolder {
+ private final ViewGroup mParent;
+ private final List<ViewGroup> mRows;
+ private int mCellCountPerRow;
+
+ private boolean mHideDirectShareExpansion = false;
+ private int mDirectShareMinHeight = 0;
+ private int mDirectShareCurrHeight = 0;
+ private int mDirectShareMaxHeight = 0;
+
+ private final boolean[] mCellVisibility;
+
+ private final Supplier<ChooserListAdapter> mListAdapterSupplier;
+
+ DirectShareViewHolder(ViewGroup parent, List<ViewGroup> rows, int cellCountPerRow,
+ int viewType, Supplier<ChooserListAdapter> listAdapterSupplier) {
+ super(rows.size() * cellCountPerRow, parent, viewType);
+
+ this.mParent = parent;
+ this.mRows = rows;
+ this.mCellCountPerRow = cellCountPerRow;
+ this.mCellVisibility = new boolean[rows.size() * cellCountPerRow];
+ this.mListAdapterSupplier = listAdapterSupplier;
+ }
+
+ public ViewGroup addView(int index, View v) {
+ ViewGroup row = getRowByIndex(index);
+ row.addView(v);
+ mCells[index] = v;
+
+ return row;
+ }
+
+ public ViewGroup getViewGroup() {
+ return mParent;
+ }
+
+ public ViewGroup getRowByIndex(int index) {
+ return mRows.get(index / mCellCountPerRow);
+ }
+
+ public ViewGroup getRow(int rowNumber) {
+ return mRows.get(rowNumber);
+ }
+
+ public void measure() {
+ final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+ getRow(0).measure(spec, spec);
+ getRow(1).measure(spec, spec);
+
+ mDirectShareMinHeight = getRow(0).getMeasuredHeight();
+ mDirectShareCurrHeight = mDirectShareCurrHeight > 0
+ ? mDirectShareCurrHeight : mDirectShareMinHeight;
+ mDirectShareMaxHeight = 2 * mDirectShareMinHeight;
+ }
+
+ public int getMeasuredRowHeight() {
+ return mDirectShareCurrHeight;
+ }
+
+ public int getMinRowHeight() {
+ return mDirectShareMinHeight;
+ }
+
+ public void setViewVisibility(int i, int visibility) {
+ final View v = getView(i);
+ if (visibility == View.VISIBLE) {
+ mCellVisibility[i] = true;
+ v.setVisibility(visibility);
+ v.setAlpha(1.0f);
+ } else if (visibility == View.INVISIBLE && mCellVisibility[i]) {
+ mCellVisibility[i] = false;
+
+ ValueAnimator fadeAnim = ObjectAnimator.ofFloat(v, "alpha", 1.0f, 0f);
+ fadeAnim.setDuration(NO_DIRECT_SHARE_ANIM_IN_MILLIS);
+ fadeAnim.setInterpolator(new AccelerateInterpolator(1.0f));
+ fadeAnim.addListener(new AnimatorListenerAdapter() {
+ public void onAnimationEnd(Animator animation) {
+ v.setVisibility(View.INVISIBLE);
+ }
+ });
+ fadeAnim.start();
+ }
+ }
+
+ public void handleScroll(RecyclerView view, int y, int oldy, int maxTargetsPerRow) {
+ // only exit early if fully collapsed, otherwise onListRebuilt() with shifting
+ // targets can lock us into an expanded mode
+ boolean notExpanded = mDirectShareCurrHeight == mDirectShareMinHeight;
+ if (notExpanded) {
+ if (mHideDirectShareExpansion) {
+ return;
+ }
+
+ // only expand if we have more than maxTargetsPerRow, and delay that decision
+ // until they start to scroll
+ ChooserListAdapter adapter = mListAdapterSupplier.get();
+ int validTargets = adapter.getSelectableServiceTargetCount();
+ if (validTargets <= maxTargetsPerRow) {
+ mHideDirectShareExpansion = true;
+ return;
+ }
+ }
+
+ int yDiff = (int) ((oldy - y) * DIRECT_SHARE_EXPANSION_RATE);
+
+ int prevHeight = mDirectShareCurrHeight;
+ int newHeight = Math.min(prevHeight + yDiff, mDirectShareMaxHeight);
+ newHeight = Math.max(newHeight, mDirectShareMinHeight);
+ yDiff = newHeight - prevHeight;
+
+ updateDirectShareRowHeight(view, yDiff, newHeight);
+ }
+
+ void expand(RecyclerView view) {
+ updateDirectShareRowHeight(view, mDirectShareMaxHeight - mDirectShareCurrHeight,
+ mDirectShareMaxHeight);
+ }
+
+ void collapse(RecyclerView view) {
+ updateDirectShareRowHeight(view, mDirectShareMinHeight - mDirectShareCurrHeight,
+ mDirectShareMinHeight);
+ }
+
+ private void updateDirectShareRowHeight(RecyclerView view, int yDiff, int newHeight) {
+ if (view == null || view.getChildCount() == 0 || yDiff == 0) {
+ return;
+ }
+
+ // locate the item to expand, and offset the rows below that one
+ boolean foundExpansion = false;
+ for (int i = 0; i < view.getChildCount(); i++) {
+ View child = view.getChildAt(i);
+
+ if (foundExpansion) {
+ child.offsetTopAndBottom(yDiff);
+ } else {
+ if (child.getTag() != null && child.getTag() instanceof DirectShareViewHolder) {
+ int widthSpec = MeasureSpec.makeMeasureSpec(child.getWidth(),
+ MeasureSpec.EXACTLY);
+ int heightSpec = MeasureSpec.makeMeasureSpec(newHeight,
+ MeasureSpec.EXACTLY);
+ child.measure(widthSpec, heightSpec);
+ child.getLayoutParams().height = child.getMeasuredHeight();
+ child.layout(child.getLeft(), child.getTop(), child.getRight(),
+ child.getTop() + child.getMeasuredHeight());
+
+ foundExpansion = true;
+ }
+ }
+ }
+
+ if (foundExpansion) {
+ mDirectShareCurrHeight = newHeight;
+ }
+ }
+ }
+
+ static class ServiceResultInfo {
+ public final DisplayResolveInfo originalTarget;
+ public final List<ChooserTarget> resultTargets;
+ public final UserHandle userHandle;
+
+ public ServiceResultInfo(DisplayResolveInfo ot, List<ChooserTarget> rt,
+ UserHandle userHandle) {
+ originalTarget = ot;
+ resultTargets = rt;
+ this.userHandle = userHandle;
+ }
+ }
+
+ static class ChooserTargetRankingInfo {
+ public final List<AppTarget> scores;
+ public final UserHandle userHandle;
+
+ 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 internally to round image corners while obeying view padding.
+ */
+ public static class RoundedRectImageView extends ImageView {
+ private int mRadius = 0;
+ private Path mPath = new Path();
+ private Paint mOverlayPaint = new Paint(0);
+ private Paint mRoundRectPaint = new Paint(0);
+ private Paint mTextPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ private String mExtraImageCount = null;
+
+ public RoundedRectImageView(Context context) {
+ super(context);
+ }
+
+ public RoundedRectImageView(Context context, AttributeSet attrs) {
+ this(context, attrs, 0);
+ }
+
+ public RoundedRectImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+ this(context, attrs, defStyleAttr, 0);
+ }
+
+ public RoundedRectImageView(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ mRadius = context.getResources().getDimensionPixelSize(R.dimen.chooser_corner_radius);
+
+ mOverlayPaint.setColor(0x99000000);
+ mOverlayPaint.setStyle(Paint.Style.FILL);
+
+ mRoundRectPaint.setColor(context.getResources().getColor(R.color.chooser_row_divider));
+ mRoundRectPaint.setStyle(Paint.Style.STROKE);
+ mRoundRectPaint.setStrokeWidth(context.getResources()
+ .getDimensionPixelSize(R.dimen.chooser_preview_image_border));
+
+ mTextPaint.setColor(Color.WHITE);
+ mTextPaint.setTextSize(context.getResources()
+ .getDimensionPixelSize(R.dimen.chooser_preview_image_font_size));
+ mTextPaint.setTextAlign(Paint.Align.CENTER);
+ }
+
+ private void updatePath(int width, int height) {
+ mPath.reset();
+
+ int imageWidth = width - getPaddingRight() - getPaddingLeft();
+ int imageHeight = height - getPaddingBottom() - getPaddingTop();
+ mPath.addRoundRect(getPaddingLeft(), getPaddingTop(), imageWidth, imageHeight, mRadius,
+ mRadius, Path.Direction.CW);
+ }
+
+ /**
+ * Sets the corner radius on all corners
+ *
+ * param radius 0 for no radius, &gt; 0 for a visible corner radius
+ */
+ public void setRadius(int radius) {
+ mRadius = radius;
+ updatePath(getWidth(), getHeight());
+ }
+
+ /**
+ * Display an overlay with extra image count on 3rd image
+ */
+ public void setExtraImageCount(int count) {
+ if (count > 0) {
+ this.mExtraImageCount = "+" + count;
+ } else {
+ this.mExtraImageCount = null;
+ }
+ }
+
+ @Override
+ protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
+ super.onSizeChanged(width, height, oldWidth, oldHeight);
+ updatePath(width, height);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ if (mRadius != 0) {
+ canvas.clipPath(mPath);
+ }
+
+ super.onDraw(canvas);
+
+ int x = getPaddingLeft();
+ int y = getPaddingRight();
+ int width = getWidth() - getPaddingRight() - getPaddingLeft();
+ int height = getHeight() - getPaddingBottom() - getPaddingTop();
+ if (mExtraImageCount != null) {
+ canvas.drawRect(x, y, width, height, mOverlayPaint);
+
+ int xPos = canvas.getWidth() / 2;
+ int yPos = (int) ((canvas.getHeight() / 2.0f)
+ - ((mTextPaint.descent() + mTextPaint.ascent()) / 2.0f));
+
+ canvas.drawText(mExtraImageCount, xPos, yPos, mTextPaint);
+ }
+
+ canvas.drawRoundRect(x, y, width, height, mRadius, mRadius, mRoundRectPaint);
+ }
+ }
+
+ /**
+ * A helper class to track app's readiness for the scene transition animation.
+ * The app is ready when both the image is laid out and the drawer offset is calculated.
+ */
+ private class EnterTransitionAnimationDelegate implements View.OnLayoutChangeListener {
+ private boolean mPreviewReady = false;
+ private boolean mOffsetCalculated = false;
+
+ void postponeTransition() {
+ postponeEnterTransition();
+ }
+
+ void markImagePreviewReady() {
+ if (!mPreviewReady) {
+ mPreviewReady = true;
+ maybeStartListenForLayout();
+ }
+ }
+
+ void markOffsetCalculated() {
+ if (!mOffsetCalculated) {
+ mOffsetCalculated = true;
+ maybeStartListenForLayout();
+ }
+ }
+
+ private void maybeStartListenForLayout() {
+ if (mPreviewReady && mOffsetCalculated && mResolverDrawerLayout != null) {
+ if (mResolverDrawerLayout.isInLayout()) {
+ startPostponedEnterTransition();
+ } else {
+ mResolverDrawerLayout.addOnLayoutChangeListener(this);
+ mResolverDrawerLayout.requestLayout();
+ }
+ }
+ }
+
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
+ int oldTop, int oldRight, int oldBottom) {
+ v.removeOnLayoutChangeListener(this);
+ startPostponedEnterTransition();
+ }
+ }
+
+ /**
+ * Used in combination with the scene transition when launching the image editor
+ */
+ private static class FinishAnimation extends AlphaAnimation implements
+ Animation.AnimationListener {
+ private Activity mActivity;
+ private View mRootView;
+ private final float mFromAlpha;
+
+ FinishAnimation(Activity activity, View rootView) {
+ super(rootView.getAlpha(), 0.0f);
+ mActivity = activity;
+ mRootView = rootView;
+ mFromAlpha = rootView.getAlpha();
+ setInterpolator(new LinearInterpolator());
+ long duration = activity.getWindow().getTransitionBackgroundFadeDuration();
+ setDuration(duration);
+ // The scene transition animation looks better when it's not overlapped with this
+ // fade-out animation thus the delay.
+ // It is most likely that the image editor will cause this activity to stop and this
+ // animation will be cancelled in the background without running (i.e. we'll animate
+ // only when this activity remains partially visible after the image editor launch).
+ setStartOffset(duration);
+ super.setAnimationListener(this);
+ }
+
+ @Override
+ public void setAnimationListener(AnimationListener listener) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public void cancel() {
+ mRootView.setAlpha(mFromAlpha);
+ cleanup();
+ super.cancel();
+ }
+
+ @Override
+ public void onAnimationStart(Animation animation) {
+ }
+
+ @Override
+ public void onAnimationEnd(Animation animation) {
+ if (mActivity != null) {
+ mActivity.finish();
+ cleanup();
+ }
+ }
+
+ @Override
+ public void onAnimationRepeat(Animation animation) {
+ }
+
+ private void cleanup() {
+ mActivity = null;
+ mRootView = null;
+ }
+ }
+
+ @Override
+ protected void maybeLogProfileChange() {
+ getChooserActivityLogger().logShareheetProfileChanged();
+ }
+
+ private boolean shouldNearbyShareBeFirstInRankedRow() {
+ return ActivityManager.isLowRamDeviceStatic() && mIsNearbyShareFirstTargetInRankedApp;
+ }
+
+ private boolean shouldNearbyShareBeIncludedAsActionButton() {
+ return !shouldNearbyShareBeFirstInRankedRow();
}
}
diff --git a/java/src/com/android/intentresolver/ChooserActivityLogger.java b/java/src/com/android/intentresolver/ChooserActivityLogger.java
new file mode 100644
index 00000000..1daae01a
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserActivityLogger.java
@@ -0,0 +1,246 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import android.content.Intent;
+import android.provider.MediaStore;
+
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.UiEvent;
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.util.FrameworkStatsLog;
+
+/**
+ * Interface for writing Sharesheet atoms to statsd log.
+ * @hide
+ */
+public interface ChooserActivityLogger {
+ /** Logs a UiEventReported event for the system sharesheet completing initial start-up. */
+ void logShareStarted(int eventId, String packageName, String mimeType, int appProvidedDirect,
+ int appProvidedApp, boolean isWorkprofile, int previewType, String intent);
+
+ /** Logs a UiEventReported event for the system sharesheet when the user selects a target. */
+ void logShareTargetSelected(int targetType, String packageName, int positionPicked,
+ boolean isPinned);
+
+ /** Logs a UiEventReported event for the system sharesheet being triggered by the user. */
+ default void logSharesheetTriggered() {
+ log(SharesheetStandardEvent.SHARESHEET_TRIGGERED, getInstanceId());
+ }
+
+ /** Logs a UiEventReported event for the system sharesheet completing loading app targets. */
+ default void logSharesheetAppLoadComplete() {
+ log(SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE, getInstanceId());
+ }
+
+ /**
+ * Logs a UiEventReported event for the system sharesheet completing loading service targets.
+ */
+ default void logSharesheetDirectLoadComplete() {
+ log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_COMPLETE, getInstanceId());
+ }
+
+ /**
+ * Logs a UiEventReported event for the system sharesheet timing out loading service targets.
+ */
+ default void logSharesheetDirectLoadTimeout() {
+ log(SharesheetStandardEvent.SHARESHEET_DIRECT_LOAD_TIMEOUT, getInstanceId());
+ }
+
+ /**
+ * Logs a UiEventReported event for the system sharesheet switching
+ * between work and main profile.
+ */
+ default void logShareheetProfileChanged() {
+ log(SharesheetStandardEvent.SHARESHEET_PROFILE_CHANGED, getInstanceId());
+ }
+
+ /** Logs a UiEventReported event for the system sharesheet getting expanded or collapsed. */
+ default void logSharesheetExpansionChanged(boolean isCollapsed) {
+ log(isCollapsed ? SharesheetStandardEvent.SHARESHEET_COLLAPSED :
+ SharesheetStandardEvent.SHARESHEET_EXPANDED, getInstanceId());
+ }
+
+ /**
+ * Logs a UiEventReported event for the system sharesheet app share ranking timing out.
+ */
+ default void logSharesheetAppShareRankingTimeout() {
+ log(SharesheetStandardEvent.SHARESHEET_APP_SHARE_RANKING_TIMEOUT, getInstanceId());
+ }
+
+ /**
+ * Logs a UiEventReported event for the system sharesheet when direct share row is empty.
+ */
+ default void logSharesheetEmptyDirectShareRow() {
+ log(SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW, getInstanceId());
+ }
+
+ /**
+ * Logs a UiEventReported event for a given share activity
+ * @param event
+ * @param instanceId
+ */
+ void log(UiEventLogger.UiEventEnum event, InstanceId instanceId);
+
+ /**
+ *
+ * @return
+ */
+ InstanceId getInstanceId();
+
+ /**
+ * The UiEvent enums that this class can log.
+ */
+ enum SharesheetStartedEvent implements UiEventLogger.UiEventEnum {
+ @UiEvent(doc = "Basic system Sharesheet has started and is visible.")
+ SHARE_STARTED(228);
+
+ private final int mId;
+ SharesheetStartedEvent(int id) {
+ mId = id;
+ }
+ @Override
+ public int getId() {
+ return mId;
+ }
+ }
+
+ /**
+ * The UiEvent enums that this class can log.
+ */
+ enum SharesheetTargetSelectedEvent implements UiEventLogger.UiEventEnum {
+ INVALID(0),
+ @UiEvent(doc = "User selected a service target.")
+ SHARESHEET_SERVICE_TARGET_SELECTED(232),
+ @UiEvent(doc = "User selected an app target.")
+ SHARESHEET_APP_TARGET_SELECTED(233),
+ @UiEvent(doc = "User selected a standard target.")
+ SHARESHEET_STANDARD_TARGET_SELECTED(234),
+ @UiEvent(doc = "User selected the copy target.")
+ SHARESHEET_COPY_TARGET_SELECTED(235),
+ @UiEvent(doc = "User selected the nearby target.")
+ SHARESHEET_NEARBY_TARGET_SELECTED(626),
+ @UiEvent(doc = "User selected the edit target.")
+ SHARESHEET_EDIT_TARGET_SELECTED(669);
+
+ private final int mId;
+ SharesheetTargetSelectedEvent(int id) {
+ mId = id;
+ }
+ @Override public int getId() {
+ return mId;
+ }
+
+ public static SharesheetTargetSelectedEvent fromTargetType(int targetType) {
+ switch(targetType) {
+ case ChooserActivity.SELECTION_TYPE_SERVICE:
+ return SHARESHEET_SERVICE_TARGET_SELECTED;
+ case ChooserActivity.SELECTION_TYPE_APP:
+ return SHARESHEET_APP_TARGET_SELECTED;
+ case ChooserActivity.SELECTION_TYPE_STANDARD:
+ return SHARESHEET_STANDARD_TARGET_SELECTED;
+ case ChooserActivity.SELECTION_TYPE_COPY:
+ return SHARESHEET_COPY_TARGET_SELECTED;
+ case ChooserActivity.SELECTION_TYPE_NEARBY:
+ return SHARESHEET_NEARBY_TARGET_SELECTED;
+ case ChooserActivity.SELECTION_TYPE_EDIT:
+ return SHARESHEET_EDIT_TARGET_SELECTED;
+ default:
+ return INVALID;
+ }
+ }
+ }
+
+ /**
+ * The UiEvent enums that this class can log.
+ */
+ enum SharesheetStandardEvent implements UiEventLogger.UiEventEnum {
+ INVALID(0),
+ @UiEvent(doc = "User clicked share.")
+ SHARESHEET_TRIGGERED(227),
+ @UiEvent(doc = "User changed from work to personal profile or vice versa.")
+ SHARESHEET_PROFILE_CHANGED(229),
+ @UiEvent(doc = "User expanded target list.")
+ SHARESHEET_EXPANDED(230),
+ @UiEvent(doc = "User collapsed target list.")
+ SHARESHEET_COLLAPSED(231),
+ @UiEvent(doc = "Sharesheet app targets is fully populated.")
+ SHARESHEET_APP_LOAD_COMPLETE(322),
+ @UiEvent(doc = "Sharesheet direct targets is fully populated.")
+ SHARESHEET_DIRECT_LOAD_COMPLETE(323),
+ @UiEvent(doc = "Sharesheet direct targets timed out.")
+ SHARESHEET_DIRECT_LOAD_TIMEOUT(324),
+ @UiEvent(doc = "Sharesheet app share ranking timed out.")
+ SHARESHEET_APP_SHARE_RANKING_TIMEOUT(831),
+ @UiEvent(doc = "Sharesheet empty direct share row.")
+ SHARESHEET_EMPTY_DIRECT_SHARE_ROW(828);
+
+ private final int mId;
+ SharesheetStandardEvent(int id) {
+ mId = id;
+ }
+ @Override public int getId() {
+ return mId;
+ }
+ }
+
+ /**
+ * Returns the enum used in sharesheet started atom to indicate what preview type was used.
+ */
+ default int typeFromPreviewInt(int previewType) {
+ switch(previewType) {
+ case ChooserActivity.CONTENT_PREVIEW_IMAGE:
+ return FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_IMAGE;
+ case ChooserActivity.CONTENT_PREVIEW_FILE:
+ return FrameworkStatsLog.SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_FILE;
+ case ChooserActivity.CONTENT_PREVIEW_TEXT:
+ default:
+ return FrameworkStatsLog
+ .SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_TYPE_UNKNOWN;
+ }
+ }
+
+ /**
+ * Returns the enum used in sharesheet started atom to indicate what intent triggers the
+ * ChooserActivity.
+ */
+ default int typeFromIntentString(String intent) {
+ if (intent == null) {
+ return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_DEFAULT;
+ }
+ switch (intent) {
+ case Intent.ACTION_VIEW:
+ return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_VIEW;
+ case Intent.ACTION_EDIT:
+ return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_EDIT;
+ case Intent.ACTION_SEND:
+ return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_SEND;
+ case Intent.ACTION_SENDTO:
+ return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_SENDTO;
+ case Intent.ACTION_SEND_MULTIPLE:
+ return FrameworkStatsLog
+ .SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_SEND_MULTIPLE;
+ case MediaStore.ACTION_IMAGE_CAPTURE:
+ return FrameworkStatsLog
+ .SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_IMAGE_CAPTURE;
+ case Intent.ACTION_MAIN:
+ return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_MAIN;
+ default:
+ return FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_DEFAULT;
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserActivityLoggerImpl.java b/java/src/com/android/intentresolver/ChooserActivityLoggerImpl.java
new file mode 100644
index 00000000..08a345bc
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserActivityLoggerImpl.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.InstanceIdSequence;
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.logging.UiEventLoggerImpl;
+import com.android.internal.util.FrameworkStatsLog;
+
+/**
+ * Standard implementation of ChooserActivityLogger interface.
+ * @hide
+ */
+public class ChooserActivityLoggerImpl implements ChooserActivityLogger {
+ private static final int SHARESHEET_INSTANCE_ID_MAX = (1 << 13);
+
+ private UiEventLogger mUiEventLogger = new UiEventLoggerImpl();
+ // A small per-notification ID, used for statsd logging.
+ private InstanceId mInstanceId;
+ private static InstanceIdSequence sInstanceIdSequence;
+
+ @Override
+ public void logShareStarted(int eventId, String packageName, String mimeType,
+ int appProvidedDirect, int appProvidedApp, boolean isWorkprofile, int previewType,
+ String intent) {
+ FrameworkStatsLog.write(FrameworkStatsLog.SHARESHEET_STARTED,
+ /* event_id = 1 */ SharesheetStartedEvent.SHARE_STARTED.getId(),
+ /* package_name = 2 */ packageName,
+ /* instance_id = 3 */ getInstanceId().getId(),
+ /* mime_type = 4 */ mimeType,
+ /* num_app_provided_direct_targets = 5 */ appProvidedDirect,
+ /* num_app_provided_app_targets = 6 */ appProvidedApp,
+ /* is_workprofile = 7 */ isWorkprofile,
+ /* previewType = 8 */ typeFromPreviewInt(previewType),
+ /* intentType = 9 */ typeFromIntentString(intent));
+ }
+
+ @Override
+ public void logShareTargetSelected(int targetType, String packageName, int positionPicked,
+ boolean isPinned) {
+ FrameworkStatsLog.write(FrameworkStatsLog.RANKING_SELECTED,
+ /* event_id = 1 */ SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(),
+ /* package_name = 2 */ packageName,
+ /* instance_id = 3 */ getInstanceId().getId(),
+ /* position_picked = 4 */ positionPicked,
+ /* is_pinned = 5 */ isPinned);
+ }
+
+ @Override
+ public void log(UiEventLogger.UiEventEnum event, InstanceId instanceId) {
+ mUiEventLogger.logWithInstanceId(
+ event,
+ 0,
+ null,
+ instanceId);
+ }
+
+ @Override
+ public InstanceId getInstanceId() {
+ if (mInstanceId == null) {
+ if (sInstanceIdSequence == null) {
+ sInstanceIdSequence = new InstanceIdSequence(SHARESHEET_INSTANCE_ID_MAX);
+ }
+ mInstanceId = sInstanceIdSequence.newInstanceId();
+ }
+ return mInstanceId;
+ }
+
+}
diff --git a/java/src/com/android/intentresolver/ChooserFlags.java b/java/src/com/android/intentresolver/ChooserFlags.java
new file mode 100644
index 00000000..67f9046f
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserFlags.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import android.app.prediction.AppPredictionManager;
+
+/**
+ * Common flags for {@link ChooserListAdapter} and {@link ChooserActivity}.
+ */
+public class ChooserFlags {
+
+ /**
+ * Whether to use {@link AppPredictionManager} to query for direct share targets (as opposed to
+ * talking directly to {@link android.content.pm.ShortcutManager}.
+ */
+ // TODO(b/123089490): Replace with system flag
+ static final boolean USE_PREDICTION_MANAGER_FOR_DIRECT_TARGETS = true;
+}
+
diff --git a/java/src/com/android/intentresolver/ChooserGridLayoutManager.java b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
new file mode 100644
index 00000000..7c4b0c1f
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserGridLayoutManager.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+import com.android.internal.widget.GridLayoutManager;
+import com.android.internal.widget.RecyclerView;
+
+/**
+ * For a11y and per {@link RecyclerView#onInitializeAccessibilityNodeInfo}, override
+ * methods to ensure proper row counts.
+ */
+public class ChooserGridLayoutManager extends GridLayoutManager {
+
+ private boolean mVerticalScrollEnabled = true;
+
+ /**
+ * Constructor used when layout manager is set in XML by RecyclerView attribute
+ * "layoutManager". If spanCount is not specified in the XML, it defaults to a
+ * single column.
+ *
+ */
+ public ChooserGridLayoutManager(Context context, AttributeSet attrs, int defStyleAttr,
+ int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ /**
+ * Creates a vertical GridLayoutManager
+ *
+ * @param context Current context, will be used to access resources.
+ * @param spanCount The number of columns in the grid
+ */
+ public ChooserGridLayoutManager(Context context, int spanCount) {
+ super(context, spanCount);
+ }
+
+ /**
+ * @param context Current context, will be used to access resources.
+ * @param spanCount The number of columns or rows in the grid
+ * @param orientation Layout orientation. Should be {@link #HORIZONTAL} or {@link
+ * #VERTICAL}.
+ * @param reverseLayout When set to true, layouts from end to start.
+ */
+ public ChooserGridLayoutManager(Context context, int spanCount, int orientation,
+ boolean reverseLayout) {
+ super(context, spanCount, orientation, reverseLayout);
+ }
+
+ @Override
+ public int getRowCountForAccessibility(RecyclerView.Recycler recycler,
+ RecyclerView.State state) {
+ // Do not count the footer view in the official count
+ return super.getRowCountForAccessibility(recycler, state) - 1;
+ }
+
+ void setVerticalScrollEnabled(boolean verticalScrollEnabled) {
+ mVerticalScrollEnabled = verticalScrollEnabled;
+ }
+
+ @Override
+ public boolean canScrollVertically() {
+ return mVerticalScrollEnabled && super.canScrollVertically();
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java
new file mode 100644
index 00000000..6ddaffd7
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -0,0 +1,845 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
+import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
+
+import android.app.ActivityManager;
+import android.app.prediction.AppPredictor;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.LabeledIntent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.DeviceConfig;
+import android.service.chooser.ChooserTarget;
+import android.text.Layout;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.TextView;
+
+import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
+import com.android.intentresolver.chooser.ChooserTargetInfo;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
+import com.android.intentresolver.chooser.SelectableTargetInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class ChooserListAdapter extends ResolverListAdapter {
+ private static final String TAG = "ChooserListAdapter";
+ private static final boolean DEBUG = false;
+
+ private boolean mEnableStackedApps = true;
+
+ public static final int NO_POSITION = -1;
+ public static final int TARGET_BAD = -1;
+ public static final int TARGET_CALLER = 0;
+ public static final int TARGET_SERVICE = 1;
+ public static final int TARGET_STANDARD = 2;
+ public static final int TARGET_STANDARD_AZ = 3;
+
+ private static final int MAX_SUGGESTED_APP_TARGETS = 4;
+ private static final int MAX_CHOOSER_TARGETS_PER_APP = 2;
+
+ /** {@link #getBaseScore} */
+ public static final float CALLER_TARGET_SCORE_BOOST = 900.f;
+ /** {@link #getBaseScore} */
+ public static final float SHORTCUT_TARGET_SCORE_BOOST = 90.f;
+ private static final float PINNED_SHORTCUT_TARGET_SCORE_BOOST = 1000.f;
+
+ private final int mMaxShortcutTargetsPerApp;
+ private final ChooserListCommunicator mChooserListCommunicator;
+ private final SelectableTargetInfo.SelectableTargetInfoCommunicator
+ mSelectableTargetInfoCommunicator;
+ private final ChooserActivityLogger mChooserActivityLogger;
+
+ private int mNumShortcutResults = 0;
+ private final Map<TargetInfo, AsyncTask> mIconLoaders = new HashMap<>();
+ private boolean mApplySharingAppLimits;
+
+ // Reserve spots for incoming direct share targets by adding placeholders
+ private ChooserTargetInfo
+ mPlaceHolderTargetInfo = new ChooserActivity.PlaceHolderTargetInfo();
+ private final List<ChooserTargetInfo> mServiceTargets = new ArrayList<>();
+ private final List<DisplayResolveInfo> mCallerTargets = new ArrayList<>();
+
+ private final ChooserActivity.BaseChooserTargetComparator mBaseTargetComparator =
+ new ChooserActivity.BaseChooserTargetComparator();
+ private boolean mListViewDataChanged = false;
+
+ // Sorted list of DisplayResolveInfos for the alphabetical app section.
+ private List<DisplayResolveInfo> mSortedList = new ArrayList<>();
+ private AppPredictor mAppPredictor;
+ private AppPredictor.Callback mAppPredictorCallback;
+
+ private LoadDirectShareIconTaskProvider mTestLoadDirectShareTaskProvider;
+
+ // For pinned direct share labels, if the text spans multiple lines, the TextView will consume
+ // the full width, even if the characters actually take up less than that. Measure the actual
+ // line widths and constrain the View's width based upon that so that the pin doesn't end up
+ // very far from the text.
+ private final View.OnLayoutChangeListener mPinTextSpacingListener =
+ new View.OnLayoutChangeListener() {
+ @Override
+ public void onLayoutChange(View v, int left, int top, int right, int bottom,
+ int oldLeft, int oldTop, int oldRight, int oldBottom) {
+ TextView textView = (TextView) v;
+ Layout layout = textView.getLayout();
+ if (layout != null) {
+ int textWidth = 0;
+ for (int line = 0; line < layout.getLineCount(); line++) {
+ textWidth = Math.max((int) Math.ceil(layout.getLineMax(line)),
+ textWidth);
+ }
+ int desiredWidth = textWidth + textView.getPaddingLeft()
+ + textView.getPaddingRight();
+ if (textView.getWidth() > desiredWidth) {
+ ViewGroup.LayoutParams params = textView.getLayoutParams();
+ params.width = desiredWidth;
+ textView.setLayoutParams(params);
+ // Need to wait until layout pass is over before requesting layout.
+ textView.post(() -> textView.requestLayout());
+ }
+ textView.removeOnLayoutChangeListener(this);
+ }
+ }
+ };
+
+ public ChooserListAdapter(Context context, List<Intent> payloadIntents,
+ Intent[] initialIntents, List<ResolveInfo> rList,
+ boolean filterLastUsed, ResolverListController resolverListController,
+ ChooserListCommunicator chooserListCommunicator,
+ SelectableTargetInfo.SelectableTargetInfoCommunicator selectableTargetInfoCommunicator,
+ PackageManager packageManager,
+ ChooserActivityLogger chooserActivityLogger) {
+ // Don't send the initial intents through the shared ResolverActivity path,
+ // we want to separate them into a different section.
+ super(context, payloadIntents, null, rList, filterLastUsed,
+ resolverListController, chooserListCommunicator, false);
+
+ mMaxShortcutTargetsPerApp =
+ context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp);
+ mChooserListCommunicator = chooserListCommunicator;
+ createPlaceHolders();
+ mSelectableTargetInfoCommunicator = selectableTargetInfoCommunicator;
+ mChooserActivityLogger = chooserActivityLogger;
+
+ if (initialIntents != null) {
+ for (int i = 0; i < initialIntents.length; i++) {
+ final Intent ii = initialIntents[i];
+ if (ii == null) {
+ continue;
+ }
+
+ // We reimplement Intent#resolveActivityInfo here because if we have an
+ // implicit intent, we want the ResolveInfo returned by PackageManager
+ // instead of one we reconstruct ourselves. The ResolveInfo returned might
+ // have extra metadata and resolvePackageName set and we want to respect that.
+ ResolveInfo ri = null;
+ ActivityInfo ai = null;
+ final ComponentName cn = ii.getComponent();
+ if (cn != null) {
+ try {
+ ai = packageManager.getActivityInfo(ii.getComponent(), 0);
+ ri = new ResolveInfo();
+ ri.activityInfo = ai;
+ } catch (PackageManager.NameNotFoundException ignored) {
+ // ai will == null below
+ }
+ }
+ if (ai == null) {
+ // Because of AIDL bug, resolveActivity can't accept subclasses of Intent.
+ final Intent rii = (ii.getClass() == Intent.class) ? ii : new Intent(ii);
+ ri = packageManager.resolveActivity(rii, PackageManager.MATCH_DEFAULT_ONLY);
+ ai = ri != null ? ri.activityInfo : null;
+ }
+ if (ai == null) {
+ Log.w(TAG, "No activity found for " + ii);
+ continue;
+ }
+ UserManager userManager =
+ (UserManager) context.getSystemService(Context.USER_SERVICE);
+ if (ii instanceof LabeledIntent) {
+ LabeledIntent li = (LabeledIntent) ii;
+ ri.resolvePackageName = li.getSourcePackage();
+ ri.labelRes = li.getLabelResource();
+ ri.nonLocalizedLabel = li.getNonLocalizedLabel();
+ ri.icon = li.getIconResource();
+ ri.iconResourceId = ri.icon;
+ }
+ if (userManager.isManagedProfile()) {
+ ri.noResourceId = true;
+ ri.icon = 0;
+ }
+ mCallerTargets.add(new DisplayResolveInfo(ii, ri, ii, makePresentationGetter(ri)));
+ if (mCallerTargets.size() == MAX_SUGGESTED_APP_TARGETS) break;
+ }
+ }
+ mApplySharingAppLimits = DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
+ true);
+ }
+
+ AppPredictor getAppPredictor() {
+ return mAppPredictor;
+ }
+
+ @Override
+ public void handlePackagesChanged() {
+ if (DEBUG) {
+ Log.d(TAG, "clearing queryTargets on package change");
+ }
+ createPlaceHolders();
+ mChooserListCommunicator.onHandlePackagesChanged(this);
+
+ }
+
+ @Override
+ public void notifyDataSetChanged() {
+ if (!mListViewDataChanged) {
+ mChooserListCommunicator.sendListViewUpdateMessage(getUserHandle());
+ mListViewDataChanged = true;
+ }
+ }
+
+ void refreshListView() {
+ if (mListViewDataChanged) {
+ super.notifyDataSetChanged();
+ }
+ mListViewDataChanged = false;
+ }
+
+ private void createPlaceHolders() {
+ mNumShortcutResults = 0;
+ mServiceTargets.clear();
+ for (int i = 0; i < mChooserListCommunicator.getMaxRankedTargets(); i++) {
+ mServiceTargets.add(mPlaceHolderTargetInfo);
+ }
+ }
+
+ @Override
+ View onCreateView(ViewGroup parent) {
+ return mInflater.inflate(
+ R.layout.resolve_grid_item, parent, false);
+ }
+
+ @Override
+ protected void onBindView(View view, TargetInfo info, int position) {
+ final ViewHolder holder = (ViewHolder) view.getTag();
+
+ if (info == null) {
+ holder.icon.setImageDrawable(
+ mContext.getDrawable(R.drawable.resolver_icon_placeholder));
+ return;
+ }
+
+ if (info instanceof DisplayResolveInfo) {
+ DisplayResolveInfo dri = (DisplayResolveInfo) info;
+ holder.bindLabel(dri.getDisplayLabel(), dri.getExtendedInfo(), alwaysShowSubLabel());
+ startDisplayResolveInfoIconLoading(holder, dri);
+ } else {
+ holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
+
+ if (info instanceof SelectableTargetInfo) {
+ SelectableTargetInfo selectableInfo = (SelectableTargetInfo) info;
+ // direct share targets should append the application name for a better readout
+ DisplayResolveInfo rInfo = selectableInfo.getDisplayResolveInfo();
+ CharSequence appName = rInfo != null ? rInfo.getDisplayLabel() : "";
+ CharSequence extendedInfo = selectableInfo.getExtendedInfo();
+ String contentDescription = String.join(" ", selectableInfo.getDisplayLabel(),
+ extendedInfo != null ? extendedInfo : "", appName);
+ holder.updateContentDescription(contentDescription);
+ startSelectableTargetInfoIconLoading(holder, selectableInfo);
+ } else {
+ holder.bindIcon(info);
+ }
+ }
+
+ // If target is loading, show a special placeholder shape in the label, make unclickable
+ if (info instanceof ChooserActivity.PlaceHolderTargetInfo) {
+ final int maxWidth = mContext.getResources().getDimensionPixelSize(
+ R.dimen.chooser_direct_share_label_placeholder_max_width);
+ holder.text.setMaxWidth(maxWidth);
+ holder.text.setBackground(mContext.getResources().getDrawable(
+ R.drawable.chooser_direct_share_label_placeholder, mContext.getTheme()));
+ // Prevent rippling by removing background containing ripple
+ holder.itemView.setBackground(null);
+ } else {
+ holder.text.setMaxWidth(Integer.MAX_VALUE);
+ holder.text.setBackground(null);
+ holder.itemView.setBackground(holder.defaultItemViewBackground);
+ }
+
+ // Always remove the spacing listener, attach as needed to direct share targets below.
+ holder.text.removeOnLayoutChangeListener(mPinTextSpacingListener);
+
+ if (info instanceof MultiDisplayResolveInfo) {
+ // If the target is grouped show an indicator
+ Drawable bkg = mContext.getDrawable(R.drawable.chooser_group_background);
+ holder.text.setPaddingRelative(0, 0, bkg.getIntrinsicWidth() /* end */, 0);
+ holder.text.setBackground(bkg);
+ } else if (info.isPinned() && (getPositionTargetType(position) == TARGET_STANDARD
+ || getPositionTargetType(position) == TARGET_SERVICE)) {
+ // If the appShare or directShare target is pinned and in the suggested row show a
+ // pinned indicator
+ Drawable bkg = mContext.getDrawable(R.drawable.chooser_pinned_background);
+ holder.text.setPaddingRelative(bkg.getIntrinsicWidth() /* start */, 0, 0, 0);
+ holder.text.setBackground(bkg);
+ holder.text.addOnLayoutChangeListener(mPinTextSpacingListener);
+ } else {
+ holder.text.setBackground(null);
+ holder.text.setPaddingRelative(0, 0, 0, 0);
+ }
+ }
+
+ private void startDisplayResolveInfoIconLoading(ViewHolder holder, DisplayResolveInfo info) {
+ LoadIconTask task = (LoadIconTask) mIconLoaders.get(info);
+ if (task == null) {
+ task = new LoadIconTask(info, holder);
+ mIconLoaders.put(info, task);
+ task.execute();
+ } else {
+ // The holder was potentially changed as the underlying items were
+ // reshuffled, so reset the target holder
+ task.setViewHolder(holder);
+ }
+ }
+
+ private void startSelectableTargetInfoIconLoading(
+ ViewHolder holder, SelectableTargetInfo info) {
+ LoadDirectShareIconTask task = (LoadDirectShareIconTask) mIconLoaders.get(info);
+ if (task == null) {
+ task = mTestLoadDirectShareTaskProvider == null
+ ? new LoadDirectShareIconTask(info)
+ : mTestLoadDirectShareTaskProvider.get();
+ mIconLoaders.put(info, task);
+ task.loadIcon();
+ }
+ task.setViewHolder(holder);
+ }
+
+ void updateAlphabeticalList() {
+ new AsyncTask<Void, Void, List<DisplayResolveInfo>>() {
+ @Override
+ protected List<DisplayResolveInfo> doInBackground(Void... voids) {
+ List<DisplayResolveInfo> allTargets = new ArrayList<>();
+ allTargets.addAll(mDisplayList);
+ allTargets.addAll(mCallerTargets);
+ if (!mEnableStackedApps) {
+ return allTargets;
+ }
+ // Consolidate multiple targets from same app.
+ Map<String, DisplayResolveInfo> consolidated = new HashMap<>();
+ for (DisplayResolveInfo info : allTargets) {
+ String resolvedTarget = info.getResolvedComponentName().getPackageName()
+ + '#' + info.getDisplayLabel();
+ DisplayResolveInfo multiDri = consolidated.get(resolvedTarget);
+ if (multiDri == null) {
+ consolidated.put(resolvedTarget, info);
+ } else if (multiDri instanceof MultiDisplayResolveInfo) {
+ ((MultiDisplayResolveInfo) multiDri).addTarget(info);
+ } else {
+ // create consolidated target from the single DisplayResolveInfo
+ MultiDisplayResolveInfo multiDisplayResolveInfo =
+ new MultiDisplayResolveInfo(resolvedTarget, multiDri);
+ multiDisplayResolveInfo.addTarget(info);
+ consolidated.put(resolvedTarget, multiDisplayResolveInfo);
+ }
+ }
+ List<DisplayResolveInfo> groupedTargets = new ArrayList<>();
+ groupedTargets.addAll(consolidated.values());
+ Collections.sort(groupedTargets, new ChooserActivity.AzInfoComparator(mContext));
+ return groupedTargets;
+ }
+ @Override
+ protected void onPostExecute(List<DisplayResolveInfo> newList) {
+ mSortedList = newList;
+ notifyDataSetChanged();
+ }
+ }.execute();
+ }
+
+ @Override
+ public int getCount() {
+ return getRankedTargetCount() + getAlphaTargetCount()
+ + getSelectableServiceTargetCount() + getCallerTargetCount();
+ }
+
+ @Override
+ public int getUnfilteredCount() {
+ int appTargets = super.getUnfilteredCount();
+ if (appTargets > mChooserListCommunicator.getMaxRankedTargets()) {
+ appTargets = appTargets + mChooserListCommunicator.getMaxRankedTargets();
+ }
+ return appTargets + getSelectableServiceTargetCount() + getCallerTargetCount();
+ }
+
+
+ public int getCallerTargetCount() {
+ return mCallerTargets.size();
+ }
+
+ /**
+ * Filter out placeholders and non-selectable service targets
+ */
+ public int getSelectableServiceTargetCount() {
+ int count = 0;
+ for (ChooserTargetInfo info : mServiceTargets) {
+ if (info instanceof SelectableTargetInfo) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public int getServiceTargetCount() {
+ if (mChooserListCommunicator.isSendAction(mChooserListCommunicator.getTargetIntent())
+ && !ActivityManager.isLowRamDeviceStatic()) {
+ return Math.min(mServiceTargets.size(), mChooserListCommunicator.getMaxRankedTargets());
+ }
+
+ return 0;
+ }
+
+ int getAlphaTargetCount() {
+ int groupedCount = mSortedList.size();
+ int ungroupedCount = mCallerTargets.size() + mDisplayList.size();
+ return ungroupedCount > mChooserListCommunicator.getMaxRankedTargets() ? groupedCount : 0;
+ }
+
+ /**
+ * Fetch ranked app target count
+ */
+ public int getRankedTargetCount() {
+ int spacesAvailable =
+ mChooserListCommunicator.getMaxRankedTargets() - getCallerTargetCount();
+ return Math.min(spacesAvailable, super.getCount());
+ }
+
+ public int getPositionTargetType(int position) {
+ int offset = 0;
+
+ final int serviceTargetCount = getServiceTargetCount();
+ if (position < serviceTargetCount) {
+ return TARGET_SERVICE;
+ }
+ offset += serviceTargetCount;
+
+ final int callerTargetCount = getCallerTargetCount();
+ if (position - offset < callerTargetCount) {
+ return TARGET_CALLER;
+ }
+ offset += callerTargetCount;
+
+ final int rankedTargetCount = getRankedTargetCount();
+ if (position - offset < rankedTargetCount) {
+ return TARGET_STANDARD;
+ }
+ offset += rankedTargetCount;
+
+ final int standardTargetCount = getAlphaTargetCount();
+ if (position - offset < standardTargetCount) {
+ return TARGET_STANDARD_AZ;
+ }
+
+ return TARGET_BAD;
+ }
+
+ @Override
+ public TargetInfo getItem(int position) {
+ return targetInfoForPosition(position, true);
+ }
+
+
+ /**
+ * Find target info for a given position.
+ * Since ChooserActivity displays several sections of content, determine which
+ * section provides this item.
+ */
+ @Override
+ public TargetInfo targetInfoForPosition(int position, boolean filtered) {
+ if (position == NO_POSITION) {
+ return null;
+ }
+
+ int offset = 0;
+
+ // Direct share targets
+ final int serviceTargetCount = filtered ? getServiceTargetCount() :
+ getSelectableServiceTargetCount();
+ if (position < serviceTargetCount) {
+ return mServiceTargets.get(position);
+ }
+ offset += serviceTargetCount;
+
+ // Targets provided by calling app
+ final int callerTargetCount = getCallerTargetCount();
+ if (position - offset < callerTargetCount) {
+ return mCallerTargets.get(position - offset);
+ }
+ offset += callerTargetCount;
+
+ // Ranked standard app targets
+ final int rankedTargetCount = getRankedTargetCount();
+ if (position - offset < rankedTargetCount) {
+ return filtered ? super.getItem(position - offset)
+ : getDisplayResolveInfo(position - offset);
+ }
+ offset += rankedTargetCount;
+
+ // Alphabetical complete app target list.
+ if (position - offset < getAlphaTargetCount() && !mSortedList.isEmpty()) {
+ return mSortedList.get(position - offset);
+ }
+
+ return null;
+ }
+
+ // Check whether {@code dri} should be added into mDisplayList.
+ @Override
+ protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {
+ // Checks if this info is already listed in callerTargets.
+ for (TargetInfo existingInfo : mCallerTargets) {
+ if (mResolverListCommunicator
+ .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) {
+ return false;
+ }
+ }
+ return super.shouldAddResolveInfo(dri);
+ }
+
+ /**
+ * Fetch surfaced direct share target info
+ */
+ public List<ChooserTargetInfo> getSurfacedTargetInfo() {
+ int maxSurfacedTargets = mChooserListCommunicator.getMaxRankedTargets();
+ return mServiceTargets.subList(0,
+ Math.min(maxSurfacedTargets, getSelectableServiceTargetCount()));
+ }
+
+
+ /**
+ * Evaluate targets for inclusion in the direct share area. May not be included
+ * if score is too low.
+ */
+ public void addServiceResults(DisplayResolveInfo origTarget, List<ChooserTarget> targets,
+ @ChooserActivity.ShareTargetType int targetType,
+ Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos) {
+ if (DEBUG) {
+ Log.d(TAG, "addServiceResults " + origTarget.getResolvedComponentName() + ", "
+ + targets.size()
+ + " targets");
+ }
+ if (targets.size() == 0) {
+ return;
+ }
+ final float baseScore = getBaseScore(origTarget, targetType);
+ Collections.sort(targets, mBaseTargetComparator);
+ final boolean isShortcutResult =
+ (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
+ || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
+ final int maxTargets = isShortcutResult ? mMaxShortcutTargetsPerApp
+ : MAX_CHOOSER_TARGETS_PER_APP;
+ final int targetsLimit = mApplySharingAppLimits ? Math.min(targets.size(), maxTargets)
+ : targets.size();
+ float lastScore = 0;
+ boolean shouldNotify = false;
+ for (int i = 0, count = targetsLimit; i < count; i++) {
+ final ChooserTarget target = targets.get(i);
+ float targetScore = target.getScore();
+ if (mApplySharingAppLimits) {
+ targetScore *= baseScore;
+ if (i > 0 && targetScore >= lastScore) {
+ // Apply a decay so that the top app can't crowd out everything else.
+ // This incents ChooserTargetServices to define what's truly better.
+ targetScore = lastScore * 0.95f;
+ }
+ }
+ ShortcutInfo shortcutInfo = isShortcutResult ? directShareToShortcutInfos.get(target)
+ : null;
+ if ((shortcutInfo != null) && shortcutInfo.isPinned()) {
+ targetScore += PINNED_SHORTCUT_TARGET_SCORE_BOOST;
+ }
+ UserHandle userHandle = getUserHandle();
+ Context contextAsUser = mContext.createContextAsUser(userHandle, 0 /* flags */);
+ boolean isInserted = insertServiceTarget(new SelectableTargetInfo(contextAsUser,
+ origTarget, target, targetScore, mSelectableTargetInfoCommunicator,
+ shortcutInfo));
+
+ if (isInserted && isShortcutResult) {
+ mNumShortcutResults++;
+ }
+
+ shouldNotify |= isInserted;
+
+ if (DEBUG) {
+ Log.d(TAG, " => " + target.toString() + " score=" + targetScore
+ + " base=" + target.getScore()
+ + " lastScore=" + lastScore
+ + " baseScore=" + baseScore
+ + " applyAppLimit=" + mApplySharingAppLimits);
+ }
+
+ lastScore = targetScore;
+ }
+
+ if (shouldNotify) {
+ notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * The return number have to exceed a minimum limit to make direct share area expandable. When
+ * append direct share targets is enabled, return count of all available targets parking in the
+ * memory; otherwise, it is shortcuts count which will help reduce the amount of visible
+ * shuffling due to older-style direct share targets.
+ */
+ int getNumServiceTargetsForExpand() {
+ return mNumShortcutResults;
+ }
+
+ /**
+ * Use the scoring system along with artificial boosts to create up to 4 distinct buckets:
+ * <ol>
+ * <li>App-supplied targets
+ * <li>Shortcuts ranked via App Prediction Manager
+ * <li>Shortcuts ranked via legacy heuristics
+ * <li>Legacy direct share targets
+ * </ol>
+ */
+ public float getBaseScore(
+ DisplayResolveInfo target,
+ @ChooserActivity.ShareTargetType int targetType) {
+ if (target == null) {
+ return CALLER_TARGET_SCORE_BOOST;
+ }
+ float score = super.getScore(target);
+ if (targetType == TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER
+ || targetType == TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE) {
+ return score * SHORTCUT_TARGET_SCORE_BOOST;
+ }
+ return score;
+ }
+
+ /**
+ * Calling this marks service target loading complete, and will attempt to no longer
+ * update the direct share area.
+ */
+ public void completeServiceTargetLoading() {
+ mServiceTargets.removeIf(o -> o instanceof ChooserActivity.PlaceHolderTargetInfo);
+ if (mServiceTargets.isEmpty()) {
+ mServiceTargets.add(new ChooserActivity.EmptyTargetInfo());
+ mChooserActivityLogger.logSharesheetEmptyDirectShareRow();
+ }
+ notifyDataSetChanged();
+ }
+
+ private boolean insertServiceTarget(ChooserTargetInfo chooserTargetInfo) {
+ // Avoid inserting any potentially late results
+ if (mServiceTargets.size() == 1
+ && mServiceTargets.get(0) instanceof ChooserActivity.EmptyTargetInfo) {
+ return false;
+ }
+
+ // Check for duplicates and abort if found
+ for (ChooserTargetInfo otherTargetInfo : mServiceTargets) {
+ if (chooserTargetInfo.isSimilar(otherTargetInfo)) {
+ return false;
+ }
+ }
+
+ int currentSize = mServiceTargets.size();
+ final float newScore = chooserTargetInfo.getModifiedScore();
+ for (int i = 0; i < Math.min(currentSize, mChooserListCommunicator.getMaxRankedTargets());
+ i++) {
+ final ChooserTargetInfo serviceTarget = mServiceTargets.get(i);
+ if (serviceTarget == null) {
+ mServiceTargets.set(i, chooserTargetInfo);
+ return true;
+ } else if (newScore > serviceTarget.getModifiedScore()) {
+ mServiceTargets.add(i, chooserTargetInfo);
+ return true;
+ }
+ }
+
+ if (currentSize < mChooserListCommunicator.getMaxRankedTargets()) {
+ mServiceTargets.add(chooserTargetInfo);
+ return true;
+ }
+
+ return false;
+ }
+
+ public ChooserTarget getChooserTargetForValue(int value) {
+ return mServiceTargets.get(value).getChooserTarget();
+ }
+
+ protected boolean alwaysShowSubLabel() {
+ // Always show a subLabel for visual consistency across list items. Show an empty
+ // subLabel if the subLabel is the same as the label
+ return true;
+ }
+
+ /**
+ * Rather than fully sorting the input list, this sorting task will put the top k elements
+ * in the head of input list and fill the tail with other elements in undetermined order.
+ */
+ @Override
+ AsyncTask<List<ResolvedComponentInfo>,
+ Void,
+ List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) {
+ return new AsyncTask<List<ResolvedComponentInfo>,
+ Void,
+ List<ResolvedComponentInfo>>() {
+ @Override
+ protected List<ResolvedComponentInfo> doInBackground(
+ List<ResolvedComponentInfo>... params) {
+ Trace.beginSection("ChooserListAdapter#SortingTask");
+ mResolverListController.topK(params[0],
+ mChooserListCommunicator.getMaxRankedTargets());
+ Trace.endSection();
+ return params[0];
+ }
+ @Override
+ protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
+ processSortedList(sortedComponents, doPostProcessing);
+ if (doPostProcessing) {
+ mChooserListCommunicator.updateProfileViewButton();
+ notifyDataSetChanged();
+ }
+ }
+ };
+ }
+
+ public void setAppPredictor(AppPredictor appPredictor) {
+ mAppPredictor = appPredictor;
+ }
+
+ public void setAppPredictorCallback(AppPredictor.Callback appPredictorCallback) {
+ mAppPredictorCallback = appPredictorCallback;
+ }
+
+ public void destroyAppPredictor() {
+ if (getAppPredictor() != null) {
+ getAppPredictor().unregisterPredictionUpdates(mAppPredictorCallback);
+ getAppPredictor().destroy();
+ setAppPredictor(null);
+ }
+ }
+
+ /**
+ * An alias for onBindView to use with unit tests.
+ */
+ @VisibleForTesting
+ public void testViewBind(View view, TargetInfo info, int position) {
+ onBindView(view, info, position);
+ }
+
+ @VisibleForTesting
+ public void setTestLoadDirectShareTaskProvider(LoadDirectShareIconTaskProvider provider) {
+ mTestLoadDirectShareTaskProvider = provider;
+ }
+
+ /**
+ * Necessary methods to communicate between {@link ChooserListAdapter}
+ * and {@link ChooserActivity}.
+ */
+ @VisibleForTesting
+ public interface ChooserListCommunicator extends ResolverListCommunicator {
+
+ int getMaxRankedTargets();
+
+ void sendListViewUpdateMessage(UserHandle userHandle);
+
+ boolean isSendAction(Intent targetIntent);
+ }
+
+ /**
+ * Loads direct share targets icons.
+ */
+ @VisibleForTesting
+ public class LoadDirectShareIconTask extends AsyncTask<Void, Void, Void> {
+ private final SelectableTargetInfo mTargetInfo;
+ private ViewHolder mViewHolder;
+
+ private LoadDirectShareIconTask(SelectableTargetInfo targetInfo) {
+ mTargetInfo = targetInfo;
+ }
+
+ @Override
+ protected Void doInBackground(Void... voids) {
+ mTargetInfo.loadIcon();
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void arg) {
+ if (mViewHolder != null) {
+ mViewHolder.bindIcon(mTargetInfo);
+ notifyDataSetChanged();
+ }
+ }
+
+ /**
+ * Specifies a view holder that will be updated when the task is completed.
+ */
+ public void setViewHolder(ViewHolder viewHolder) {
+ mViewHolder = viewHolder;
+ mViewHolder.bindIcon(mTargetInfo);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * An alias for execute to use with unit tests.
+ */
+ public void loadIcon() {
+ execute();
+ }
+ }
+
+ /**
+ * An interface for the unit tests to override icon loading task creation
+ */
+ @VisibleForTesting
+ public interface LoadDirectShareIconTaskProvider {
+ /**
+ * Provides an instance of the task.
+ * @return
+ */
+ LoadDirectShareIconTask get();
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
new file mode 100644
index 00000000..da78fc81
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserMultiProfilePagerAdapter.java
@@ -0,0 +1,321 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_SHARE_WITH_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_PERSONAL_APPS;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE;
+
+import android.annotation.Nullable;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.os.UserHandle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.widget.GridLayoutManager;
+import com.android.internal.widget.PagerAdapter;
+import com.android.internal.widget.RecyclerView;
+
+/**
+ * A {@link PagerAdapter} which describes the work and personal profile share sheet screens.
+ */
+@VisibleForTesting
+public class ChooserMultiProfilePagerAdapter extends AbstractMultiProfilePagerAdapter {
+ private static final int SINGLE_CELL_SPAN_SIZE = 1;
+
+ private final ChooserProfileDescriptor[] mItems;
+ private final boolean mIsSendAction;
+ private int mBottomOffset;
+ private int mMaxTargetsPerRow;
+
+ ChooserMultiProfilePagerAdapter(Context context,
+ ChooserActivity.ChooserGridAdapter adapter,
+ UserHandle personalProfileUserHandle,
+ UserHandle workProfileUserHandle,
+ boolean isSendAction, int maxTargetsPerRow) {
+ super(context, /* currentPage */ 0, personalProfileUserHandle, workProfileUserHandle);
+ mItems = new ChooserProfileDescriptor[] {
+ createProfileDescriptor(adapter)
+ };
+ mIsSendAction = isSendAction;
+ mMaxTargetsPerRow = maxTargetsPerRow;
+ }
+
+ ChooserMultiProfilePagerAdapter(Context context,
+ ChooserActivity.ChooserGridAdapter personalAdapter,
+ ChooserActivity.ChooserGridAdapter workAdapter,
+ @Profile int defaultProfile,
+ UserHandle personalProfileUserHandle,
+ UserHandle workProfileUserHandle,
+ boolean isSendAction, int maxTargetsPerRow) {
+ super(context, /* currentPage */ defaultProfile, personalProfileUserHandle,
+ workProfileUserHandle);
+ mItems = new ChooserProfileDescriptor[] {
+ createProfileDescriptor(personalAdapter),
+ createProfileDescriptor(workAdapter)
+ };
+ mIsSendAction = isSendAction;
+ mMaxTargetsPerRow = maxTargetsPerRow;
+ }
+
+ private ChooserProfileDescriptor createProfileDescriptor(
+ ChooserActivity.ChooserGridAdapter adapter) {
+ final LayoutInflater inflater = LayoutInflater.from(getContext());
+ final ViewGroup rootView =
+ (ViewGroup) inflater.inflate(R.layout.chooser_list_per_profile, null, false);
+ ChooserProfileDescriptor profileDescriptor =
+ new ChooserProfileDescriptor(rootView, adapter);
+ profileDescriptor.recyclerView.setAccessibilityDelegateCompat(
+ new ChooserRecyclerViewAccessibilityDelegate(profileDescriptor.recyclerView));
+ return profileDescriptor;
+ }
+
+ public void setMaxTargetsPerRow(int maxTargetsPerRow) {
+ mMaxTargetsPerRow = maxTargetsPerRow;
+ }
+
+ RecyclerView getListViewForIndex(int index) {
+ return getItem(index).recyclerView;
+ }
+
+ @Override
+ ChooserProfileDescriptor getItem(int pageIndex) {
+ return mItems[pageIndex];
+ }
+
+ @Override
+ int getItemCount() {
+ return mItems.length;
+ }
+
+ @Override
+ @VisibleForTesting
+ public ChooserActivity.ChooserGridAdapter getAdapterForIndex(int pageIndex) {
+ return mItems[pageIndex].chooserGridAdapter;
+ }
+
+ @Override
+ @Nullable
+ ChooserListAdapter getListAdapterForUserHandle(UserHandle userHandle) {
+ if (getActiveListAdapter().getUserHandle().equals(userHandle)) {
+ return getActiveListAdapter();
+ } else if (getInactiveListAdapter() != null
+ && getInactiveListAdapter().getUserHandle().equals(userHandle)) {
+ return getInactiveListAdapter();
+ }
+ return null;
+ }
+
+ @Override
+ void setupListAdapter(int pageIndex) {
+ final RecyclerView recyclerView = getItem(pageIndex).recyclerView;
+ ChooserActivity.ChooserGridAdapter chooserGridAdapter =
+ getItem(pageIndex).chooserGridAdapter;
+ GridLayoutManager glm = (GridLayoutManager) recyclerView.getLayoutManager();
+ glm.setSpanCount(mMaxTargetsPerRow);
+ glm.setSpanSizeLookup(
+ new GridLayoutManager.SpanSizeLookup() {
+ @Override
+ public int getSpanSize(int position) {
+ return chooserGridAdapter.shouldCellSpan(position)
+ ? SINGLE_CELL_SPAN_SIZE
+ : glm.getSpanCount();
+ }
+ });
+ }
+
+ @Override
+ @VisibleForTesting
+ public ChooserListAdapter getActiveListAdapter() {
+ return getAdapterForIndex(getCurrentPage()).getListAdapter();
+ }
+
+ @Override
+ @VisibleForTesting
+ public ChooserListAdapter getInactiveListAdapter() {
+ if (getCount() == 1) {
+ return null;
+ }
+ return getAdapterForIndex(1 - getCurrentPage()).getListAdapter();
+ }
+
+ @Override
+ public ResolverListAdapter getPersonalListAdapter() {
+ return getAdapterForIndex(PROFILE_PERSONAL).getListAdapter();
+ }
+
+ @Override
+ @Nullable
+ public ResolverListAdapter getWorkListAdapter() {
+ return getAdapterForIndex(PROFILE_WORK).getListAdapter();
+ }
+
+ @Override
+ ChooserActivity.ChooserGridAdapter getCurrentRootAdapter() {
+ return getAdapterForIndex(getCurrentPage());
+ }
+
+ @Override
+ RecyclerView getActiveAdapterView() {
+ return getListViewForIndex(getCurrentPage());
+ }
+
+ @Override
+ @Nullable
+ RecyclerView getInactiveAdapterView() {
+ if (getCount() == 1) {
+ return null;
+ }
+ return getListViewForIndex(1 - getCurrentPage());
+ }
+
+ @Override
+ String getMetricsCategory() {
+ return ResolverActivity.METRICS_CATEGORY_CHOOSER;
+ }
+
+ @Override
+ protected void showWorkProfileOffEmptyState(ResolverListAdapter activeListAdapter,
+ View.OnClickListener listener) {
+ showEmptyState(activeListAdapter,
+ getWorkAppPausedTitle(),
+ /* subtitle = */ null,
+ listener);
+ }
+
+ @Override
+ protected void showNoPersonalToWorkIntentsEmptyState(ResolverListAdapter activeListAdapter) {
+ if (mIsSendAction) {
+ showEmptyState(activeListAdapter,
+ getCrossProfileBlockedTitle(),
+ getCantShareWithWorkMessage());
+ } else {
+ showEmptyState(activeListAdapter,
+ getCrossProfileBlockedTitle(),
+ getCantAccessWorkMessage());
+ }
+ }
+
+ @Override
+ protected void showNoWorkToPersonalIntentsEmptyState(ResolverListAdapter activeListAdapter) {
+ if (mIsSendAction) {
+ showEmptyState(activeListAdapter,
+ getCrossProfileBlockedTitle(),
+ getCantShareWithPersonalMessage());
+ } else {
+ showEmptyState(activeListAdapter,
+ getCrossProfileBlockedTitle(),
+ getCantAccessPersonalMessage());
+ }
+ }
+
+ @Override
+ protected void showNoPersonalAppsAvailableEmptyState(ResolverListAdapter listAdapter) {
+ showEmptyState(listAdapter, getNoPersonalAppsAvailableMessage(), /* subtitle= */ null);
+
+ }
+
+ @Override
+ protected void showNoWorkAppsAvailableEmptyState(ResolverListAdapter listAdapter) {
+ showEmptyState(listAdapter, getNoWorkAppsAvailableMessage(), /* subtitle = */ null);
+ }
+
+ private String getWorkAppPausedTitle() {
+ return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_WORK_PAUSED_TITLE,
+ () -> getContext().getString(R.string.resolver_turn_on_work_apps));
+ }
+
+ private String getCrossProfileBlockedTitle() {
+ return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ () -> getContext().getString(R.string.resolver_cross_profile_blocked));
+ }
+
+ private String getCantShareWithWorkMessage() {
+ return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_CANT_SHARE_WITH_WORK,
+ () -> getContext().getString(
+ R.string.resolver_cant_share_with_work_apps_explanation));
+ }
+
+ private String getCantShareWithPersonalMessage() {
+ return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_CANT_SHARE_WITH_PERSONAL,
+ () -> getContext().getString(
+ R.string.resolver_cant_share_with_personal_apps_explanation));
+ }
+
+ private String getCantAccessWorkMessage() {
+ return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_CANT_ACCESS_WORK,
+ () -> getContext().getString(
+ R.string.resolver_cant_access_work_apps_explanation));
+ }
+
+ private String getCantAccessPersonalMessage() {
+ return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_CANT_ACCESS_PERSONAL,
+ () -> getContext().getString(
+ R.string.resolver_cant_access_personal_apps_explanation));
+ }
+
+ private String getNoWorkAppsAvailableMessage() {
+ return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_NO_WORK_APPS,
+ () -> getContext().getString(
+ R.string.resolver_no_work_apps_available));
+ }
+
+ private String getNoPersonalAppsAvailableMessage() {
+ return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_NO_PERSONAL_APPS,
+ () -> getContext().getString(
+ R.string.resolver_no_personal_apps_available));
+ }
+
+
+ void setEmptyStateBottomOffset(int bottomOffset) {
+ mBottomOffset = bottomOffset;
+ }
+
+ @Override
+ protected void setupContainerPadding(View container) {
+ int initialBottomPadding = getContext().getResources().getDimensionPixelSize(
+ R.dimen.resolver_empty_state_container_padding_bottom);
+ container.setPadding(container.getPaddingLeft(), container.getPaddingTop(),
+ container.getPaddingRight(), initialBottomPadding + mBottomOffset);
+ }
+
+ class ChooserProfileDescriptor extends ProfileDescriptor {
+ private ChooserActivity.ChooserGridAdapter chooserGridAdapter;
+ private RecyclerView recyclerView;
+ ChooserProfileDescriptor(ViewGroup rootView, ChooserActivity.ChooserGridAdapter adapter) {
+ super(rootView);
+ chooserGridAdapter = adapter;
+ recyclerView = rootView.findViewById(com.android.internal.R.id.resolver_list);
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
new file mode 100644
index 00000000..67571b44
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserRecyclerViewAccessibilityDelegate.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import android.annotation.NonNull;
+import android.graphics.Rect;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.widget.RecyclerView;
+import com.android.internal.widget.RecyclerViewAccessibilityDelegate;
+
+class ChooserRecyclerViewAccessibilityDelegate extends RecyclerViewAccessibilityDelegate {
+ private final Rect mTempRect = new Rect();
+ private final int[] mConsumed = new int[2];
+
+ ChooserRecyclerViewAccessibilityDelegate(RecyclerView recyclerView) {
+ super(recyclerView);
+ }
+
+ @Override
+ public boolean onRequestSendAccessibilityEvent(
+ @NonNull ViewGroup host,
+ @NonNull View view,
+ @NonNull AccessibilityEvent event) {
+ boolean result = super.onRequestSendAccessibilityEvent(host, view, event);
+ if (result && event.getEventType() == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED) {
+ ensureViewOnScreenVisibility((RecyclerView) host, view);
+ }
+ return result;
+ }
+
+ /**
+ * Bring the view that received accessibility focus on the screen.
+ * The method's logic is based on a model where RecyclerView is a child of another scrollable
+ * component (ResolverDrawerLayout) and can be partially scrolled off the screen. In that case,
+ * RecyclerView's children that are positioned fully within RecyclerView bounds but scrolled
+ * out of the screen by the outer component, when selected by the accessibility navigation will
+ * remain off the screen (as neither components detect such specific case).
+ * If the view that receiving accessibility focus is scrolled of the screen, perform the nested
+ * scrolling to make in visible.
+ */
+ private void ensureViewOnScreenVisibility(RecyclerView recyclerView, View view) {
+ View child = recyclerView.findContainingItemView(view);
+ if (child == null) {
+ return;
+ }
+ recyclerView.getBoundsOnScreen(mTempRect, true);
+ int recyclerOnScreenTop = mTempRect.top;
+ int recyclerOnScreenBottom = mTempRect.bottom;
+ child.getBoundsOnScreen(mTempRect);
+ int dy = 0;
+ // if needed, do the page-length scroll instead of just a row-length scroll as
+ // ResolverDrawerLayout snaps to the compact view and the row-length scroll can be snapped
+ // back right away.
+ if (mTempRect.top < recyclerOnScreenTop) {
+ // snap to the bottom
+ dy = mTempRect.bottom - recyclerOnScreenBottom;
+ } else if (mTempRect.bottom > recyclerOnScreenBottom) {
+ // snap to the top
+ dy = mTempRect.top - recyclerOnScreenTop;
+ }
+ nestedVerticalScrollBy(recyclerView, dy);
+ }
+
+ private void nestedVerticalScrollBy(RecyclerView recyclerView, int dy) {
+ if (dy == 0) {
+ return;
+ }
+ recyclerView.startNestedScroll(View.SCROLL_AXIS_VERTICAL);
+ if (recyclerView.dispatchNestedPreScroll(0, dy, mConsumed, null)) {
+ dy -= mConsumed[1];
+ }
+ recyclerView.scrollBy(0, dy);
+ recyclerView.stopNestedScroll();
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
new file mode 100644
index 00000000..ae08ace2
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserStackedAppDialogFragment.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.intentresolver;
+
+import android.content.DialogInterface;
+import android.content.pm.PackageManager;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.UserHandle;
+
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.MultiDisplayResolveInfo;
+
+/**
+ * Shows individual actions for a "stacked" app target - such as an app with multiple posting
+ * streams represented in the Sharesheet.
+ */
+public class ChooserStackedAppDialogFragment extends ChooserTargetActionsDialogFragment
+ implements DialogInterface.OnClickListener {
+
+ static final String WHICH_KEY = "which_key";
+ static final String MULTI_DRI_KEY = "multi_dri_key";
+
+ private MultiDisplayResolveInfo mMultiDisplayResolveInfo;
+ private int mParentWhich;
+
+ public ChooserStackedAppDialogFragment() {}
+
+ void setStateFromBundle(Bundle b) {
+ mMultiDisplayResolveInfo = (MultiDisplayResolveInfo) b.get(MULTI_DRI_KEY);
+ mTargetInfos = mMultiDisplayResolveInfo.getTargets();
+ mUserHandle = (UserHandle) b.get(USER_HANDLE_KEY);
+ mParentWhich = b.getInt(WHICH_KEY);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(WHICH_KEY, mParentWhich);
+ outState.putParcelable(MULTI_DRI_KEY, mMultiDisplayResolveInfo);
+ }
+
+ @Override
+ protected CharSequence getItemLabel(DisplayResolveInfo dri) {
+ final PackageManager pm = getContext().getPackageManager();
+ return dri.getResolveInfo().loadLabel(pm);
+ }
+
+ @Override
+ protected Drawable getItemIcon(DisplayResolveInfo dri) {
+
+ // Show no icon for the group disambig dialog, null hides the imageview
+ return null;
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ mMultiDisplayResolveInfo.setSelected(which);
+ ((ChooserActivity) getActivity()).startSelected(mParentWhich, false, true);
+ dismiss();
+ }
+}
diff --git a/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
new file mode 100644
index 00000000..ffd173c7
--- /dev/null
+++ b/java/src/com/android/intentresolver/ChooserTargetActionsDialogFragment.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.intentresolver;
+
+import static android.content.Context.ACTIVITY_SERVICE;
+
+import static com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;
+
+import static java.util.stream.Collectors.toList;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.app.Dialog;
+import android.app.DialogFragment;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.IntentFilter;
+import android.content.SharedPreferences;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.util.Pair;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+
+import com.android.internal.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * Shows a dialog with actions to take on a chooser target.
+ */
+public class ChooserTargetActionsDialogFragment extends DialogFragment
+ implements DialogInterface.OnClickListener {
+
+ protected ArrayList<DisplayResolveInfo> mTargetInfos = new ArrayList<>();
+ protected UserHandle mUserHandle;
+ protected String mShortcutId;
+ protected String mShortcutTitle;
+ protected boolean mIsShortcutPinned;
+ protected IntentFilter mIntentFilter;
+
+ public static final String USER_HANDLE_KEY = "user_handle";
+ public static final String TARGET_INFOS_KEY = "target_infos";
+ public static final String SHORTCUT_ID_KEY = "shortcut_id";
+ public static final String SHORTCUT_TITLE_KEY = "shortcut_title";
+ public static final String IS_SHORTCUT_PINNED_KEY = "is_shortcut_pinned";
+ public static final String INTENT_FILTER_KEY = "intent_filter";
+
+ public ChooserTargetActionsDialogFragment() {}
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ setStateFromBundle(savedInstanceState);
+ } else {
+ setStateFromBundle(getArguments());
+ }
+ }
+
+ void setStateFromBundle(Bundle b) {
+ mTargetInfos = (ArrayList<DisplayResolveInfo>) b.get(TARGET_INFOS_KEY);
+ mUserHandle = (UserHandle) b.get(USER_HANDLE_KEY);
+ mShortcutId = b.getString(SHORTCUT_ID_KEY);
+ mShortcutTitle = b.getString(SHORTCUT_TITLE_KEY);
+ mIsShortcutPinned = b.getBoolean(IS_SHORTCUT_PINNED_KEY);
+ mIntentFilter = (IntentFilter) b.get(INTENT_FILTER_KEY);
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+
+ outState.putParcelable(ChooserTargetActionsDialogFragment.USER_HANDLE_KEY,
+ mUserHandle);
+ outState.putParcelableArrayList(ChooserTargetActionsDialogFragment.TARGET_INFOS_KEY,
+ mTargetInfos);
+ outState.putString(ChooserTargetActionsDialogFragment.SHORTCUT_ID_KEY, mShortcutId);
+ outState.putBoolean(ChooserTargetActionsDialogFragment.IS_SHORTCUT_PINNED_KEY,
+ mIsShortcutPinned);
+ outState.putString(ChooserTargetActionsDialogFragment.SHORTCUT_TITLE_KEY, mShortcutTitle);
+ outState.putParcelable(ChooserTargetActionsDialogFragment.INTENT_FILTER_KEY, mIntentFilter);
+ }
+
+ /**
+ * Recreate the layout from scratch to match new Sharesheet redlines
+ */
+ @Override
+ public View onCreateView(LayoutInflater inflater,
+ @Nullable ViewGroup container,
+ Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ setStateFromBundle(savedInstanceState);
+ } else {
+ setStateFromBundle(getArguments());
+ }
+ // Make the background transparent to show dialog rounding
+ Optional.of(getDialog()).map(Dialog::getWindow)
+ .ifPresent(window -> {
+ window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+ });
+
+ // Fetch UI details from target info
+ List<Pair<Drawable, CharSequence>> items = mTargetInfos.stream().map(dri -> {
+ return new Pair<>(getItemIcon(dri), getItemLabel(dri));
+ }).collect(toList());
+
+ View v = inflater.inflate(R.layout.chooser_dialog, container, false);
+
+ TextView title = v.findViewById(com.android.internal.R.id.title);
+ ImageView icon = v.findViewById(com.android.internal.R.id.icon);
+ RecyclerView rv = v.findViewById(com.android.internal.R.id.listContainer);
+
+ final ResolveInfoPresentationGetter pg = getProvidingAppPresentationGetter();
+ title.setText(isShortcutTarget() ? mShortcutTitle : pg.getLabel());
+ icon.setImageDrawable(pg.getIcon(mUserHandle));
+ rv.setAdapter(new VHAdapter(items));
+
+ return v;
+ }
+
+ class VHAdapter extends RecyclerView.Adapter<VH> {
+
+ List<Pair<Drawable, CharSequence>> mItems;
+
+ VHAdapter(List<Pair<Drawable, CharSequence>> items) {
+ mItems = items;
+ }
+
+ @NonNull
+ @Override
+ public VH onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
+ return new VH(LayoutInflater.from(parent.getContext()).inflate(
+ R.layout.chooser_dialog_item, parent, false));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull VH holder, int position) {
+ holder.bind(mItems.get(position), position);
+ }
+
+ @Override
+ public int getItemCount() {
+ return mItems.size();
+ }
+ }
+
+ class VH extends RecyclerView.ViewHolder {
+ TextView mLabel;
+ ImageView mIcon;
+
+ VH(@NonNull View itemView) {
+ super(itemView);
+ mLabel = itemView.findViewById(com.android.internal.R.id.text);
+ mIcon = itemView.findViewById(com.android.internal.R.id.icon);
+ }
+
+ public void bind(Pair<Drawable, CharSequence> item, int position) {
+ mLabel.setText(item.second);
+
+ if (item.first == null) {
+ mIcon.setVisibility(View.GONE);
+ } else {
+ mIcon.setVisibility(View.VISIBLE);
+ mIcon.setImageDrawable(item.first);
+ }
+
+ itemView.setOnClickListener(v -> onClick(getDialog(), position));
+ }
+ }
+
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ if (isShortcutTarget()) {
+ toggleShortcutPinned(mTargetInfos.get(which).getResolvedComponentName());
+ } else {
+ pinComponent(mTargetInfos.get(which).getResolvedComponentName());
+ }
+ ((ChooserActivity) getActivity()).handlePackagesChanged();
+ dismiss();
+ }
+
+ private void toggleShortcutPinned(ComponentName name) {
+ if (mIntentFilter == null) {
+ return;
+ }
+ // Fetch existing pinned shortcuts of the given package.
+ List<String> pinnedShortcuts = getPinnedShortcutsFromPackageAsUser(getContext(),
+ mUserHandle, mIntentFilter, name.getPackageName());
+ // If the shortcut has already been pinned, unpin it; otherwise, pin it.
+ if (mIsShortcutPinned) {
+ pinnedShortcuts.remove(mShortcutId);
+ } else {
+ pinnedShortcuts.add(mShortcutId);
+ }
+ // Update pinned shortcut list in ShortcutService via LauncherApps
+ getContext().getSystemService(LauncherApps.class).pinShortcuts(
+ name.getPackageName(), pinnedShortcuts, mUserHandle);
+ }
+
+ private static List<String> getPinnedShortcutsFromPackageAsUser(Context context,
+ UserHandle user, IntentFilter filter, String packageName) {
+ Context contextAsUser = context.createContextAsUser(user, 0 /* flags */);
+ List<ShortcutManager.ShareShortcutInfo> targets = contextAsUser.getSystemService(
+ ShortcutManager.class).getShareTargets(filter);
+ return targets.stream()
+ .map(ShortcutManager.ShareShortcutInfo::getShortcutInfo)
+ .filter(s -> s.isPinned() && s.getPackage().equals(packageName))
+ .map(ShortcutInfo::getId)
+ .collect(Collectors.toList());
+ }
+
+ private void pinComponent(ComponentName name) {
+ SharedPreferences sp = ChooserActivity.getPinnedSharedPrefs(getContext());
+ final String key = name.flattenToString();
+ boolean currentVal = sp.getBoolean(name.flattenToString(), false);
+ if (currentVal) {
+ sp.edit().remove(key).apply();
+ } else {
+ sp.edit().putBoolean(key, true).apply();
+ }
+ }
+
+ private Drawable getPinIcon(boolean isPinned) {
+ return isPinned
+ ? getContext().getDrawable(com.android.internal.R.drawable.ic_close)
+ : getContext().getDrawable(R.drawable.ic_chooser_pin_dialog);
+ }
+
+ private CharSequence getPinLabel(boolean isPinned, CharSequence targetLabel) {
+ return isPinned
+ ? getResources().getString(R.string.unpin_specific_target, targetLabel)
+ : getResources().getString(R.string.pin_specific_target, targetLabel);
+ }
+
+ @NonNull
+ protected CharSequence getItemLabel(DisplayResolveInfo dri) {
+ final PackageManager pm = getContext().getPackageManager();
+ return getPinLabel(isPinned(dri),
+ isShortcutTarget() ? mShortcutTitle : dri.getResolveInfo().loadLabel(pm));
+ }
+
+ @Nullable
+ protected Drawable getItemIcon(DisplayResolveInfo dri) {
+ return getPinIcon(isPinned(dri));
+ }
+
+ private ResolveInfoPresentationGetter getProvidingAppPresentationGetter() {
+ final ActivityManager am = (ActivityManager) getContext()
+ .getSystemService(ACTIVITY_SERVICE);
+ final int iconDpi = am.getLauncherLargeIconDensity();
+
+ // Use the matching application icon and label for the title, any TargetInfo will do
+ return new ResolveInfoPresentationGetter(getContext(), iconDpi,
+ mTargetInfos.get(0).getResolveInfo());
+ }
+
+ private boolean isPinned(DisplayResolveInfo dri) {
+ return isShortcutTarget() ? mIsShortcutPinned : dri.isPinned();
+ }
+
+ private boolean isShortcutTarget() {
+ return mShortcutId != null;
+ }
+}
diff --git a/java/src/com/android/intentresolver/IntentForwarderActivity.java b/java/src/com/android/intentresolver/IntentForwarderActivity.java
new file mode 100644
index 00000000..9b853c95
--- /dev/null
+++ b/java/src/com/android/intentresolver/IntentForwarderActivity.java
@@ -0,0 +1,436 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK;
+import static android.content.pm.PackageManager.MATCH_DEFAULT_ONLY;
+
+import static com.android.intentresolver.ResolverActivity.EXTRA_CALLING_USER;
+import static com.android.intentresolver.ResolverActivity.EXTRA_SELECTED_PROFILE;
+
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.app.ActivityThread;
+import android.app.AppGlobals;
+import android.app.admin.DevicePolicyManager;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
+import android.metrics.LogMaker;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.Settings;
+import android.util.Slog;
+import android.widget.Toast;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+/**
+ * This is used in conjunction with
+ * {@link DevicePolicyManager#addCrossProfileIntentFilter} to enable intents to
+ * be passed in and out of a managed profile.
+ */
+public class IntentForwarderActivity extends Activity {
+ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+ public static String TAG = "IntentForwarderActivity";
+
+ public static String FORWARD_INTENT_TO_PARENT
+ = "com.android.internal.app.ForwardIntentToParent";
+
+ public static String FORWARD_INTENT_TO_MANAGED_PROFILE
+ = "com.android.internal.app.ForwardIntentToManagedProfile";
+
+ private static final Set<String> ALLOWED_TEXT_MESSAGE_SCHEMES
+ = new HashSet<>(Arrays.asList("sms", "smsto", "mms", "mmsto"));
+
+ private static final String TEL_SCHEME = "tel";
+
+ private static final ComponentName RESOLVER_COMPONENT_NAME =
+ new ComponentName("android", ResolverActivity.class.getName());
+
+ private Injector mInjector;
+
+ private MetricsLogger mMetricsLogger;
+ protected ExecutorService mExecutorService;
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mExecutorService.shutdown();
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ mInjector = createInjector();
+ mExecutorService = Executors.newSingleThreadExecutor();
+
+ Intent intentReceived = getIntent();
+ String className = intentReceived.getComponent().getClassName();
+ final int targetUserId;
+ final String userMessage;
+ if (className.equals(FORWARD_INTENT_TO_PARENT)) {
+ userMessage = getForwardToPersonalMessage();
+ targetUserId = getProfileParent();
+
+ getMetricsLogger().write(
+ new LogMaker(MetricsEvent.ACTION_SWITCH_SHARE_PROFILE)
+ .setSubtype(MetricsEvent.PARENT_PROFILE));
+ } else if (className.equals(FORWARD_INTENT_TO_MANAGED_PROFILE)) {
+ userMessage = getForwardToWorkMessage();
+ targetUserId = getManagedProfile();
+
+ getMetricsLogger().write(
+ new LogMaker(MetricsEvent.ACTION_SWITCH_SHARE_PROFILE)
+ .setSubtype(MetricsEvent.MANAGED_PROFILE));
+ } else {
+ Slog.wtf(TAG, IntentForwarderActivity.class.getName() + " cannot be called directly");
+ userMessage = null;
+ targetUserId = UserHandle.USER_NULL;
+ }
+ if (targetUserId == UserHandle.USER_NULL) {
+ // This covers the case where there is no parent / managed profile.
+ finish();
+ return;
+ }
+ if (Intent.ACTION_CHOOSER.equals(intentReceived.getAction())) {
+ launchChooserActivityWithCorrectTab(intentReceived, className);
+ return;
+ }
+
+ final int callingUserId = getUserId();
+ final Intent newIntent = canForward(intentReceived, getUserId(), targetUserId,
+ mInjector.getIPackageManager(), getContentResolver());
+
+ if (newIntent == null) {
+ Slog.wtf(TAG, "the intent: " + intentReceived + " cannot be forwarded from user "
+ + callingUserId + " to user " + targetUserId);
+ finish();
+ return;
+ }
+
+ newIntent.prepareToLeaveUser(callingUserId);
+ final CompletableFuture<ResolveInfo> targetResolveInfoFuture =
+ mInjector.resolveActivityAsUser(newIntent, MATCH_DEFAULT_ONLY, targetUserId);
+ targetResolveInfoFuture
+ .thenApplyAsync(targetResolveInfo -> {
+ if (isResolverActivityResolveInfo(targetResolveInfo)) {
+ launchResolverActivityWithCorrectTab(intentReceived, className, newIntent,
+ callingUserId, targetUserId);
+ return targetResolveInfo;
+ }
+ startActivityAsCaller(newIntent, targetUserId);
+ return targetResolveInfo;
+ }, mExecutorService)
+ .thenAcceptAsync(result -> {
+ maybeShowDisclosure(intentReceived, result, userMessage);
+ finish();
+ }, getApplicationContext().getMainExecutor());
+ }
+
+ private String getForwardToPersonalMessage() {
+ return getSystemService(DevicePolicyManager.class).getResources().getString(
+ FORWARD_INTENT_TO_PERSONAL,
+ () -> getString(com.android.internal.R.string.forward_intent_to_owner));
+ }
+
+ private String getForwardToWorkMessage() {
+ return getSystemService(DevicePolicyManager.class).getResources().getString(
+ FORWARD_INTENT_TO_WORK,
+ () -> getString(com.android.internal.R.string.forward_intent_to_work));
+ }
+
+ private boolean isIntentForwarderResolveInfo(ResolveInfo resolveInfo) {
+ if (resolveInfo == null) {
+ return false;
+ }
+ ActivityInfo activityInfo = resolveInfo.activityInfo;
+ if (activityInfo == null) {
+ return false;
+ }
+ if (!"android".equals(activityInfo.packageName)) {
+ return false;
+ }
+ return activityInfo.name.equals(FORWARD_INTENT_TO_PARENT)
+ || activityInfo.name.equals(FORWARD_INTENT_TO_MANAGED_PROFILE);
+ }
+
+ private boolean isResolverActivityResolveInfo(@Nullable ResolveInfo resolveInfo) {
+ return resolveInfo != null
+ && resolveInfo.activityInfo != null
+ && RESOLVER_COMPONENT_NAME.equals(resolveInfo.activityInfo.getComponentName());
+ }
+
+ private void maybeShowDisclosure(
+ Intent intentReceived, ResolveInfo resolveInfo, @Nullable String message) {
+ if (shouldShowDisclosure(resolveInfo, intentReceived) && message != null) {
+ mInjector.showToast(message, Toast.LENGTH_LONG);
+ }
+ }
+
+ private void startActivityAsCaller(Intent newIntent, int userId) {
+ try {
+ startActivityAsCaller(
+ newIntent,
+ /* options= */ null,
+ /* ignoreTargetSecurity= */ false,
+ userId);
+ } catch (RuntimeException e) {
+ Slog.wtf(TAG, "Unable to launch as UID " + getLaunchedFromUid() + " package "
+ + getLaunchedFromPackage() + ", while running in "
+ + ActivityThread.currentProcessName(), e);
+ }
+ }
+
+ private void launchChooserActivityWithCorrectTab(Intent intentReceived, String className) {
+ // When showing the sharesheet, instead of forwarding to the other profile,
+ // we launch the sharesheet in the current user and select the other tab.
+ // This fixes b/152866292 where the user can not go back to the original profile
+ // when cross-profile intents are disabled.
+ int selectedProfile = findSelectedProfile(className);
+ sanitizeIntent(intentReceived);
+ intentReceived.putExtra(EXTRA_SELECTED_PROFILE, selectedProfile);
+ Intent innerIntent = intentReceived.getParcelableExtra(Intent.EXTRA_INTENT);
+ if (innerIntent == null) {
+ Slog.wtf(TAG, "Cannot start a chooser intent with no extra " + Intent.EXTRA_INTENT);
+ return;
+ }
+ sanitizeIntent(innerIntent);
+ startActivityAsCaller(intentReceived, null, false, getUserId());
+ finish();
+ }
+
+ private void launchResolverActivityWithCorrectTab(Intent intentReceived, String className,
+ Intent newIntent, int callingUserId, int targetUserId) {
+ // When showing the intent resolver, instead of forwarding to the other profile,
+ // we launch it in the current user and select the other tab. This fixes b/155874820.
+ //
+ // In the case when there are 0 targets in the current profile and >1 apps in the other
+ // profile, the package manager launches the intent resolver in the other profile.
+ // If that's the case, we launch the resolver in the target user instead (other profile).
+ ResolveInfo callingResolveInfo = mInjector.resolveActivityAsUser(
+ newIntent, MATCH_DEFAULT_ONLY, callingUserId).join();
+ int userId = isIntentForwarderResolveInfo(callingResolveInfo)
+ ? targetUserId : callingUserId;
+ int selectedProfile = findSelectedProfile(className);
+ sanitizeIntent(intentReceived);
+ intentReceived.putExtra(EXTRA_SELECTED_PROFILE, selectedProfile);
+ intentReceived.putExtra(EXTRA_CALLING_USER, UserHandle.of(callingUserId));
+ startActivityAsCaller(intentReceived, null, false, userId);
+ finish();
+ }
+
+ private int findSelectedProfile(String className) {
+ if (className.equals(FORWARD_INTENT_TO_PARENT)) {
+ return ChooserActivity.PROFILE_PERSONAL;
+ } else if (className.equals(FORWARD_INTENT_TO_MANAGED_PROFILE)) {
+ return ChooserActivity.PROFILE_WORK;
+ }
+ return -1;
+ }
+
+ private boolean shouldShowDisclosure(@Nullable ResolveInfo ri, Intent intent) {
+ if (!isDeviceProvisioned()) {
+ return false;
+ }
+ if (ri == null || ri.activityInfo == null) {
+ return true;
+ }
+ if (ri.activityInfo.applicationInfo.isSystemApp()
+ && (isDialerIntent(intent) || isTextMessageIntent(intent))) {
+ return false;
+ }
+ return !isTargetResolverOrChooserActivity(ri.activityInfo);
+ }
+
+ private boolean isDeviceProvisioned() {
+ return Settings.Global.getInt(getContentResolver(),
+ Settings.Global.DEVICE_PROVISIONED, /* def= */ 0) != 0;
+ }
+
+ private boolean isTextMessageIntent(Intent intent) {
+ return (Intent.ACTION_SENDTO.equals(intent.getAction()) || isViewActionIntent(intent))
+ && ALLOWED_TEXT_MESSAGE_SCHEMES.contains(intent.getScheme());
+ }
+
+ private boolean isDialerIntent(Intent intent) {
+ return Intent.ACTION_DIAL.equals(intent.getAction())
+ || Intent.ACTION_CALL.equals(intent.getAction())
+ || Intent.ACTION_CALL_PRIVILEGED.equals(intent.getAction())
+ || Intent.ACTION_CALL_EMERGENCY.equals(intent.getAction())
+ || (isViewActionIntent(intent) && TEL_SCHEME.equals(intent.getScheme()));
+ }
+
+ private boolean isViewActionIntent(Intent intent) {
+ return Intent.ACTION_VIEW.equals(intent.getAction())
+ && intent.hasCategory(Intent.CATEGORY_BROWSABLE);
+ }
+
+ private boolean isTargetResolverOrChooserActivity(ActivityInfo activityInfo) {
+ if (!"android".equals(activityInfo.packageName)) {
+ return false;
+ }
+ return ResolverActivity.class.getName().equals(activityInfo.name)
+ || ChooserActivity.class.getName().equals(activityInfo.name);
+ }
+
+ /**
+ * Check whether the intent can be forwarded to target user. Return the intent used for
+ * forwarding if it can be forwarded, {@code null} otherwise.
+ */
+ static Intent canForward(Intent incomingIntent, int sourceUserId, int targetUserId,
+ IPackageManager packageManager, ContentResolver contentResolver) {
+ Intent forwardIntent = new Intent(incomingIntent);
+ forwardIntent.addFlags(
+ Intent.FLAG_ACTIVITY_FORWARD_RESULT | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
+ sanitizeIntent(forwardIntent);
+
+ Intent intentToCheck = forwardIntent;
+ if (Intent.ACTION_CHOOSER.equals(forwardIntent.getAction())) {
+ return null;
+ }
+ if (forwardIntent.getSelector() != null) {
+ intentToCheck = forwardIntent.getSelector();
+ }
+ String resolvedType = intentToCheck.resolveTypeIfNeeded(contentResolver);
+ sanitizeIntent(intentToCheck);
+ try {
+ if (packageManager.canForwardTo(
+ intentToCheck, resolvedType, sourceUserId, targetUserId)) {
+ return forwardIntent;
+ }
+ } catch (RemoteException e) {
+ Slog.e(TAG, "PackageManagerService is dead?");
+ }
+ return null;
+ }
+
+ /**
+ * Returns the userId of the managed profile for this device or UserHandle.USER_NULL if there is
+ * no managed profile.
+ *
+ * TODO: Remove the assumption that there is only one managed profile
+ * on the device.
+ */
+ private int getManagedProfile() {
+ List<UserInfo> relatedUsers = mInjector.getUserManager().getProfiles(UserHandle.myUserId());
+ for (UserInfo userInfo : relatedUsers) {
+ if (userInfo.isManagedProfile()) return userInfo.id;
+ }
+ Slog.wtf(TAG, FORWARD_INTENT_TO_MANAGED_PROFILE
+ + " has been called, but there is no managed profile");
+ return UserHandle.USER_NULL;
+ }
+
+ /**
+ * Returns the userId of the profile parent or UserHandle.USER_NULL if there is
+ * no parent.
+ */
+ private int getProfileParent() {
+ UserInfo parent = mInjector.getUserManager().getProfileParent(UserHandle.myUserId());
+ if (parent == null) {
+ Slog.wtf(TAG, FORWARD_INTENT_TO_PARENT
+ + " has been called, but there is no parent");
+ return UserHandle.USER_NULL;
+ }
+ return parent.id;
+ }
+
+ /**
+ * Sanitize the intent in place.
+ */
+ private static void sanitizeIntent(Intent intent) {
+ // Apps should not be allowed to target a specific package/ component in the target user.
+ intent.setPackage(null);
+ intent.setComponent(null);
+ }
+
+ protected MetricsLogger getMetricsLogger() {
+ if (mMetricsLogger == null) {
+ mMetricsLogger = new MetricsLogger();
+ }
+ return mMetricsLogger;
+ }
+
+ @VisibleForTesting
+ protected Injector createInjector() {
+ return new InjectorImpl();
+ }
+
+ private class InjectorImpl implements Injector {
+
+ @Override
+ public IPackageManager getIPackageManager() {
+ return AppGlobals.getPackageManager();
+ }
+
+ @Override
+ public UserManager getUserManager() {
+ return getSystemService(UserManager.class);
+ }
+
+ @Override
+ public PackageManager getPackageManager() {
+ return IntentForwarderActivity.this.getPackageManager();
+ }
+
+ @Override
+ @Nullable
+ public CompletableFuture<ResolveInfo> resolveActivityAsUser(
+ Intent intent, int flags, int userId) {
+ return CompletableFuture.supplyAsync(
+ () -> getPackageManager().resolveActivityAsUser(intent, flags, userId));
+ }
+
+ @Override
+ public void showToast(String message, int duration) {
+ Toast.makeText(IntentForwarderActivity.this, message, duration).show();
+ }
+ }
+
+ public interface Injector {
+ IPackageManager getIPackageManager();
+
+ UserManager getUserManager();
+
+ PackageManager getPackageManager();
+
+ CompletableFuture<ResolveInfo> resolveActivityAsUser(Intent intent, int flags, int userId);
+
+ void showToast(String message, int duration);
+ }
+}
diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java
new file mode 100644
index 00000000..453a6e84
--- /dev/null
+++ b/java/src/com/android/intentresolver/ResolverActivity.java
@@ -0,0 +1,2301 @@
+/*
+ * 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 static android.Manifest.permission.INTERACT_ACROSS_PROFILES;
+import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.FORWARD_INTENT_TO_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERSONAL_TAB_ACCESSIBILITY;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY;
+import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
+import static android.content.PermissionChecker.PID_UNKNOWN;
+import static android.view.WindowManager.LayoutParams.SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS;
+
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.annotation.UiThread;
+import android.app.Activity;
+import android.app.ActivityManager;
+import android.app.ActivityThread;
+import android.app.VoiceInteractor.PickOptionRequest;
+import android.app.VoiceInteractor.PickOptionRequest.Option;
+import android.app.VoiceInteractor.Prompt;
+import android.app.admin.DevicePolicyEventLogger;
+import android.app.admin.DevicePolicyManager;
+import android.compat.annotation.UnsupportedAppUsage;
+import android.content.BroadcastReceiver;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.PermissionChecker;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.content.pm.UserInfo;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Insets;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.PatternMatcher;
+import android.os.RemoteException;
+import android.os.StrictMode;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.provider.MediaStore;
+import android.provider.Settings;
+import android.stats.devicepolicy.DevicePolicyEnums;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.Window;
+import android.view.WindowInsets;
+import android.view.WindowManager;
+import android.widget.AbsListView;
+import android.widget.AdapterView;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.ImageView;
+import android.widget.ListView;
+import android.widget.Space;
+import android.widget.TabHost;
+import android.widget.TabWidget;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter.Profile;
+import com.android.intentresolver.chooser.ChooserTargetInfo;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.content.PackageMonitor;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto;
+import com.android.internal.util.LatencyTracker;
+import com.android.internal.widget.ResolverDrawerLayout;
+import com.android.internal.widget.ViewPager;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * This activity is displayed when the system attempts to start an Intent for
+ * which there is more than one matching activity, allowing the user to decide
+ * which to go to. It is not normally used directly by application developers.
+ */
+@UiThread
+public class ResolverActivity extends Activity implements
+ ResolverListAdapter.ResolverListCommunicator {
+
+ @UnsupportedAppUsage
+ public ResolverActivity() {
+ mIsIntentPicker = getClass().equals(ResolverActivity.class);
+ }
+
+ protected ResolverActivity(boolean isIntentPicker) {
+ mIsIntentPicker = isIntentPicker;
+ }
+
+ private boolean mSafeForwardingMode;
+ private Button mAlwaysButton;
+ private Button mOnceButton;
+ protected View mProfileView;
+ private int mLastSelected = AbsListView.INVALID_POSITION;
+ private boolean mResolvingHome = false;
+ private String mProfileSwitchMessage;
+ private int mLayoutId;
+ @VisibleForTesting
+ protected final ArrayList<Intent> mIntents = new ArrayList<>();
+ private PickTargetOptionRequest mPickOptionRequest;
+ private String mReferrerPackage;
+ private CharSequence mTitle;
+ private int mDefaultTitleResId;
+ // Expected to be true if this object is ResolverActivity or is ResolverWrapperActivity.
+ private final boolean mIsIntentPicker;
+
+ // Whether or not this activity supports choosing a default handler for the intent.
+ @VisibleForTesting
+ protected boolean mSupportsAlwaysUseOption;
+ protected ResolverDrawerLayout mResolverDrawerLayout;
+ @UnsupportedAppUsage
+ protected PackageManager mPm;
+ protected int mLaunchedFromUid;
+
+ private static final String TAG = "ResolverActivity";
+ private static final boolean DEBUG = false;
+ private static final String LAST_SHOWN_TAB_KEY = "last_shown_tab_key";
+
+ private boolean mRegistered;
+
+ protected Insets mSystemWindowInsets = null;
+ private Space mFooterSpacer = null;
+
+ /** See {@link #setRetainInOnStop}. */
+ private boolean mRetainInOnStop;
+
+ private static final String EXTRA_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args";
+ private static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
+ private static final String OPEN_LINKS_COMPONENT_KEY = "app_link_state";
+ protected static final String METRICS_CATEGORY_RESOLVER = "intent_resolver";
+ protected static final String METRICS_CATEGORY_CHOOSER = "intent_chooser";
+
+ /** Tracks if we should ignore future broadcasts telling us the work profile is enabled */
+ private boolean mWorkProfileHasBeenEnabled = false;
+
+ @VisibleForTesting
+ public static boolean ENABLE_TABBED_VIEW = true;
+ private static final String TAB_TAG_PERSONAL = "personal";
+ private static final String TAB_TAG_WORK = "work";
+
+ private PackageMonitor mPersonalPackageMonitor;
+ private PackageMonitor mWorkPackageMonitor;
+
+ @VisibleForTesting
+ protected AbstractMultiProfilePagerAdapter mMultiProfilePagerAdapter;
+
+ // Intent extra for connected audio devices
+ public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device";
+
+ /**
+ * Integer extra to indicate which profile should be automatically selected.
+ * <p>Can only be used if there is a work profile.
+ * <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}.
+ */
+ static final String EXTRA_SELECTED_PROFILE =
+ "com.android.internal.app.ResolverActivity.EXTRA_SELECTED_PROFILE";
+
+ /**
+ * {@link UserHandle} extra to indicate the user of the user that the starting intent
+ * originated from.
+ * <p>This is not necessarily the same as {@link #getUserId()} or {@link UserHandle#myUserId()},
+ * as there are edge cases when the intent resolver is launched in the other profile.
+ * For example, when we have 0 resolved apps in current profile and multiple resolved
+ * apps in the other profile, opening a link from the current profile launches the intent
+ * resolver in the other one. b/148536209 for more info.
+ */
+ static final String EXTRA_CALLING_USER =
+ "com.android.internal.app.ResolverActivity.EXTRA_CALLING_USER";
+
+ static final int PROFILE_PERSONAL = AbstractMultiProfilePagerAdapter.PROFILE_PERSONAL;
+ static final int PROFILE_WORK = AbstractMultiProfilePagerAdapter.PROFILE_WORK;
+
+ private BroadcastReceiver mWorkProfileStateReceiver;
+ private UserHandle mHeaderCreatorUser;
+
+ private UserHandle mWorkProfileUserHandle;
+
+ 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,
+ com.android.internal.R.string.whichViewApplicationNamed,
+ com.android.internal.R.string.whichViewApplicationLabel),
+ EDIT(Intent.ACTION_EDIT,
+ com.android.internal.R.string.whichEditApplication,
+ com.android.internal.R.string.whichEditApplicationNamed,
+ com.android.internal.R.string.whichEditApplicationLabel),
+ SEND(Intent.ACTION_SEND,
+ com.android.internal.R.string.whichSendApplication,
+ com.android.internal.R.string.whichSendApplicationNamed,
+ com.android.internal.R.string.whichSendApplicationLabel),
+ SENDTO(Intent.ACTION_SENDTO,
+ com.android.internal.R.string.whichSendToApplication,
+ com.android.internal.R.string.whichSendToApplicationNamed,
+ com.android.internal.R.string.whichSendToApplicationLabel),
+ SEND_MULTIPLE(Intent.ACTION_SEND_MULTIPLE,
+ com.android.internal.R.string.whichSendApplication,
+ com.android.internal.R.string.whichSendApplicationNamed,
+ com.android.internal.R.string.whichSendApplicationLabel),
+ CAPTURE_IMAGE(MediaStore.ACTION_IMAGE_CAPTURE,
+ com.android.internal.R.string.whichImageCaptureApplication,
+ com.android.internal.R.string.whichImageCaptureApplicationNamed,
+ com.android.internal.R.string.whichImageCaptureApplicationLabel),
+ DEFAULT(null,
+ com.android.internal.R.string.whichApplication,
+ com.android.internal.R.string.whichApplicationNamed,
+ com.android.internal.R.string.whichApplicationLabel),
+ HOME(Intent.ACTION_MAIN,
+ com.android.internal.R.string.whichHomeApplication,
+ com.android.internal.R.string.whichHomeApplicationNamed,
+ com.android.internal.R.string.whichHomeApplicationLabel);
+
+ // titles for layout that deals with http(s) intents
+ public static final int BROWSABLE_TITLE_RES =
+ com.android.internal.R.string.whichOpenLinksWith;
+ public static final int BROWSABLE_HOST_TITLE_RES =
+ com.android.internal.R.string.whichOpenHostLinksWith;
+ public static final int BROWSABLE_HOST_APP_TITLE_RES =
+ com.android.internal.R.string.whichOpenHostLinksWithApp;
+ public static final int BROWSABLE_APP_TITLE_RES =
+ com.android.internal.R.string.whichOpenLinksWithApp;
+
+ public final String action;
+ public final int titleRes;
+ public final int namedTitleRes;
+ public final @StringRes int labelRes;
+
+ ActionTitle(String action, int titleRes, int namedTitleRes, @StringRes int labelRes) {
+ this.action = action;
+ this.titleRes = titleRes;
+ this.namedTitleRes = namedTitleRes;
+ this.labelRes = labelRes;
+ }
+
+ public static ActionTitle forAction(String action) {
+ for (ActionTitle title : values()) {
+ if (title != HOME && action != null && action.equals(title.action)) {
+ return title;
+ }
+ }
+ return DEFAULT;
+ }
+ }
+
+ protected PackageMonitor createPackageMonitor(ResolverListAdapter listAdapter) {
+ return new PackageMonitor() {
+ @Override
+ public void onSomePackagesChanged() {
+ listAdapter.handlePackagesChanged();
+ updateProfileViewButton();
+ }
+
+ @Override
+ public boolean onPackageChanged(String packageName, int uid, String[] components) {
+ // We care about all package changes, not just the whole package itself which is
+ // default behavior.
+ return true;
+ }
+ };
+ }
+
+ 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()
+ final Intent intent = makeMyIntent();
+ final Set<String> categories = intent.getCategories();
+ if (Intent.ACTION_MAIN.equals(intent.getAction())
+ && categories != null
+ && categories.size() == 1
+ && categories.contains(Intent.CATEGORY_HOME)) {
+ // Note: this field is not set to true in the compatibility version.
+ mResolvingHome = true;
+ }
+
+ setSafeForwardingMode(true);
+
+ onCreate(savedInstanceState, intent, null, 0, null, null, true);
+ }
+
+ /**
+ * Compatibility version for other bundled services that use this overload without
+ * a default title resource
+ */
+ @UnsupportedAppUsage
+ protected void onCreate(Bundle savedInstanceState, Intent intent,
+ CharSequence title, Intent[] initialIntents,
+ List<ResolveInfo> rList, boolean supportsAlwaysUseOption) {
+ onCreate(savedInstanceState, intent, title, 0, initialIntents, rList,
+ supportsAlwaysUseOption);
+ }
+
+ protected void onCreate(Bundle savedInstanceState, Intent intent,
+ CharSequence title, int defaultTitleRes, Intent[] initialIntents,
+ List<ResolveInfo> rList, boolean supportsAlwaysUseOption) {
+ setTheme(appliedThemeResId());
+ super.onCreate(savedInstanceState);
+
+ // 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;
+ }
+
+ mPm = getPackageManager();
+
+ mReferrerPackage = getReferrerPackageName();
+
+ // Add our initial intent as the first item, regardless of what else has already been added.
+ mIntents.add(0, new Intent(intent));
+ mTitle = title;
+ mDefaultTitleResId = defaultTitleRes;
+
+ mSupportsAlwaysUseOption = supportsAlwaysUseOption;
+ mWorkProfileUserHandle = fetchWorkProfileUserProfile();
+
+ // The last argument of createResolverListAdapter is whether to do special handling
+ // of the last used choice to highlight it in the list. We need to always
+ // turn this off when running under voice interaction, since it results in
+ // a more complicated UI that the current voice interaction flow is not able
+ // to handle. We also turn it off when the work tab is shown to simplify the UX.
+ boolean filterLastUsed = mSupportsAlwaysUseOption && !isVoiceInteraction()
+ && !shouldShowTabs();
+ mMultiProfilePagerAdapter = createMultiProfilePagerAdapter(initialIntents, rList, filterLastUsed);
+ if (configureContentView()) {
+ return;
+ }
+
+ mPersonalPackageMonitor = createPackageMonitor(
+ mMultiProfilePagerAdapter.getPersonalListAdapter());
+ mPersonalPackageMonitor.register(
+ this, getMainLooper(), getPersonalProfileUserHandle(), false);
+ if (shouldShowTabs()) {
+ mWorkPackageMonitor = createPackageMonitor(
+ mMultiProfilePagerAdapter.getWorkListAdapter());
+ mWorkPackageMonitor.register(this, getMainLooper(), getWorkProfileUserHandle(), false);
+ }
+
+ mRegistered = true;
+
+ final ResolverDrawerLayout rdl = findViewById(com.android.internal.R.id.contentPanel);
+ if (rdl != null) {
+ rdl.setOnDismissedListener(new ResolverDrawerLayout.OnDismissedListener() {
+ @Override
+ public void onDismissed() {
+ finish();
+ }
+ });
+
+ boolean hasTouchScreen = getPackageManager()
+ .hasSystemFeature(PackageManager.FEATURE_TOUCHSCREEN);
+
+ if (isVoiceInteraction() || !hasTouchScreen) {
+ rdl.setCollapsed(false);
+ }
+
+ rdl.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+ | View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ rdl.setOnApplyWindowInsetsListener(this::onApplyWindowInsets);
+
+ mResolverDrawerLayout = rdl;
+ }
+
+ mProfileView = findViewById(com.android.internal.R.id.profile_button);
+ if (mProfileView != null) {
+ mProfileView.setOnClickListener(this::onProfileClick);
+ updateProfileViewButton();
+ }
+
+ final Set<String> categories = intent.getCategories();
+ MetricsLogger.action(this, mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem()
+ ? MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_APP_FEATURED
+ : MetricsProto.MetricsEvent.ACTION_SHOW_APP_DISAMBIG_NONE_FEATURED,
+ intent.getAction() + ":" + intent.getType() + ":"
+ + (categories != null ? Arrays.toString(categories.toArray()) : ""));
+ }
+
+ protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter(
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed) {
+ AbstractMultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null;
+ if (shouldShowTabs()) {
+ resolverMultiProfilePagerAdapter =
+ createResolverMultiProfilePagerAdapterForTwoProfiles(
+ initialIntents, rList, filterLastUsed);
+ } else {
+ resolverMultiProfilePagerAdapter = createResolverMultiProfilePagerAdapterForOneProfile(
+ initialIntents, rList, filterLastUsed);
+ }
+ return resolverMultiProfilePagerAdapter;
+ }
+
+ private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForOneProfile(
+ Intent[] initialIntents,
+ List<ResolveInfo> rList, boolean filterLastUsed) {
+ ResolverListAdapter adapter = createResolverListAdapter(
+ /* context */ this,
+ /* payloadIntents */ mIntents,
+ initialIntents,
+ rList,
+ filterLastUsed,
+ /* userHandle */ UserHandle.of(UserHandle.myUserId()));
+ return new ResolverMultiProfilePagerAdapter(
+ /* context */ this,
+ adapter,
+ getPersonalProfileUserHandle(),
+ /* workProfileUserHandle= */ null);
+ }
+
+ private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles(
+ Intent[] initialIntents,
+ List<ResolveInfo> rList,
+ boolean filterLastUsed) {
+ // In the edge case when we have 0 apps in the current profile and >1 apps in the other,
+ // the intent resolver is started in the other profile. Since this is the only case when
+ // this happens, we check for it here and set the current profile's tab.
+ int selectedProfile = getCurrentProfile();
+ UserHandle intentUser = getIntent().hasExtra(EXTRA_CALLING_USER)
+ ? getIntent().getParcelableExtra(EXTRA_CALLING_USER)
+ : getUser();
+ if (!getUser().equals(intentUser)) {
+ if (getPersonalProfileUserHandle().equals(intentUser)) {
+ selectedProfile = PROFILE_PERSONAL;
+ } else if (getWorkProfileUserHandle().equals(intentUser)) {
+ selectedProfile = PROFILE_WORK;
+ }
+ } else {
+ int selectedProfileExtra = getSelectedProfileExtra();
+ if (selectedProfileExtra != -1) {
+ selectedProfile = selectedProfileExtra;
+ }
+ }
+ // We only show the default app for the profile of the current user. The filterLastUsed
+ // flag determines whether to show a default app and that app is not shown in the
+ // resolver list. So filterLastUsed should be false for the other profile.
+ ResolverListAdapter personalAdapter = createResolverListAdapter(
+ /* context */ this,
+ /* payloadIntents */ mIntents,
+ selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
+ rList,
+ (filterLastUsed && UserHandle.myUserId()
+ == getPersonalProfileUserHandle().getIdentifier()),
+ /* userHandle */ getPersonalProfileUserHandle());
+ UserHandle workProfileUserHandle = getWorkProfileUserHandle();
+ ResolverListAdapter workAdapter = createResolverListAdapter(
+ /* context */ this,
+ /* payloadIntents */ mIntents,
+ selectedProfile == PROFILE_WORK ? initialIntents : null,
+ rList,
+ (filterLastUsed && UserHandle.myUserId()
+ == workProfileUserHandle.getIdentifier()),
+ /* userHandle */ workProfileUserHandle);
+ return new ResolverMultiProfilePagerAdapter(
+ /* context */ this,
+ personalAdapter,
+ workAdapter,
+ selectedProfile,
+ getPersonalProfileUserHandle(),
+ getWorkProfileUserHandle(),
+ /* shouldShowNoCrossProfileIntentsEmptyState= */ getUser().equals(intentUser));
+ }
+
+ 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() {
+ int selectedProfile = -1;
+ if (getIntent().hasExtra(EXTRA_SELECTED_PROFILE)) {
+ selectedProfile = getIntent().getIntExtra(EXTRA_SELECTED_PROFILE, /* defValue = */ -1);
+ if (selectedProfile != PROFILE_PERSONAL && selectedProfile != PROFILE_WORK) {
+ throw new IllegalArgumentException(EXTRA_SELECTED_PROFILE + " has invalid value "
+ + selectedProfile + ". Must be either ResolverActivity.PROFILE_PERSONAL or "
+ + "ResolverActivity.PROFILE_WORK.");
+ }
+ }
+ return selectedProfile;
+ }
+
+ protected @Profile int getCurrentProfile() {
+ return (UserHandle.myUserId() == UserHandle.USER_SYSTEM ? PROFILE_PERSONAL : PROFILE_WORK);
+ }
+
+ protected UserHandle getPersonalProfileUserHandle() {
+ return UserHandle.of(ActivityManager.getCurrentUser());
+ }
+ protected @Nullable UserHandle getWorkProfileUserHandle() {
+ return mWorkProfileUserHandle;
+ }
+
+ protected @Nullable UserHandle fetchWorkProfileUserProfile() {
+ mWorkProfileUserHandle = null;
+ UserManager userManager = getSystemService(UserManager.class);
+ for (final UserInfo userInfo : userManager.getProfiles(ActivityManager.getCurrentUser())) {
+ if (userInfo.isManagedProfile()) {
+ mWorkProfileUserHandle = userInfo.getUserHandle();
+ }
+ }
+ return mWorkProfileUserHandle;
+ }
+
+ private boolean hasWorkProfile() {
+ return getWorkProfileUserHandle() != null;
+ }
+
+ protected boolean shouldShowTabs() {
+ return hasWorkProfile() && ENABLE_TABBED_VIEW;
+ }
+
+ protected void onProfileClick(View v) {
+ final DisplayResolveInfo dri =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile();
+ if (dri == null) {
+ return;
+ }
+
+ // Do not show the profile switch message anymore.
+ mProfileSwitchMessage = null;
+
+ onTargetSelected(dri, false);
+ finish();
+ }
+
+ /**
+ * 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(
+ titleCont.getPaddingLeft(),
+ titleCont.getPaddingTop(),
+ titleCont.getPaddingRight(),
+ getResources().getDimensionPixelSize(R.dimen.resolver_title_padding_bottom));
+ View buttonBar = findViewById(com.android.internal.R.id.button_bar);
+ buttonBar.setPadding(
+ buttonBar.getPaddingLeft(),
+ getResources().getDimensionPixelSize(R.dimen.resolver_button_bar_spacing),
+ buttonBar.getPaddingRight(),
+ getResources().getDimensionPixelSize(R.dimen.resolver_button_bar_spacing));
+ }
+
+ @Override // ResolverListCommunicator
+ public void sendVoiceChoicesIfNeeded() {
+ if (!isVoiceInteraction()) {
+ // Clearly not needed.
+ return;
+ }
+
+ int count = mMultiProfilePagerAdapter.getActiveListAdapter().getCount();
+ final Option[] options = new Option[count];
+ for (int i = 0, N = options.length; i < N; i++) {
+ TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter().getItem(i);
+ if (target == null) {
+ // If this occurs, a new set of targets is being loaded. Let that complete,
+ // and have the next call to send voice choices proceed instead.
+ return;
+ }
+ options[i] = optionForChooserTarget(target, i);
+ }
+
+ mPickOptionRequest = new PickTargetOptionRequest(
+ new Prompt(getTitle()), options, null);
+ getVoiceInteractor().submitRequest(mPickOptionRequest);
+ }
+
+ Option optionForChooserTarget(TargetInfo target, int index) {
+ return new Option(target.getDisplayLabel(), index);
+ }
+
+ protected final void setAdditionalTargets(Intent[] intents) {
+ if (intents != null) {
+ for (Intent intent : intents) {
+ mIntents.add(intent);
+ }
+ }
+ }
+
+ @Override // SelectableTargetInfoCommunicator ResolverListCommunicator
+ public Intent getTargetIntent() {
+ return mIntents.isEmpty() ? null : mIntents.get(0);
+ }
+
+ protected String getReferrerPackageName() {
+ final Uri referrer = getReferrer();
+ if (referrer != null && "android-app".equals(referrer.getScheme())) {
+ return referrer.getHost();
+ }
+ return null;
+ }
+
+ public int getLayoutResource() {
+ return R.layout.resolver_list;
+ }
+
+ @Override // ResolverListCommunicator
+ public void updateProfileViewButton() {
+ if (mProfileView == null) {
+ return;
+ }
+
+ final DisplayResolveInfo dri =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile();
+ if (dri != null && !shouldShowTabs()) {
+ mProfileView.setVisibility(View.VISIBLE);
+ View text = mProfileView.findViewById(com.android.internal.R.id.profile_button);
+ if (!(text instanceof TextView)) {
+ text = mProfileView.findViewById(com.android.internal.R.id.text1);
+ }
+ ((TextView) text).setText(dri.getDisplayLabel());
+ } else {
+ mProfileView.setVisibility(View.GONE);
+ }
+ }
+
+ private void setProfileSwitchMessage(int contentUserHint) {
+ if (contentUserHint != UserHandle.USER_CURRENT &&
+ contentUserHint != UserHandle.myUserId()) {
+ UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
+ UserInfo originUserInfo = userManager.getUserInfo(contentUserHint);
+ boolean originIsManaged = originUserInfo != null ? originUserInfo.isManagedProfile()
+ : false;
+ boolean targetIsManaged = userManager.isManagedProfile();
+ if (originIsManaged && !targetIsManaged) {
+ mProfileSwitchMessage = getForwardToPersonalMsg();
+ } else if (!originIsManaged && targetIsManaged) {
+ mProfileSwitchMessage = getForwardToWorkMsg();
+ }
+ }
+ }
+
+ private String getForwardToPersonalMsg() {
+ return getSystemService(DevicePolicyManager.class).getResources().getString(
+ FORWARD_INTENT_TO_PERSONAL,
+ () -> getString(com.android.internal.R.string.forward_intent_to_owner));
+ }
+
+ private String getForwardToWorkMsg() {
+ return getSystemService(DevicePolicyManager.class).getResources().getString(
+ FORWARD_INTENT_TO_WORK,
+ () -> getString(com.android.internal.R.string.forward_intent_to_work));
+ }
+
+ /**
+ * Turn on launch mode that is safe to use when forwarding intents received from
+ * applications and running in system processes. This mode uses Activity.startActivityAsCaller
+ * instead of the normal Activity.startActivity for launching the activity selected
+ * by the user.
+ *
+ * <p>This mode is set to true by default if the activity is initialized through
+ * {@link #onCreate(android.os.Bundle)}. If a subclass calls one of the other onCreate
+ * methods, it is set to false by default. You must set it before calling one of the
+ * 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) {
+ mSafeForwardingMode = safeForwarding;
+ }
+
+ protected CharSequence getTitleForAction(Intent intent, int defaultTitleRes) {
+ final ActionTitle title = mResolvingHome
+ ? ActionTitle.HOME
+ : ActionTitle.forAction(intent.getAction());
+
+ // While there may already be a filtered item, we can only use it in the title if the list
+ // is already sorted and all information relevant to it is already in the list.
+ final boolean named =
+ mMultiProfilePagerAdapter.getActiveListAdapter().getFilteredPosition() >= 0;
+ if (title == ActionTitle.DEFAULT && defaultTitleRes != 0) {
+ return getString(defaultTitleRes);
+ } else {
+ return named
+ ? getString(title.namedTitleRes, mMultiProfilePagerAdapter
+ .getActiveListAdapter().getFilteredItem().getDisplayLabel())
+ : getString(title.titleRes);
+ }
+ }
+
+ void dismiss() {
+ if (!isFinishing()) {
+ finish();
+ }
+ }
+
+ @Override
+ protected void onRestart() {
+ super.onRestart();
+ if (!mRegistered) {
+ mPersonalPackageMonitor.register(this, getMainLooper(),
+ getPersonalProfileUserHandle(), false);
+ if (shouldShowTabs()) {
+ if (mWorkPackageMonitor == null) {
+ mWorkPackageMonitor = createPackageMonitor(
+ mMultiProfilePagerAdapter.getWorkListAdapter());
+ }
+ mWorkPackageMonitor.register(this, getMainLooper(),
+ getWorkProfileUserHandle(), false);
+ }
+ mRegistered = true;
+ }
+ if (shouldShowTabs() && mMultiProfilePagerAdapter.isWaitingToEnableWorkProfile()) {
+ if (mMultiProfilePagerAdapter.isQuietModeEnabled(getWorkProfileUserHandle())) {
+ mMultiProfilePagerAdapter.markWorkProfileEnabledBroadcastReceived();
+ }
+ }
+ mMultiProfilePagerAdapter.getActiveListAdapter().handlePackagesChanged();
+ updateProfileViewButton();
+ }
+
+ @Override
+ protected 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;
+ }
+ }
+
+ @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) {
+ super.onSaveInstanceState(outState);
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ if (viewPager != null) {
+ outState.putInt(LAST_SHOWN_TAB_KEY, viewPager.getCurrentItem());
+ }
+ }
+
+ @Override
+ protected void onRestoreInstanceState(Bundle savedInstanceState) {
+ super.onRestoreInstanceState(savedInstanceState);
+ resetButtonBar();
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ if (viewPager != null) {
+ viewPager.setCurrentItem(savedInstanceState.getInt(LAST_SHOWN_TAB_KEY));
+ }
+ mMultiProfilePagerAdapter.clearInactiveProfileCache();
+ }
+
+ private boolean hasManagedProfile() {
+ UserManager userManager = (UserManager) getSystemService(Context.USER_SERVICE);
+ if (userManager == null) {
+ return false;
+ }
+
+ try {
+ List<UserInfo> profiles = userManager.getProfiles(getUserId());
+ for (UserInfo userInfo : profiles) {
+ if (userInfo != null && userInfo.isManagedProfile()) {
+ return true;
+ }
+ }
+ } catch (SecurityException e) {
+ return false;
+ }
+ return false;
+ }
+
+ private boolean supportsManagedProfiles(ResolveInfo resolveInfo) {
+ try {
+ ApplicationInfo appInfo = getPackageManager().getApplicationInfo(
+ resolveInfo.activityInfo.packageName, 0 /* default flags */);
+ return appInfo.targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP;
+ } catch (NameNotFoundException e) {
+ return false;
+ }
+ }
+
+ private void setAlwaysButtonEnabled(boolean hasValidSelection, int checkedPos,
+ boolean filtered) {
+ if (!mMultiProfilePagerAdapter.getCurrentUserHandle().equals(getUser())) {
+ // Never allow the inactive profile to always open an app.
+ mAlwaysButton.setEnabled(false);
+ return;
+ }
+ boolean enabled = false;
+ ResolveInfo ri = null;
+ if (hasValidSelection) {
+ ri = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .resolveInfoForPosition(checkedPos, filtered);
+ if (ri == null) {
+ Log.e(TAG, "Invalid position supplied to setAlwaysButtonEnabled");
+ return;
+ } else if (ri.targetUserId != UserHandle.USER_CURRENT) {
+ Log.e(TAG, "Attempted to set selection to resolve info for another user");
+ return;
+ } else {
+ enabled = true;
+ }
+
+ mAlwaysButton.setText(getResources()
+ .getString(R.string.activity_resolver_use_always));
+ }
+
+ if (ri != null) {
+ ActivityInfo activityInfo = ri.activityInfo;
+
+ boolean hasRecordPermission =
+ mPm.checkPermission(android.Manifest.permission.RECORD_AUDIO,
+ activityInfo.packageName)
+ == android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+ if (!hasRecordPermission) {
+ // OK, we know the record permission, is this a capture device
+ boolean hasAudioCapture =
+ getIntent().getBooleanExtra(
+ ResolverActivity.EXTRA_IS_AUDIO_CAPTURE_DEVICE, false);
+ enabled = !hasAudioCapture;
+ }
+ }
+ mAlwaysButton.setEnabled(enabled);
+ }
+
+ 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,
+ () -> getString(
+ com.android.internal.R.string.activity_resolver_work_profiles_support,
+ launcherName),
+ 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) {
+ if (isAutolaunching()) {
+ return;
+ }
+ if (mIsIntentPicker) {
+ ((ResolverMultiProfilePagerAdapter) mMultiProfilePagerAdapter)
+ .setUseLayoutWithDefault(useLayoutWithDefault());
+ }
+ if (mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(listAdapter)) {
+ mMultiProfilePagerAdapter.showEmptyResolverListEmptyState(listAdapter);
+ } else {
+ mMultiProfilePagerAdapter.showListView(listAdapter);
+ }
+ // showEmptyResolverListEmptyState can mark the tab as loaded,
+ // which is a precondition for auto launching
+ if (rebuildCompleted && maybeAutolaunchActivity()) {
+ return;
+ }
+ if (doPostProcessing) {
+ maybeCreateHeader(listAdapter);
+ resetButtonBar();
+ onListRebuilt(listAdapter, rebuildCompleted);
+ }
+ }
+
+ 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) {
+ safelyStartActivityAsUser(cti, user, null);
+ }
+
+ protected void safelyStartActivityAsUser(
+ TargetInfo cti, UserHandle user, @Nullable Bundle options) {
+ // We're dispatching intents that might be coming from legacy apps, so
+ // don't kill ourselves.
+ StrictMode.disableDeathOnFileUriExposure();
+ try {
+ safelyStartActivityInternal(cti, user, options);
+ } finally {
+ StrictMode.enableDeathOnFileUriExposure();
+ }
+ }
+
+ private void safelyStartActivityInternal(
+ TargetInfo cti, UserHandle user, @Nullable Bundle options) {
+ // If the target is suspended, the activity will not be successfully launched.
+ // Do not unregister from package manager updates in this case
+ if (!cti.isSuspended() && mRegistered) {
+ if (mPersonalPackageMonitor != null) {
+ mPersonalPackageMonitor.unregister();
+ }
+ if (mWorkPackageMonitor != null) {
+ mWorkPackageMonitor.unregister();
+ }
+ mRegistered = false;
+ }
+ // If needed, show that intent is forwarded
+ // from managed profile to owner or other way around.
+ if (mProfileSwitchMessage != null) {
+ Toast.makeText(this, mProfileSwitchMessage, Toast.LENGTH_LONG).show();
+ }
+ if (!mSafeForwardingMode) {
+ if (cti.startAsUser(this, options, user)) {
+ onActivityStarted(cti);
+ maybeLogCrossProfileTargetLaunch(cti, user);
+ }
+ return;
+ }
+ try {
+ if (cti.startAsCaller(this, options, user.getIdentifier())) {
+ onActivityStarted(cti);
+ maybeLogCrossProfileTargetLaunch(cti, user);
+ }
+ } catch (RuntimeException e) {
+ Slog.wtf(TAG, "Unable to launch as uid " + mLaunchedFromUid
+ + " 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 instanceof ChooserTargetInfo ? "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) {
+ 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), 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.
+ */
+ private boolean configureContentView() {
+ if (mMultiProfilePagerAdapter.getActiveListAdapter() == null) {
+ throw new IllegalStateException("mMultiProfilePagerAdapter.getCurrentListAdapter() "
+ + "cannot be null.");
+ }
+ Trace.beginSection("configureContentView");
+ // We partially rebuild the inactive adapter to determine if we should auto launch
+ // isTabLoaded will be true here if the empty state screen is shown instead of the list.
+ boolean rebuildCompleted = mMultiProfilePagerAdapter.rebuildActiveTab(true)
+ || mMultiProfilePagerAdapter.getActiveListAdapter().isTabLoaded();
+ if (shouldShowTabs()) {
+ boolean rebuildInactiveCompleted = mMultiProfilePagerAdapter.rebuildInactiveTab(false)
+ || mMultiProfilePagerAdapter.getInactiveListAdapter().isTabLoaded();
+ rebuildCompleted = rebuildCompleted && rebuildInactiveCompleted;
+ }
+
+ if (shouldUseMiniResolver()) {
+ configureMiniResolverContent();
+ Trace.endSection();
+ return false;
+ }
+
+ if (useLayoutWithDefault()) {
+ mLayoutId = R.layout.resolver_list_with_default;
+ } else {
+ mLayoutId = getLayoutResource();
+ }
+ setContentView(mLayoutId);
+ mMultiProfilePagerAdapter.setupViewPager(findViewById(com.android.internal.R.id.profile_pager));
+ boolean result = postRebuildList(rebuildCompleted);
+ Trace.endSection();
+ return result;
+ }
+
+ /**
+ * Mini resolver is shown when the user is choosing between browser[s] in this profile and a
+ * single app in the other profile (see shouldUseMiniResolver()). It shows the single app icon
+ * and asks the user if they'd like to open that cross-profile app or use the in-profile
+ * browser.
+ */
+ private void configureMiniResolverContent() {
+ mLayoutId = R.layout.miniresolver;
+ setContentView(mLayoutId);
+
+ DisplayResolveInfo sameProfileResolveInfo =
+ mMultiProfilePagerAdapter.getActiveListAdapter().mDisplayList.get(0);
+ boolean inWorkProfile = getCurrentProfile() == PROFILE_WORK;
+
+ ResolverListAdapter inactiveAdapter = mMultiProfilePagerAdapter.getInactiveListAdapter();
+ DisplayResolveInfo otherProfileResolveInfo = inactiveAdapter.mDisplayList.get(0);
+
+ // Load the icon asynchronously
+ ImageView icon = findViewById(com.android.internal.R.id.icon);
+ ResolverListAdapter.LoadIconTask iconTask = inactiveAdapter.new LoadIconTask(
+ otherProfileResolveInfo, new ResolverListAdapter.ViewHolder(icon));
+ iconTask.execute();
+
+ ((TextView) findViewById(com.android.internal.R.id.open_cross_profile)).setText(
+ getResources().getString(
+ inWorkProfile ? R.string.miniresolver_open_in_personal
+ : R.string.miniresolver_open_in_work,
+ otherProfileResolveInfo.getDisplayLabel()));
+ ((Button) findViewById(com.android.internal.R.id.use_same_profile_browser)).setText(
+ inWorkProfile ? R.string.miniresolver_use_work_browser
+ : R.string.miniresolver_use_personal_browser);
+
+ findViewById(com.android.internal.R.id.use_same_profile_browser).setOnClickListener(
+ v -> {
+ safelyStartActivity(sameProfileResolveInfo);
+ finish();
+ });
+
+ findViewById(com.android.internal.R.id.button_open).setOnClickListener(v -> {
+ Intent intent = otherProfileResolveInfo.getResolvedIntent();
+ safelyStartActivityAsUser(otherProfileResolveInfo,
+ inactiveAdapter.mResolverListController.getUserHandle());
+ finish();
+ });
+ }
+
+ /**
+ * Mini resolver should be used when all of the following are true:
+ * 1. This is the intent picker (ResolverActivity).
+ * 2. This profile only has web browser matches.
+ * 3. The other profile has a single non-browser match.
+ */
+ private boolean shouldUseMiniResolver() {
+ if (!mIsIntentPicker) {
+ return false;
+ }
+ if (mMultiProfilePagerAdapter.getActiveListAdapter() == null
+ || mMultiProfilePagerAdapter.getInactiveListAdapter() == null) {
+ return false;
+ }
+ List<DisplayResolveInfo> sameProfileList =
+ mMultiProfilePagerAdapter.getActiveListAdapter().mDisplayList;
+ List<DisplayResolveInfo> otherProfileList =
+ mMultiProfilePagerAdapter.getInactiveListAdapter().mDisplayList;
+
+ if (sameProfileList.isEmpty()) {
+ Log.d(TAG, "No targets in the current profile");
+ return false;
+ }
+
+ if (otherProfileList.size() != 1) {
+ Log.d(TAG, "Found " + otherProfileList.size() + " resolvers in the other profile");
+ return false;
+ }
+
+ if (otherProfileList.get(0).getResolveInfo().handleAllWebDataURI) {
+ Log.d(TAG, "Other profile is a web browser");
+ return false;
+ }
+
+ for (DisplayResolveInfo info : sameProfileList) {
+ if (!info.getResolveInfo().handleAllWebDataURI) {
+ Log.d(TAG, "Non-browser found in this profile");
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * 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.
+ */
+ final boolean postRebuildListInternal(boolean rebuildCompleted) {
+ int count = mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();
+
+ // We only rebuild asynchronously when we have multiple elements to sort. In the case where
+ // we're already done, we can check if we should auto-launch immediately.
+ if (rebuildCompleted && maybeAutolaunchActivity()) {
+ return true;
+ }
+
+ setupViewVisibilities();
+
+ if (shouldShowTabs()) {
+ setupProfileTabs();
+ }
+
+ return false;
+ }
+
+ private int isPermissionGranted(String permission, int uid) {
+ return ActivityManager.checkComponentPermission(permission, uid,
+ /* owningUid= */-1, /* exported= */ true);
+ }
+
+ /**
+ * @return {@code true} if a resolved target is autolaunched, otherwise {@code false}
+ */
+ private boolean maybeAutolaunchActivity() {
+ int numberOfProfiles = mMultiProfilePagerAdapter.getItemCount();
+ if (numberOfProfiles == 1 && maybeAutolaunchIfSingleTarget()) {
+ return true;
+ } else if (numberOfProfiles == 2
+ && mMultiProfilePagerAdapter.getActiveListAdapter().isTabLoaded()
+ && mMultiProfilePagerAdapter.getInactiveListAdapter().isTabLoaded()
+ && (maybeAutolaunchIfNoAppsOnInactiveTab()
+ || maybeAutolaunchIfCrossProfileSupported())) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean maybeAutolaunchIfSingleTarget() {
+ int count = mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();
+ if (count != 1) {
+ return false;
+ }
+
+ if (mMultiProfilePagerAdapter.getActiveListAdapter().getOtherProfile() != null) {
+ return false;
+ }
+
+ // Only one target, so we're a candidate to auto-launch!
+ final TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .targetInfoForPosition(0, false);
+ if (shouldAutoLaunchSingleChoice(target)) {
+ safelyStartActivity(target);
+ finish();
+ return true;
+ }
+ return false;
+ }
+
+ private boolean maybeAutolaunchIfNoAppsOnInactiveTab() {
+ int count = mMultiProfilePagerAdapter.getActiveListAdapter().getUnfilteredCount();
+ if (count != 1) {
+ return false;
+ }
+ ResolverListAdapter inactiveListAdapter =
+ mMultiProfilePagerAdapter.getInactiveListAdapter();
+ if (inactiveListAdapter.getUnfilteredCount() != 0) {
+ return false;
+ }
+ TargetInfo target = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .targetInfoForPosition(0, false);
+ safelyStartActivity(target);
+ finish();
+ return true;
+ }
+
+ /**
+ * When we have a personal and a work profile, we auto launch in the following scenario:
+ * - There is 1 resolved target on each profile
+ * - That target is the same app on both profiles
+ * - The target app has permission to communicate cross profiles
+ * - The target app has declared it supports cross-profile communication via manifest metadata
+ */
+ private boolean maybeAutolaunchIfCrossProfileSupported() {
+ ResolverListAdapter activeListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();
+ int count = activeListAdapter.getUnfilteredCount();
+ if (count != 1) {
+ return false;
+ }
+ ResolverListAdapter inactiveListAdapter =
+ mMultiProfilePagerAdapter.getInactiveListAdapter();
+ if (inactiveListAdapter.getUnfilteredCount() != 1) {
+ return false;
+ }
+ TargetInfo activeProfileTarget = activeListAdapter
+ .targetInfoForPosition(0, false);
+ TargetInfo inactiveProfileTarget = inactiveListAdapter.targetInfoForPosition(0, false);
+ if (!Objects.equals(activeProfileTarget.getResolvedComponentName(),
+ inactiveProfileTarget.getResolvedComponentName())) {
+ return false;
+ }
+ if (!shouldAutoLaunchSingleChoice(activeProfileTarget)) {
+ return false;
+ }
+ String packageName = activeProfileTarget.getResolvedComponentName().getPackageName();
+ if (!canAppInteractCrossProfiles(packageName)) {
+ return false;
+ }
+
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_AUTOLAUNCH_CROSS_PROFILE_TARGET)
+ .setBoolean(activeListAdapter.getUserHandle()
+ .equals(getPersonalProfileUserHandle()))
+ .setStrings(getMetricsCategory())
+ .write();
+ safelyStartActivity(activeProfileTarget);
+ finish();
+ return true;
+ }
+
+ /**
+ * Returns whether the package has the necessary permissions to interact across profiles on
+ * behalf of a given user.
+ *
+ * <p>This means meeting the following condition:
+ * <ul>
+ * <li>The app's {@link ApplicationInfo#crossProfile} flag must be true, and at least
+ * one of the following conditions must be fulfilled</li>
+ * <li>{@code Manifest.permission.INTERACT_ACROSS_USERS_FULL} granted.</li>
+ * <li>{@code Manifest.permission.INTERACT_ACROSS_USERS} granted.</li>
+ * <li>{@code Manifest.permission.INTERACT_ACROSS_PROFILES} granted, or the corresponding
+ * AppOps {@code android:interact_across_profiles} is set to "allow".</li>
+ * </ul>
+ *
+ */
+ private boolean canAppInteractCrossProfiles(String packageName) {
+ ApplicationInfo applicationInfo;
+ try {
+ applicationInfo = getPackageManager().getApplicationInfo(packageName, 0);
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Package " + packageName + " does not exist on current user.");
+ return false;
+ }
+ if (!applicationInfo.crossProfile) {
+ return false;
+ }
+
+ int packageUid = applicationInfo.uid;
+
+ if (isPermissionGranted(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
+ packageUid) == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+ if (isPermissionGranted(android.Manifest.permission.INTERACT_ACROSS_USERS, packageUid)
+ == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+ if (PermissionChecker.checkPermissionForPreflight(this, INTERACT_ACROSS_PROFILES,
+ PID_UNKNOWN, packageUid, packageName) == PackageManager.PERMISSION_GRANTED) {
+ return true;
+ }
+ return false;
+ }
+
+ private boolean isAutolaunching() {
+ return !mRegistered && isFinishing();
+ }
+
+ private void setupProfileTabs() {
+ maybeHideDivider();
+ TabHost tabHost = findViewById(com.android.internal.R.id.profile_tabhost);
+ tabHost.setup();
+ ViewPager viewPager = findViewById(com.android.internal.R.id.profile_pager);
+ viewPager.setSaveEnabled(false);
+
+ Button personalButton = (Button) getLayoutInflater().inflate(
+ R.layout.resolver_profile_tab_button, tabHost.getTabWidget(), false);
+ personalButton.setText(getPersonalTabLabel());
+ personalButton.setContentDescription(getPersonalTabAccessibilityLabel());
+
+ TabHost.TabSpec tabSpec = tabHost.newTabSpec(TAB_TAG_PERSONAL)
+ .setContent(com.android.internal.R.id.profile_pager)
+ .setIndicator(personalButton);
+ tabHost.addTab(tabSpec);
+
+ Button workButton = (Button) getLayoutInflater().inflate(
+ R.layout.resolver_profile_tab_button, tabHost.getTabWidget(), false);
+ workButton.setText(getWorkTabLabel());
+ workButton.setContentDescription(getWorkTabAccessibilityLabel());
+
+ tabSpec = tabHost.newTabSpec(TAB_TAG_WORK)
+ .setContent(com.android.internal.R.id.profile_pager)
+ .setIndicator(workButton);
+ tabHost.addTab(tabSpec);
+
+ TabWidget tabWidget = tabHost.getTabWidget();
+ tabWidget.setVisibility(View.VISIBLE);
+ updateActiveTabStyle(tabHost);
+
+ tabHost.setOnTabChangedListener(tabId -> {
+ updateActiveTabStyle(tabHost);
+ if (TAB_TAG_PERSONAL.equals(tabId)) {
+ viewPager.setCurrentItem(0);
+ } else {
+ viewPager.setCurrentItem(1);
+ }
+ setupViewVisibilities();
+ maybeLogProfileChange();
+ onProfileTabSelected();
+ DevicePolicyEventLogger
+ .createEvent(DevicePolicyEnums.RESOLVER_SWITCH_TABS)
+ .setInt(viewPager.getCurrentItem())
+ .setStrings(getMetricsCategory())
+ .write();
+ });
+
+ viewPager.setVisibility(View.VISIBLE);
+ tabHost.setCurrentTab(mMultiProfilePagerAdapter.getCurrentPage());
+ mMultiProfilePagerAdapter.setOnProfileSelectedListener(
+ new AbstractMultiProfilePagerAdapter.OnProfileSelectedListener() {
+ @Override
+ public void onProfileSelected(int index) {
+ tabHost.setCurrentTab(index);
+ resetButtonBar();
+ resetCheckedItem();
+ }
+
+ @Override
+ public void onProfilePageStateChanged(int state) {
+ onHorizontalSwipeStateChanged(state);
+ }
+ });
+ mMultiProfilePagerAdapter.setOnSwitchOnWorkSelectedListener(
+ () -> {
+ final View workTab = tabHost.getTabWidget().getChildAt(1);
+ workTab.setFocusable(true);
+ workTab.setFocusableInTouchMode(true);
+ workTab.requestFocus();
+ });
+ }
+
+ private String getPersonalTabLabel() {
+ return getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_PERSONAL_TAB, () -> getString(R.string.resolver_personal_tab));
+ }
+
+ private String getWorkTabLabel() {
+ return getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_WORK_TAB, () -> getString(R.string.resolver_work_tab));
+ }
+
+ void onHorizontalSwipeStateChanged(int state) {}
+
+ private void maybeHideDivider() {
+ if (!mIsIntentPicker) {
+ return;
+ }
+ final View divider = findViewById(com.android.internal.R.id.divider);
+ if (divider == null) {
+ return;
+ }
+ 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;
+ }
+ mLastSelected = ListView.INVALID_POSITION;
+ ListView inactiveListView = (ListView) mMultiProfilePagerAdapter.getInactiveAdapterView();
+ if (inactiveListView.getCheckedItemCount() > 0) {
+ inactiveListView.setItemChecked(inactiveListView.getCheckedItemPosition(), false);
+ }
+ }
+
+ private String getPersonalTabAccessibilityLabel() {
+ return getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_PERSONAL_TAB_ACCESSIBILITY,
+ () -> getString(R.string.resolver_personal_tab_accessibility));
+ }
+
+ private String getWorkTabAccessibilityLabel() {
+ return getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_WORK_TAB_ACCESSIBILITY,
+ () -> getString(R.string.resolver_work_tab_accessibility));
+ }
+
+ private static int getAttrColor(Context context, int attr) {
+ TypedArray ta = context.obtainStyledAttributes(new int[]{attr});
+ int colorAccent = ta.getColor(0, 0);
+ ta.recycle();
+ return colorAccent;
+ }
+
+ private void updateActiveTabStyle(TabHost tabHost) {
+ int currentTab = tabHost.getCurrentTab();
+ TextView selected = (TextView) tabHost.getTabWidget().getChildAt(currentTab);
+ TextView unselected = (TextView) tabHost.getTabWidget().getChildAt(1 - currentTab);
+ selected.setSelected(true);
+ unselected.setSelected(false);
+ }
+
+ private void setupViewVisibilities() {
+ ResolverListAdapter activeListAdapter = mMultiProfilePagerAdapter.getActiveListAdapter();
+ if (!mMultiProfilePagerAdapter.shouldShowEmptyStateScreen(activeListAdapter)) {
+ addUseDifferentAppLabelIfNecessary(activeListAdapter);
+ }
+ }
+
+ /**
+ * 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);
+ }
+ }
+
+ private void setupAdapterListView(ListView listView, ItemClickListener listener) {
+ listView.setOnItemClickListener(listener);
+ listView.setOnItemLongClickListener(listener);
+
+ if (mSupportsAlwaysUseOption) {
+ listView.setChoiceMode(AbsListView.CHOICE_MODE_SINGLE);
+ }
+ }
+
+ /**
+ * Configure the area above the app selection list (title, content preview, etc).
+ */
+ private void maybeCreateHeader(ResolverListAdapter listAdapter) {
+ if (mHeaderCreatorUser != null
+ && !listAdapter.getUserHandle().equals(mHeaderCreatorUser)) {
+ return;
+ }
+ if (!shouldShowTabs()
+ && listAdapter.getCount() == 0 && listAdapter.getPlaceholderCount() == 0) {
+ final TextView titleView = findViewById(com.android.internal.R.id.title);
+ if (titleView != null) {
+ titleView.setVisibility(View.GONE);
+ }
+ }
+
+ CharSequence title = mTitle != null
+ ? mTitle
+ : getTitleForAction(getTargetIntent(), mDefaultTitleResId);
+
+ if (!TextUtils.isEmpty(title)) {
+ final TextView titleView = findViewById(com.android.internal.R.id.title);
+ if (titleView != null) {
+ titleView.setText(title);
+ }
+ setTitle(title);
+ }
+
+ final ImageView iconView = findViewById(com.android.internal.R.id.icon);
+ if (iconView != null) {
+ listAdapter.loadFilteredItemIconTaskAsync(iconView);
+ }
+ mHeaderCreatorUser = listAdapter.getUserHandle();
+ }
+
+ 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);
+ mOnceButton.setEnabled(false);
+
+ int filteredPosition = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getFilteredPosition();
+ if (useLayoutWithDefault() && filteredPosition != ListView.INVALID_POSITION) {
+ setAlwaysButtonEnabled(true, filteredPosition, false);
+ mOnceButton.setEnabled(true);
+ // Focus the button if we already have the default option
+ mOnceButton.requestFocus();
+ return;
+ }
+
+ // When the items load in, if an item was already selected, enable the buttons
+ ListView currentAdapterView = (ListView) mMultiProfilePagerAdapter.getActiveAdapterView();
+ if (currentAdapterView != null
+ && currentAdapterView.getCheckedItemPosition() != ListView.INVALID_POSITION) {
+ setAlwaysButtonEnabled(true, currentAdapterView.getCheckedItemPosition(), true);
+ mOnceButton.setEnabled(true);
+ }
+ }
+
+ @Override // ResolverListCommunicator
+ public 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;
+ if (mMultiProfilePagerAdapter.getCurrentUserHandle().getIdentifier()
+ == UserHandle.myUserId()) {
+ currentUserAdapterHasFilteredItem =
+ mMultiProfilePagerAdapter.getActiveListAdapter().hasFilteredItem();
+ } else {
+ currentUserAdapterHasFilteredItem =
+ mMultiProfilePagerAdapter.getInactiveListAdapter().hasFilteredItem();
+ }
+ return mSupportsAlwaysUseOption && currentUserAdapterHasFilteredItem;
+ }
+
+ /**
+ * 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) {
+ mRetainInOnStop = retainInOnStop;
+ }
+
+ /**
+ * Check a simple match for the component of two ResolveInfos.
+ */
+ @Override // ResolverListCommunicator
+ public 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())
+ && mMultiProfilePagerAdapter.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;
+ }
+ 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;
+ mMultiProfilePagerAdapter.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();
+ }
+ }
+ };
+ }
+
+ @VisibleForTesting
+ public static final class ResolvedComponentInfo {
+ public final ComponentName name;
+ private final List<Intent> mIntents = new ArrayList<>();
+ private final List<ResolveInfo> mResolveInfos = new ArrayList<>();
+ private boolean mPinned;
+ private boolean mFixedAtTop;
+
+ public ResolvedComponentInfo(ComponentName name, Intent intent, ResolveInfo info) {
+ this.name = name;
+ 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;
+ }
+
+ public boolean isFixedAtTop() {
+ return mFixedAtTop;
+ }
+
+ public void setFixedAtTop(boolean isFixedAtTop) {
+ mFixedAtTop = isFixedAtTop;
+ }
+ }
+
+ class ItemClickListener implements AdapterView.OnItemClickListener,
+ AdapterView.OnItemLongClickListener {
+ @Override
+ public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+ final ListView listView = parent instanceof ListView ? (ListView) parent : null;
+ if (listView != null) {
+ position -= listView.getHeaderViewsCount();
+ }
+ if (position < 0) {
+ // Header views don't count.
+ return;
+ }
+ // If we're still loading, we can't yet enable the buttons.
+ if (mMultiProfilePagerAdapter.getActiveListAdapter()
+ .resolveInfoForPosition(position, true) == null) {
+ return;
+ }
+ ListView currentAdapterView =
+ (ListView) mMultiProfilePagerAdapter.getActiveAdapterView();
+ final int checkedPos = currentAdapterView.getCheckedItemPosition();
+ final boolean hasValidSelection = checkedPos != ListView.INVALID_POSITION;
+ if (!useLayoutWithDefault()
+ && (!hasValidSelection || mLastSelected != checkedPos)
+ && mAlwaysButton != null) {
+ setAlwaysButtonEnabled(hasValidSelection, checkedPos, true);
+ mOnceButton.setEnabled(hasValidSelection);
+ if (hasValidSelection) {
+ currentAdapterView.smoothScrollToPosition(checkedPos);
+ mOnceButton.requestFocus();
+ }
+ mLastSelected = checkedPos;
+ } else {
+ startSelected(position, false, true);
+ }
+ }
+
+ @Override
+ public boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id) {
+ final ListView listView = parent instanceof ListView ? (ListView) parent : null;
+ if (listView != null) {
+ position -= listView.getHeaderViewsCount();
+ }
+ if (position < 0) {
+ // Header views don't count.
+ return false;
+ }
+ ResolveInfo ri = mMultiProfilePagerAdapter.getActiveListAdapter()
+ .resolveInfoForPosition(position, true);
+ showTargetDetails(ri);
+ return true;
+ }
+
+ }
+
+ static final boolean isSpecificUriMatch(int match) {
+ match = match&IntentFilter.MATCH_CATEGORY_MASK;
+ return match >= IntentFilter.MATCH_CATEGORY_HOST
+ && match <= IntentFilter.MATCH_CATEGORY_PATH;
+ }
+
+ static class PickTargetOptionRequest extends PickOptionRequest {
+ public PickTargetOptionRequest(@Nullable Prompt prompt, Option[] options,
+ @Nullable Bundle extras) {
+ super(prompt, options, extras);
+ }
+
+ @Override
+ public void onCancel() {
+ super.onCancel();
+ final ResolverActivity ra = (ResolverActivity) getActivity();
+ if (ra != null) {
+ ra.mPickOptionRequest = null;
+ ra.finish();
+ }
+ }
+
+ @Override
+ public void onPickOptionResult(boolean finished, Option[] selections, Bundle result) {
+ super.onPickOptionResult(finished, selections, result);
+ if (selections.length != 1) {
+ // TODO In a better world we would filter the UI presented here and let the
+ // user refine. Maybe later.
+ return;
+ }
+
+ final ResolverActivity ra = (ResolverActivity) getActivity();
+ if (ra != null) {
+ final TargetInfo ti = ra.mMultiProfilePagerAdapter.getActiveListAdapter()
+ .getItem(selections[0].getIndex());
+ if (ra.onTargetSelected(ti, false)) {
+ ra.mPickOptionRequest = null;
+ ra.finish();
+ }
+ }
+ }
+ }
+
+ protected void maybeLogProfileChange() {}
+}
diff --git a/java/src/com/android/intentresolver/ResolverComparatorModel.java b/java/src/com/android/intentresolver/ResolverComparatorModel.java
new file mode 100644
index 00000000..79160c84
--- /dev/null
+++ b/java/src/com/android/intentresolver/ResolverComparatorModel.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import android.content.ComponentName;
+import android.content.pm.ResolveInfo;
+
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * A ranking model for resolver targets, providing ordering and (optionally) numerical scoring.
+ *
+ * As required by the {@link Comparator} contract, objects returned by {@code getComparator()} must
+ * apply a total ordering on its inputs consistent across all calls to {@code Comparator#compare()}.
+ * Other query methods and ranking feedback should refer to that same ordering, so implementors are
+ * generally advised to "lock in" an immutable snapshot of their model data when this object is
+ * initialized (preferring to replace the entire {@code ResolverComparatorModel} instance if the
+ * backing data needs to be updated in the future).
+ */
+interface ResolverComparatorModel {
+ /**
+ * Get a {@code Comparator} that can be used to sort {@code ResolveInfo} targets according to
+ * the model ranking.
+ */
+ Comparator<ResolveInfo> getComparator();
+
+ /**
+ * Get the numerical score, if any, that the model assigns to the component with the specified
+ * {@code name}. Scores range from zero to one, with one representing the highest possible
+ * likelihood that the user will select that component as the target. Implementations that don't
+ * assign numerical scores are <em>recommended</em> to return a value of 0 for all components.
+ */
+ float getScore(ComponentName name);
+
+ /**
+ * Notify the model that the user selected a target. (Models may log this information, use it as
+ * a feedback signal for their ranking, etc.) Because the data in this
+ * {@code ResolverComparatorModel} instance is immutable, clients will need to get an up-to-date
+ * instance in order to see any changes in the ranking that might result from this feedback.
+ */
+ void notifyOnTargetSelected(ComponentName componentName);
+}
diff --git a/java/src/com/android/intentresolver/ResolverListAdapter.java b/java/src/com/android/intentresolver/ResolverListAdapter.java
new file mode 100644
index 00000000..898d8c8e
--- /dev/null
+++ b/java/src/com/android/intentresolver/ResolverListAdapter.java
@@ -0,0 +1,1163 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import static android.content.Context.ACTIVITY_SERVICE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.PermissionChecker;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LabeledIntent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.AsyncTask;
+import android.os.RemoteException;
+import android.os.Trace;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+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;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ResolverListAdapter extends BaseAdapter {
+ private static final String TAG = "ResolverListAdapter";
+
+ private final List<Intent> mIntents;
+ private final Intent[] mInitialIntents;
+ private final List<ResolveInfo> mBaseResolveList;
+ private final PackageManager mPm;
+ protected final Context mContext;
+ private static ColorMatrixColorFilter sSuspendedMatrixColorFilter;
+ private final int mIconDpi;
+ protected ResolveInfo mLastChosen;
+ private DisplayResolveInfo mOtherProfile;
+ ResolverListController mResolverListController;
+ private int mPlaceholderCount;
+
+ protected final LayoutInflater mInflater;
+
+ // This one is the list that the Adapter will actually present.
+ List<DisplayResolveInfo> mDisplayList;
+ private List<ResolvedComponentInfo> mUnfilteredResolveList;
+
+ private int mLastChosenPosition = -1;
+ private boolean mFilterLastUsed;
+ final ResolverListCommunicator mResolverListCommunicator;
+ private Runnable mPostListReadyRunnable;
+ private final boolean mIsAudioCaptureDevice;
+ private boolean mIsTabLoaded;
+
+ public ResolverListAdapter(Context context, List<Intent> payloadIntents,
+ Intent[] initialIntents, List<ResolveInfo> rList,
+ boolean filterLastUsed,
+ ResolverListController resolverListController,
+ ResolverListCommunicator resolverListCommunicator,
+ boolean isAudioCaptureDevice) {
+ mContext = context;
+ mIntents = payloadIntents;
+ mInitialIntents = initialIntents;
+ mBaseResolveList = rList;
+ mInflater = LayoutInflater.from(context);
+ mPm = context.getPackageManager();
+ mDisplayList = new ArrayList<>();
+ mFilterLastUsed = filterLastUsed;
+ mResolverListController = resolverListController;
+ mResolverListCommunicator = resolverListCommunicator;
+ mIsAudioCaptureDevice = isAudioCaptureDevice;
+ final ActivityManager am = (ActivityManager) mContext.getSystemService(ACTIVITY_SERVICE);
+ mIconDpi = am.getLauncherLargeIconDensity();
+ }
+
+ public void handlePackagesChanged() {
+ mResolverListCommunicator.onHandlePackagesChanged(this);
+ }
+
+ public void setPlaceholderCount(int count) {
+ mPlaceholderCount = count;
+ }
+
+ public int getPlaceholderCount() {
+ return mPlaceholderCount;
+ }
+
+ @Nullable
+ public DisplayResolveInfo getFilteredItem() {
+ if (mFilterLastUsed && mLastChosenPosition >= 0) {
+ // Not using getItem since it offsets to dodge this position for the list
+ return mDisplayList.get(mLastChosenPosition);
+ }
+ return null;
+ }
+
+ public DisplayResolveInfo getOtherProfile() {
+ return mOtherProfile;
+ }
+
+ public int getFilteredPosition() {
+ if (mFilterLastUsed && mLastChosenPosition >= 0) {
+ return mLastChosenPosition;
+ }
+ return AbsListView.INVALID_POSITION;
+ }
+
+ public boolean hasFilteredItem() {
+ return mFilterLastUsed && mLastChosen != null;
+ }
+
+ public float getScore(DisplayResolveInfo target) {
+ return mResolverListController.getScore(target);
+ }
+
+ /**
+ * Returns the app share score of the given {@code componentName}.
+ */
+ public float getScore(ComponentName componentName) {
+ return mResolverListController.getScore(componentName);
+ }
+
+ public void updateModel(ComponentName componentName) {
+ mResolverListController.updateModel(componentName);
+ }
+
+ public void updateChooserCounts(String packageName, String action) {
+ mResolverListController.updateChooserCounts(
+ packageName, getUserHandle().getIdentifier(), action);
+ }
+
+ List<ResolvedComponentInfo> getUnfilteredResolveList() {
+ return mUnfilteredResolveList;
+ }
+
+ /**
+ * Rebuild the list of resolvers. When rebuilding is complete, queue the {@code onPostListReady}
+ * callback on the main handler with {@code rebuildCompleted} true.
+ *
+ * In some cases some parts will need some asynchronous work to complete. Then this will first
+ * immediately queue {@code onPostListReady} (on the main handler) with {@code rebuildCompleted}
+ * false; only when the asynchronous work completes will this then go on to queue another
+ * {@code onPostListReady} callback with {@code rebuildCompleted} true.
+ *
+ * The {@code doPostProcessing} parameter is used to specify whether to update the UI and
+ * load additional targets (e.g. direct share) after the list has been rebuilt. We may choose
+ * to skip that step if we're only loading the inactive profile's resolved apps to know the
+ * number of targets.
+ *
+ * @return Whether the list building was completed synchronously. If not, we'll queue the
+ * {@code onPostListReady} callback first with {@code rebuildCompleted} false, and then again
+ * with {@code rebuildCompleted} true at the end of some newly-launched asynchronous work.
+ * Otherwise the callback is only queued once, with {@code rebuildCompleted} true.
+ */
+ protected boolean rebuildList(boolean doPostProcessing) {
+ Trace.beginSection("ResolverListAdapter#rebuildList");
+ mDisplayList.clear();
+ mIsTabLoaded = false;
+ mLastChosenPosition = -1;
+
+ List<ResolvedComponentInfo> currentResolveList = getInitialRebuiltResolveList();
+
+ /* TODO: this seems like unnecessary extra complexity; why do we need to do this "primary"
+ * (i.e. "eligibility") filtering before evaluating the "other profile" special-treatment,
+ * but the "secondary" (i.e. "priority") filtering after? Are there in fact cases where the
+ * eligibility conditions will filter out a result that would've otherwise gotten the "other
+ * profile" treatment? Or, are there cases where the priority conditions *would* filter out
+ * a result, but we *want* that result to get the "other profile" treatment, so we only
+ * filter *after* evaluating the special-treatment conditions? If the answer to either is
+ * "no," then the filtering steps can be consolidated. (And that also makes the "unfiltered
+ * list" bookkeeping a little cleaner.)
+ */
+ mUnfilteredResolveList = performPrimaryResolveListFiltering(currentResolveList);
+
+ // So far we only support a single other profile at a time.
+ // The first one we see gets special treatment.
+ ResolvedComponentInfo otherProfileInfo =
+ getFirstNonCurrentUserResolvedComponentInfo(currentResolveList);
+ updateOtherProfileTreatment(otherProfileInfo);
+ if (otherProfileInfo != null) {
+ currentResolveList.remove(otherProfileInfo);
+ /* TODO: the previous line removed the "other profile info" item from
+ * mUnfilteredResolveList *ONLY IF* that variable is an alias for the same List instance
+ * as currentResolveList (i.e., if no items were filtered out as the result of the
+ * earlier "primary" filtering). It seems wrong for our behavior to depend on that.
+ * Should we:
+ * A. replicate the above removal to mUnfilteredResolveList (which is idempotent, so we
+ * don't even have to check whether they're aliases); or
+ * B. break the alias relationship by copying currentResolveList to a new
+ * mUnfilteredResolveList instance if necessary before removing otherProfileInfo?
+ * In other words: do we *want* otherProfileInfo in the "unfiltered" results? Either
+ * way, we'll need one of the changes suggested above.
+ */
+ }
+
+ // If no results have yet been filtered, mUnfilteredResolveList is an alias for the same
+ // List instance as currentResolveList. Then we need to make a copy to store as the
+ // mUnfilteredResolveList if we go on to filter any more items. Otherwise we've already
+ // copied the original unfiltered items to a separate List instance and can now filter
+ // the remainder in-place without any further bookkeeping.
+ boolean needsCopyOfUnfiltered = (mUnfilteredResolveList == currentResolveList);
+ List<ResolvedComponentInfo> originalList = performSecondaryResolveListFiltering(
+ currentResolveList, needsCopyOfUnfiltered);
+ if (originalList != null) {
+ // Only need the originalList value if there was a modification (otherwise it's null
+ // and shouldn't overwrite mUnfilteredResolveList).
+ mUnfilteredResolveList = originalList;
+ }
+
+ boolean result =
+ finishRebuildingListWithFilteredResults(currentResolveList, doPostProcessing);
+ Trace.endSection();
+ return result;
+ }
+
+ /**
+ * Get the full (unfiltered) set of {@code ResolvedComponentInfo} records for all resolvers
+ * to be considered in a newly-rebuilt list. This list will be filtered and ranked before the
+ * rebuild is complete.
+ */
+ List<ResolvedComponentInfo> getInitialRebuiltResolveList() {
+ if (mBaseResolveList != null) {
+ List<ResolvedComponentInfo> currentResolveList = new ArrayList<>();
+ mResolverListController.addResolveListDedupe(currentResolveList,
+ mResolverListCommunicator.getTargetIntent(),
+ mBaseResolveList);
+ return currentResolveList;
+ } else {
+ return mResolverListController.getResolversForIntent(
+ /* shouldGetResolvedFilter= */ true,
+ mResolverListCommunicator.shouldGetActivityMetadata(),
+ mResolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ mIntents);
+ }
+ }
+
+ /**
+ * Remove ineligible activities from {@code currentResolveList} (if non-null), in-place. More
+ * broadly, filtering logic should apply in the "primary" stage if it should preclude items from
+ * receiving the "other profile" special-treatment described in {@code rebuildList()}.
+ *
+ * @return A copy of the original {@code currentResolveList}, if any items were removed, or a
+ * (possibly null) reference to the original list otherwise. (That is, this always returns a
+ * list of all the unfiltered items, but if no items were filtered, it's just an alias for the
+ * same list that was passed in).
+ */
+ @Nullable
+ List<ResolvedComponentInfo> performPrimaryResolveListFiltering(
+ @Nullable List<ResolvedComponentInfo> currentResolveList) {
+ /* TODO: mBaseResolveList appears to be(?) some kind of configured mode. Why is it not
+ * subject to filterIneligibleActivities, even though all the other logic still applies
+ * (including "secondary" filtering)? (This also relates to the earlier question; do we
+ * believe there's an item that would be eligible for "other profile" special treatment,
+ * except we want to filter it out as ineligible... but only if we're not in
+ * "mBaseResolveList mode"? */
+ if ((mBaseResolveList != null) || (currentResolveList == null)) {
+ return currentResolveList;
+ }
+
+ List<ResolvedComponentInfo> originalList =
+ mResolverListController.filterIneligibleActivities(currentResolveList, true);
+ return (originalList == null) ? currentResolveList : originalList;
+ }
+
+ /**
+ * Remove low-priority activities from {@code currentResolveList} (if non-null), in place. More
+ * broadly, filtering logic should apply in the "secondary" stage to prevent items from
+ * appearing in the rebuilt-list results, while still considering those items for the "other
+ * profile" special-treatment described in {@code rebuildList()}.
+ *
+ * @return the same (possibly null) List reference as {@code currentResolveList} if the list is
+ * unmodified as a result of filtering; or, if some item(s) were removed, then either a copy of
+ * the original {@code currentResolveList} (if {@code returnCopyOfOriginalListIfModified} is
+ * true), or null (otherwise).
+ */
+ @Nullable
+ List<ResolvedComponentInfo> performSecondaryResolveListFiltering(
+ @Nullable List<ResolvedComponentInfo> currentResolveList,
+ boolean returnCopyOfOriginalListIfModified) {
+ if ((currentResolveList == null) || currentResolveList.isEmpty()) {
+ return currentResolveList;
+ }
+ return mResolverListController.filterLowPriority(
+ currentResolveList, returnCopyOfOriginalListIfModified);
+ }
+
+ /**
+ * Update the special "other profile" UI treatment based on the components resolved for a
+ * newly-built list.
+ *
+ * @param otherProfileInfo the first {@code ResolvedComponentInfo} specifying a
+ * {@code targetUserId} other than {@code USER_CURRENT}, or null if no such component info was
+ * found in the process of rebuilding the list (or if any such candidates were already removed
+ * due to "primary filtering").
+ */
+ void updateOtherProfileTreatment(@Nullable ResolvedComponentInfo otherProfileInfo) {
+ mLastChosen = null;
+
+ if (otherProfileInfo != null) {
+ mOtherProfile = makeOtherProfileDisplayResolveInfo(
+ mContext, otherProfileInfo, mPm, mResolverListCommunicator, mIconDpi);
+ } else {
+ mOtherProfile = null;
+ try {
+ mLastChosen = mResolverListController.getLastChosen();
+ // TODO: does this also somehow need to update mLastChosenPosition? If so, maybe
+ // the current method should also take responsibility for re-initializing
+ // mLastChosenPosition, where it's currently done at the start of rebuildList()?
+ // (Why is this related to the presence of mOtherProfile in fhe first place?)
+ } catch (RemoteException re) {
+ Log.d(TAG, "Error calling getLastChosenActivity\n" + re);
+ }
+ }
+ }
+
+ /**
+ * Prepare the appropriate placeholders to eventually display the final set of resolved
+ * components in a newly-rebuilt list, and spawn an asynchronous sorting task if necessary.
+ * This eventually results in a {@code onPostListReady} callback with {@code rebuildCompleted}
+ * true; if any asynchronous work is required, that will first be preceded by a separate
+ * occurrence of the callback with {@code rebuildCompleted} false (once there are placeholders
+ * set up to represent the pending asynchronous results).
+ * @return Whether we were able to do all the work to prepare the list for display
+ * synchronously; if false, there will eventually be two separate {@code onPostListReady}
+ * callbacks, first with placeholders to represent pending asynchronous results, then later when
+ * the results are ready for presentation.
+ */
+ boolean finishRebuildingListWithFilteredResults(
+ @Nullable List<ResolvedComponentInfo> filteredResolveList, boolean doPostProcessing) {
+ if (filteredResolveList == null || filteredResolveList.size() < 2) {
+ // No asynchronous work to do.
+ setPlaceholderCount(0);
+ processSortedList(filteredResolveList, doPostProcessing);
+ return true;
+ }
+
+ int placeholderCount = filteredResolveList.size();
+ if (mResolverListCommunicator.useLayoutWithDefault()) {
+ --placeholderCount;
+ }
+ setPlaceholderCount(placeholderCount);
+
+ // Send an "incomplete" list-ready while the async task is running.
+ postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ false);
+ createSortingTask(doPostProcessing).execute(filteredResolveList);
+ return false;
+ }
+
+ AsyncTask<List<ResolvedComponentInfo>,
+ Void,
+ List<ResolvedComponentInfo>> createSortingTask(boolean doPostProcessing) {
+ return new AsyncTask<List<ResolvedComponentInfo>,
+ Void,
+ List<ResolvedComponentInfo>>() {
+ @Override
+ protected List<ResolvedComponentInfo> doInBackground(
+ List<ResolvedComponentInfo>... params) {
+ mResolverListController.sort(params[0]);
+ return params[0];
+ }
+ @Override
+ protected void onPostExecute(List<ResolvedComponentInfo> sortedComponents) {
+ processSortedList(sortedComponents, doPostProcessing);
+ notifyDataSetChanged();
+ if (doPostProcessing) {
+ mResolverListCommunicator.updateProfileViewButton();
+ }
+ }
+ };
+ }
+
+ protected void processSortedList(List<ResolvedComponentInfo> sortedComponents,
+ boolean doPostProcessing) {
+ final int n = sortedComponents != null ? sortedComponents.size() : 0;
+ Trace.beginSection("ResolverListAdapter#processSortedList:" + n);
+ if (n != 0) {
+ // First put the initial items at the top.
+ if (mInitialIntents != null) {
+ for (int i = 0; i < mInitialIntents.length; i++) {
+ Intent ii = mInitialIntents[i];
+ if (ii == null) {
+ continue;
+ }
+ // Because of AIDL bug, resolveActivityInfo can't accept subclasses of Intent.
+ final Intent rii = (ii.getClass() == Intent.class) ? ii : new Intent(ii);
+ ActivityInfo ai = rii.resolveActivityInfo(mPm, 0);
+ if (ai == null) {
+ Log.w(TAG, "No activity found for " + ii);
+ continue;
+ }
+ ResolveInfo ri = new ResolveInfo();
+ ri.activityInfo = ai;
+ UserManager userManager =
+ (UserManager) mContext.getSystemService(Context.USER_SERVICE);
+ if (ii instanceof LabeledIntent) {
+ LabeledIntent li = (LabeledIntent) ii;
+ ri.resolvePackageName = li.getSourcePackage();
+ ri.labelRes = li.getLabelResource();
+ ri.nonLocalizedLabel = li.getNonLocalizedLabel();
+ ri.icon = li.getIconResource();
+ ri.iconResourceId = ri.icon;
+ }
+ if (userManager.isManagedProfile()) {
+ ri.noResourceId = true;
+ ri.icon = 0;
+ }
+
+ addResolveInfo(new DisplayResolveInfo(ii, ri,
+ ri.loadLabel(mPm), null, ii, makePresentationGetter(ri)));
+ }
+ }
+
+
+ for (ResolvedComponentInfo rci : sortedComponents) {
+ final ResolveInfo ri = rci.getResolveInfoAt(0);
+ if (ri != null) {
+ addResolveInfoWithAlternates(rci);
+ }
+ }
+ }
+
+ mResolverListCommunicator.sendVoiceChoicesIfNeeded();
+ postListReadyRunnable(doPostProcessing, /* rebuildCompleted */ true);
+ mIsTabLoaded = true;
+ Trace.endSection();
+ }
+
+ /**
+ * Some necessary methods for creating the list are initiated in onCreate and will also
+ * determine the layout known. We therefore can't update the UI inline and post to the
+ * handler thread to update after the current task is finished.
+ * @param doPostProcessing Whether to update the UI and load additional direct share targets
+ * after the list has been rebuilt
+ * @param rebuildCompleted Whether the list has been completely rebuilt
+ */
+ void postListReadyRunnable(boolean doPostProcessing, boolean rebuildCompleted) {
+ if (mPostListReadyRunnable == null) {
+ mPostListReadyRunnable = new Runnable() {
+ @Override
+ public void run() {
+ mResolverListCommunicator.onPostListReady(ResolverListAdapter.this,
+ doPostProcessing, rebuildCompleted);
+ mPostListReadyRunnable = null;
+ }
+ };
+ mContext.getMainThreadHandler().post(mPostListReadyRunnable);
+ }
+ }
+
+ private void addResolveInfoWithAlternates(ResolvedComponentInfo rci) {
+ final int count = rci.getCount();
+ final Intent intent = rci.getIntentAt(0);
+ final ResolveInfo add = rci.getResolveInfoAt(0);
+ final Intent replaceIntent =
+ mResolverListCommunicator.getReplacementIntent(add.activityInfo, intent);
+ final Intent defaultIntent = mResolverListCommunicator.getReplacementIntent(
+ add.activityInfo, mResolverListCommunicator.getTargetIntent());
+ final DisplayResolveInfo
+ dri = new DisplayResolveInfo(intent, add,
+ replaceIntent != null ? replaceIntent : defaultIntent, makePresentationGetter(add));
+ dri.setPinned(rci.isPinned());
+ if (rci.isPinned()) {
+ Log.i(TAG, "Pinned item: " + rci.name);
+ }
+ addResolveInfo(dri);
+ if (replaceIntent == intent) {
+ // Only add alternates if we didn't get a specific replacement from
+ // the caller. If we have one it trumps potential alternates.
+ for (int i = 1, n = count; i < n; i++) {
+ final Intent altIntent = rci.getIntentAt(i);
+ dri.addAlternateSourceIntent(altIntent);
+ }
+ }
+ updateLastChosenPosition(add);
+ }
+
+ private void updateLastChosenPosition(ResolveInfo info) {
+ // If another profile is present, ignore the last chosen entry.
+ if (mOtherProfile != null) {
+ mLastChosenPosition = -1;
+ return;
+ }
+ if (mLastChosen != null
+ && mLastChosen.activityInfo.packageName.equals(info.activityInfo.packageName)
+ && mLastChosen.activityInfo.name.equals(info.activityInfo.name)) {
+ mLastChosenPosition = mDisplayList.size() - 1;
+ }
+ }
+
+ // We assume that at this point we've already filtered out the only intent for a different
+ // targetUserId which we're going to use.
+ private void addResolveInfo(DisplayResolveInfo dri) {
+ if (dri != null && dri.getResolveInfo() != null
+ && dri.getResolveInfo().targetUserId == UserHandle.USER_CURRENT) {
+ if (shouldAddResolveInfo(dri)) {
+ mDisplayList.add(dri);
+ Log.i(TAG, "Add DisplayResolveInfo component: " + dri.getResolvedComponentName()
+ + ", intent component: " + dri.getResolvedIntent().getComponent());
+ }
+ }
+ }
+
+ // Check whether {@code dri} should be added into mDisplayList.
+ protected boolean shouldAddResolveInfo(DisplayResolveInfo dri) {
+ // Checks if this info is already listed in display.
+ for (DisplayResolveInfo existingInfo : mDisplayList) {
+ if (mResolverListCommunicator
+ .resolveInfoMatch(dri.getResolveInfo(), existingInfo.getResolveInfo())) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Nullable
+ public ResolveInfo resolveInfoForPosition(int position, boolean filtered) {
+ TargetInfo target = targetInfoForPosition(position, filtered);
+ if (target != null) {
+ return target.getResolveInfo();
+ }
+ return null;
+ }
+
+ @Nullable
+ public TargetInfo targetInfoForPosition(int position, boolean filtered) {
+ if (filtered) {
+ return getItem(position);
+ }
+ if (mDisplayList.size() > position) {
+ return mDisplayList.get(position);
+ }
+ return null;
+ }
+
+ public int getCount() {
+ int totalSize = mDisplayList == null || mDisplayList.isEmpty() ? mPlaceholderCount :
+ mDisplayList.size();
+ if (mFilterLastUsed && mLastChosenPosition >= 0) {
+ totalSize--;
+ }
+ return totalSize;
+ }
+
+ public int getUnfilteredCount() {
+ return mDisplayList.size();
+ }
+
+ @Nullable
+ public TargetInfo getItem(int position) {
+ if (mFilterLastUsed && mLastChosenPosition >= 0 && position >= mLastChosenPosition) {
+ position++;
+ }
+ if (mDisplayList.size() > position) {
+ return mDisplayList.get(position);
+ } else {
+ return null;
+ }
+ }
+
+ public long getItemId(int position) {
+ return position;
+ }
+
+ public int getDisplayResolveInfoCount() {
+ return mDisplayList.size();
+ }
+
+ public DisplayResolveInfo getDisplayResolveInfo(int index) {
+ // Used to query services. We only query services for primary targets, not alternates.
+ return mDisplayList.get(index);
+ }
+
+ public final View getView(int position, View convertView, ViewGroup parent) {
+ View view = convertView;
+ if (view == null) {
+ view = createView(parent);
+ }
+ onBindView(view, getItem(position), position);
+ return view;
+ }
+
+ public final View createView(ViewGroup parent) {
+ final View view = onCreateView(parent);
+ final ViewHolder holder = new ViewHolder(view);
+ view.setTag(holder);
+ return view;
+ }
+
+ View onCreateView(ViewGroup parent) {
+ return mInflater.inflate(
+ R.layout.resolve_list_item, parent, false);
+ }
+
+ public final void bindView(int position, View view) {
+ onBindView(view, getItem(position), position);
+ }
+
+ protected void onBindView(View view, TargetInfo info, int position) {
+ final ViewHolder holder = (ViewHolder) view.getTag();
+ if (info == null) {
+ holder.icon.setImageDrawable(
+ mContext.getDrawable(R.drawable.resolver_icon_placeholder));
+ return;
+ }
+
+ if (info instanceof DisplayResolveInfo
+ && !((DisplayResolveInfo) info).hasDisplayLabel()) {
+ getLoadLabelTask((DisplayResolveInfo) info, holder).execute();
+ } else {
+ holder.bindLabel(info.getDisplayLabel(), info.getExtendedInfo(), alwaysShowSubLabel());
+ }
+
+ if (info instanceof DisplayResolveInfo
+ && !((DisplayResolveInfo) info).hasDisplayIcon()) {
+ new LoadIconTask((DisplayResolveInfo) info, holder).execute();
+ } else {
+ holder.bindIcon(info);
+ }
+ }
+
+ protected LoadLabelTask getLoadLabelTask(DisplayResolveInfo info, ViewHolder holder) {
+ return new LoadLabelTask(info, holder);
+ }
+
+ public void onDestroy() {
+ if (mPostListReadyRunnable != null) {
+ mContext.getMainThreadHandler().removeCallbacks(mPostListReadyRunnable);
+ mPostListReadyRunnable = null;
+ }
+ if (mResolverListController != null) {
+ mResolverListController.destroy();
+ }
+ }
+
+ private static ColorMatrixColorFilter getSuspendedColorMatrix() {
+ if (sSuspendedMatrixColorFilter == null) {
+
+ int grayValue = 127;
+ float scale = 0.5f; // half bright
+
+ ColorMatrix tempBrightnessMatrix = new ColorMatrix();
+ float[] mat = tempBrightnessMatrix.getArray();
+ mat[0] = scale;
+ mat[6] = scale;
+ mat[12] = scale;
+ mat[4] = grayValue;
+ mat[9] = grayValue;
+ mat[14] = grayValue;
+
+ ColorMatrix matrix = new ColorMatrix();
+ matrix.setSaturation(0.0f);
+ matrix.preConcat(tempBrightnessMatrix);
+ sSuspendedMatrixColorFilter = new ColorMatrixColorFilter(matrix);
+ }
+ return sSuspendedMatrixColorFilter;
+ }
+
+ ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo ai) {
+ return new ActivityInfoPresentationGetter(mContext, mIconDpi, ai);
+ }
+
+ ResolveInfoPresentationGetter makePresentationGetter(ResolveInfo ri) {
+ return new ResolveInfoPresentationGetter(mContext, mIconDpi, ri);
+ }
+
+ Drawable loadIconForResolveInfo(ResolveInfo ri) {
+ // Load icons based on the current process. If in work profile icons should be badged.
+ return makePresentationGetter(ri).getIcon(getUserHandle());
+ }
+
+ void loadFilteredItemIconTaskAsync(@NonNull ImageView iconView) {
+ final DisplayResolveInfo iconInfo = getFilteredItem();
+ if (iconView != null && iconInfo != null) {
+ new AsyncTask<Void, Void, Drawable>() {
+ @Override
+ protected Drawable doInBackground(Void... params) {
+ return loadIconForResolveInfo(iconInfo.getResolveInfo());
+ }
+
+ @Override
+ protected void onPostExecute(Drawable d) {
+ iconView.setImageDrawable(d);
+ }
+ }.execute();
+ }
+ }
+
+ @VisibleForTesting
+ public UserHandle getUserHandle() {
+ return mResolverListController.getUserHandle();
+ }
+
+ protected List<ResolvedComponentInfo> getResolversForUser(UserHandle userHandle) {
+ return mResolverListController.getResolversForIntentAsUser(true,
+ mResolverListCommunicator.shouldGetActivityMetadata(),
+ mResolverListCommunicator.shouldGetOnlyDefaultActivities(),
+ mIntents, userHandle);
+ }
+
+ protected List<Intent> getIntents() {
+ return mIntents;
+ }
+
+ protected boolean isTabLoaded() {
+ return mIsTabLoaded;
+ }
+
+ protected void markTabLoaded() {
+ mIsTabLoaded = true;
+ }
+
+ protected boolean alwaysShowSubLabel() {
+ return false;
+ }
+
+ /**
+ * Find the first element in a list of {@code ResolvedComponentInfo} objects whose
+ * {@code ResolveInfo} specifies a {@code targetUserId} other than the current user.
+ * @return the first ResolvedComponentInfo targeting a non-current user, or null if there are
+ * none (or if the list itself is null).
+ */
+ private static ResolvedComponentInfo getFirstNonCurrentUserResolvedComponentInfo(
+ @Nullable List<ResolvedComponentInfo> resolveList) {
+ if (resolveList == null) {
+ return null;
+ }
+
+ for (ResolvedComponentInfo info : resolveList) {
+ ResolveInfo resolveInfo = info.getResolveInfoAt(0);
+ if (resolveInfo.targetUserId != UserHandle.USER_CURRENT) {
+ return info;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Set up a {@code DisplayResolveInfo} to provide "special treatment" for the first "other"
+ * profile in the resolve list (i.e., the first non-current profile to appear as the target user
+ * of an element in the resolve list).
+ */
+ private static DisplayResolveInfo makeOtherProfileDisplayResolveInfo(
+ Context context,
+ ResolvedComponentInfo resolvedComponentInfo,
+ PackageManager pm,
+ ResolverListCommunicator resolverListCommunicator,
+ int iconDpi) {
+ ResolveInfo resolveInfo = resolvedComponentInfo.getResolveInfoAt(0);
+
+ Intent pOrigIntent = resolverListCommunicator.getReplacementIntent(
+ resolveInfo.activityInfo,
+ resolvedComponentInfo.getIntentAt(0));
+ Intent replacementIntent = resolverListCommunicator.getReplacementIntent(
+ resolveInfo.activityInfo,
+ resolverListCommunicator.getTargetIntent());
+
+ ResolveInfoPresentationGetter presentationGetter =
+ new ResolveInfoPresentationGetter(context, iconDpi, resolveInfo);
+
+ return new DisplayResolveInfo(
+ resolvedComponentInfo.getIntentAt(0),
+ resolveInfo,
+ resolveInfo.loadLabel(pm),
+ resolveInfo.loadLabel(pm),
+ pOrigIntent != null ? pOrigIntent : replacementIntent,
+ presentationGetter);
+ }
+
+ /**
+ * Necessary methods to communicate between {@link ResolverListAdapter}
+ * and {@link ResolverActivity}.
+ */
+ interface ResolverListCommunicator {
+
+ boolean resolveInfoMatch(ResolveInfo lhs, ResolveInfo rhs);
+
+ Intent getReplacementIntent(ActivityInfo activityInfo, Intent defIntent);
+
+ void onPostListReady(ResolverListAdapter listAdapter, boolean updateUi,
+ boolean rebuildCompleted);
+
+ void sendVoiceChoicesIfNeeded();
+
+ void updateProfileViewButton();
+
+ boolean useLayoutWithDefault();
+
+ boolean shouldGetActivityMetadata();
+
+ /**
+ * @return true to filter only apps that can handle
+ * {@link android.content.Intent#CATEGORY_DEFAULT} intents
+ */
+ default boolean shouldGetOnlyDefaultActivities() { return true; };
+
+ Intent getTargetIntent();
+
+ void onHandlePackagesChanged(ResolverListAdapter listAdapter);
+ }
+
+ /**
+ * A view holder.
+ */
+ @VisibleForTesting
+ public static class ViewHolder {
+ public View itemView;
+ public Drawable defaultItemViewBackground;
+
+ public TextView text;
+ public TextView text2;
+ public ImageView icon;
+
+ @VisibleForTesting
+ public ViewHolder(View view) {
+ itemView = view;
+ defaultItemViewBackground = view.getBackground();
+ text = (TextView) view.findViewById(com.android.internal.R.id.text1);
+ text2 = (TextView) view.findViewById(com.android.internal.R.id.text2);
+ icon = (ImageView) view.findViewById(com.android.internal.R.id.icon);
+ }
+
+ public void bindLabel(CharSequence label, CharSequence subLabel, boolean showSubLabel) {
+ text.setText(label);
+
+ if (TextUtils.equals(label, subLabel)) {
+ subLabel = null;
+ }
+
+ text2.setText(subLabel);
+ if (showSubLabel || subLabel != null) {
+ text2.setVisibility(View.VISIBLE);
+ } else {
+ text2.setVisibility(View.GONE);
+ }
+
+ itemView.setContentDescription(null);
+ }
+
+ public void updateContentDescription(String description) {
+ itemView.setContentDescription(description);
+ }
+
+ public void bindIcon(TargetInfo info) {
+ icon.setImageDrawable(info.getDisplayIcon(itemView.getContext()));
+ if (info.isSuspended()) {
+ icon.setColorFilter(getSuspendedColorMatrix());
+ } else {
+ icon.setColorFilter(null);
+ }
+ }
+ }
+
+ protected class LoadLabelTask extends AsyncTask<Void, Void, CharSequence[]> {
+ private final DisplayResolveInfo mDisplayResolveInfo;
+ private final ViewHolder mHolder;
+
+ protected LoadLabelTask(DisplayResolveInfo dri, ViewHolder holder) {
+ mDisplayResolveInfo = dri;
+ mHolder = holder;
+ }
+
+ @Override
+ protected CharSequence[] doInBackground(Void... voids) {
+ ResolveInfoPresentationGetter pg =
+ makePresentationGetter(mDisplayResolveInfo.getResolveInfo());
+
+ if (mIsAudioCaptureDevice) {
+ // This is an audio capture device, so check record permissions
+ ActivityInfo activityInfo = mDisplayResolveInfo.getResolveInfo().activityInfo;
+ String packageName = activityInfo.packageName;
+
+ int uid = activityInfo.applicationInfo.uid;
+ boolean hasRecordPermission =
+ PermissionChecker.checkPermissionForPreflight(
+ mContext,
+ android.Manifest.permission.RECORD_AUDIO, -1, uid,
+ packageName)
+ == android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+ if (!hasRecordPermission) {
+ // Doesn't have record permission, so warn the user
+ return new CharSequence[] {
+ pg.getLabel(),
+ mContext.getString(R.string.usb_device_resolve_prompt_warn)
+ };
+ }
+ }
+
+ return new CharSequence[] {
+ pg.getLabel(),
+ pg.getSubLabel()
+ };
+ }
+
+ @Override
+ protected void onPostExecute(CharSequence[] result) {
+ mDisplayResolveInfo.setDisplayLabel(result[0]);
+ mDisplayResolveInfo.setExtendedInfo(result[1]);
+ mHolder.bindLabel(result[0], result[1], alwaysShowSubLabel());
+ }
+ }
+
+ class LoadIconTask extends AsyncTask<Void, Void, Drawable> {
+ protected final DisplayResolveInfo mDisplayResolveInfo;
+ private final ResolveInfo mResolveInfo;
+ private ViewHolder mHolder;
+
+ LoadIconTask(DisplayResolveInfo dri, ViewHolder holder) {
+ mDisplayResolveInfo = dri;
+ mResolveInfo = dri.getResolveInfo();
+ mHolder = holder;
+ }
+
+ @Override
+ protected Drawable doInBackground(Void... params) {
+ return loadIconForResolveInfo(mResolveInfo);
+ }
+
+ @Override
+ protected void onPostExecute(Drawable d) {
+ if (getOtherProfile() == mDisplayResolveInfo) {
+ mResolverListCommunicator.updateProfileViewButton();
+ } else if (!mDisplayResolveInfo.hasDisplayIcon()) {
+ mDisplayResolveInfo.setDisplayIcon(d);
+ mHolder.bindIcon(mDisplayResolveInfo);
+ // Notify in case view is already bound to resolve the race conditions on
+ // low end devices
+ notifyDataSetChanged();
+ }
+ }
+
+ public void setViewHolder(ViewHolder holder) {
+ mHolder = holder;
+ mHolder.bindIcon(mDisplayResolveInfo);
+ }
+ }
+
+ /**
+ * Loads the icon and label for the provided ResolveInfo.
+ */
+ @VisibleForTesting
+ public static class ResolveInfoPresentationGetter extends ActivityInfoPresentationGetter {
+ private final ResolveInfo mRi;
+ public ResolveInfoPresentationGetter(Context ctx, int iconDpi, ResolveInfo ri) {
+ super(ctx, iconDpi, ri.activityInfo);
+ mRi = ri;
+ }
+
+ @Override
+ Drawable getIconSubstituteInternal() {
+ Drawable dr = null;
+ try {
+ // Do not use ResolveInfo#getIconResource() as it defaults to the app
+ if (mRi.resolvePackageName != null && mRi.icon != 0) {
+ dr = loadIconFromResource(
+ mPm.getResourcesForApplication(mRi.resolvePackageName), mRi.icon);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
+ + "couldn't find resources for package", e);
+ }
+
+ // Fall back to ActivityInfo if no icon is found via ResolveInfo
+ if (dr == null) dr = super.getIconSubstituteInternal();
+
+ return dr;
+ }
+
+ @Override
+ String getAppSubLabelInternal() {
+ // Will default to app name if no intent filter or activity label set, make sure to
+ // check if subLabel matches label before final display
+ return mRi.loadLabel(mPm).toString();
+ }
+
+ @Override
+ String getAppLabelForSubstitutePermission() {
+ // Will default to app name if no activity label set
+ return mRi.getComponentInfo().loadLabel(mPm).toString();
+ }
+ }
+
+ /**
+ * Loads the icon and label for the provided ActivityInfo.
+ */
+ @VisibleForTesting
+ public static class ActivityInfoPresentationGetter extends
+ TargetPresentationGetter {
+ private final ActivityInfo mActivityInfo;
+ public ActivityInfoPresentationGetter(Context ctx, int iconDpi,
+ ActivityInfo activityInfo) {
+ super(ctx, iconDpi, activityInfo.applicationInfo);
+ mActivityInfo = activityInfo;
+ }
+
+ @Override
+ Drawable getIconSubstituteInternal() {
+ Drawable dr = null;
+ try {
+ // Do not use ActivityInfo#getIconResource() as it defaults to the app
+ if (mActivityInfo.icon != 0) {
+ dr = loadIconFromResource(
+ mPm.getResourcesForApplication(mActivityInfo.applicationInfo),
+ mActivityInfo.icon);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Log.e(TAG, "SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON permission granted but "
+ + "couldn't find resources for package", e);
+ }
+
+ return dr;
+ }
+
+ @Override
+ String getAppSubLabelInternal() {
+ // Will default to app name if no activity label set, make sure to check if subLabel
+ // matches label before final display
+ return (String) mActivityInfo.loadLabel(mPm);
+ }
+
+ @Override
+ String getAppLabelForSubstitutePermission() {
+ return getAppSubLabelInternal();
+ }
+ }
+
+ /**
+ * Loads the icon and label for the provided ApplicationInfo. Defaults to using the application
+ * icon and label over any IntentFilter or Activity icon to increase user understanding, with an
+ * exception for applications that hold the right permission. Always attempts to use available
+ * resources over PackageManager loading mechanisms so badging can be done by iconloader. Uses
+ * Strings to strip creative formatting.
+ */
+ private abstract static class TargetPresentationGetter {
+ @Nullable abstract Drawable getIconSubstituteInternal();
+ @Nullable abstract String getAppSubLabelInternal();
+ @Nullable abstract String getAppLabelForSubstitutePermission();
+
+ private Context mCtx;
+ private final int mIconDpi;
+ private final boolean mHasSubstitutePermission;
+ private final ApplicationInfo mAi;
+
+ protected PackageManager mPm;
+
+ TargetPresentationGetter(Context ctx, int iconDpi, ApplicationInfo ai) {
+ mCtx = ctx;
+ mPm = ctx.getPackageManager();
+ mAi = ai;
+ mIconDpi = iconDpi;
+ mHasSubstitutePermission = PackageManager.PERMISSION_GRANTED == mPm.checkPermission(
+ android.Manifest.permission.SUBSTITUTE_SHARE_TARGET_APP_NAME_AND_ICON,
+ mAi.packageName);
+ }
+
+ public Drawable getIcon(UserHandle userHandle) {
+ return new BitmapDrawable(mCtx.getResources(), getIconBitmap(userHandle));
+ }
+
+ public Bitmap getIconBitmap(@Nullable UserHandle userHandle) {
+ Drawable dr = null;
+ if (mHasSubstitutePermission) {
+ dr = getIconSubstituteInternal();
+ }
+
+ if (dr == null) {
+ try {
+ if (mAi.icon != 0) {
+ dr = loadIconFromResource(mPm.getResourcesForApplication(mAi), mAi.icon);
+ }
+ } catch (PackageManager.NameNotFoundException ignore) {
+ }
+ }
+
+ // Fall back to ApplicationInfo#loadIcon if nothing has been loaded
+ if (dr == null) {
+ dr = mAi.loadIcon(mPm);
+ }
+
+ SimpleIconFactory sif = SimpleIconFactory.obtain(mCtx);
+ Bitmap icon = sif.createUserBadgedIconBitmap(dr, userHandle);
+ sif.recycle();
+
+ return icon;
+ }
+
+ public String getLabel() {
+ String label = null;
+ // Apps with the substitute permission will always show the activity label as the
+ // app label if provided
+ if (mHasSubstitutePermission) {
+ label = getAppLabelForSubstitutePermission();
+ }
+
+ if (label == null) {
+ label = (String) mAi.loadLabel(mPm);
+ }
+
+ return label;
+ }
+
+ public String getSubLabel() {
+ // Apps with the substitute permission will always show the resolve info label as the
+ // sublabel if provided
+ if (mHasSubstitutePermission){
+ String appSubLabel = getAppSubLabelInternal();
+ // Use the resolve info label as sublabel if it is set
+ if(!TextUtils.isEmpty(appSubLabel)
+ && !TextUtils.equals(appSubLabel, getLabel())){
+ return appSubLabel;
+ }
+ return null;
+ }
+ return getAppSubLabelInternal();
+ }
+
+ protected String loadLabelFromResource(Resources res, int resId) {
+ return res.getString(resId);
+ }
+
+ @Nullable
+ protected Drawable loadIconFromResource(Resources res, int resId) {
+ return res.getDrawableForDensity(resId, mIconDpi);
+ }
+
+ }
+}
diff --git a/java/src/com/android/intentresolver/ResolverListController.java b/java/src/com/android/intentresolver/ResolverListController.java
new file mode 100644
index 00000000..ff616ce0
--- /dev/null
+++ b/java/src/com/android/intentresolver/ResolverListController.java
@@ -0,0 +1,413 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+package com.android.intentresolver;
+
+import android.annotation.WorkerThread;
+import android.app.ActivityManager;
+import android.app.AppGlobals;
+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.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.Log;
+
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.PriorityQueue;
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * A helper for the ResolverActivity that exposes methods to retrieve, filter and sort its list of
+ * resolvers.
+ */
+public class ResolverListController {
+
+ private final Context mContext;
+ private final PackageManager mpm;
+ private final int mLaunchedFromUid;
+
+ // Needed for sorting resolvers.
+ private final Intent mTargetIntent;
+ private final String mReferrerPackage;
+
+ private static final String TAG = "ResolverListController";
+ private static final boolean DEBUG = false;
+ private final UserHandle mUserHandle;
+
+ private AbstractResolverComparator mResolverComparator;
+ private boolean isComputed = false;
+
+ public ResolverListController(
+ Context context,
+ PackageManager pm,
+ Intent targetIntent,
+ String referrerPackage,
+ int launchedFromUid,
+ UserHandle userHandle) {
+ this(context, pm, targetIntent, referrerPackage, launchedFromUid, userHandle,
+ new ResolverRankerServiceResolverComparator(
+ context, targetIntent, referrerPackage, null, null));
+ }
+
+ public ResolverListController(
+ Context context,
+ PackageManager pm,
+ Intent targetIntent,
+ String referrerPackage,
+ int launchedFromUid,
+ UserHandle userHandle,
+ AbstractResolverComparator resolverComparator) {
+ mContext = context;
+ mpm = pm;
+ mLaunchedFromUid = launchedFromUid;
+ mTargetIntent = targetIntent;
+ mReferrerPackage = referrerPackage;
+ mUserHandle = userHandle;
+ mResolverComparator = resolverComparator;
+ }
+
+ @VisibleForTesting
+ public ResolveInfo getLastChosen() throws RemoteException {
+ return AppGlobals.getPackageManager().getLastChosenActivity(
+ mTargetIntent, mTargetIntent.resolveTypeIfNeeded(mContext.getContentResolver()),
+ PackageManager.MATCH_DEFAULT_ONLY);
+ }
+
+ @VisibleForTesting
+ public void setLastChosen(Intent intent, IntentFilter filter, int match)
+ throws RemoteException {
+ AppGlobals.getPackageManager().setLastChosenActivity(intent,
+ intent.resolveType(mContext.getContentResolver()),
+ PackageManager.MATCH_DEFAULT_ONLY,
+ 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(
+ boolean shouldGetResolvedFilter,
+ boolean shouldGetActivityMetadata,
+ boolean shouldGetOnlyDefaultActivities,
+ List<Intent> intents,
+ UserHandle userHandle) {
+ int baseFlags = (shouldGetOnlyDefaultActivities ? PackageManager.MATCH_DEFAULT_ONLY : 0)
+ | PackageManager.MATCH_DIRECT_BOOT_AWARE
+ | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
+ | (shouldGetResolvedFilter ? PackageManager.GET_RESOLVED_FILTER : 0)
+ | (shouldGetActivityMetadata ? PackageManager.GET_META_DATA : 0);
+ return getResolversForIntentAsUserInternal(intents, userHandle, baseFlags);
+ }
+
+ private List<ResolverActivity.ResolvedComponentInfo> getResolversForIntentAsUserInternal(
+ List<Intent> intents,
+ UserHandle userHandle,
+ int baseFlags) {
+ List<ResolverActivity.ResolvedComponentInfo> resolvedComponents = null;
+ for (int i = 0, N = intents.size(); i < N; i++) {
+ Intent intent = intents.get(i);
+ int flags = baseFlags;
+ if (intent.isWebIntent()
+ || (intent.getFlags() & Intent.FLAG_ACTIVITY_MATCH_EXTERNAL) != 0) {
+ flags |= PackageManager.MATCH_INSTANT;
+ }
+ // Because of AIDL bug, queryIntentActivitiesAsUser can't accept subclasses of Intent.
+ intent = (intent.getClass() == Intent.class) ? intent : new Intent(
+ intent);
+ final List<ResolveInfo> infos = mpm.queryIntentActivitiesAsUser(intent, flags,
+ userHandle);
+ if (infos != null) {
+ if (resolvedComponents == null) {
+ resolvedComponents = new ArrayList<>();
+ }
+ addResolveListDedupe(resolvedComponents, intent, infos);
+ }
+ }
+ return resolvedComponents;
+ }
+
+ @VisibleForTesting
+ public UserHandle getUserHandle() {
+ return mUserHandle;
+ }
+
+ @VisibleForTesting
+ public void addResolveListDedupe(List<ResolverActivity.ResolvedComponentInfo> into,
+ Intent intent,
+ List<ResolveInfo> from) {
+ final int fromCount = from.size();
+ final int intoCount = into.size();
+ for (int i = 0; i < fromCount; i++) {
+ final ResolveInfo newInfo = from.get(i);
+ 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);
+ if (isSameResolvedComponent(newInfo, rci)) {
+ found = true;
+ rci.add(intent, newInfo);
+ break;
+ }
+ }
+ if (!found) {
+ final ComponentName name = new ComponentName(
+ newInfo.activityInfo.packageName, newInfo.activityInfo.name);
+ final ResolverActivity.ResolvedComponentInfo rci =
+ new ResolverActivity.ResolvedComponentInfo(name, intent, newInfo);
+ rci.setPinned(isComponentPinned(name));
+ rci.setFixedAtTop(isFixedAtTop(name));
+ into.add(rci);
+ }
+ }
+ }
+
+
+ /**
+ * Whether this component is pinned by the user. Always false for resolver; overridden in
+ * Chooser.
+ */
+ public boolean isComponentPinned(ComponentName name) {
+ return false;
+ }
+
+ /**
+ * Whether this component is fixed at top in the ranked apps list. Always false for Resolver;
+ * overridden in Chooser.
+ */
+ public boolean isFixedAtTop(ComponentName name) {
+ return false;
+ }
+
+ // Filter out any activities that the launched uid does not have permission for.
+ // To preserve the inputList, optionally will return the original list if any modification has
+ // been made.
+ @VisibleForTesting
+ public ArrayList<ResolverActivity.ResolvedComponentInfo> filterIneligibleActivities(
+ List<ResolverActivity.ResolvedComponentInfo> inputList,
+ boolean returnCopyOfOriginalListIfModified) {
+ ArrayList<ResolverActivity.ResolvedComponentInfo> listToReturn = null;
+ for (int i = inputList.size()-1; i >= 0; i--) {
+ ActivityInfo ai = inputList.get(i)
+ .getResolveInfoAt(0).activityInfo;
+ int granted = ActivityManager.checkComponentPermission(
+ ai.permission, mLaunchedFromUid,
+ ai.applicationInfo.uid, ai.exported);
+
+ if (granted != PackageManager.PERMISSION_GRANTED
+ || isComponentFiltered(ai.getComponentName())) {
+ // Access not allowed! We're about to filter an item,
+ // so modify the unfiltered version if it hasn't already been modified.
+ if (returnCopyOfOriginalListIfModified && listToReturn == null) {
+ listToReturn = new ArrayList<>(inputList);
+ }
+ inputList.remove(i);
+ }
+ }
+ return listToReturn;
+ }
+
+ // Filter out any low priority items.
+ //
+ // 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;
+ // Only display the first matches that are either of equal
+ // priority or have asked to be default options.
+ ResolverActivity.ResolvedComponentInfo rci0 = inputList.get(0);
+ ResolveInfo r0 = rci0.getResolveInfoAt(0);
+ int N = inputList.size();
+ for (int i = 1; i < N; i++) {
+ ResolveInfo ri = inputList.get(i).getResolveInfoAt(0);
+ if (DEBUG) Log.v(
+ TAG,
+ r0.activityInfo.name + "=" +
+ r0.priority + "/" + r0.isDefault + " vs " +
+ ri.activityInfo.name + "=" +
+ ri.priority + "/" + ri.isDefault);
+ if (r0.priority != ri.priority ||
+ r0.isDefault != ri.isDefault) {
+ while (i < N) {
+ if (returnCopyOfOriginalListIfModified && listToReturn == null) {
+ listToReturn = new ArrayList<>(inputList);
+ }
+ inputList.remove(i);
+ N--;
+ }
+ }
+ }
+ return listToReturn;
+ }
+
+ private class ComputeCallback implements AbstractResolverComparator.AfterCompute {
+
+ private CountDownLatch mFinishComputeSignal;
+
+ public ComputeCallback(CountDownLatch finishComputeSignal) {
+ mFinishComputeSignal = finishComputeSignal;
+ }
+
+ public void afterCompute () {
+ mFinishComputeSignal.countDown();
+ }
+ }
+
+ private void compute(List<ResolverActivity.ResolvedComponentInfo> inputList)
+ throws InterruptedException {
+ if (mResolverComparator == null) {
+ Log.d(TAG, "Comparator has already been destroyed; skipped.");
+ return;
+ }
+ final CountDownLatch finishComputeSignal = new CountDownLatch(1);
+ ComputeCallback callback = new ComputeCallback(finishComputeSignal);
+ mResolverComparator.setCallBack(callback);
+ mResolverComparator.compute(inputList);
+ finishComputeSignal.await();
+ isComputed = true;
+ }
+
+ @VisibleForTesting
+ @WorkerThread
+ public void sort(List<ResolverActivity.ResolvedComponentInfo> inputList) {
+ try {
+ long beforeRank = System.currentTimeMillis();
+ if (!isComputed) {
+ compute(inputList);
+ }
+ Collections.sort(inputList, mResolverComparator);
+
+ long afterRank = System.currentTimeMillis();
+ if (DEBUG) {
+ Log.d(TAG, "Time Cost: " + Long.toString(afterRank - beforeRank));
+ }
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Compute & Sort was interrupted: " + e);
+ }
+ }
+
+ @VisibleForTesting
+ @WorkerThread
+ public void topK(List<ResolverActivity.ResolvedComponentInfo> inputList, int k) {
+ if (inputList == null || inputList.isEmpty() || k <= 0) {
+ return;
+ }
+ if (inputList.size() <= k) {
+ // Fall into normal sort when number of ranked elements
+ // needed is not smaller than size of input list.
+ sort(inputList);
+ return;
+ }
+ try {
+ long beforeRank = System.currentTimeMillis();
+ if (!isComputed) {
+ compute(inputList);
+ }
+
+ // Top of this heap has lowest rank.
+ PriorityQueue<ResolverActivity.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
+ // to update in input list, starting from the last position.
+ 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);
+ 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.
+ inputList.set(pointer--, minHeap.poll());
+ minHeap.add(ci);
+ } else {
+ // When ranked no higher than top of heap, update input list
+ // with this new element.
+ inputList.set(pointer--, ci);
+ }
+ }
+
+ // Now we have top k elements in heap, update first
+ // k positions of input list with them.
+ while (!minHeap.isEmpty()) {
+ inputList.set(pointer--, minHeap.poll());
+ }
+
+ long afterRank = System.currentTimeMillis();
+ if (DEBUG) {
+ Log.d(TAG, "Time Cost for top " + k + " targets: "
+ + Long.toString(afterRank - beforeRank));
+ }
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Compute & greatestOf was interrupted: " + e);
+ }
+ }
+
+ private static boolean isSameResolvedComponent(ResolveInfo a,
+ ResolverActivity.ResolvedComponentInfo b) {
+ final ActivityInfo ai = a.activityInfo;
+ return ai.packageName.equals(b.name.getPackageName())
+ && ai.name.equals(b.name.getClassName());
+ }
+
+ boolean isComponentFiltered(ComponentName componentName) {
+ return false;
+ }
+
+ @VisibleForTesting
+ public float getScore(DisplayResolveInfo target) {
+ return mResolverComparator.getScore(target.getResolvedComponentName());
+ }
+
+ /**
+ * Returns the app share score of the given {@code componentName}.
+ */
+ public float getScore(ComponentName componentName) {
+ return mResolverComparator.getScore(componentName);
+ }
+
+ public void updateModel(ComponentName componentName) {
+ mResolverComparator.updateModel(componentName);
+ }
+
+ public void updateChooserCounts(String packageName, int userId, String action) {
+ mResolverComparator.updateChooserCounts(packageName, userId, action);
+ }
+
+ public void destroy() {
+ mResolverComparator.destroy();
+ }
+}
diff --git a/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
new file mode 100644
index 00000000..56d326c1
--- /dev/null
+++ b/java/src/com/android/intentresolver/ResolverMultiProfilePagerAdapter.java
@@ -0,0 +1,279 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_PERSONAL;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CANT_ACCESS_WORK;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_CROSS_PROFILE_BLOCKED_TITLE;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_PERSONAL_APPS;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_NO_WORK_APPS;
+import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PAUSED_TITLE;
+
+import android.annotation.Nullable;
+import android.app.admin.DevicePolicyManager;
+import android.content.Context;
+import android.os.UserHandle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.widget.PagerAdapter;
+
+/**
+ * A {@link PagerAdapter} which describes the work and personal profile intent resolver screens.
+ */
+@VisibleForTesting
+public class ResolverMultiProfilePagerAdapter extends AbstractMultiProfilePagerAdapter {
+
+ private final ResolverProfileDescriptor[] mItems;
+ private final boolean mShouldShowNoCrossProfileIntentsEmptyState;
+ private boolean mUseLayoutWithDefault;
+
+ ResolverMultiProfilePagerAdapter(Context context,
+ ResolverListAdapter adapter,
+ UserHandle personalProfileUserHandle,
+ UserHandle workProfileUserHandle) {
+ super(context, /* currentPage */ 0, personalProfileUserHandle, workProfileUserHandle);
+ mItems = new ResolverProfileDescriptor[] {
+ createProfileDescriptor(adapter)
+ };
+ mShouldShowNoCrossProfileIntentsEmptyState = true;
+ }
+
+ ResolverMultiProfilePagerAdapter(Context context,
+ ResolverListAdapter personalAdapter,
+ ResolverListAdapter workAdapter,
+ @Profile int defaultProfile,
+ UserHandle personalProfileUserHandle,
+ UserHandle workProfileUserHandle,
+ boolean shouldShowNoCrossProfileIntentsEmptyState) {
+ super(context, /* currentPage */ defaultProfile, personalProfileUserHandle,
+ workProfileUserHandle);
+ mItems = new ResolverProfileDescriptor[] {
+ createProfileDescriptor(personalAdapter),
+ createProfileDescriptor(workAdapter)
+ };
+ mShouldShowNoCrossProfileIntentsEmptyState = shouldShowNoCrossProfileIntentsEmptyState;
+ }
+
+ private ResolverProfileDescriptor createProfileDescriptor(
+ ResolverListAdapter adapter) {
+ final LayoutInflater inflater = LayoutInflater.from(getContext());
+ final ViewGroup rootView =
+ (ViewGroup) inflater.inflate(R.layout.resolver_list_per_profile, null, false);
+ return new ResolverProfileDescriptor(rootView, adapter);
+ }
+
+ ListView getListViewForIndex(int index) {
+ return getItem(index).listView;
+ }
+
+ @Override
+ ResolverProfileDescriptor getItem(int pageIndex) {
+ return mItems[pageIndex];
+ }
+
+ @Override
+ int getItemCount() {
+ return mItems.length;
+ }
+
+ @Override
+ void setupListAdapter(int pageIndex) {
+ final ListView listView = getItem(pageIndex).listView;
+ listView.setAdapter(getItem(pageIndex).resolverListAdapter);
+ }
+
+ @Override
+ @VisibleForTesting
+ public ResolverListAdapter getAdapterForIndex(int pageIndex) {
+ return mItems[pageIndex].resolverListAdapter;
+ }
+
+ @Override
+ public ViewGroup instantiateItem(ViewGroup container, int position) {
+ setupListAdapter(position);
+ return super.instantiateItem(container, position);
+ }
+
+ @Override
+ @Nullable
+ ResolverListAdapter getListAdapterForUserHandle(UserHandle userHandle) {
+ if (getActiveListAdapter().getUserHandle().equals(userHandle)) {
+ return getActiveListAdapter();
+ } else if (getInactiveListAdapter() != null
+ && getInactiveListAdapter().getUserHandle().equals(userHandle)) {
+ return getInactiveListAdapter();
+ }
+ return null;
+ }
+
+ @Override
+ @VisibleForTesting
+ public ResolverListAdapter getActiveListAdapter() {
+ return getAdapterForIndex(getCurrentPage());
+ }
+
+ @Override
+ @VisibleForTesting
+ public ResolverListAdapter getInactiveListAdapter() {
+ if (getCount() == 1) {
+ return null;
+ }
+ return getAdapterForIndex(1 - getCurrentPage());
+ }
+
+ @Override
+ public ResolverListAdapter getPersonalListAdapter() {
+ return getAdapterForIndex(PROFILE_PERSONAL);
+ }
+
+ @Override
+ @Nullable
+ public ResolverListAdapter getWorkListAdapter() {
+ return getAdapterForIndex(PROFILE_WORK);
+ }
+
+ @Override
+ ResolverListAdapter getCurrentRootAdapter() {
+ return getActiveListAdapter();
+ }
+
+ @Override
+ ListView getActiveAdapterView() {
+ return getListViewForIndex(getCurrentPage());
+ }
+
+ @Override
+ @Nullable
+ ViewGroup getInactiveAdapterView() {
+ if (getCount() == 1) {
+ return null;
+ }
+ return getListViewForIndex(1 - getCurrentPage());
+ }
+
+ @Override
+ String getMetricsCategory() {
+ return ResolverActivity.METRICS_CATEGORY_RESOLVER;
+ }
+
+ @Override
+ boolean allowShowNoCrossProfileIntentsEmptyState() {
+ return mShouldShowNoCrossProfileIntentsEmptyState;
+ }
+
+ @Override
+ protected void showWorkProfileOffEmptyState(ResolverListAdapter activeListAdapter,
+ View.OnClickListener listener) {
+ showEmptyState(activeListAdapter,
+ getWorkAppPausedTitle(),
+ /* subtitle = */ null,
+ listener);
+ }
+
+ @Override
+ protected void showNoPersonalToWorkIntentsEmptyState(ResolverListAdapter activeListAdapter) {
+ showEmptyState(activeListAdapter,
+ getCrossProfileBlockedTitle(),
+ getCantAccessWorkMessage());
+ }
+
+ @Override
+ protected void showNoWorkToPersonalIntentsEmptyState(ResolverListAdapter activeListAdapter) {
+ showEmptyState(activeListAdapter,
+ getCrossProfileBlockedTitle(),
+ getCantAccessPersonalMessage());
+ }
+
+ @Override
+ protected void showNoPersonalAppsAvailableEmptyState(ResolverListAdapter listAdapter) {
+ showEmptyState(listAdapter,
+ getNoPersonalAppsAvailableMessage(),
+ /* subtitle = */ null);
+ }
+
+ @Override
+ protected void showNoWorkAppsAvailableEmptyState(ResolverListAdapter listAdapter) {
+ showEmptyState(listAdapter,
+ getNoWorkAppsAvailableMessage(),
+ /* subtitle= */ null);
+ }
+
+ private String getWorkAppPausedTitle() {
+ return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_WORK_PAUSED_TITLE,
+ () -> getContext().getString(R.string.resolver_turn_on_work_apps));
+ }
+
+ private String getCrossProfileBlockedTitle() {
+ return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_CROSS_PROFILE_BLOCKED_TITLE,
+ () -> getContext().getString(R.string.resolver_cross_profile_blocked));
+ }
+
+ private String getCantAccessWorkMessage() {
+ return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_CANT_ACCESS_WORK,
+ () -> getContext().getString(
+ R.string.resolver_cant_access_work_apps_explanation));
+ }
+
+ private String getCantAccessPersonalMessage() {
+ return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_CANT_ACCESS_PERSONAL,
+ () -> getContext().getString(
+ R.string.resolver_cant_access_personal_apps_explanation));
+ }
+
+ private String getNoWorkAppsAvailableMessage() {
+ return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_NO_WORK_APPS,
+ () -> getContext().getString(
+ R.string.resolver_no_work_apps_available));
+ }
+
+ private String getNoPersonalAppsAvailableMessage() {
+ return getContext().getSystemService(DevicePolicyManager.class).getResources().getString(
+ RESOLVER_NO_PERSONAL_APPS,
+ () -> getContext().getString(
+ R.string.resolver_no_personal_apps_available));
+ }
+
+ void setUseLayoutWithDefault(boolean useLayoutWithDefault) {
+ mUseLayoutWithDefault = useLayoutWithDefault;
+ }
+
+ @Override
+ protected void setupContainerPadding(View container) {
+ int bottom = mUseLayoutWithDefault ? container.getPaddingBottom() : 0;
+ container.setPadding(container.getPaddingLeft(), container.getPaddingTop(),
+ container.getPaddingRight(), bottom);
+ }
+
+ class ResolverProfileDescriptor extends ProfileDescriptor {
+ private ResolverListAdapter resolverListAdapter;
+ final ListView listView;
+ ResolverProfileDescriptor(ViewGroup rootView, ResolverListAdapter adapter) {
+ super(rootView);
+ resolverListAdapter = adapter;
+ listView = rootView.findViewById(com.android.internal.R.id.resolver_list);
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ResolverRankerServiceResolverComparator.java b/java/src/com/android/intentresolver/ResolverRankerServiceResolverComparator.java
new file mode 100644
index 00000000..be3e6f18
--- /dev/null
+++ b/java/src/com/android/intentresolver/ResolverRankerServiceResolverComparator.java
@@ -0,0 +1,599 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.usage.UsageStats;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.ResolveInfo;
+import android.metrics.LogMaker;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.service.resolver.IResolverRankerResult;
+import android.service.resolver.IResolverRankerService;
+import android.service.resolver.ResolverRankerService;
+import android.service.resolver.ResolverTarget;
+import android.util.Log;
+
+import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
+
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Ranks and compares packages based on usage stats and uses the {@link ResolverRankerService}.
+ */
+class ResolverRankerServiceResolverComparator extends AbstractResolverComparator {
+ private static final String TAG = "RRSResolverComparator";
+
+ private static final boolean DEBUG = false;
+
+ // One week
+ private static final long USAGE_STATS_PERIOD = 1000 * 60 * 60 * 24 * 7;
+
+ private static final long RECENCY_TIME_PERIOD = 1000 * 60 * 60 * 12;
+
+ private static final float RECENCY_MULTIPLIER = 2.f;
+
+ // timeout for establishing connections with a ResolverRankerService.
+ private static final int CONNECTION_COST_TIMEOUT_MILLIS = 200;
+
+ private final Collator mCollator;
+ private final Map<String, UsageStats> mStats;
+ private final long mCurrentTime;
+ private final long mSinceTime;
+ private final LinkedHashMap<ComponentName, ResolverTarget> mTargetsDict = new LinkedHashMap<>();
+ private final String mReferrerPackage;
+ private final Object mLock = new Object();
+ private ArrayList<ResolverTarget> mTargets;
+ private String mAction;
+ private ComponentName mResolvedRankerName;
+ private ComponentName mRankerServiceName;
+ private IResolverRankerService mRanker;
+ private ResolverRankerServiceConnection mConnection;
+ private Context mContext;
+ private CountDownLatch mConnectSignal;
+ private ResolverRankerServiceComparatorModel mComparatorModel;
+
+ public ResolverRankerServiceResolverComparator(Context context, Intent intent,
+ String referrerPackage, AfterCompute afterCompute,
+ ChooserActivityLogger chooserActivityLogger) {
+ super(context, intent);
+ mCollator = Collator.getInstance(context.getResources().getConfiguration().locale);
+ mReferrerPackage = referrerPackage;
+ mContext = context;
+
+ mCurrentTime = System.currentTimeMillis();
+ mSinceTime = mCurrentTime - USAGE_STATS_PERIOD;
+ mStats = mUsm.queryAndAggregateUsageStats(mSinceTime, mCurrentTime);
+ mAction = intent.getAction();
+ mRankerServiceName = new ComponentName(mContext, this.getClass());
+ setCallBack(afterCompute);
+ setChooserActivityLogger(chooserActivityLogger);
+
+ mComparatorModel = buildUpdatedModel();
+ }
+
+ @Override
+ public void handleResultMessage(Message msg) {
+ if (msg.what != RANKER_SERVICE_RESULT) {
+ return;
+ }
+ if (msg.obj == null) {
+ Log.e(TAG, "Receiving null prediction results.");
+ return;
+ }
+ final List<ResolverTarget> receivedTargets = (List<ResolverTarget>) msg.obj;
+ if (receivedTargets != null && mTargets != null
+ && receivedTargets.size() == mTargets.size()) {
+ final int size = mTargets.size();
+ boolean isUpdated = false;
+ for (int i = 0; i < size; ++i) {
+ final float predictedProb =
+ receivedTargets.get(i).getSelectProbability();
+ if (predictedProb != mTargets.get(i).getSelectProbability()) {
+ mTargets.get(i).setSelectProbability(predictedProb);
+ isUpdated = true;
+ }
+ }
+ if (isUpdated) {
+ mRankerServiceName = mResolvedRankerName;
+ mComparatorModel = buildUpdatedModel();
+ }
+ } else {
+ Log.e(TAG, "Sizes of sent and received ResolverTargets diff.");
+ }
+ }
+
+ // compute features for each target according to usage stats of targets.
+ @Override
+ public void doCompute(List<ResolvedComponentInfo> targets) {
+ final long recentSinceTime = mCurrentTime - RECENCY_TIME_PERIOD;
+
+ float mostRecencyScore = 1.0f;
+ float mostTimeSpentScore = 1.0f;
+ float mostLaunchScore = 1.0f;
+ float mostChooserScore = 1.0f;
+
+ for (ResolvedComponentInfo target : targets) {
+ final ResolverTarget resolverTarget = new ResolverTarget();
+ mTargetsDict.put(target.name, resolverTarget);
+ final UsageStats pkStats = mStats.get(target.name.getPackageName());
+ if (pkStats != null) {
+ // Only count recency for apps that weren't the caller
+ // since the caller is always the most recent.
+ // Persistent processes muck this up, so omit them too.
+ if (!target.name.getPackageName().equals(mReferrerPackage)
+ && !isPersistentProcess(target)) {
+ final float recencyScore =
+ (float) Math.max(pkStats.getLastTimeUsed() - recentSinceTime, 0);
+ resolverTarget.setRecencyScore(recencyScore);
+ if (recencyScore > mostRecencyScore) {
+ mostRecencyScore = recencyScore;
+ }
+ }
+ final float timeSpentScore = (float) pkStats.getTotalTimeInForeground();
+ resolverTarget.setTimeSpentScore(timeSpentScore);
+ if (timeSpentScore > mostTimeSpentScore) {
+ mostTimeSpentScore = timeSpentScore;
+ }
+ final float launchScore = (float) pkStats.mLaunchCount;
+ resolverTarget.setLaunchScore(launchScore);
+ if (launchScore > mostLaunchScore) {
+ mostLaunchScore = launchScore;
+ }
+
+ float chooserScore = 0.0f;
+ if (pkStats.mChooserCounts != null && mAction != null
+ && pkStats.mChooserCounts.get(mAction) != null) {
+ chooserScore = (float) pkStats.mChooserCounts.get(mAction)
+ .getOrDefault(mContentType, 0);
+ if (mAnnotations != null) {
+ final int size = mAnnotations.length;
+ for (int i = 0; i < size; i++) {
+ chooserScore += (float) pkStats.mChooserCounts.get(mAction)
+ .getOrDefault(mAnnotations[i], 0);
+ }
+ }
+ }
+ if (DEBUG) {
+ if (mAction == null) {
+ Log.d(TAG, "Action type is null");
+ } else {
+ Log.d(TAG, "Chooser Count of " + mAction + ":" +
+ target.name.getPackageName() + " is " +
+ Float.toString(chooserScore));
+ }
+ }
+ resolverTarget.setChooserScore(chooserScore);
+ if (chooserScore > mostChooserScore) {
+ mostChooserScore = chooserScore;
+ }
+ }
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "compute - mostRecencyScore: " + mostRecencyScore
+ + " mostTimeSpentScore: " + mostTimeSpentScore
+ + " mostLaunchScore: " + mostLaunchScore
+ + " mostChooserScore: " + mostChooserScore);
+ }
+
+ mTargets = new ArrayList<>(mTargetsDict.values());
+ for (ResolverTarget target : mTargets) {
+ final float recency = target.getRecencyScore() / mostRecencyScore;
+ setFeatures(target, recency * recency * RECENCY_MULTIPLIER,
+ target.getLaunchScore() / mostLaunchScore,
+ target.getTimeSpentScore() / mostTimeSpentScore,
+ target.getChooserScore() / mostChooserScore);
+ addDefaultSelectProbability(target);
+ if (DEBUG) {
+ Log.d(TAG, "Scores: " + target);
+ }
+ }
+ predictSelectProbabilities(mTargets);
+
+ mComparatorModel = buildUpdatedModel();
+ }
+
+ @Override
+ public int compare(ResolveInfo lhs, ResolveInfo rhs) {
+ return mComparatorModel.getComparator().compare(lhs, rhs);
+ }
+
+ @Override
+ public float getScore(ComponentName name) {
+ return mComparatorModel.getScore(name);
+ }
+
+ // update ranking model when the connection to it is valid.
+ @Override
+ public void updateModel(ComponentName componentName) {
+ synchronized (mLock) {
+ mComparatorModel.notifyOnTargetSelected(componentName);
+ }
+ }
+
+ // unbind the service and clear unhandled messges.
+ @Override
+ public void destroy() {
+ mHandler.removeMessages(RANKER_SERVICE_RESULT);
+ mHandler.removeMessages(RANKER_RESULT_TIMEOUT);
+ if (mConnection != null) {
+ mContext.unbindService(mConnection);
+ mConnection.destroy();
+ }
+ afterCompute();
+ if (DEBUG) {
+ Log.d(TAG, "Unbinded Resolver Ranker.");
+ }
+ }
+
+ // connect to a ranking service.
+ private void initRanker(Context context) {
+ synchronized (mLock) {
+ if (mConnection != null && mRanker != null) {
+ if (DEBUG) {
+ Log.d(TAG, "Ranker still exists; reusing the existing one.");
+ }
+ return;
+ }
+ }
+ Intent intent = resolveRankerService();
+ if (intent == null) {
+ return;
+ }
+ mConnectSignal = new CountDownLatch(1);
+ mConnection = new ResolverRankerServiceConnection(mConnectSignal);
+ context.bindServiceAsUser(intent, mConnection, Context.BIND_AUTO_CREATE, UserHandle.SYSTEM);
+ }
+
+ // resolve the service for ranking.
+ private Intent resolveRankerService() {
+ Intent intent = new Intent(ResolverRankerService.SERVICE_INTERFACE);
+ final List<ResolveInfo> resolveInfos = mPm.queryIntentServices(intent, 0);
+ for (ResolveInfo resolveInfo : resolveInfos) {
+ if (resolveInfo == null || resolveInfo.serviceInfo == null
+ || resolveInfo.serviceInfo.applicationInfo == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Failed to retrieve a ranker: " + resolveInfo);
+ }
+ continue;
+ }
+ ComponentName componentName = new ComponentName(
+ resolveInfo.serviceInfo.applicationInfo.packageName,
+ resolveInfo.serviceInfo.name);
+ try {
+ final String perm = mPm.getServiceInfo(componentName, 0).permission;
+ if (!ResolverRankerService.BIND_PERMISSION.equals(perm)) {
+ Log.w(TAG, "ResolverRankerService " + componentName + " does not require"
+ + " permission " + ResolverRankerService.BIND_PERMISSION
+ + " - this service will not be queried for "
+ + "ResolverRankerServiceResolverComparator. add android:permission=\""
+ + ResolverRankerService.BIND_PERMISSION + "\""
+ + " to the <service> tag for " + componentName
+ + " in the manifest.");
+ continue;
+ }
+ if (PackageManager.PERMISSION_GRANTED != mPm.checkPermission(
+ ResolverRankerService.HOLD_PERMISSION,
+ resolveInfo.serviceInfo.packageName)) {
+ Log.w(TAG, "ResolverRankerService " + componentName + " does not hold"
+ + " permission " + ResolverRankerService.HOLD_PERMISSION
+ + " - this service will not be queried for "
+ + "ResolverRankerServiceResolverComparator.");
+ continue;
+ }
+ } catch (NameNotFoundException e) {
+ Log.e(TAG, "Could not look up service " + componentName
+ + "; component name not found");
+ continue;
+ }
+ if (DEBUG) {
+ Log.d(TAG, "Succeeded to retrieve a ranker: " + componentName);
+ }
+ mResolvedRankerName = componentName;
+ intent.setComponent(componentName);
+ return intent;
+ }
+ return null;
+ }
+
+ private class ResolverRankerServiceConnection implements ServiceConnection {
+ private final CountDownLatch mConnectSignal;
+
+ public ResolverRankerServiceConnection(CountDownLatch connectSignal) {
+ mConnectSignal = connectSignal;
+ }
+
+ public final IResolverRankerResult resolverRankerResult =
+ new IResolverRankerResult.Stub() {
+ @Override
+ public void sendResult(List<ResolverTarget> targets) throws RemoteException {
+ if (DEBUG) {
+ Log.d(TAG, "Sending Result back to Resolver: " + targets);
+ }
+ synchronized (mLock) {
+ final Message msg = Message.obtain();
+ msg.what = RANKER_SERVICE_RESULT;
+ msg.obj = targets;
+ mHandler.sendMessage(msg);
+ }
+ }
+ };
+
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ if (DEBUG) {
+ Log.d(TAG, "onServiceConnected: " + name);
+ }
+ synchronized (mLock) {
+ mRanker = IResolverRankerService.Stub.asInterface(service);
+ mComparatorModel = buildUpdatedModel();
+ mConnectSignal.countDown();
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ if (DEBUG) {
+ Log.d(TAG, "onServiceDisconnected: " + name);
+ }
+ synchronized (mLock) {
+ destroy();
+ }
+ }
+
+ public void destroy() {
+ synchronized (mLock) {
+ mRanker = null;
+ mComparatorModel = buildUpdatedModel();
+ }
+ }
+ }
+
+ @Override
+ void beforeCompute() {
+ super.beforeCompute();
+ mTargetsDict.clear();
+ mTargets = null;
+ mRankerServiceName = new ComponentName(mContext, this.getClass());
+ mComparatorModel = buildUpdatedModel();
+ mResolvedRankerName = null;
+ initRanker(mContext);
+ }
+
+ // predict select probabilities if ranking service is valid.
+ private void predictSelectProbabilities(List<ResolverTarget> targets) {
+ if (mConnection == null) {
+ if (DEBUG) {
+ Log.d(TAG, "Has not found valid ResolverRankerService; Skip Prediction");
+ }
+ } else {
+ try {
+ mConnectSignal.await(CONNECTION_COST_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+ synchronized (mLock) {
+ if (mRanker != null) {
+ mRanker.predict(targets, mConnection.resolverRankerResult);
+ return;
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Ranker has not been initialized; skip predict.");
+ }
+ }
+ }
+ } catch (InterruptedException e) {
+ Log.e(TAG, "Error in Wait for Service Connection.");
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error in Predict: " + e);
+ }
+ }
+ afterCompute();
+ }
+
+ // adds select prob as the default values, according to a pre-trained Logistic Regression model.
+ private void addDefaultSelectProbability(ResolverTarget target) {
+ float sum = 2.5543f * target.getLaunchScore() + 2.8412f * target.getTimeSpentScore() +
+ 0.269f * target.getRecencyScore() + 4.2222f * target.getChooserScore();
+ target.setSelectProbability((float) (1.0 / (1.0 + Math.exp(1.6568f - sum))));
+ }
+
+ // sets features for each target
+ private void setFeatures(ResolverTarget target, float recencyScore, float launchScore,
+ float timeSpentScore, float chooserScore) {
+ target.setRecencyScore(recencyScore);
+ target.setLaunchScore(launchScore);
+ target.setTimeSpentScore(timeSpentScore);
+ target.setChooserScore(chooserScore);
+ }
+
+ static boolean isPersistentProcess(ResolvedComponentInfo rci) {
+ if (rci != null && rci.getCount() > 0) {
+ return (rci.getResolveInfoAt(0).activityInfo.applicationInfo.flags &
+ ApplicationInfo.FLAG_PERSISTENT) != 0;
+ }
+ return false;
+ }
+
+ /**
+ * Re-construct a {@code ResolverRankerServiceComparatorModel} to replace the current model
+ * instance (if any) using the up-to-date {@code ResolverRankerServiceResolverComparator} ivar
+ * values.
+ *
+ * TODO: each time we replace the model instance, we're either updating the model to use
+ * adjusted data (which is appropriate), or we're providing a (late) value for one of our ivars
+ * that wasn't available the last time the model was updated. For those latter cases, we should
+ * just avoid creating the model altogether until we have all the prerequisites we'll need. Then
+ * we can probably simplify the logic in {@code ResolverRankerServiceComparatorModel} since we
+ * won't need to handle edge cases when the model data isn't fully prepared.
+ * (In some cases, these kinds of "updates" might interleave -- e.g., we might have finished
+ * initializing the first time and now want to adjust some data, but still need to wait for
+ * changes to propagate to the other ivars before rebuilding the model.)
+ */
+ private ResolverRankerServiceComparatorModel buildUpdatedModel() {
+ // TODO: we don't currently guarantee that the underlying target list/map won't be mutated,
+ // so the ResolverComparatorModel may provide inconsistent results. We should make immutable
+ // copies of the data (waiting for any necessary remaining data before creating the model).
+ return new ResolverRankerServiceComparatorModel(
+ mStats,
+ mTargetsDict,
+ mTargets,
+ mCollator,
+ mRanker,
+ mRankerServiceName,
+ (mAnnotations != null),
+ mPm);
+ }
+
+ /**
+ * Implementation of a {@code ResolverComparatorModel} that provides the same ranking logic as
+ * the legacy {@code ResolverRankerServiceResolverComparator}, as a refactoring step toward
+ * removing the complex legacy API.
+ */
+ static class ResolverRankerServiceComparatorModel implements ResolverComparatorModel {
+ private final Map<String, UsageStats> mStats; // Treat as immutable.
+ private final Map<ComponentName, ResolverTarget> mTargetsDict; // Treat as immutable.
+ private final List<ResolverTarget> mTargets; // Treat as immutable.
+ private final Collator mCollator;
+ private final IResolverRankerService mRanker;
+ private final ComponentName mRankerServiceName;
+ private final boolean mAnnotationsUsed;
+ private final PackageManager mPm;
+
+ // TODO: it doesn't look like we should have to pass both targets and targetsDict, but it's
+ // not written in a way that makes it clear whether we can derive one from the other (at
+ // least in this constructor).
+ ResolverRankerServiceComparatorModel(
+ Map<String, UsageStats> stats,
+ Map<ComponentName, ResolverTarget> targetsDict,
+ List<ResolverTarget> targets,
+ Collator collator,
+ IResolverRankerService ranker,
+ ComponentName rankerServiceName,
+ boolean annotationsUsed,
+ PackageManager pm) {
+ mStats = stats;
+ mTargetsDict = targetsDict;
+ mTargets = targets;
+ mCollator = collator;
+ mRanker = ranker;
+ mRankerServiceName = rankerServiceName;
+ mAnnotationsUsed = annotationsUsed;
+ mPm = pm;
+ }
+
+ @Override
+ public Comparator<ResolveInfo> getComparator() {
+ // TODO: doCompute() doesn't seem to be concerned about null-checking mStats. Is that
+ // a bug there, or do we have a way of knowing it will be non-null under certain
+ // conditions?
+ return (lhs, rhs) -> {
+ if (mStats != null) {
+ final ResolverTarget lhsTarget = mTargetsDict.get(new ComponentName(
+ lhs.activityInfo.packageName, lhs.activityInfo.name));
+ final ResolverTarget rhsTarget = mTargetsDict.get(new ComponentName(
+ rhs.activityInfo.packageName, rhs.activityInfo.name));
+
+ if (lhsTarget != null && rhsTarget != null) {
+ final int selectProbabilityDiff = Float.compare(
+ rhsTarget.getSelectProbability(), lhsTarget.getSelectProbability());
+
+ if (selectProbabilityDiff != 0) {
+ return selectProbabilityDiff > 0 ? 1 : -1;
+ }
+ }
+ }
+
+ CharSequence sa = lhs.loadLabel(mPm);
+ if (sa == null) sa = lhs.activityInfo.name;
+ CharSequence sb = rhs.loadLabel(mPm);
+ if (sb == null) sb = rhs.activityInfo.name;
+
+ return mCollator.compare(sa.toString().trim(), sb.toString().trim());
+ };
+ }
+
+ @Override
+ public float getScore(ComponentName name) {
+ final ResolverTarget target = mTargetsDict.get(name);
+ if (target != null) {
+ return target.getSelectProbability();
+ }
+ return 0;
+ }
+
+ @Override
+ public void notifyOnTargetSelected(ComponentName componentName) {
+ if (mRanker != null) {
+ try {
+ int selectedPos = new ArrayList<ComponentName>(mTargetsDict.keySet())
+ .indexOf(componentName);
+ if (selectedPos >= 0 && mTargets != null) {
+ final float selectedProbability = getScore(componentName);
+ int order = 0;
+ for (ResolverTarget target : mTargets) {
+ if (target.getSelectProbability() > selectedProbability) {
+ order++;
+ }
+ }
+ logMetrics(order);
+ mRanker.train(mTargets, selectedPos);
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Selected a unknown component: " + componentName);
+ }
+ }
+ } catch (RemoteException e) {
+ Log.e(TAG, "Error in Train: " + e);
+ }
+ } else {
+ if (DEBUG) {
+ Log.d(TAG, "Ranker is null; skip updateModel.");
+ }
+ }
+ }
+
+ /** Records metrics for evaluation. */
+ private void logMetrics(int selectedPos) {
+ if (mRankerServiceName != null) {
+ MetricsLogger metricsLogger = new MetricsLogger();
+ LogMaker log = new LogMaker(MetricsEvent.ACTION_TARGET_SELECTED);
+ log.setComponentName(mRankerServiceName);
+ log.addTaggedData(MetricsEvent.FIELD_IS_CATEGORY_USED, mAnnotationsUsed ? 1 : 0);
+ log.addTaggedData(MetricsEvent.FIELD_RANKED_POSITION, selectedPos);
+ metricsLogger.write(log);
+ }
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ResolverViewPager.java b/java/src/com/android/intentresolver/ResolverViewPager.java
new file mode 100644
index 00000000..1c234526
--- /dev/null
+++ b/java/src/com/android/intentresolver/ResolverViewPager.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.internal.widget.ViewPager;
+
+/**
+ * A {@link ViewPager} which wraps around its tallest child's height.
+ * <p>Normally {@link ViewPager} instances expand their height to cover all remaining space in
+ * the layout.
+ * <p>This class is used for the intent resolver and share sheet's tabbed view.
+ */
+public class ResolverViewPager extends ViewPager {
+
+ private boolean mSwipingEnabled = true;
+
+ public ResolverViewPager(Context context) {
+ super(context);
+ }
+
+ public ResolverViewPager(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public ResolverViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ }
+
+ public ResolverViewPager(Context context, AttributeSet attrs,
+ int defStyleAttr, int defStyleRes) {
+ super(context, attrs, defStyleAttr, defStyleRes);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ if (MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.AT_MOST) {
+ return;
+ }
+ widthMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY);
+ int height = getMeasuredHeight();
+ int maxHeight = 0;
+ for (int i = 0; i < getChildCount(); i++) {
+ View child = getChildAt(i);
+ child.measure(widthMeasureSpec,
+ MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
+ if (maxHeight < child.getMeasuredHeight()) {
+ maxHeight = child.getMeasuredHeight();
+ }
+ }
+ if (maxHeight > 0) {
+ height = maxHeight;
+ }
+ heightMeasureSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY);
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ }
+
+ /**
+ * Sets whether swiping sideways should happen.
+ * <p>Note that swiping is always disabled for RTL layouts (b/159110029 for context).
+ */
+ void setSwipingEnabled(boolean swipingEnabled) {
+ mSwipingEnabled = swipingEnabled;
+ }
+
+ @Override
+ public boolean onInterceptTouchEvent(MotionEvent ev) {
+ return !isLayoutRtl() && mSwipingEnabled && super.onInterceptTouchEvent(ev);
+ }
+}
diff --git a/java/src/com/android/intentresolver/SimpleIconFactory.java b/java/src/com/android/intentresolver/SimpleIconFactory.java
new file mode 100644
index 00000000..b05b4f68
--- /dev/null
+++ b/java/src/com/android/intentresolver/SimpleIconFactory.java
@@ -0,0 +1,752 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import static android.content.Context.ACTIVITY_SERVICE;
+import static android.graphics.Paint.DITHER_FLAG;
+import static android.graphics.Paint.FILTER_BITMAP_FLAG;
+import static android.graphics.drawable.AdaptiveIconDrawable.getExtraInsetFraction;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.graphics.Bitmap;
+import android.graphics.BlurMaskFilter;
+import android.graphics.BlurMaskFilter.Blur;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PaintFlagsDrawFilter;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.AdaptiveIconDrawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.DrawableWrapper;
+import android.os.UserHandle;
+import android.util.AttributeSet;
+import android.util.Pools.SynchronizedPool;
+import android.util.TypedValue;
+
+import org.xmlpull.v1.XmlPullParser;
+
+import java.nio.ByteBuffer;
+import java.util.Optional;
+
+
+/**
+ * @deprecated Use the Launcher3 Iconloaderlib at packages/apps/Launcher3/iconloaderlib. This class
+ * is a temporary fork of Iconloader. It combines all necessary methods to render app icons that are
+ * possibly badged. It is intended to be used only by Sharesheet for the Q release with custom code.
+ */
+@Deprecated
+public class SimpleIconFactory {
+
+
+ private static final SynchronizedPool<SimpleIconFactory> sPool =
+ new SynchronizedPool<>(Runtime.getRuntime().availableProcessors());
+
+ private static final int DEFAULT_WRAPPER_BACKGROUND = Color.WHITE;
+ private static final float BLUR_FACTOR = 1.5f / 48;
+
+ private Context mContext;
+ private Canvas mCanvas;
+ private PackageManager mPm;
+
+ private int mFillResIconDpi;
+ private int mIconBitmapSize;
+ private int mBadgeBitmapSize;
+ private int mWrapperBackgroundColor;
+
+ private Drawable mWrapperIcon;
+ private final Rect mOldBounds = new Rect();
+
+ /**
+ * Obtain a SimpleIconFactory from a pool objects.
+ *
+ * @deprecated Do not use, functionality will be replaced by iconloader lib eventually.
+ */
+ @Deprecated
+ public static SimpleIconFactory obtain(Context ctx) {
+ SimpleIconFactory instance = sPool.acquire();
+ if (instance == null) {
+ final ActivityManager am = (ActivityManager) ctx.getSystemService(ACTIVITY_SERVICE);
+ final int iconDpi = (am == null) ? 0 : am.getLauncherLargeIconDensity();
+
+ final int iconSize = getIconSizeFromContext(ctx);
+ final int badgeSize = getBadgeSizeFromContext(ctx);
+ instance = new SimpleIconFactory(ctx, iconDpi, iconSize, badgeSize);
+ instance.setWrapperBackgroundColor(Color.WHITE);
+ }
+
+ return instance;
+ }
+
+ private static int getAttrDimFromContext(Context ctx, @AttrRes int attrId, String errorMsg) {
+ final Resources res = ctx.getResources();
+ TypedValue outVal = new TypedValue();
+ if (!ctx.getTheme().resolveAttribute(attrId, outVal, true)) {
+ throw new IllegalStateException(errorMsg);
+ }
+ return res.getDimensionPixelSize(outVal.resourceId);
+ }
+
+ private static int getIconSizeFromContext(Context ctx) {
+ return getAttrDimFromContext(ctx,
+ com.android.internal.R.attr.iconfactoryIconSize,
+ "Expected theme to define iconfactoryIconSize.");
+ }
+
+ private static int getBadgeSizeFromContext(Context ctx) {
+ return getAttrDimFromContext(ctx,
+ com.android.internal.R.attr.iconfactoryBadgeSize,
+ "Expected theme to define iconfactoryBadgeSize.");
+ }
+
+ /**
+ * Recycles the SimpleIconFactory so others may use it.
+ *
+ * @deprecated Do not use, functionality will be replaced by iconloader lib eventually.
+ */
+ @Deprecated
+ public void recycle() {
+ // Return to default background color
+ setWrapperBackgroundColor(Color.WHITE);
+ sPool.release(this);
+ }
+
+ /**
+ * @deprecated Do not use, functionality will be replaced by iconloader lib eventually.
+ */
+ @Deprecated
+ private SimpleIconFactory(Context context, int fillResIconDpi, int iconBitmapSize,
+ int badgeBitmapSize) {
+ mContext = context.getApplicationContext();
+ mPm = mContext.getPackageManager();
+ mIconBitmapSize = iconBitmapSize;
+ mBadgeBitmapSize = badgeBitmapSize;
+ mFillResIconDpi = fillResIconDpi;
+
+ mCanvas = new Canvas();
+ mCanvas.setDrawFilter(new PaintFlagsDrawFilter(DITHER_FLAG, FILTER_BITMAP_FLAG));
+
+ // Normalizer init
+ // Use twice the icon size as maximum size to avoid scaling down twice.
+ mMaxSize = iconBitmapSize * 2;
+ mBitmap = Bitmap.createBitmap(mMaxSize, mMaxSize, Bitmap.Config.ALPHA_8);
+ mScaleCheckCanvas = new Canvas(mBitmap);
+ mPixels = new byte[mMaxSize * mMaxSize];
+ mLeftBorder = new float[mMaxSize];
+ mRightBorder = new float[mMaxSize];
+ mBounds = new Rect();
+ mAdaptiveIconBounds = new Rect();
+ mAdaptiveIconScale = SCALE_NOT_INITIALIZED;
+
+ // Shadow generator init
+ mDefaultBlurMaskFilter = new BlurMaskFilter(iconBitmapSize * BLUR_FACTOR,
+ Blur.NORMAL);
+ }
+
+ /**
+ * Sets the background color used for wrapped adaptive icon
+ *
+ * @deprecated Do not use, functionality will be replaced by iconloader lib eventually.
+ */
+ @Deprecated
+ void setWrapperBackgroundColor(int color) {
+ mWrapperBackgroundColor = (Color.alpha(color) < 255) ? DEFAULT_WRAPPER_BACKGROUND : color;
+ }
+
+ /**
+ * Creates bitmap using the source drawable and various parameters.
+ * The bitmap is visually normalized with other icons and has enough spacing to add shadow.
+ * Note: this method has been modified from iconloaderlib to remove a profile diff check.
+ *
+ * @param icon source of the icon associated with a user that has no badge,
+ * likely user 0
+ * @param user info can be used for a badge
+ * @return a bitmap suitable for disaplaying as an icon at various system UIs.
+ *
+ * @deprecated Do not use, functionality will be replaced by iconloader lib eventually.
+ */
+ @Deprecated
+ Bitmap createUserBadgedIconBitmap(@Nullable Drawable icon, @Nullable UserHandle user) {
+ float [] scale = new float[1];
+
+ // If no icon is provided use the system default
+ if (icon == null) {
+ icon = getFullResDefaultActivityIcon(mFillResIconDpi);
+ }
+ icon = normalizeAndWrapToAdaptiveIcon(icon, null, scale);
+ Bitmap bitmap = createIconBitmap(icon, scale[0]);
+ if (icon instanceof AdaptiveIconDrawable) {
+ mCanvas.setBitmap(bitmap);
+ recreateIcon(Bitmap.createBitmap(bitmap), mCanvas);
+ mCanvas.setBitmap(null);
+ }
+
+ final Bitmap result;
+ if (user != null /* if modification from iconloaderlib */) {
+ BitmapDrawable drawable = new FixedSizeBitmapDrawable(bitmap);
+ Drawable badged = mPm.getUserBadgedIcon(drawable, user);
+ if (badged instanceof BitmapDrawable) {
+ result = ((BitmapDrawable) badged).getBitmap();
+ } else {
+ result = createIconBitmap(badged, 1f);
+ }
+ } else {
+ result = bitmap;
+ }
+
+ return result;
+ }
+
+ /**
+ * Creates bitmap using the source drawable and flattened pre-rendered app icon.
+ * The bitmap is visually normalized with other icons and has enough spacing to add shadow.
+ * This is custom functionality added to Iconloaderlib that will need to be ported.
+ *
+ * @param icon source of the icon associated with a user that has no badge
+ * @param renderedAppIcon pre-rendered app icon to use as a badge, likely the output
+ * of createUserBadgedIconBitmap for user 0
+ * @return a bitmap suitable for disaplaying as an icon at various system UIs.
+ *
+ * @deprecated Do not use, functionality will be replaced by iconloader lib eventually.
+ */
+ @Deprecated
+ public Bitmap createAppBadgedIconBitmap(@Nullable Drawable icon, Bitmap renderedAppIcon) {
+ // If no icon is provided use the system default
+ if (icon == null) {
+ icon = getFullResDefaultActivityIcon(mFillResIconDpi);
+ }
+
+ // Direct share icons cannot be adaptive, most will arrive as bitmaps. To get reliable
+ // presentation, force all DS icons to be circular. Scale DS image so it completely fills.
+ int w = icon.getIntrinsicWidth();
+ int h = icon.getIntrinsicHeight();
+ float scale = 1;
+ if (h > w && w > 0) {
+ scale = (float) h / w;
+ } else if (w > h && h > 0) {
+ scale = (float) w / h;
+ }
+ Bitmap bitmap = createIconBitmapNoInsetOrMask(icon, scale);
+ bitmap = maskBitmapToCircle(bitmap);
+ icon = new BitmapDrawable(mContext.getResources(), bitmap);
+
+ // We now have a circular masked and scaled icon, inset and apply shadow
+ scale = getScale(icon, null);
+ bitmap = createIconBitmap(icon, scale);
+
+ mCanvas.setBitmap(bitmap);
+ recreateIcon(Bitmap.createBitmap(bitmap), mCanvas);
+
+ if (renderedAppIcon != null) {
+ // Now scale down and apply the badge to the bottom right corner of the flattened icon
+ renderedAppIcon = Bitmap.createScaledBitmap(renderedAppIcon, mBadgeBitmapSize,
+ mBadgeBitmapSize, false);
+
+ // Paint the provided badge on top of the flattened icon
+ mCanvas.drawBitmap(renderedAppIcon, mIconBitmapSize - mBadgeBitmapSize,
+ mIconBitmapSize - mBadgeBitmapSize, null);
+ }
+
+ mCanvas.setBitmap(null);
+
+ return bitmap;
+ }
+
+ private Bitmap maskBitmapToCircle(Bitmap bitmap) {
+ final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),
+ bitmap.getHeight(), Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(output);
+ final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.DITHER_FLAG
+ | Paint.FILTER_BITMAP_FLAG);
+
+ // Apply an offset to enable shadow to be drawn
+ final int size = bitmap.getWidth();
+ int offset = Math.max((int) Math.ceil(BLUR_FACTOR * size), 1);
+
+ // Draw mask
+ paint.setColor(0xffffffff);
+ canvas.drawARGB(0, 0, 0, 0);
+ canvas.drawCircle(bitmap.getWidth() / 2f,
+ bitmap.getHeight() / 2f,
+ bitmap.getWidth() / 2f - offset,
+ paint);
+
+ // Draw masked bitmap
+ paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN));
+ final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight());
+ canvas.drawBitmap(bitmap, rect, rect, paint);
+
+ return output;
+ }
+
+ private static Drawable getFullResDefaultActivityIcon(int iconDpi) {
+ return Resources.getSystem().getDrawableForDensity(android.R.mipmap.sym_def_app_icon,
+ iconDpi);
+ }
+
+ private Bitmap createIconBitmap(Drawable icon, float scale) {
+ return createIconBitmap(icon, scale, mIconBitmapSize, true, false);
+ }
+
+ private Bitmap createIconBitmapNoInsetOrMask(Drawable icon, float scale) {
+ return createIconBitmap(icon, scale, mIconBitmapSize, false, true);
+ }
+
+ /**
+ * @param icon drawable that should be flattened to a bitmap
+ * @param scale the scale to apply before drawing {@param icon} on the canvas
+ * @param insetAdiForShadow when rendering AdaptiveIconDrawables inset to make room for a shadow
+ * @param ignoreAdiMask when rendering AdaptiveIconDrawables ignore the current system mask
+ */
+ private Bitmap createIconBitmap(Drawable icon, float scale, int size, boolean insetAdiForShadow,
+ boolean ignoreAdiMask) {
+ Bitmap bitmap = Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888);
+
+ mCanvas.setBitmap(bitmap);
+ mOldBounds.set(icon.getBounds());
+
+ if (icon instanceof AdaptiveIconDrawable) {
+ final AdaptiveIconDrawable adi = (AdaptiveIconDrawable) icon;
+
+ // By default assumes the output bitmap will have a shadow directly applied and makes
+ // room for it by insetting. If there are intermediate steps before applying the shadow
+ // insetting is disableable.
+ int offset = Math.round(size * (1 - scale) / 2);
+ if (insetAdiForShadow) {
+ offset = Math.max((int) Math.ceil(BLUR_FACTOR * size), offset);
+ }
+ Rect bounds = new Rect(offset, offset, size - offset, size - offset);
+
+ // AdaptiveIconDrawables are by default masked by the user's icon shape selection.
+ // If further masking is to be done, directly render to avoid the system masking.
+ if (ignoreAdiMask) {
+ final int cX = bounds.width() / 2;
+ final int cY = bounds.height() / 2;
+ final float portScale = 1f / (1 + 2 * getExtraInsetFraction());
+ final int insetWidth = (int) (bounds.width() / (portScale * 2));
+ final int insetHeight = (int) (bounds.height() / (portScale * 2));
+
+ Rect childRect = new Rect(cX - insetWidth, cY - insetHeight, cX + insetWidth,
+ cY + insetHeight);
+ Optional.ofNullable(adi.getBackground()).ifPresent(drawable -> {
+ drawable.setBounds(childRect);
+ drawable.draw(mCanvas);
+ });
+ Optional.ofNullable(adi.getForeground()).ifPresent(drawable -> {
+ drawable.setBounds(childRect);
+ drawable.draw(mCanvas);
+ });
+ } else {
+ adi.setBounds(bounds);
+ adi.draw(mCanvas);
+ }
+ } else {
+ if (icon instanceof BitmapDrawable) {
+ BitmapDrawable bitmapDrawable = (BitmapDrawable) icon;
+ Bitmap b = bitmapDrawable.getBitmap();
+ if (bitmap != null && b.getDensity() == Bitmap.DENSITY_NONE) {
+ bitmapDrawable.setTargetDensity(mContext.getResources().getDisplayMetrics());
+ }
+ }
+ int width = size;
+ int height = size;
+
+ int intrinsicWidth = icon.getIntrinsicWidth();
+ int intrinsicHeight = icon.getIntrinsicHeight();
+ if (intrinsicWidth > 0 && intrinsicHeight > 0) {
+ // Scale the icon proportionally to the icon dimensions
+ final float ratio = (float) intrinsicWidth / intrinsicHeight;
+ if (intrinsicWidth > intrinsicHeight) {
+ height = (int) (width / ratio);
+ } else if (intrinsicHeight > intrinsicWidth) {
+ width = (int) (height * ratio);
+ }
+ }
+ final int left = (size - width) / 2;
+ final int top = (size - height) / 2;
+ icon.setBounds(left, top, left + width, top + height);
+ mCanvas.save();
+ mCanvas.scale(scale, scale, size / 2, size / 2);
+ icon.draw(mCanvas);
+ mCanvas.restore();
+
+ }
+
+ icon.setBounds(mOldBounds);
+ mCanvas.setBitmap(null);
+ return bitmap;
+ }
+
+ private Drawable normalizeAndWrapToAdaptiveIcon(Drawable icon, RectF outIconBounds,
+ float[] outScale) {
+ float scale = 1f;
+
+ if (mWrapperIcon == null) {
+ mWrapperIcon = mContext.getDrawable(
+ R.drawable.iconfactory_adaptive_icon_drawable_wrapper).mutate();
+ }
+
+ AdaptiveIconDrawable dr = (AdaptiveIconDrawable) mWrapperIcon;
+ dr.setBounds(0, 0, 1, 1);
+ scale = getScale(icon, outIconBounds);
+ if (!(icon instanceof AdaptiveIconDrawable)) {
+ FixedScaleDrawable fsd = ((FixedScaleDrawable) dr.getForeground());
+ fsd.setDrawable(icon);
+ fsd.setScale(scale);
+ icon = dr;
+ scale = getScale(icon, outIconBounds);
+
+ ((ColorDrawable) dr.getBackground()).setColor(mWrapperBackgroundColor);
+ }
+
+ outScale[0] = scale;
+ return icon;
+ }
+
+
+ /* Normalization block */
+
+ private static final float SCALE_NOT_INITIALIZED = 0;
+ // Ratio of icon visible area to full icon size for a square shaped icon
+ private static final float MAX_SQUARE_AREA_FACTOR = 375.0f / 576;
+ // Ratio of icon visible area to full icon size for a circular shaped icon
+ private static final float MAX_CIRCLE_AREA_FACTOR = 380.0f / 576;
+
+ private static final float CIRCLE_AREA_BY_RECT = (float) Math.PI / 4;
+
+ // Slope used to calculate icon visible area to full icon size for any generic shaped icon.
+ private static final float LINEAR_SCALE_SLOPE =
+ (MAX_CIRCLE_AREA_FACTOR - MAX_SQUARE_AREA_FACTOR) / (1 - CIRCLE_AREA_BY_RECT);
+
+ private static final int MIN_VISIBLE_ALPHA = 40;
+
+ private float mAdaptiveIconScale;
+ private final Rect mAdaptiveIconBounds;
+ private final Rect mBounds;
+ private final int mMaxSize;
+ private final byte[] mPixels;
+ private final float[] mLeftBorder;
+ private final float[] mRightBorder;
+ private final Bitmap mBitmap;
+ private final Canvas mScaleCheckCanvas;
+
+ /**
+ * Returns the amount by which the {@param d} should be scaled (in both dimensions) so that it
+ * matches the design guidelines for a launcher icon.
+ *
+ * We first calculate the convex hull of the visible portion of the icon.
+ * This hull then compared with the bounding rectangle of the hull to find how closely it
+ * resembles a circle and a square, by comparing the ratio of the areas. Note that this is not
+ * an ideal solution but it gives satisfactory result without affecting the performance.
+ *
+ * This closeness is used to determine the ratio of hull area to the full icon size.
+ * Refer {@link #MAX_CIRCLE_AREA_FACTOR} and {@link #MAX_SQUARE_AREA_FACTOR}
+ *
+ * @param outBounds optional rect to receive the fraction distance from each edge.
+ */
+ private synchronized float getScale(@NonNull Drawable d, @Nullable RectF outBounds) {
+ if (d instanceof AdaptiveIconDrawable) {
+ if (mAdaptiveIconScale != SCALE_NOT_INITIALIZED) {
+ if (outBounds != null) {
+ outBounds.set(mAdaptiveIconBounds);
+ }
+ return mAdaptiveIconScale;
+ }
+ }
+ int width = d.getIntrinsicWidth();
+ int height = d.getIntrinsicHeight();
+ if (width <= 0 || height <= 0) {
+ width = width <= 0 || width > mMaxSize ? mMaxSize : width;
+ height = height <= 0 || height > mMaxSize ? mMaxSize : height;
+ } else if (width > mMaxSize || height > mMaxSize) {
+ int max = Math.max(width, height);
+ width = mMaxSize * width / max;
+ height = mMaxSize * height / max;
+ }
+
+ mBitmap.eraseColor(Color.TRANSPARENT);
+ d.setBounds(0, 0, width, height);
+ d.draw(mScaleCheckCanvas);
+
+ ByteBuffer buffer = ByteBuffer.wrap(mPixels);
+ buffer.rewind();
+ mBitmap.copyPixelsToBuffer(buffer);
+
+ // Overall bounds of the visible icon.
+ int topY = -1;
+ int bottomY = -1;
+ int leftX = mMaxSize + 1;
+ int rightX = -1;
+
+ // Create border by going through all pixels one row at a time and for each row find
+ // the first and the last non-transparent pixel. Set those values to mLeftBorder and
+ // mRightBorder and use -1 if there are no visible pixel in the row.
+
+ // buffer position
+ int index = 0;
+ // buffer shift after every row, width of buffer = mMaxSize
+ int rowSizeDiff = mMaxSize - width;
+ // first and last position for any row.
+ int firstX, lastX;
+
+ for (int y = 0; y < height; y++) {
+ firstX = lastX = -1;
+ for (int x = 0; x < width; x++) {
+ if ((mPixels[index] & 0xFF) > MIN_VISIBLE_ALPHA) {
+ if (firstX == -1) {
+ firstX = x;
+ }
+ lastX = x;
+ }
+ index++;
+ }
+ index += rowSizeDiff;
+
+ mLeftBorder[y] = firstX;
+ mRightBorder[y] = lastX;
+
+ // If there is at least one visible pixel, update the overall bounds.
+ if (firstX != -1) {
+ bottomY = y;
+ if (topY == -1) {
+ topY = y;
+ }
+
+ leftX = Math.min(leftX, firstX);
+ rightX = Math.max(rightX, lastX);
+ }
+ }
+
+ if (topY == -1 || rightX == -1) {
+ // No valid pixels found. Do not scale.
+ return 1;
+ }
+
+ convertToConvexArray(mLeftBorder, 1, topY, bottomY);
+ convertToConvexArray(mRightBorder, -1, topY, bottomY);
+
+ // Area of the convex hull
+ float area = 0;
+ for (int y = 0; y < height; y++) {
+ if (mLeftBorder[y] <= -1) {
+ continue;
+ }
+ area += mRightBorder[y] - mLeftBorder[y] + 1;
+ }
+
+ // Area of the rectangle required to fit the convex hull
+ float rectArea = (bottomY + 1 - topY) * (rightX + 1 - leftX);
+ float hullByRect = area / rectArea;
+
+ float scaleRequired;
+ if (hullByRect < CIRCLE_AREA_BY_RECT) {
+ scaleRequired = MAX_CIRCLE_AREA_FACTOR;
+ } else {
+ scaleRequired = MAX_SQUARE_AREA_FACTOR + LINEAR_SCALE_SLOPE * (1 - hullByRect);
+ }
+ mBounds.left = leftX;
+ mBounds.right = rightX;
+
+ mBounds.top = topY;
+ mBounds.bottom = bottomY;
+
+ if (outBounds != null) {
+ outBounds.set(((float) mBounds.left) / width, ((float) mBounds.top) / height,
+ 1 - ((float) mBounds.right) / width,
+ 1 - ((float) mBounds.bottom) / height);
+ }
+ float areaScale = area / (width * height);
+ // Use sqrt of the final ratio as the images is scaled across both width and height.
+ float scale = areaScale > scaleRequired ? (float) Math.sqrt(scaleRequired / areaScale) : 1;
+ if (d instanceof AdaptiveIconDrawable && mAdaptiveIconScale == SCALE_NOT_INITIALIZED) {
+ mAdaptiveIconScale = scale;
+ mAdaptiveIconBounds.set(mBounds);
+ }
+ return scale;
+ }
+
+ /**
+ * Modifies {@param xCoordinates} to represent a convex border. Fills in all missing values
+ * (except on either ends) with appropriate values.
+ * @param xCoordinates map of x coordinate per y.
+ * @param direction 1 for left border and -1 for right border.
+ * @param topY the first Y position (inclusive) with a valid value.
+ * @param bottomY the last Y position (inclusive) with a valid value.
+ */
+ private static void convertToConvexArray(
+ float[] xCoordinates, int direction, int topY, int bottomY) {
+ int total = xCoordinates.length;
+ // The tangent at each pixel.
+ float[] angles = new float[total - 1];
+
+ int first = topY; // First valid y coordinate
+ int last = -1; // Last valid y coordinate which didn't have a missing value
+
+ float lastAngle = Float.MAX_VALUE;
+
+ for (int i = topY + 1; i <= bottomY; i++) {
+ if (xCoordinates[i] <= -1) {
+ continue;
+ }
+ int start;
+
+ if (lastAngle == Float.MAX_VALUE) {
+ start = first;
+ } else {
+ float currentAngle = (xCoordinates[i] - xCoordinates[last]) / (i - last);
+ start = last;
+ // If this position creates a concave angle, keep moving up until we find a
+ // position which creates a convex angle.
+ if ((currentAngle - lastAngle) * direction < 0) {
+ while (start > first) {
+ start--;
+ currentAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start);
+ if ((currentAngle - angles[start]) * direction >= 0) {
+ break;
+ }
+ }
+ }
+ }
+
+ // Reset from last check
+ lastAngle = (xCoordinates[i] - xCoordinates[start]) / (i - start);
+ // Update all the points from start.
+ for (int j = start; j < i; j++) {
+ angles[j] = lastAngle;
+ xCoordinates[j] = xCoordinates[start] + lastAngle * (j - start);
+ }
+ last = i;
+ }
+ }
+
+ /* Shadow generator block */
+
+ private static final float KEY_SHADOW_DISTANCE = 1f / 48;
+ private static final int KEY_SHADOW_ALPHA = 10;
+ private static final int AMBIENT_SHADOW_ALPHA = 7;
+
+ private Paint mBlurPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
+ private Paint mDrawPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
+ private BlurMaskFilter mDefaultBlurMaskFilter;
+
+ private synchronized void recreateIcon(Bitmap icon, Canvas out) {
+ recreateIcon(icon, mDefaultBlurMaskFilter, AMBIENT_SHADOW_ALPHA, KEY_SHADOW_ALPHA, out);
+ }
+
+ private synchronized void recreateIcon(Bitmap icon, BlurMaskFilter blurMaskFilter,
+ int ambientAlpha, int keyAlpha, Canvas out) {
+ int[] offset = new int[2];
+ mBlurPaint.setMaskFilter(blurMaskFilter);
+ Bitmap shadow = icon.extractAlpha(mBlurPaint, offset);
+
+ // Draw ambient shadow
+ mDrawPaint.setAlpha(ambientAlpha);
+ out.drawBitmap(shadow, offset[0], offset[1], mDrawPaint);
+
+ // Draw key shadow
+ mDrawPaint.setAlpha(keyAlpha);
+ out.drawBitmap(shadow, offset[0], offset[1] + KEY_SHADOW_DISTANCE * mIconBitmapSize,
+ mDrawPaint);
+
+ // Draw the icon
+ mDrawPaint.setAlpha(255); // TODO if b/128609682 not fixed by launch use .setAlpha(254)
+ out.drawBitmap(icon, 0, 0, mDrawPaint);
+ }
+
+ /* Classes */
+
+ /**
+ * Extension of {@link DrawableWrapper} which scales the child drawables by a fixed amount.
+ */
+ public static class FixedScaleDrawable extends DrawableWrapper {
+
+ private static final float LEGACY_ICON_SCALE = .7f * .6667f;
+ private float mScaleX, mScaleY;
+
+ public FixedScaleDrawable() {
+ super(new ColorDrawable());
+ mScaleX = LEGACY_ICON_SCALE;
+ mScaleY = LEGACY_ICON_SCALE;
+ }
+
+ @Override
+ public void draw(@NonNull Canvas canvas) {
+ int saveCount = canvas.save();
+ canvas.scale(mScaleX, mScaleY,
+ getBounds().exactCenterX(), getBounds().exactCenterY());
+ super.draw(canvas);
+ canvas.restoreToCount(saveCount);
+ }
+
+ @Override
+ public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs) { }
+
+ @Override
+ public void inflate(Resources r, XmlPullParser parser, AttributeSet attrs, Theme theme) { }
+
+ /**
+ * Sets the scale associated with this drawable
+ * @param scale
+ */
+ public void setScale(float scale) {
+ float h = getIntrinsicHeight();
+ float w = getIntrinsicWidth();
+ mScaleX = scale * LEGACY_ICON_SCALE;
+ mScaleY = scale * LEGACY_ICON_SCALE;
+ if (h > w && w > 0) {
+ mScaleX *= w / h;
+ } else if (w > h && h > 0) {
+ mScaleY *= h / w;
+ }
+ }
+ }
+
+ /**
+ * An extension of {@link BitmapDrawable} which returns the bitmap pixel size as intrinsic size.
+ * This allows the badging to be done based on the action bitmap size rather than
+ * the scaled bitmap size.
+ */
+ private static class FixedSizeBitmapDrawable extends BitmapDrawable {
+
+ FixedSizeBitmapDrawable(Bitmap bitmap) {
+ super(null, bitmap);
+ }
+
+ @Override
+ public int getIntrinsicHeight() {
+ return getBitmap().getWidth();
+ }
+
+ @Override
+ public int getIntrinsicWidth() {
+ return getBitmap().getWidth();
+ }
+ }
+
+}
diff --git a/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java b/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
new file mode 100644
index 00000000..1c763071
--- /dev/null
+++ b/java/src/com/android/intentresolver/chooser/ChooserTargetInfo.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.chooser;
+
+import android.service.chooser.ChooserTarget;
+import android.text.TextUtils;
+
+/**
+ * A TargetInfo for Direct Share. Includes a {@link ChooserTarget} representing the
+ * Direct Share deep link into an application.
+ */
+public interface ChooserTargetInfo extends TargetInfo {
+ float getModifiedScore();
+
+ ChooserTarget getChooserTarget();
+
+ /**
+ * Do not label as 'equals', since this doesn't quite work
+ * as intended with java 8.
+ */
+ default boolean isSimilar(ChooserTargetInfo other) {
+ if (other == null) return false;
+
+ ChooserTarget ct1 = getChooserTarget();
+ ChooserTarget ct2 = other.getChooserTarget();
+
+ // If either is null, there is not enough info to make an informed decision
+ // about equality, so just exit
+ if (ct1 == null || ct2 == null) return false;
+
+ if (ct1.getComponentName().equals(ct2.getComponentName())
+ && TextUtils.equals(getDisplayLabel(), other.getDisplayLabel())
+ && TextUtils.equals(getExtendedInfo(), other.getExtendedInfo())) {
+ return true;
+ }
+
+ return false;
+ }
+}
diff --git a/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
new file mode 100644
index 00000000..e7ffe3c6
--- /dev/null
+++ b/java/src/com/android/intentresolver/chooser/DisplayResolveInfo.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.chooser;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.UserHandle;
+
+import com.android.intentresolver.ResolverActivity;
+import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A TargetInfo plus additional information needed to render it (such as icon and label) and
+ * resolve it to an activity.
+ */
+public class DisplayResolveInfo implements TargetInfo, Parcelable {
+ private final ResolveInfo mResolveInfo;
+ private CharSequence mDisplayLabel;
+ private Drawable mDisplayIcon;
+ private CharSequence mExtendedInfo;
+ private final Intent mResolvedIntent;
+ private final List<Intent> mSourceIntents = new ArrayList<>();
+ private boolean mIsSuspended;
+ private ResolveInfoPresentationGetter mResolveInfoPresentationGetter;
+ private boolean mPinned = false;
+
+ public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, Intent pOrigIntent,
+ ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
+ this(originalIntent, pri, null /*mDisplayLabel*/, null /*mExtendedInfo*/, pOrigIntent,
+ resolveInfoPresentationGetter);
+ }
+
+ public DisplayResolveInfo(Intent originalIntent, ResolveInfo pri, CharSequence pLabel,
+ CharSequence pInfo, @NonNull Intent resolvedIntent,
+ @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
+ mSourceIntents.add(originalIntent);
+ mResolveInfo = pri;
+ mDisplayLabel = pLabel;
+ mExtendedInfo = pInfo;
+ mResolveInfoPresentationGetter = resolveInfoPresentationGetter;
+
+ final Intent intent = new Intent(resolvedIntent);
+ intent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT
+ | Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
+ final ActivityInfo ai = mResolveInfo.activityInfo;
+ intent.setComponent(new ComponentName(ai.applicationInfo.packageName, ai.name));
+
+ mIsSuspended = (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
+
+ mResolvedIntent = intent;
+ }
+
+ private DisplayResolveInfo(DisplayResolveInfo other, Intent fillInIntent, int flags,
+ ResolveInfoPresentationGetter resolveInfoPresentationGetter) {
+ mSourceIntents.addAll(other.getAllSourceIntents());
+ mResolveInfo = other.mResolveInfo;
+ mDisplayLabel = other.mDisplayLabel;
+ mDisplayIcon = other.mDisplayIcon;
+ mExtendedInfo = other.mExtendedInfo;
+ mResolvedIntent = new Intent(other.mResolvedIntent);
+ mResolvedIntent.fillIn(fillInIntent, flags);
+ mResolveInfoPresentationGetter = resolveInfoPresentationGetter;
+ }
+
+ DisplayResolveInfo(DisplayResolveInfo other) {
+ mSourceIntents.addAll(other.getAllSourceIntents());
+ mResolveInfo = other.mResolveInfo;
+ mDisplayLabel = other.mDisplayLabel;
+ mDisplayIcon = other.mDisplayIcon;
+ mExtendedInfo = other.mExtendedInfo;
+ mResolvedIntent = other.mResolvedIntent;
+ mResolveInfoPresentationGetter = other.mResolveInfoPresentationGetter;
+ }
+
+ public ResolveInfo getResolveInfo() {
+ return mResolveInfo;
+ }
+
+ public CharSequence getDisplayLabel() {
+ if (mDisplayLabel == null && mResolveInfoPresentationGetter != null) {
+ mDisplayLabel = mResolveInfoPresentationGetter.getLabel();
+ mExtendedInfo = mResolveInfoPresentationGetter.getSubLabel();
+ }
+ return mDisplayLabel;
+ }
+
+ public boolean hasDisplayLabel() {
+ return mDisplayLabel != null;
+ }
+
+ public void setDisplayLabel(CharSequence displayLabel) {
+ mDisplayLabel = displayLabel;
+ }
+
+ public void setExtendedInfo(CharSequence extendedInfo) {
+ mExtendedInfo = extendedInfo;
+ }
+
+ public Drawable getDisplayIcon(Context context) {
+ return mDisplayIcon;
+ }
+
+ @Override
+ public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
+ return new DisplayResolveInfo(this, fillInIntent, flags, mResolveInfoPresentationGetter);
+ }
+
+ @Override
+ public List<Intent> getAllSourceIntents() {
+ return mSourceIntents;
+ }
+
+ public void addAlternateSourceIntent(Intent alt) {
+ mSourceIntents.add(alt);
+ }
+
+ public void setDisplayIcon(Drawable icon) {
+ mDisplayIcon = icon;
+ }
+
+ public boolean hasDisplayIcon() {
+ return mDisplayIcon != null;
+ }
+
+ public CharSequence getExtendedInfo() {
+ return mExtendedInfo;
+ }
+
+ public Intent getResolvedIntent() {
+ return mResolvedIntent;
+ }
+
+ @Override
+ public ComponentName getResolvedComponentName() {
+ return new ComponentName(mResolveInfo.activityInfo.packageName,
+ mResolveInfo.activityInfo.name);
+ }
+
+ @Override
+ public boolean start(Activity activity, Bundle options) {
+ activity.startActivity(mResolvedIntent, options);
+ return true;
+ }
+
+ @Override
+ public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
+ prepareIntentForCrossProfileLaunch(mResolvedIntent, userId);
+ activity.startActivityAsCaller(mResolvedIntent, options, false, userId);
+ return true;
+ }
+
+ @Override
+ public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
+ prepareIntentForCrossProfileLaunch(mResolvedIntent, user.getIdentifier());
+ activity.startActivityAsUser(mResolvedIntent, options, user);
+ return false;
+ }
+
+ public boolean isSuspended() {
+ return mIsSuspended;
+ }
+
+ @Override
+ public boolean isPinned() {
+ return mPinned;
+ }
+
+ public void setPinned(boolean pinned) {
+ mPinned = pinned;
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeCharSequence(mDisplayLabel);
+ dest.writeCharSequence(mExtendedInfo);
+ dest.writeParcelable(mResolvedIntent, 0);
+ dest.writeTypedList(mSourceIntents);
+ dest.writeBoolean(mIsSuspended);
+ dest.writeBoolean(mPinned);
+ dest.writeParcelable(mResolveInfo, 0);
+ }
+
+ public static final Parcelable.Creator<DisplayResolveInfo> CREATOR =
+ new Parcelable.Creator<DisplayResolveInfo>() {
+ public DisplayResolveInfo createFromParcel(Parcel in) {
+ return new DisplayResolveInfo(in);
+ }
+
+ public DisplayResolveInfo[] newArray(int size) {
+ return new DisplayResolveInfo[size];
+ }
+ };
+
+ private static void prepareIntentForCrossProfileLaunch(Intent intent, int targetUserId) {
+ final int currentUserId = UserHandle.myUserId();
+ if (targetUserId != currentUserId) {
+ intent.fixUris(currentUserId);
+ }
+ }
+
+ private DisplayResolveInfo(Parcel in) {
+ mDisplayLabel = in.readCharSequence();
+ mExtendedInfo = in.readCharSequence();
+ mResolvedIntent = in.readParcelable(null /* ClassLoader */, android.content.Intent.class);
+ in.readTypedList(mSourceIntents, Intent.CREATOR);
+ mIsSuspended = in.readBoolean();
+ mPinned = in.readBoolean();
+ mResolveInfo = in.readParcelable(null /* ClassLoader */, android.content.pm.ResolveInfo.class);
+ }
+}
diff --git a/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
new file mode 100644
index 00000000..5133d997
--- /dev/null
+++ b/java/src/com/android/intentresolver/chooser/MultiDisplayResolveInfo.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.chooser;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.os.UserHandle;
+
+import com.android.intentresolver.ResolverActivity;
+
+import java.util.ArrayList;
+
+/**
+ * Represents a "stack" of chooser targets for various activities within the same component.
+ */
+public class MultiDisplayResolveInfo extends DisplayResolveInfo {
+
+ ArrayList<DisplayResolveInfo> mTargetInfos = new ArrayList<>();
+ // We'll use this DRI for basic presentation info - eg icon, name.
+ final DisplayResolveInfo mBaseInfo;
+ // Index of selected target
+ private int mSelected = -1;
+
+ /**
+ * @param firstInfo A representative DRI to use for the main icon, title, etc for this Info.
+ */
+ public MultiDisplayResolveInfo(String packageName, DisplayResolveInfo firstInfo) {
+ super(firstInfo);
+ mBaseInfo = firstInfo;
+ mTargetInfos.add(firstInfo);
+ }
+
+ @Override
+ public CharSequence getExtendedInfo() {
+ // Never show subtitle for stacked apps
+ return null;
+ }
+
+ /**
+ * Add another DisplayResolveInfo to the list included for this target.
+ */
+ public void addTarget(DisplayResolveInfo target) {
+ mTargetInfos.add(target);
+ }
+
+ /**
+ * List of all DisplayResolveInfos included in this target.
+ */
+ public ArrayList<DisplayResolveInfo> getTargets() {
+ return mTargetInfos;
+ }
+
+ public void setSelected(int selected) {
+ mSelected = selected;
+ }
+
+ /**
+ * Return selected target.
+ */
+ public DisplayResolveInfo getSelectedTarget() {
+ return hasSelected() ? mTargetInfos.get(mSelected) : null;
+ }
+
+ /**
+ * Whether or not the user has selected a specific target for this MultiInfo.
+ */
+ public boolean hasSelected() {
+ return mSelected >= 0;
+ }
+
+ @Override
+ public boolean start(Activity activity, Bundle options) {
+ return mTargetInfos.get(mSelected).start(activity, options);
+ }
+
+ @Override
+ public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
+ return mTargetInfos.get(mSelected).startAsCaller(activity, options, userId);
+ }
+
+ @Override
+ public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
+ return mTargetInfos.get(mSelected).startAsUser(activity, options, user);
+ }
+
+}
diff --git a/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
new file mode 100644
index 00000000..220870f2
--- /dev/null
+++ b/java/src/com/android/intentresolver/chooser/NotSelectableTargetInfo.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.chooser;
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.service.chooser.ChooserTarget;
+
+import com.android.intentresolver.ResolverActivity;
+
+import java.util.List;
+
+/**
+ * Distinguish between targets that selectable by the user, vs those that are
+ * placeholders for the system while information is loading in an async manner.
+ */
+public abstract class NotSelectableTargetInfo implements ChooserTargetInfo {
+
+ public 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;
+ }
+
+ public List<Intent> getAllSourceIntents() {
+ return null;
+ }
+
+ public float getModifiedScore() {
+ return -0.1f;
+ }
+
+ public ChooserTarget getChooserTarget() {
+ return null;
+ }
+
+ public boolean isSuspended() {
+ return false;
+ }
+
+ public boolean isPinned() {
+ return false;
+ }
+}
diff --git a/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
new file mode 100644
index 00000000..1610d0fd
--- /dev/null
+++ b/java/src/com/android/intentresolver/chooser/SelectableTargetInfo.java
@@ -0,0 +1,339 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.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.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.LauncherApps;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.service.chooser.ChooserTarget;
+import android.text.SpannableStringBuilder;
+import android.util.Log;
+
+import com.android.intentresolver.ChooserActivity;
+import com.android.intentresolver.ResolverActivity;
+import com.android.intentresolver.ResolverListAdapter.ActivityInfoPresentationGetter;
+import com.android.intentresolver.SimpleIconFactory;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Live target, currently selectable by the user.
+ * @see NotSelectableTargetInfo
+ */
+public final class SelectableTargetInfo implements ChooserTargetInfo {
+ private static final String TAG = "SelectableTargetInfo";
+
+ private final Context mContext;
+ private final DisplayResolveInfo mSourceInfo;
+ private final ResolveInfo mBackupResolveInfo;
+ private final ChooserTarget mChooserTarget;
+ private final String mDisplayLabel;
+ private final PackageManager mPm;
+ private final SelectableTargetInfoCommunicator mSelectableTargetInfoCommunicator;
+ @GuardedBy("this")
+ private ShortcutInfo mShortcutInfo;
+ private Drawable mBadgeIcon = null;
+ private CharSequence mBadgeContentDescription;
+ @GuardedBy("this")
+ private Drawable mDisplayIcon;
+ private final Intent mFillInIntent;
+ private final int mFillInFlags;
+ private final boolean mIsPinned;
+ private final float mModifiedScore;
+ private boolean mIsSuspended = false;
+
+ public SelectableTargetInfo(Context context, DisplayResolveInfo sourceInfo,
+ ChooserTarget chooserTarget,
+ float modifiedScore, SelectableTargetInfoCommunicator selectableTargetInfoComunicator,
+ @Nullable ShortcutInfo shortcutInfo) {
+ mContext = context;
+ mSourceInfo = sourceInfo;
+ mChooserTarget = chooserTarget;
+ mModifiedScore = modifiedScore;
+ mPm = mContext.getPackageManager();
+ mSelectableTargetInfoCommunicator = selectableTargetInfoComunicator;
+ mShortcutInfo = shortcutInfo;
+ mIsPinned = shortcutInfo != null && shortcutInfo.isPinned();
+ if (sourceInfo != null) {
+ final ResolveInfo ri = sourceInfo.getResolveInfo();
+ if (ri != null) {
+ final ActivityInfo ai = ri.activityInfo;
+ if (ai != null && ai.applicationInfo != null) {
+ final PackageManager pm = mContext.getPackageManager();
+ mBadgeIcon = pm.getApplicationIcon(ai.applicationInfo);
+ mBadgeContentDescription = pm.getApplicationLabel(ai.applicationInfo);
+ mIsSuspended =
+ (ai.applicationInfo.flags & ApplicationInfo.FLAG_SUSPENDED) != 0;
+ }
+ }
+ }
+
+ if (sourceInfo != null) {
+ mBackupResolveInfo = null;
+ } else {
+ mBackupResolveInfo =
+ mContext.getPackageManager().resolveActivity(getResolvedIntent(), 0);
+ }
+
+ mFillInIntent = null;
+ mFillInFlags = 0;
+
+ mDisplayLabel = sanitizeDisplayLabel(chooserTarget.getTitle());
+ }
+
+ private SelectableTargetInfo(SelectableTargetInfo other,
+ Intent fillInIntent, int flags) {
+ mContext = other.mContext;
+ mPm = other.mPm;
+ mSelectableTargetInfoCommunicator = other.mSelectableTargetInfoCommunicator;
+ mSourceInfo = other.mSourceInfo;
+ mBackupResolveInfo = other.mBackupResolveInfo;
+ mChooserTarget = other.mChooserTarget;
+ mBadgeIcon = other.mBadgeIcon;
+ mBadgeContentDescription = other.mBadgeContentDescription;
+ synchronized (other) {
+ mShortcutInfo = other.mShortcutInfo;
+ mDisplayIcon = other.mDisplayIcon;
+ }
+ mFillInIntent = fillInIntent;
+ mFillInFlags = flags;
+ mModifiedScore = other.mModifiedScore;
+ mIsPinned = other.mIsPinned;
+
+ mDisplayLabel = sanitizeDisplayLabel(mChooserTarget.getTitle());
+ }
+
+ private String sanitizeDisplayLabel(CharSequence label) {
+ SpannableStringBuilder sb = new SpannableStringBuilder(label);
+ sb.clearSpans();
+ return sb.toString();
+ }
+
+ public boolean isSuspended() {
+ return mIsSuspended;
+ }
+
+ public DisplayResolveInfo getDisplayResolveInfo() {
+ return mSourceInfo;
+ }
+
+ /**
+ * Load display icon, if needed.
+ */
+ public void loadIcon() {
+ ShortcutInfo shortcutInfo;
+ Drawable icon;
+ synchronized (this) {
+ shortcutInfo = mShortcutInfo;
+ icon = mDisplayIcon;
+ }
+ if (icon == null && shortcutInfo != null) {
+ icon = getChooserTargetIconDrawable(mChooserTarget, shortcutInfo);
+ synchronized (this) {
+ mDisplayIcon = icon;
+ mShortcutInfo = null;
+ }
+ }
+ }
+
+ private Drawable getChooserTargetIconDrawable(ChooserTarget target,
+ @Nullable ShortcutInfo shortcutInfo) {
+ Drawable directShareIcon = null;
+
+ // First get the target drawable and associated activity info
+ final Icon icon = target.getIcon();
+ if (icon != null) {
+ directShareIcon = icon.loadDrawable(mContext);
+ } else if (shortcutInfo != null) {
+ LauncherApps launcherApps = (LauncherApps) mContext.getSystemService(
+ Context.LAUNCHER_APPS_SERVICE);
+ directShareIcon = launcherApps.getShortcutIconDrawable(shortcutInfo, 0);
+ }
+
+ if (directShareIcon == null) return null;
+
+ ActivityInfo info = null;
+ try {
+ info = mPm.getActivityInfo(target.getComponentName(), 0);
+ } catch (PackageManager.NameNotFoundException error) {
+ Log.e(TAG, "Could not find activity associated with ChooserTarget");
+ }
+
+ if (info == null) return null;
+
+ // Now fetch app icon and raster with no badging even in work profile
+ Bitmap appIcon = mSelectableTargetInfoCommunicator.makePresentationGetter(info)
+ .getIconBitmap(null);
+
+ // Raster target drawable with appIcon as a badge
+ SimpleIconFactory sif = SimpleIconFactory.obtain(mContext);
+ Bitmap directShareBadgedIcon = sif.createAppBadgedIconBitmap(directShareIcon, appIcon);
+ sif.recycle();
+
+ return new BitmapDrawable(mContext.getResources(), directShareBadgedIcon);
+ }
+
+ public float getModifiedScore() {
+ return mModifiedScore;
+ }
+
+ @Override
+ public Intent getResolvedIntent() {
+ if (mSourceInfo != null) {
+ return mSourceInfo.getResolvedIntent();
+ }
+
+ final Intent targetIntent = new Intent(mSelectableTargetInfoCommunicator.getTargetIntent());
+ targetIntent.setComponent(mChooserTarget.getComponentName());
+ targetIntent.putExtras(mChooserTarget.getIntentExtras());
+ return targetIntent;
+ }
+
+ @Override
+ public ComponentName getResolvedComponentName() {
+ if (mSourceInfo != null) {
+ return mSourceInfo.getResolvedComponentName();
+ } else if (mBackupResolveInfo != null) {
+ return new ComponentName(mBackupResolveInfo.activityInfo.packageName,
+ mBackupResolveInfo.activityInfo.name);
+ }
+ return null;
+ }
+
+ private Intent getBaseIntentToSend() {
+ Intent result = getResolvedIntent();
+ if (result == null) {
+ Log.e(TAG, "ChooserTargetInfo: no base intent available to send");
+ } else {
+ result = new Intent(result);
+ if (mFillInIntent != null) {
+ result.fillIn(mFillInIntent, mFillInFlags);
+ }
+ result.fillIn(mSelectableTargetInfoCommunicator.getReferrerFillInIntent(), 0);
+ }
+ return result;
+ }
+
+ @Override
+ public boolean start(Activity activity, Bundle options) {
+ throw new RuntimeException("ChooserTargets should be started as caller.");
+ }
+
+ @Override
+ public boolean startAsCaller(ResolverActivity activity, Bundle options, int userId) {
+ final Intent intent = getBaseIntentToSend();
+ if (intent == null) {
+ return false;
+ }
+ intent.setComponent(mChooserTarget.getComponentName());
+ intent.putExtras(mChooserTarget.getIntentExtras());
+
+ // Important: we will ignore the target security checks in ActivityManager
+ // if and only if the ChooserTarget's target package is the same package
+ // where we got the ChooserTargetService that provided it. This lets a
+ // ChooserTargetService provide a non-exported or permission-guarded target
+ // to the chooser for the user to pick.
+ //
+ // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere
+ // so we'll obey the caller's normal security checks.
+ final boolean ignoreTargetSecurity = mSourceInfo != null
+ && mSourceInfo.getResolvedComponentName().getPackageName()
+ .equals(mChooserTarget.getComponentName().getPackageName());
+ activity.startActivityAsCaller(intent, options, ignoreTargetSecurity, userId);
+ return true;
+ }
+
+ @Override
+ public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
+ throw new RuntimeException("ChooserTargets should be started as caller.");
+ }
+
+ @Override
+ public ResolveInfo getResolveInfo() {
+ return mSourceInfo != null ? mSourceInfo.getResolveInfo() : mBackupResolveInfo;
+ }
+
+ @Override
+ public CharSequence getDisplayLabel() {
+ return mDisplayLabel;
+ }
+
+ @Override
+ public CharSequence getExtendedInfo() {
+ // ChooserTargets have badge icons, so we won't show the extended info to disambiguate.
+ return null;
+ }
+
+ @Override
+ public synchronized Drawable getDisplayIcon(Context context) {
+ return mDisplayIcon;
+ }
+
+ public ChooserTarget getChooserTarget() {
+ return mChooserTarget;
+ }
+
+ @Override
+ public TargetInfo cloneFilledIn(Intent fillInIntent, int flags) {
+ return new SelectableTargetInfo(this, fillInIntent, flags);
+ }
+
+ @Override
+ public List<Intent> getAllSourceIntents() {
+ final List<Intent> results = new ArrayList<>();
+ if (mSourceInfo != null) {
+ // We only queried the service for the first one in our sourceinfo.
+ results.add(mSourceInfo.getAllSourceIntents().get(0));
+ }
+ return results;
+ }
+
+ @Override
+ public boolean isPinned() {
+ return mIsPinned;
+ }
+
+ /**
+ * Necessary methods to communicate between {@link SelectableTargetInfo}
+ * and {@link ResolverActivity} or {@link ChooserActivity}.
+ */
+ public interface SelectableTargetInfoCommunicator {
+
+ ActivityInfoPresentationGetter makePresentationGetter(ActivityInfo info);
+
+ Intent getTargetIntent();
+
+ Intent getReferrerFillInIntent();
+ }
+}
diff --git a/java/src/com/android/intentresolver/chooser/TargetInfo.java b/java/src/com/android/intentresolver/chooser/TargetInfo.java
new file mode 100644
index 00000000..fabb26c2
--- /dev/null
+++ b/java/src/com/android/intentresolver/chooser/TargetInfo.java
@@ -0,0 +1,133 @@
+/*
+ * Copyright (C) 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.chooser;
+
+
+import android.app.Activity;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.UserHandle;
+
+import com.android.intentresolver.ResolverActivity;
+
+import java.util.List;
+
+/**
+ * A single target as represented in the chooser.
+ */
+public interface TargetInfo {
+ /**
+ * Get the resolved intent that represents this target. Note that this may not be the
+ * intent that will be launched by calling one of the <code>start</code> methods provided;
+ * this is the intent that will be credited with the launch.
+ *
+ * @return the resolved intent for this target
+ */
+ Intent getResolvedIntent();
+
+ /**
+ * 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.
+ *
+ * @return the resolved ComponentName for this target
+ */
+ ComponentName getResolvedComponentName();
+
+ /**
+ * 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.
+ *
+ * @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);
+
+ /**
+ * Start the activity referenced by this target as a given user.
+ *
+ * @param activity calling activity performing the launch
+ * @param options ActivityOptions bundle
+ * @param user handle for the user to start the activity as
+ * @return true if the start completed successfully
+ */
+ boolean startAsUser(Activity activity, Bundle options, UserHandle user);
+
+ /**
+ * Return the ResolveInfo about how and why this target matched the original query
+ * for available targets.
+ *
+ * @return ResolveInfo representing this target's match
+ */
+ ResolveInfo getResolveInfo();
+
+ /**
+ * Return the human-readable text label for this target.
+ *
+ * @return user-visible target label
+ */
+ CharSequence getDisplayLabel();
+
+ /**
+ * Return any extended info for this target. This may be used to disambiguate
+ * otherwise identical targets.
+ *
+ * @return human-readable disambig string or null if none present
+ */
+ CharSequence getExtendedInfo();
+
+ /**
+ * @return The drawable that should be used to represent this target including badge
+ * @param context
+ */
+ Drawable getDisplayIcon(Context context);
+
+ /**
+ * Clone this target with the given fill-in information.
+ */
+ TargetInfo cloneFilledIn(Intent fillInIntent, int flags);
+
+ /**
+ * @return the list of supported source intents deduped against this single target
+ */
+ List<Intent> getAllSourceIntents();
+
+ /**
+ * @return true if this target cannot be selected by the user
+ */
+ boolean isSuspended();
+
+ /**
+ * @return true if this target should be pinned to the front by the request of the user
+ */
+ boolean isPinned();
+}
diff --git a/java/tests/AndroidManifest.xml b/java/tests/AndroidManifest.xml
index 1c7506b6..bfe3a39f 100644
--- a/java/tests/AndroidManifest.xml
+++ b/java/tests/AndroidManifest.xml
@@ -27,7 +27,6 @@
<application>
<uses-library android:name="android.test.runner" />
<activity android:name="com.android.intentresolver.ChooserWrapperActivity" />
- <activity android:name="com.android.internal.app.ChooserWrapperActivity" />
</application>
<instrumentation android:name="android.testing.TestableInstrumentation"
diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerFake.java b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerFake.java
new file mode 100644
index 00000000..e4146cc5
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ChooserActivityLoggerFake.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import com.android.internal.logging.InstanceId;
+import com.android.internal.logging.UiEventLogger;
+import com.android.internal.util.FrameworkStatsLog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ChooserActivityLoggerFake implements ChooserActivityLogger {
+ static class CallRecord {
+ // shared fields between all logs
+ public int atomId;
+ public String packageName;
+ public InstanceId instanceId;
+
+ // generic log field
+ public UiEventLogger.UiEventEnum event;
+
+ // share started fields
+ public String mimeType;
+ public int appProvidedDirect;
+ public int appProvidedApp;
+ public boolean isWorkprofile;
+ public int previewType;
+ public String intent;
+
+ // share completed fields
+ public int targetType;
+ public int positionPicked;
+ public boolean isPinned;
+
+ CallRecord(int atomId, UiEventLogger.UiEventEnum eventId,
+ String packageName, InstanceId instanceId) {
+ this.atomId = atomId;
+ this.packageName = packageName;
+ this.instanceId = instanceId;
+ this.event = eventId;
+ }
+
+ CallRecord(int atomId, String packageName, InstanceId instanceId, String mimeType,
+ int appProvidedDirect, int appProvidedApp, boolean isWorkprofile, int previewType,
+ String intent) {
+ this.atomId = atomId;
+ this.packageName = packageName;
+ this.instanceId = instanceId;
+ this.mimeType = mimeType;
+ this.appProvidedDirect = appProvidedDirect;
+ this.appProvidedApp = appProvidedApp;
+ this.isWorkprofile = isWorkprofile;
+ this.previewType = previewType;
+ this.intent = intent;
+ }
+
+ CallRecord(int atomId, String packageName, InstanceId instanceId, int targetType,
+ int positionPicked, boolean isPinned) {
+ this.atomId = atomId;
+ this.packageName = packageName;
+ this.instanceId = instanceId;
+ this.targetType = targetType;
+ this.positionPicked = positionPicked;
+ this.isPinned = isPinned;
+ }
+
+ }
+ private List<CallRecord> mCalls = new ArrayList<>();
+
+ public int numCalls() {
+ return mCalls.size();
+ }
+
+ List<CallRecord> getCalls() {
+ return mCalls;
+ }
+
+ CallRecord get(int index) {
+ return mCalls.get(index);
+ }
+
+ UiEventLogger.UiEventEnum event(int index) {
+ return mCalls.get(index).event;
+ }
+
+ public void removeCallsForUiEventsOfType(int uiEventType) {
+ mCalls.removeIf(
+ call ->
+ (call.atomId == FrameworkStatsLog.UI_EVENT_REPORTED)
+ && (call.event.getId() == uiEventType));
+ }
+
+ @Override
+ public void logShareStarted(int eventId, String packageName, String mimeType,
+ int appProvidedDirect, int appProvidedApp, boolean isWorkprofile, int previewType,
+ String intent) {
+ mCalls.add(new CallRecord(FrameworkStatsLog.SHARESHEET_STARTED, packageName,
+ getInstanceId(), mimeType, appProvidedDirect, appProvidedApp, isWorkprofile,
+ previewType, intent));
+ }
+
+ @Override
+ public void logShareTargetSelected(int targetType, String packageName, int positionPicked,
+ boolean isPinned) {
+ mCalls.add(new CallRecord(FrameworkStatsLog.RANKING_SELECTED, packageName, getInstanceId(),
+ SharesheetTargetSelectedEvent.fromTargetType(targetType).getId(), positionPicked,
+ isPinned));
+ }
+
+ @Override
+ public void log(UiEventLogger.UiEventEnum event, InstanceId instanceId) {
+ mCalls.add(new CallRecord(FrameworkStatsLog.UI_EVENT_REPORTED,
+ event, "", instanceId));
+ }
+
+ @Override
+ public InstanceId getInstanceId() {
+ return InstanceId.fakeInstanceId(-1);
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
new file mode 100644
index 00000000..080f1e41
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import static org.mockito.Mockito.mock;
+
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.os.UserHandle;
+
+import com.android.intentresolver.chooser.TargetInfo;
+import com.android.internal.logging.MetricsLogger;
+
+import java.util.List;
+import java.util.function.Function;
+
+/**
+ * Singleton providing overrides to be applied by any {@code IChooserWrapper} used in testing.
+ * We cannot directly mock the activity created since instrumentation creates it, so instead we use
+ * this singleton to modify behavior.
+ */
+public class ChooserActivityOverrideData {
+ private static ChooserActivityOverrideData sInstance = null;
+
+ public static ChooserActivityOverrideData getInstance() {
+ if (sInstance == null) {
+ sInstance = new ChooserActivityOverrideData();
+ }
+ return sInstance;
+ }
+
+ @SuppressWarnings("Since15")
+ public Function<PackageManager, PackageManager> createPackageManager;
+ public Function<TargetInfo, Boolean> onSafelyStartCallback;
+ public Function<ChooserListAdapter, Void> onQueryDirectShareTargets;
+ public ResolverListController resolverListController;
+ public ResolverListController workResolverListController;
+ public Boolean isVoiceInteraction;
+ public boolean isImageType;
+ public Cursor resolverCursor;
+ public boolean resolverForceException;
+ public Bitmap previewThumbnail;
+ public MetricsLogger metricsLogger;
+ public ChooserActivityLogger chooserActivityLogger;
+ public int alternateProfileSetting;
+ public Resources resources;
+ public UserHandle workProfileUserHandle;
+ public boolean hasCrossProfileIntents;
+ public boolean isQuietModeEnabled;
+ public boolean isWorkProfileUserRunning;
+ public boolean isWorkProfileUserUnlocked;
+ public AbstractMultiProfilePagerAdapter.Injector multiPagerAdapterInjector;
+ public PackageManager packageManager;
+
+ public void reset() {
+ onSafelyStartCallback = null;
+ onQueryDirectShareTargets = null;
+ isVoiceInteraction = null;
+ createPackageManager = null;
+ previewThumbnail = null;
+ isImageType = false;
+ resolverCursor = null;
+ resolverForceException = false;
+ resolverListController = mock(ResolverListController.class);
+ workResolverListController = mock(ResolverListController.class);
+ metricsLogger = mock(MetricsLogger.class);
+ chooserActivityLogger = new ChooserActivityLoggerFake();
+ alternateProfileSetting = 0;
+ resources = null;
+ workProfileUserHandle = null;
+ hasCrossProfileIntents = true;
+ isQuietModeEnabled = false;
+ isWorkProfileUserRunning = true;
+ isWorkProfileUserUnlocked = true;
+ packageManager = null;
+ multiPagerAdapterInjector = new AbstractMultiProfilePagerAdapter.Injector() {
+ @Override
+ public boolean hasCrossProfileIntents(List<Intent> intents, int sourceUserId,
+ int targetUserId) {
+ return hasCrossProfileIntents;
+ }
+
+ @Override
+ public boolean isQuietModeEnabled(UserHandle workProfileUserHandle) {
+ return isQuietModeEnabled;
+ }
+
+ @Override
+ public void requestQuietModeEnabled(boolean enabled,
+ UserHandle workProfileUserHandle) {
+ isQuietModeEnabled = enabled;
+ }
+ };
+ }
+
+ private ChooserActivityOverrideData() {}
+}
+
diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
index 552b4e0d..0e9f010e 100644
--- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
+++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
@@ -33,15 +33,15 @@ import android.net.Uri;
import android.os.UserHandle;
import android.util.Size;
-import com.android.internal.app.AbstractMultiProfilePagerAdapter;
-import com.android.internal.app.ChooserActivityLogger;
-import com.android.internal.app.ChooserActivityOverrideData;
-import com.android.internal.app.ChooserListAdapter;
-import com.android.internal.app.IChooserWrapper;
-import com.android.internal.app.ResolverListAdapter.ResolveInfoPresentationGetter;
-import com.android.internal.app.ResolverListController;
-import com.android.internal.app.chooser.DisplayResolveInfo;
-import com.android.internal.app.chooser.TargetInfo;
+import com.android.intentresolver.AbstractMultiProfilePagerAdapter;
+import com.android.intentresolver.ChooserActivityLogger;
+import com.android.intentresolver.ChooserActivityOverrideData;
+import com.android.intentresolver.ChooserListAdapter;
+import com.android.intentresolver.IChooserWrapper;
+import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;
+import com.android.intentresolver.ResolverListController;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.chooser.TargetInfo;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -139,7 +139,7 @@ public class ChooserWrapperActivity
}
@Override
- public void safelyStartActivity(com.android.internal.app.chooser.TargetInfo cti) {
+ public void safelyStartActivity(com.android.intentresolver.chooser.TargetInfo cti) {
if (sOverrides.onSafelyStartCallback != null
&& sOverrides.onSafelyStartCallback.apply(cti)) {
return;
diff --git a/java/tests/src/com/android/intentresolver/IChooserWrapper.java b/java/tests/src/com/android/intentresolver/IChooserWrapper.java
new file mode 100644
index 00000000..f81cd023
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/IChooserWrapper.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2021 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF 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.usage.UsageStatsManager;
+import android.content.Intent;
+import android.content.pm.ResolveInfo;
+import android.os.UserHandle;
+
+import com.android.intentresolver.ResolverListAdapter.ResolveInfoPresentationGetter;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+
+/**
+ * Test-only extended API capabilities that an instrumented ChooserActivity subclass provides in
+ * order to expose the internals for override/inspection. Implementations should apply the overrides
+ * specified by the {@code ChooserActivityOverrideData} singleton.
+ */
+public interface IChooserWrapper {
+ ChooserListAdapter getAdapter();
+ ChooserListAdapter getPersonalListAdapter();
+ ChooserListAdapter getWorkListAdapter();
+ boolean getIsSelected();
+ UsageStatsManager getUsageStatsManager();
+ DisplayResolveInfo createTestDisplayResolveInfo(Intent originalIntent, ResolveInfo pri,
+ CharSequence pLabel, CharSequence pInfo, Intent replacementIntent,
+ @Nullable ResolveInfoPresentationGetter resolveInfoPresentationGetter);
+ UserHandle getCurrentUserHandle();
+ ChooserActivityLogger getChooserActivityLogger();
+}
diff --git a/java/tests/src/com/android/intentresolver/MatcherUtils.java b/java/tests/src/com/android/intentresolver/MatcherUtils.java
new file mode 100644
index 00000000..6168968b
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/MatcherUtils.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver;
+
+import org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+
+/**
+ * Utils for helping with more customized matching options, for example matching the first
+ * occurrence of a set criteria.
+ */
+public class MatcherUtils {
+
+ /**
+ * Returns a {@link Matcher} which only matches the first occurrence of a set criteria.
+ */
+ static <T> Matcher<T> first(final Matcher<T> matcher) {
+ return new BaseMatcher<T>() {
+ boolean isFirstMatch = true;
+
+ @Override
+ public boolean matches(final Object item) {
+ if (isFirstMatch && matcher.matches(item)) {
+ isFirstMatch = false;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void describeTo(final Description description) {
+ description.appendText("Returns the first matching item");
+ }
+ };
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
new file mode 100644
index 00000000..33e7123f
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
@@ -0,0 +1,169 @@
+/*
+ * 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.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.os.UserHandle;
+import android.test.mock.MockContext;
+import android.test.mock.MockPackageManager;
+import android.test.mock.MockResources;
+
+/**
+ * Utility class used by resolver tests to create mock data
+ */
+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 ResolverActivity.ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i) {
+ return new ResolverActivity.ResolvedComponentInfo(createComponentName(i),
+ createResolverIntent(i), createResolveInfo(i, USER_SOMEONE_ELSE));
+ }
+
+ static ResolverActivity.ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i,
+ int userId) {
+ return new ResolverActivity.ResolvedComponentInfo(createComponentName(i),
+ createResolverIntent(i), createResolveInfo(i, userId));
+ }
+
+ static ComponentName createComponentName(int i) {
+ final String name = "component" + i;
+ return new ComponentName("foo.bar." + name, name);
+ }
+
+ static ResolveInfo createResolveInfo(int i, int userId) {
+ final ResolveInfo resolveInfo = new ResolveInfo();
+ resolveInfo.activityInfo = createActivityInfo(i);
+ resolveInfo.targetUserId = userId;
+ return resolveInfo;
+ }
+
+ static ActivityInfo createActivityInfo(int i) {
+ ActivityInfo ai = new ActivityInfo();
+ ai.name = "activity_name" + i;
+ ai.packageName = "foo_bar" + i;
+ ai.enabled = true;
+ ai.exported = true;
+ ai.permission = null;
+ ai.applicationInfo = createApplicationInfo();
+ return ai;
+ }
+
+ static ApplicationInfo createApplicationInfo() {
+ ApplicationInfo ai = new ApplicationInfo();
+ ai.name = "app_name";
+ ai.packageName = "foo.bar";
+ ai.enabled = true;
+ return ai;
+ }
+
+ static class PackageManagerMockedInfo {
+ public Context ctx;
+ public ApplicationInfo appInfo;
+ public ActivityInfo activityInfo;
+ public ResolveInfo resolveInfo;
+ public String setAppLabel;
+ public String setActivityLabel;
+ public String setResolveInfoLabel;
+ }
+
+ static PackageManagerMockedInfo createPackageManagerMockedInfo(boolean hasOverridePermission) {
+ final String appLabel = "app_label";
+ final String activityLabel = "activity_label";
+ final String resolveInfoLabel = "resolve_info_label";
+
+ MockContext ctx = new MockContext() {
+ @Override
+ public PackageManager getPackageManager() {
+ return new MockPackageManager() {
+ @Override
+ public int checkPermission(String permName, String pkgName) {
+ if (hasOverridePermission) return PERMISSION_GRANTED;
+ return PERMISSION_DENIED;
+ }
+ };
+ }
+
+ @Override
+ public Resources getResources() {
+ return new MockResources() {
+ @Override
+ public String getString(int id) throws NotFoundException {
+ if (id == 1) return appLabel;
+ if (id == 2) return activityLabel;
+ if (id == 3) return resolveInfoLabel;
+ return null;
+ }
+ };
+ }
+ };
+
+ ApplicationInfo appInfo = new ApplicationInfo() {
+ @Override
+ public CharSequence loadLabel(PackageManager pm) {
+ return appLabel;
+ }
+ };
+ appInfo.labelRes = 1;
+
+ ActivityInfo activityInfo = new ActivityInfo() {
+ @Override
+ public CharSequence loadLabel(PackageManager pm) {
+ return activityLabel;
+ }
+ };
+ activityInfo.labelRes = 2;
+ activityInfo.applicationInfo = appInfo;
+
+ ResolveInfo resolveInfo = new ResolveInfo() {
+ @Override
+ public CharSequence loadLabel(PackageManager pm) {
+ return resolveInfoLabel;
+ }
+ };
+ resolveInfo.activityInfo = activityInfo;
+ resolveInfo.resolvePackageName = "super.fake.packagename";
+ resolveInfo.labelRes = 3;
+
+ PackageManagerMockedInfo mockedInfo = new PackageManagerMockedInfo();
+ mockedInfo.activityInfo = activityInfo;
+ mockedInfo.appInfo = appInfo;
+ mockedInfo.ctx = ctx;
+ mockedInfo.resolveInfo = resolveInfo;
+ mockedInfo.setAppLabel = appLabel;
+ mockedInfo.setActivityLabel = activityLabel;
+ mockedInfo.setResolveInfoLabel = resolveInfoLabel;
+
+ return mockedInfo;
+ }
+
+ static Intent createResolverIntent(int i) {
+ return new Intent("intentAction" + i);
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
index f89b4586..b901fc1e 100644
--- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
+++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2021 The Android Open Source Project
+ * Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,32 +16,132 @@
package com.android.intentresolver;
+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.swipeUp;
+import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.ViewMatchers.hasSibling;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+
+import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_CHOOSER_TARGET;
+import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_DEFAULT;
+import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE;
+import static com.android.intentresolver.ChooserActivity.TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER;
+import static com.android.intentresolver.ChooserListAdapter.CALLER_TARGET_SCORE_BOOST;
+import static com.android.intentresolver.ChooserListAdapter.SHORTCUT_TARGET_SCORE_BOOST;
+import static com.android.intentresolver.MatcherUtils.first;
+
import static com.google.common.truth.Truth.assertThat;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+
+import static org.hamcrest.CoreMatchers.allOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.greaterThan;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.app.usage.UsageStatsManager;
+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.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.pm.ShortcutInfo;
+import android.content.pm.ShortcutManager.ShareShortcutInfo;
+import android.content.res.Configuration;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.drawable.Icon;
+import android.metrics.LogMaker;
+import android.net.Uri;
+import android.os.UserHandle;
+import android.provider.DeviceConfig;
+import android.service.chooser.ChooserTarget;
+import android.view.View;
+import androidx.annotation.CallSuper;
+import androidx.test.espresso.matcher.BoundedDiagnosingMatcher;
import androidx.test.platform.app.InstrumentationRegistry;
+import androidx.test.rule.ActivityTestRule;
-import com.android.internal.app.ChooserActivity;
-import com.android.internal.app.ChooserActivityTest;
+import com.android.intentresolver.ResolverActivity.ResolvedComponentInfo;
+import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.util.FrameworkStatsLog;
+import com.android.internal.widget.GridLayoutManager;
+import com.android.internal.widget.RecyclerView;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+import org.hamcrest.Matchers;
+import org.junit.Before;
import org.junit.Ignore;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import java.util.function.Function;
+/**
+ * Instrumentation tests for the IntentResolver module's Sharesheet (ChooserActivity).
+ * TODO: remove methods that supported running these tests against arbitrary ChooserActivity
+ * subclasses. Those were left over from an earlier version where IntentResolver's ChooserActivity
+ * inherited from the framework version at com.android.internal.app.ChooserActivity, and this test
+ * file inherited from the framework's version as well. Once the migration to the IntentResolver
+ * package is complete, that aspect of the test design can revert to match the style of the
+ * framework tests prior to ag/16482932.
+ * TODO: this can simply be renamed to "ChooserActivityTest" if that's ever unambiguous (i.e., if
+ * there's no risk of confusion with the framework tests that currently share the same name).
+ */
@Ignore("investigate b/241944046 and re-enabled")
@RunWith(Parameterized.class)
-public class UnbundledChooserActivityTest extends ChooserActivityTest {
+public class UnbundledChooserActivityTest {
+
+ /* --------
+ * Subclasses should copy the following section verbatim (or alternatively could specify some
+ * additional @Parameterized.Parameters, as long as the correct parameters are used to
+ * initialize the ChooserActivityTest). The subclasses should also be @RunWith the
+ * `Parameterized` runner.
+ * --------
+ */
+
private static final Function<PackageManager, PackageManager> DEFAULT_PM = pm -> pm;
private static final Function<PackageManager, PackageManager> NO_APP_PREDICTION_SERVICE_PM =
pm -> {
@@ -58,21 +158,16 @@ public class UnbundledChooserActivityTest extends ChooserActivityTest {
});
}
- @Override
- protected Intent getConcreteIntentForLaunch(Intent clientIntent) {
- Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
- clientIntent.setClass(context, com.android.intentresolver.ChooserWrapperActivity.class);
- return clientIntent;
- }
-
- @Override
- protected boolean shouldTestTogglingAppPredictionServiceAvailabilityAtRuntime() {
- // Unbundled chooser takes in app prediction availability as a parameter from the system, so
- // changing the availability conditions after the fact won't make a difference.
- return false;
- }
+ /* --------
+ * Subclasses can override the following methods to customize test behavior.
+ * --------
+ */
- @Override
+ /**
+ * Perform any necessary per-test initialization steps (subclasses may add additional steps
+ * before and/or after calling up to the superclass implementation).
+ */
+ @CallSuper
protected void setup() {
// TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the
// permissions we require (which we'll read from the manifest at runtime).
@@ -81,14 +176,2727 @@ public class UnbundledChooserActivityTest extends ChooserActivityTest {
.getUiAutomation()
.adoptShellPermissionIdentity();
- super.setup();
+ cleanOverrideData();
+ }
+
+ /**
+ * Given an intent that was constructed in a test, perform any additional configuration to
+ * specify the appropriate concrete ChooserActivity subclass. The activity launched by this
+ * intent must descend from android.intentresolver.ChooserActivity (for our ActivityTestRule), and
+ * must also implement the android.intentresolver.IChooserWrapper interface (since test code will
+ * assume the ability to make unsafe downcasts).
+ */
+ protected Intent getConcreteIntentForLaunch(Intent clientIntent) {
+ clientIntent.setClass(
+ InstrumentationRegistry.getInstrumentation().getTargetContext(),
+ com.android.intentresolver.ChooserWrapperActivity.class);
+ return clientIntent;
+ }
+
+ /**
+ * Whether {@code #testIsAppPredictionServiceAvailable} should verify the behavior after
+ * changing the availability conditions at runtime. In the unbundled chooser, the availability
+ * is cached at start and will never be re-evaluated.
+ * TODO: remove when we no longer want to test the system's on-the-fly evaluation.
+ */
+ protected boolean shouldTestTogglingAppPredictionServiceAvailabilityAtRuntime() {
+ return false;
}
+ /* --------
+ * The code in this section is unorthodox and can be simplified/reverted when we no longer need
+ * to support the parallel chooser implementations.
+ * --------
+ */
+
+ // 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 =
+ new ActivityTestRule<>(ChooserActivity.class, false, false) {
+ @Override
+ public ChooserActivity launchActivity(Intent clientIntent) {
+ return super.launchActivity(getConcreteIntentForLaunch(clientIntent));
+ }
+ };
+
+ @Before
+ public final void doPolymorphicSetup() {
+ // The base class needs a @Before-annotated setup for when it runs against the system
+ // chooser, while subclasses need to be able to specify their own setup behavior. Notably
+ // the unbundled chooser, running in user-space, needs to take additional steps before it
+ // can run #cleanOverrideData() (which writes to DeviceConfig).
+ setup();
+ }
+
+ /* --------
+ * Subclasses can ignore the remaining code and inherit the full suite of tests.
+ * --------
+ */
+
+ private static final String TEST_MIME_TYPE = "application/TestType";
+
+ 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;
+
+
public UnbundledChooserActivityTest(
int testNum,
String testName,
Function<PackageManager, PackageManager> packageManagerOverride) {
- super(testNum, testName, packageManagerOverride);
+ mPackageManagerOverride = packageManagerOverride;
+ mTestNum = testNum;
+ }
+
+ public void cleanOverrideData() {
+ ChooserActivityOverrideData.getInstance().reset();
+ ChooserActivityOverrideData.getInstance().createPackageManager = mPackageManagerOverride;
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
+ Boolean.toString(true),
+ true /* makeDefault*/);
+ }
+
+ @Test
+ public void customTitle() throws InterruptedException {
+ 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);
+ final IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(
+ Intent.createChooser(viewIntent, "chooser test"));
+
+ waitForIdle();
+ assertThat(activity.getAdapter().getCount(), is(2));
+ assertThat(activity.getAdapter().getServiceTargetCount(), is(0));
+ onView(withIdFromRuntimeResource("title")).check(matches(withText("chooser test")));
+ }
+
+ @Test
+ public void customTitleIgnoredForSendIntents() throws InterruptedException {
+ 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);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "chooser test"));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("title"))
+ .check(matches(withTextFromRuntimeResource("whichSendApplication")));
+ }
+
+ @Test
+ public void emptyTitle() throws InterruptedException {
+ 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);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("title"))
+ .check(matches(withTextFromRuntimeResource("whichSendApplication")));
+ }
+
+ @Test
+ public void emptyPreviewTitleAndThumbnail() throws InterruptedException {
+ 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);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("content_preview_title"))
+ .check(matches(not(isDisplayed())));
+ onView(withIdFromRuntimeResource("content_preview_thumbnail"))
+ .check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ public void visiblePreviewTitleWithoutThumbnail() throws InterruptedException {
+ String previewTitle = "My Content Preview Title";
+ 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);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("content_preview_title"))
+ .check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("content_preview_title"))
+ .check(matches(withText(previewTitle)));
+ onView(withIdFromRuntimeResource("content_preview_thumbnail"))
+ .check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ public void visiblePreviewTitleWithInvalidThumbnail() throws InterruptedException {
+ String previewTitle = "My Content Preview Title";
+ Intent sendIntent = createSendTextIntentWithPreview(previewTitle,
+ 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);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("content_preview_title")).check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("content_preview_thumbnail"))
+ .check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ public void visiblePreviewTitleAndThumbnail() throws InterruptedException {
+ String previewTitle = "My Content Preview Title";
+ Intent sendIntent = createSendTextIntentWithPreview(previewTitle,
+ Uri.parse("android.resource://com.android.frameworks.coretests/"
+ + com.android.frameworks.coretests.R.drawable.test320x240));
+ 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);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("content_preview_title")).check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("content_preview_thumbnail"))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test @Ignore
+ public void twoOptionsAndUserSelectsOne() throws InterruptedException {
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(resolvedComponentInfos);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ assertThat(activity.getAdapter().getCount(), is(2));
+ onView(withIdFromRuntimeResource("profile_button")).check(doesNotExist());
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0);
+ onView(withText(toChoose.activityInfo.name))
+ .perform(click());
+ waitForIdle();
+ assertThat(chosen[0], is(toChoose));
+ }
+
+ @Test @Ignore
+ public void fourOptionsStackedIntoOneTarget() throws InterruptedException {
+ Intent sendIntent = createSendTextIntent();
+
+ // create just enough targets to ensure the a-z list should be shown
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(1);
+
+ // next create 4 targets in a single app that should be stacked into a single target
+ String packageName = "xxx.yyy";
+ String appName = "aaa";
+ ComponentName cn = new ComponentName(packageName, appName);
+ Intent intent = new Intent("fakeIntent");
+ List<ResolvedComponentInfo> infosToStack = new ArrayList<>();
+ for (int i = 0; i < 4; i++) {
+ ResolveInfo resolveInfo = ResolverDataProvider.createResolveInfo(i,
+ UserHandle.USER_CURRENT);
+ resolveInfo.activityInfo.applicationInfo.name = appName;
+ resolveInfo.activityInfo.applicationInfo.packageName = packageName;
+ resolveInfo.activityInfo.packageName = packageName;
+ resolveInfo.activityInfo.name = "ccc" + i;
+ infosToStack.add(new ResolvedComponentInfo(cn, intent, resolveInfo));
+ }
+ resolvedComponentInfos.addAll(infosToStack);
+
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(resolvedComponentInfos);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // expect 1 unique targets + 1 group + 4 ranked app targets
+ assertThat(activity.getAdapter().getCount(), is(6));
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ onView(allOf(withText(appName), hasSibling(withText("")))).perform(click());
+ waitForIdle();
+
+ // clicking will launch a dialog to choose the activity within the app
+ onView(withText(appName)).check(matches(isDisplayed()));
+ int i = 0;
+ for (ResolvedComponentInfo rci: infosToStack) {
+ onView(withText("ccc" + i)).check(matches(isDisplayed()));
+ ++i;
+ }
+ }
+
+ @Test @Ignore
+ public void updateChooserCountsAndModelAfterUserSelection() throws InterruptedException {
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(resolvedComponentInfos);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ UsageStatsManager usm = activity.getUsageStatsManager();
+ verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1))
+ .topK(any(List.class), anyInt());
+ assertThat(activity.getIsSelected(), is(false));
+ ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+ return true;
+ };
+ ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0);
+ onView(withText(toChoose.activityInfo.name))
+ .perform(click());
+ waitForIdle();
+ verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1))
+ .updateChooserCounts(Mockito.anyString(), anyInt(), Mockito.anyString());
+ verify(ChooserActivityOverrideData.getInstance().resolverListController, times(1))
+ .updateModel(toChoose.activityInfo.getComponentName());
+ assertThat(activity.getIsSelected(), is(true));
+ }
+
+ @Ignore // b/148158199
+ @Test
+ public void noResultsFromPackageManager() {
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(null);
+ Intent sendIntent = createSendTextIntent();
+ final ChooserActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ final IChooserWrapper wrapper = (IChooserWrapper) activity;
+
+ waitForIdle();
+ assertThat(activity.isFinishing(), is(false));
+
+ onView(withIdFromRuntimeResource("empty")).check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("profile_pager")).check(matches(not(isDisplayed())));
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> wrapper.getAdapter().handlePackagesChanged()
+ );
+ // backward compatibility. looks like we finish when data is empty after package change
+ assertThat(activity.isFinishing(), is(true));
+ }
+
+ @Test
+ public void autoLaunchSingleResult() throws InterruptedException {
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(1);
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(resolvedComponentInfos);
+
+ Intent sendIntent = createSendTextIntent();
+ final ChooserActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ assertThat(chosen[0], is(resolvedComponentInfos.get(0).getResolveInfoAt(0)));
+ assertThat(activity.isFinishing(), is(true));
+ }
+
+ @Test @Ignore
+ public void hasOtherProfileOneOption() throws Exception {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ markWorkProfileUserAvailable();
+
+ ResolveInfo toChoose = personalResolvedComponentInfos.get(1).getResolveInfoAt(0);
+ Intent sendIntent = createSendTextIntent();
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // The other entry is filtered to the other profile slot
+ assertThat(activity.getAdapter().getCount(), is(1));
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ // Make a stable copy of the components as the original list may be modified
+ List<ResolvedComponentInfo> stableCopy =
+ createResolvedComponentsForTestWithOtherProfile(2, /* userId= */ 10);
+ waitForIdle();
+ Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
+
+ onView(first(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name)))
+ .perform(click());
+ waitForIdle();
+ assertThat(chosen[0], is(toChoose));
+ }
+
+ @Test @Ignore
+ public void hasOtherProfileTwoOptionsAndUserSelectsOne() throws Exception {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3);
+ ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0);
+
+ when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+ when(ChooserActivityOverrideData.getInstance().resolverListController.getLastChosen())
+ .thenReturn(resolvedComponentInfos.get(0).getResolveInfoAt(0));
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // The other entry is filtered to the other profile slot
+ assertThat(activity.getAdapter().getCount(), is(2));
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ // Make a stable copy of the components as the original list may be modified
+ List<ResolvedComponentInfo> stableCopy =
+ createResolvedComponentsForTestWithOtherProfile(3);
+ onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))
+ .perform(click());
+ waitForIdle();
+ assertThat(chosen[0], is(toChoose));
+ }
+
+ @Test @Ignore
+ public void hasLastChosenActivityAndOtherProfile() throws Exception {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3);
+ ResolveInfo toChoose = resolvedComponentInfos.get(1).getResolveInfoAt(0);
+
+ when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // The other entry is filtered to the last used slot
+ assertThat(activity.getAdapter().getCount(), is(2));
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ // Make a stable copy of the components as the original list may be modified
+ List<ResolvedComponentInfo> stableCopy =
+ createResolvedComponentsForTestWithOtherProfile(3);
+ onView(withText(stableCopy.get(1).getResolveInfoAt(0).activityInfo.name))
+ .perform(click());
+ waitForIdle();
+ assertThat(chosen[0], is(toChoose));
+ }
+
+ @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);
+
+ final ChooserActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withIdFromRuntimeResource("chooser_copy_button")).check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("chooser_copy_button")).perform(click());
+ ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(
+ Context.CLIPBOARD_SERVICE);
+ ClipData clipData = clipboard.getPrimaryClip();
+ assertThat("testing intent sending", is(clipData.getItemAt(0).getText()));
+
+ ClipDescription clipDescription = clipData.getDescription();
+ assertThat("text/plain", is(clipDescription.getMimeType(0)));
+
+ assertEquals(mActivityRule.getActivityResult().getResultCode(), RESULT_OK);
+ }
+
+ @Test
+ public void copyTextToClipboardLogging() 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);
+
+ MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
+ ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withIdFromRuntimeResource("chooser_copy_button")).check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("chooser_copy_button")).perform(click());
+
+ verify(mockLogger, atLeastOnce()).write(logMakerCaptor.capture());
+
+ // The last captured event is the selection of the target.
+ assertThat(logMakerCaptor.getValue().getCategory(),
+ is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SYSTEM_TARGET));
+ assertThat(logMakerCaptor.getValue().getSubtype(), is(1));
+ }
+
+
+ @Test
+ @Ignore
+ public void testNearbyShareLogging() 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);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withIdFromRuntimeResource("chooser_nearby_button")).check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("chooser_nearby_button")).perform(click());
+
+ ChooserActivityLoggerFake logger =
+ (ChooserActivityLoggerFake) activity.getChooserActivityLogger();
+
+ // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
+ logger.removeCallsForUiEventsOfType(
+ ChooserActivityLogger.SharesheetStandardEvent
+ .SHARESHEET_DIRECT_LOAD_COMPLETE.getId());
+
+ // SHARESHEET_TRIGGERED:
+ assertThat(logger.event(0).getId(),
+ is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId()));
+
+ // SHARESHEET_STARTED:
+ assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED));
+ assertThat(logger.get(1).intent, is(Intent.ACTION_SEND));
+ assertThat(logger.get(1).mimeType, is("text/plain"));
+ assertThat(logger.get(1).packageName, is(
+ InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()));
+ assertThat(logger.get(1).appProvidedApp, is(0));
+ assertThat(logger.get(1).appProvidedDirect, is(0));
+ assertThat(logger.get(1).isWorkprofile, is(false));
+ assertThat(logger.get(1).previewType, is(3));
+
+ // SHARESHEET_APP_LOAD_COMPLETE:
+ assertThat(logger.event(2).getId(),
+ is(ChooserActivityLogger
+ .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId()));
+
+ // Next are just artifacts of test set-up:
+ assertThat(logger.event(3).getId(),
+ is(ChooserActivityLogger
+ .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId()));
+ assertThat(logger.event(4).getId(),
+ is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_EXPANDED.getId()));
+
+ // SHARESHEET_NEARBY_TARGET_SELECTED:
+ assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED));
+ assertThat(logger.get(5).targetType,
+ is(ChooserActivityLogger
+ .SharesheetTargetSelectedEvent.SHARESHEET_NEARBY_TARGET_SELECTED.getId()));
+
+ // No more events.
+ assertThat(logger.numCalls(), is(6));
+ }
+
+
+
+ @Test @Ignore
+ public void testEditImageLogs() throws Exception {
+ Intent sendIntent = createSendImageIntent(
+ Uri.parse("android.resource://com.android.frameworks.coretests/"
+ + com.android.frameworks.coretests.R.drawable.test320x240));
+
+ ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap();
+ ChooserActivityOverrideData.getInstance().isImageType = true;
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withIdFromRuntimeResource("chooser_edit_button")).check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("chooser_edit_button")).perform(click());
+
+ ChooserActivityLoggerFake logger =
+ (ChooserActivityLoggerFake) activity.getChooserActivityLogger();
+
+ // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
+ logger.removeCallsForUiEventsOfType(
+ ChooserActivityLogger.SharesheetStandardEvent
+ .SHARESHEET_DIRECT_LOAD_COMPLETE.getId());
+
+ // SHARESHEET_TRIGGERED:
+ assertThat(logger.event(0).getId(),
+ is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId()));
+
+ // SHARESHEET_STARTED:
+ assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED));
+ assertThat(logger.get(1).intent, is(Intent.ACTION_SEND));
+ assertThat(logger.get(1).mimeType, is("image/png"));
+ assertThat(logger.get(1).packageName, is(
+ InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()));
+ assertThat(logger.get(1).appProvidedApp, is(0));
+ assertThat(logger.get(1).appProvidedDirect, is(0));
+ assertThat(logger.get(1).isWorkprofile, is(false));
+ assertThat(logger.get(1).previewType, is(1));
+
+ // SHARESHEET_APP_LOAD_COMPLETE:
+ assertThat(logger.event(2).getId(),
+ is(ChooserActivityLogger
+ .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId()));
+
+ // Next are just artifacts of test set-up:
+ assertThat(logger.event(3).getId(),
+ is(ChooserActivityLogger
+ .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId()));
+ assertThat(logger.event(4).getId(),
+ is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_EXPANDED.getId()));
+
+ // SHARESHEET_EDIT_TARGET_SELECTED:
+ assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED));
+ assertThat(logger.get(5).targetType,
+ is(ChooserActivityLogger
+ .SharesheetTargetSelectedEvent.SHARESHEET_EDIT_TARGET_SELECTED.getId()));
+
+ // No more events.
+ assertThat(logger.numCalls(), is(6));
+ }
+
+
+ @Test
+ public void oneVisibleImagePreview() throws InterruptedException {
+ Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/"
+ + com.android.frameworks.coretests.R.drawable.test320x240);
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap();
+ ChooserActivityOverrideData.getInstance().isImageType = true;
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("content_preview_image_1_large"))
+ .check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("content_preview_image_2_large"))
+ .check(matches(not(isDisplayed())));
+ onView(withIdFromRuntimeResource("content_preview_image_2_small"))
+ .check(matches(not(isDisplayed())));
+ onView(withIdFromRuntimeResource("content_preview_image_3_small"))
+ .check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ public void twoVisibleImagePreview() throws InterruptedException {
+ Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/"
+ + com.android.frameworks.coretests.R.drawable.test320x240);
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap();
+ ChooserActivityOverrideData.getInstance().isImageType = true;
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("content_preview_image_1_large"))
+ .check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("content_preview_image_2_large"))
+ .check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("content_preview_image_2_small"))
+ .check(matches(not(isDisplayed())));
+ onView(withIdFromRuntimeResource("content_preview_image_3_small"))
+ .check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ public void threeOrMoreVisibleImagePreview() throws InterruptedException {
+ Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/"
+ + com.android.frameworks.coretests.R.drawable.test320x240);
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ 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);
+
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(resolvedComponentInfos);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("content_preview_image_1_large"))
+ .check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("content_preview_image_2_large"))
+ .check(matches(not(isDisplayed())));
+ onView(withIdFromRuntimeResource("content_preview_image_2_small"))
+ .check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("content_preview_image_3_small"))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testOnCreateLogging() {
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
+ ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
+ waitForIdle();
+ verify(mockLogger, atLeastOnce()).write(logMakerCaptor.capture());
+ assertThat(logMakerCaptor.getAllValues().get(0).getCategory(),
+ is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN));
+ assertThat(logMakerCaptor
+ .getAllValues().get(0)
+ .getTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS),
+ is(notNullValue()));
+ assertThat(logMakerCaptor
+ .getAllValues().get(0)
+ .getTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE),
+ is(TEST_MIME_TYPE));
+ assertThat(logMakerCaptor
+ .getAllValues().get(0)
+ .getSubtype(),
+ is(MetricsEvent.PARENT_PROFILE));
+ }
+
+ @Test
+ public void testOnCreateLoggingFromWorkProfile() {
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+ ChooserActivityOverrideData.getInstance().alternateProfileSetting =
+ MetricsEvent.MANAGED_PROFILE;
+ MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
+ ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
+ waitForIdle();
+ verify(mockLogger, atLeastOnce()).write(logMakerCaptor.capture());
+ assertThat(logMakerCaptor.getAllValues().get(0).getCategory(),
+ is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN));
+ assertThat(logMakerCaptor
+ .getAllValues().get(0)
+ .getTaggedData(MetricsEvent.FIELD_TIME_TO_APP_TARGETS),
+ is(notNullValue()));
+ assertThat(logMakerCaptor
+ .getAllValues().get(0)
+ .getTaggedData(MetricsEvent.FIELD_SHARESHEET_MIMETYPE),
+ is(TEST_MIME_TYPE));
+ assertThat(logMakerCaptor
+ .getAllValues().get(0)
+ .getSubtype(),
+ is(MetricsEvent.MANAGED_PROFILE));
+ }
+
+ @Test
+ public void testEmptyPreviewLogging() {
+ Intent sendIntent = createSendTextIntentWithPreview(null, null);
+
+ MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
+ ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "empty preview logger test"));
+ waitForIdle();
+
+ verify(mockLogger, Mockito.times(1)).write(logMakerCaptor.capture());
+ // First invocation is from onCreate
+ assertThat(logMakerCaptor.getAllValues().get(0).getCategory(),
+ is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_SHOWN));
+ }
+
+ @Test
+ public void testTitlePreviewLogging() {
+ Intent sendIntent = createSendTextIntentWithPreview("TestTitle", null);
+
+ MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
+ ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ when(ChooserActivityOverrideData.getInstance().resolverListController.getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class))).thenReturn(resolvedComponentInfos);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ // Second invocation is from onCreate
+ verify(mockLogger, Mockito.times(2)).write(logMakerCaptor.capture());
+ assertThat(logMakerCaptor.getAllValues().get(0).getSubtype(),
+ is(CONTENT_PREVIEW_TEXT));
+ assertThat(logMakerCaptor.getAllValues().get(0).getCategory(),
+ is(MetricsEvent.ACTION_SHARE_WITH_PREVIEW));
+ }
+
+ @Test
+ public void testImagePreviewLogging() {
+ Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/"
+ + com.android.frameworks.coretests.R.drawable.test320x240);
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ ChooserActivityOverrideData.getInstance().previewThumbnail = createBitmap();
+ ChooserActivityOverrideData.getInstance().isImageType = true;
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(resolvedComponentInfos);
+
+ MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
+ ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ verify(mockLogger, Mockito.times(2)).write(logMakerCaptor.capture());
+ // First invocation is from onCreate
+ assertThat(logMakerCaptor.getAllValues().get(0).getSubtype(),
+ is(CONTENT_PREVIEW_IMAGE));
+ assertThat(logMakerCaptor.getAllValues().get(0).getCategory(),
+ is(MetricsEvent.ACTION_SHARE_WITH_PREVIEW));
+ }
+
+ @Test
+ public void oneVisibleFilePreview() throws InterruptedException {
+ Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf");
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+
+ 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);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("content_preview_filename")).check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("content_preview_filename"))
+ .check(matches(withText("app.pdf")));
+ onView(withIdFromRuntimeResource("content_preview_file_icon"))
+ .check(matches(isDisplayed()));
+ }
+
+
+ @Test
+ public void moreThanOneVisibleFilePreview() throws InterruptedException {
+ Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf");
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+ uris.add(uri);
+ uris.add(uri);
+
+ 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);
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("content_preview_filename"))
+ .check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("content_preview_filename"))
+ .check(matches(withText("app.pdf + 2 files")));
+ onView(withIdFromRuntimeResource("content_preview_file_icon"))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void contentProviderThrowSecurityException() throws InterruptedException {
+ Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf");
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+
+ 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);
+
+ ChooserActivityOverrideData.getInstance().resolverForceException = true;
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("content_preview_filename")).check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("content_preview_filename"))
+ .check(matches(withText("app.pdf")));
+ onView(withIdFromRuntimeResource("content_preview_file_icon"))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void contentProviderReturnsNoColumns() throws InterruptedException {
+ Uri uri = Uri.parse("content://com.android.frameworks.coretests/app.pdf");
+
+ ArrayList<Uri> uris = new ArrayList<>();
+ uris.add(uri);
+ uris.add(uri);
+
+ 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);
+
+ Cursor cursor = mock(Cursor.class);
+ when(cursor.getCount()).thenReturn(1);
+ Mockito.doNothing().when(cursor).close();
+ when(cursor.moveToFirst()).thenReturn(true);
+ when(cursor.getColumnIndex(Mockito.anyString())).thenReturn(-1);
+
+ ChooserActivityOverrideData.getInstance().resolverCursor = cursor;
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("content_preview_filename")).check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("content_preview_filename"))
+ .check(matches(withText("app.pdf + 1 file")));
+ onView(withIdFromRuntimeResource("content_preview_file_icon"))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testGetBaseScore() {
+ final float testBaseScore = 0.89f;
+
+ 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);
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getScore(Mockito.isA(DisplayResolveInfo.class)))
+ .thenReturn(testBaseScore);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ final DisplayResolveInfo testDri =
+ activity.createTestDisplayResolveInfo(sendIntent,
+ ResolverDataProvider.createResolveInfo(3, 0), "testLabel", "testInfo", sendIntent,
+ /* resolveInfoPresentationGetter */ null);
+ final ChooserListAdapter adapter = activity.getAdapter();
+
+ assertThat(adapter.getBaseScore(null, 0), is(CALLER_TARGET_SCORE_BOOST));
+ assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_DEFAULT), is(testBaseScore));
+ assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_CHOOSER_TARGET), is(testBaseScore));
+ assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE),
+ is(testBaseScore * SHORTCUT_TARGET_SCORE_BOOST));
+ assertThat(adapter.getBaseScore(testDri, TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER),
+ is(testBaseScore * SHORTCUT_TARGET_SCORE_BOOST));
+ }
+
+ @Test
+ public void testConvertToChooserTarget_predictionService() {
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(resolvedComponentInfos);
+
+ final ChooserActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ List<ShareShortcutInfo> shortcuts = createShortcuts(activity);
+
+ int[] expectedOrderAllShortcuts = {0, 1, 2, 3};
+ float[] expectedScoreAllShortcuts = {1.0f, 0.99f, 0.98f, 0.97f};
+
+ List<ChooserTarget> chooserTargets = activity.convertToChooserTarget(shortcuts, shortcuts,
+ null, TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
+ assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets,
+ expectedOrderAllShortcuts, expectedScoreAllShortcuts);
+
+ List<ShareShortcutInfo> subset = new ArrayList<>();
+ subset.add(shortcuts.get(1));
+ subset.add(shortcuts.get(2));
+ subset.add(shortcuts.get(3));
+
+ int[] expectedOrderSubset = {1, 2, 3};
+ float[] expectedScoreSubset = {0.99f, 0.98f, 0.97f};
+
+ chooserTargets = activity.convertToChooserTarget(subset, shortcuts, null,
+ TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE);
+ assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets,
+ expectedOrderSubset, expectedScoreSubset);
+ }
+
+ @Test
+ public void testConvertToChooserTarget_shortcutManager() {
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(resolvedComponentInfos);
+
+ final ChooserActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ List<ShareShortcutInfo> shortcuts = createShortcuts(activity);
+
+ int[] expectedOrderAllShortcuts = {2, 0, 3, 1};
+ float[] expectedScoreAllShortcuts = {1.0f, 0.99f, 0.99f, 0.98f};
+
+ List<ChooserTarget> chooserTargets = activity.convertToChooserTarget(shortcuts, shortcuts,
+ null, TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER);
+ assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets,
+ expectedOrderAllShortcuts, expectedScoreAllShortcuts);
+
+ List<ShareShortcutInfo> subset = new ArrayList<>();
+ subset.add(shortcuts.get(1));
+ subset.add(shortcuts.get(2));
+ subset.add(shortcuts.get(3));
+
+ int[] expectedOrderSubset = {2, 3, 1};
+ float[] expectedScoreSubset = {1.0f, 0.99f, 0.98f};
+
+ chooserTargets = activity.convertToChooserTarget(subset, shortcuts, null,
+ TARGET_TYPE_SHORTCUTS_FROM_SHORTCUT_MANAGER);
+ assertCorrectShortcutToChooserTargetConversion(shortcuts, chooserTargets,
+ expectedOrderSubset, expectedScoreSubset);
+ }
+
+ // This test is too long and too slow and should not be taken as an example for future tests.
+ @Test @Ignore
+ public void testDirectTargetSelectionLogging() throws InterruptedException {
+ 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);
+
+ // Set up resources
+ MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
+ ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
+ // Create direct share target
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(1, "");
+ ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
+
+ // Start activity
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+
+ // Insert the direct share target
+ Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
+ directShareToShortcutInfos.put(serviceTargets.get(0), null);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> activity.getAdapter().addServiceResults(
+ activity.createTestDisplayResolveInfo(sendIntent,
+ ri,
+ "testLabel",
+ "testInfo",
+ sendIntent,
+ /* resolveInfoPresentationGetter */ null),
+ serviceTargets,
+ TARGET_TYPE_CHOOSER_TARGET,
+ directShareToShortcutInfos)
+ );
+
+ // Thread.sleep shouldn't be a thing in an integration test but it's
+ // necessary here because of the way the code is structured
+ // TODO: restructure the tests b/129870719
+ Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
+
+ assertThat("Chooser should have 3 targets (2 apps, 1 direct)",
+ activity.getAdapter().getCount(), is(3));
+ assertThat("Chooser should have exactly one selectable direct target",
+ activity.getAdapter().getSelectableServiceTargetCount(), is(1));
+ assertThat("The resolver info must match the resolver info used to create the target",
+ activity.getAdapter().getItem(0).getResolveInfo(), is(ri));
+
+ // Click on the direct target
+ String name = serviceTargets.get(0).getTitle().toString();
+ onView(withText(name))
+ .perform(click());
+ waitForIdle();
+
+ // Currently we're seeing 3 invocations
+ // 1. ChooserActivity.onCreate()
+ // 2. ChooserActivity$ChooserRowAdapter.createContentPreviewView()
+ // 3. ChooserActivity.startSelected -- which is the one we're after
+ verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture());
+ assertThat(logMakerCaptor.getAllValues().get(2).getCategory(),
+ is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET));
+ String hashedName = (String) logMakerCaptor
+ .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_HASHED_TARGET_NAME);
+ assertThat("Hash is not predictable but must be obfuscated",
+ hashedName, is(not(name)));
+ assertThat("The packages shouldn't match for app target and direct target", logMakerCaptor
+ .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(-1));
+ }
+
+ // This test is too long and too slow and should not be taken as an example for future tests.
+ @Test @Ignore
+ public void testDirectTargetLoggingWithRankedAppTarget() throws InterruptedException {
+ 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);
+
+ // Set up resources
+ MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
+ ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
+ // Create direct share target
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(1,
+ resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+ ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
+
+ // Start activity
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+
+ // Insert the direct share target
+ Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
+ directShareToShortcutInfos.put(serviceTargets.get(0), null);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> activity.getAdapter().addServiceResults(
+ activity.createTestDisplayResolveInfo(sendIntent,
+ ri,
+ "testLabel",
+ "testInfo",
+ sendIntent,
+ /* resolveInfoPresentationGetter */ null),
+ serviceTargets,
+ TARGET_TYPE_CHOOSER_TARGET,
+ directShareToShortcutInfos)
+ );
+ // Thread.sleep shouldn't be a thing in an integration test but it's
+ // necessary here because of the way the code is structured
+ // TODO: restructure the tests b/129870719
+ Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
+
+ assertThat("Chooser should have 3 targets (2 apps, 1 direct)",
+ activity.getAdapter().getCount(), is(3));
+ assertThat("Chooser should have exactly one selectable direct target",
+ activity.getAdapter().getSelectableServiceTargetCount(), is(1));
+ assertThat("The resolver info must match the resolver info used to create the target",
+ activity.getAdapter().getItem(0).getResolveInfo(), is(ri));
+
+ // Click on the direct target
+ String name = serviceTargets.get(0).getTitle().toString();
+ onView(withText(name))
+ .perform(click());
+ waitForIdle();
+
+ // Currently we're seeing 3 invocations
+ // 1. ChooserActivity.onCreate()
+ // 2. ChooserActivity$ChooserRowAdapter.createContentPreviewView()
+ // 3. ChooserActivity.startSelected -- which is the one we're after
+ verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture());
+ assertThat(logMakerCaptor.getAllValues().get(2).getCategory(),
+ is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET));
+ assertThat("The packages should match for app target and direct target", logMakerCaptor
+ .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(0));
+ }
+
+ @Test @Ignore
+ public void testShortcutTargetWithApplyAppLimits() throws InterruptedException {
+ // Set up resources
+ ChooserActivityOverrideData.getInstance().resources = Mockito.spy(
+ InstrumentationRegistry.getInstrumentation().getContext().getResources());
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resources
+ .getInteger(
+ getRuntimeResourceId("config_maxShortcutTargetsPerApp", "integer")))
+ .thenReturn(1);
+ 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);
+ // Create direct share target
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(2,
+ resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+ ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
+
+ // Start activity
+ final ChooserActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ final IChooserWrapper wrapper = (IChooserWrapper) activity;
+
+ // Insert the direct share target
+ Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
+ List<ShareShortcutInfo> shortcutInfos = createShortcuts(activity);
+ directShareToShortcutInfos.put(serviceTargets.get(0),
+ shortcutInfos.get(0).getShortcutInfo());
+ directShareToShortcutInfos.put(serviceTargets.get(1),
+ shortcutInfos.get(1).getShortcutInfo());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> wrapper.getAdapter().addServiceResults(
+ wrapper.createTestDisplayResolveInfo(sendIntent,
+ ri,
+ "testLabel",
+ "testInfo",
+ sendIntent,
+ /* resolveInfoPresentationGetter */ null),
+ serviceTargets,
+ TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE,
+ directShareToShortcutInfos)
+ );
+ // Thread.sleep shouldn't be a thing in an integration test but it's
+ // necessary here because of the way the code is structured
+ // TODO: restructure the tests b/129870719
+ Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
+
+ assertThat("Chooser should have 3 targets (2 apps, 1 direct)",
+ wrapper.getAdapter().getCount(), is(3));
+ assertThat("Chooser should have exactly one selectable direct target",
+ wrapper.getAdapter().getSelectableServiceTargetCount(), is(1));
+ assertThat("The resolver info must match the resolver info used to create the target",
+ wrapper.getAdapter().getItem(0).getResolveInfo(), is(ri));
+ assertThat("The display label must match",
+ wrapper.getAdapter().getItem(0).getDisplayLabel(), is("testTitle0"));
+ }
+
+ @Test @Ignore
+ public void testShortcutTargetWithoutApplyAppLimits() throws InterruptedException {
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_SYSTEMUI,
+ SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
+ Boolean.toString(false),
+ true /* makeDefault*/);
+ // Set up resources
+ ChooserActivityOverrideData.getInstance().resources = Mockito.spy(
+ InstrumentationRegistry.getInstrumentation().getContext().getResources());
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resources
+ .getInteger(
+ getRuntimeResourceId("config_maxShortcutTargetsPerApp", "integer")))
+ .thenReturn(1);
+ 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);
+ // Create direct share target
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(2,
+ resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+ ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
+
+ // Start activity
+ final ChooserActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ final IChooserWrapper wrapper = (IChooserWrapper) activity;
+
+ // Insert the direct share target
+ Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
+ List<ShareShortcutInfo> shortcutInfos = createShortcuts(activity);
+ directShareToShortcutInfos.put(serviceTargets.get(0),
+ shortcutInfos.get(0).getShortcutInfo());
+ directShareToShortcutInfos.put(serviceTargets.get(1),
+ shortcutInfos.get(1).getShortcutInfo());
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> wrapper.getAdapter().addServiceResults(
+ wrapper.createTestDisplayResolveInfo(sendIntent,
+ ri,
+ "testLabel",
+ "testInfo",
+ sendIntent,
+ /* resolveInfoPresentationGetter */ null),
+ serviceTargets,
+ TARGET_TYPE_SHORTCUTS_FROM_PREDICTION_SERVICE,
+ directShareToShortcutInfos)
+ );
+ // Thread.sleep shouldn't be a thing in an integration test but it's
+ // necessary here because of the way the code is structured
+ // TODO: restructure the tests b/129870719
+ Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
+
+ assertThat("Chooser should have 4 targets (2 apps, 2 direct)",
+ wrapper.getAdapter().getCount(), is(4));
+ assertThat("Chooser should have exactly two selectable direct target",
+ wrapper.getAdapter().getSelectableServiceTargetCount(), is(2));
+ assertThat("The resolver info must match the resolver info used to create the target",
+ wrapper.getAdapter().getItem(0).getResolveInfo(), is(ri));
+ assertThat("The display label must match",
+ wrapper.getAdapter().getItem(0).getDisplayLabel(), is("testTitle0"));
+ assertThat("The display label must match",
+ wrapper.getAdapter().getItem(1).getDisplayLabel(), is("testTitle1"));
+ }
+
+ @Test
+ public void testUpdateMaxTargetsPerRow_columnCountIsUpdated() throws InterruptedException {
+ updateMaxTargetsPerRowResource(/* targetsPerRow= */ 4);
+ givenAppTargets(/* appCount= */ 16);
+ Intent sendIntent = createSendTextIntent();
+ final ChooserActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+
+ updateMaxTargetsPerRowResource(/* targetsPerRow= */ 6);
+ InstrumentationRegistry.getInstrumentation()
+ .runOnMainSync(() -> activity.onConfigurationChanged(
+ InstrumentationRegistry.getInstrumentation()
+ .getContext().getResources().getConfiguration()));
+
+ waitForIdle();
+ onView(withIdFromRuntimeResource("resolver_list"))
+ .check(matches(withGridColumnCount(6)));
+ }
+
+ // This test is too long and too slow and should not be taken as an example for future tests.
+ @Test @Ignore
+ public void testDirectTargetLoggingWithAppTargetNotRankedPortrait()
+ throws InterruptedException {
+ testDirectTargetLoggingWithAppTargetNotRanked(Configuration.ORIENTATION_PORTRAIT, 4);
+ }
+
+ @Test @Ignore
+ public void testDirectTargetLoggingWithAppTargetNotRankedLandscape()
+ throws InterruptedException {
+ testDirectTargetLoggingWithAppTargetNotRanked(Configuration.ORIENTATION_LANDSCAPE, 8);
+ }
+
+ private void testDirectTargetLoggingWithAppTargetNotRanked(
+ int orientation, int appTargetsExpected
+ ) throws InterruptedException {
+ Configuration configuration =
+ new Configuration(InstrumentationRegistry.getInstrumentation().getContext()
+ .getResources().getConfiguration());
+ configuration.orientation = orientation;
+
+ ChooserActivityOverrideData.getInstance().resources = Mockito.spy(
+ InstrumentationRegistry.getInstrumentation().getContext().getResources());
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resources
+ .getConfiguration())
+ .thenReturn(configuration);
+
+ 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);
+
+ // Set up resources
+ MetricsLogger mockLogger = ChooserActivityOverrideData.getInstance().metricsLogger;
+ ArgumentCaptor<LogMaker> logMakerCaptor = ArgumentCaptor.forClass(LogMaker.class);
+ // Create direct share target
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(1,
+ resolvedComponentInfos.get(14).getResolveInfoAt(0).activityInfo.packageName);
+ ResolveInfo ri = ResolverDataProvider.createResolveInfo(16, 0);
+
+ // Start activity
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ final IChooserWrapper wrapper = (IChooserWrapper) activity;
+ // Insert the direct share target
+ Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
+ directShareToShortcutInfos.put(serviceTargets.get(0), null);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> wrapper.getAdapter().addServiceResults(
+ wrapper.createTestDisplayResolveInfo(sendIntent,
+ ri,
+ "testLabel",
+ "testInfo",
+ sendIntent,
+ /* resolveInfoPresentationGetter */ null),
+ serviceTargets,
+ TARGET_TYPE_CHOOSER_TARGET,
+ directShareToShortcutInfos)
+ );
+ // Thread.sleep shouldn't be a thing in an integration test but it's
+ // necessary here because of the way the code is structured
+ // TODO: restructure the tests b/129870719
+ Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
+
+ assertThat(
+ String.format("Chooser should have %d targets (%d apps, 1 direct, 15 A-Z)",
+ appTargetsExpected + 16, appTargetsExpected),
+ wrapper.getAdapter().getCount(), is(appTargetsExpected + 16));
+ assertThat("Chooser should have exactly one selectable direct target",
+ wrapper.getAdapter().getSelectableServiceTargetCount(), is(1));
+ assertThat("The resolver info must match the resolver info used to create the target",
+ wrapper.getAdapter().getItem(0).getResolveInfo(), is(ri));
+
+ // Click on the direct target
+ String name = serviceTargets.get(0).getTitle().toString();
+ onView(withText(name))
+ .perform(click());
+ waitForIdle();
+
+ // Currently we're seeing 3 invocations
+ // 1. ChooserActivity.onCreate()
+ // 2. ChooserActivity$ChooserRowAdapter.createContentPreviewView()
+ // 3. ChooserActivity.startSelected -- which is the one we're after
+ verify(mockLogger, Mockito.times(3)).write(logMakerCaptor.capture());
+ assertThat(logMakerCaptor.getAllValues().get(2).getCategory(),
+ is(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET));
+ assertThat("The packages shouldn't match for app target and direct target", logMakerCaptor
+ .getAllValues().get(2).getTaggedData(MetricsEvent.FIELD_RANKED_POSITION), is(-1));
+ }
+
+ @Test
+ public void testWorkTab_displayedWhenWorkProfileUserAvailable() {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+ markWorkProfileUserAvailable();
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+
+ onView(withIdFromRuntimeResource("tabs")).check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testWorkTab_hiddenWhenWorkProfileUserNotAvailable() {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+
+ onView(withIdFromRuntimeResource("tabs")).check(matches(not(isDisplayed())));
+ }
+
+ @Test
+ public void testWorkTab_eachTabUsesExpectedAdapter() {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ int personalProfileTargets = 3;
+ int otherProfileTargets = 1;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(
+ personalProfileTargets + otherProfileTargets, /* userID */ 10);
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(
+ workProfileTargets);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+ markWorkProfileUserAvailable();
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+
+ assertThat(activity.getCurrentUserHandle().getIdentifier(), is(0));
+ onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+ assertThat(activity.getCurrentUserHandle().getIdentifier(), is(10));
+ assertThat(activity.getPersonalListAdapter().getCount(), is(personalProfileTargets));
+ assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets));
+ }
+
+ @Test
+ public void testWorkTab_workProfileHasExpectedNumberOfTargets() throws InterruptedException {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+ waitForIdle();
+
+ assertThat(activity.getWorkListAdapter().getCount(), is(workProfileTargets));
+ }
+
+ @Test @Ignore
+ public void testWorkTab_selectingWorkTabAppOpensAppInWorkProfile() throws InterruptedException {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+ waitForIdle();
+ // wait for the share sheet to expand
+ Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
+
+ onView(first(allOf(
+ withText(workResolvedComponentInfos.get(0)
+ .getResolveInfoAt(0).activityInfo.applicationInfo.name),
+ isDisplayed())))
+ .perform(click());
+ waitForIdle();
+ assertThat(chosen[0], is(workResolvedComponentInfos.get(0).getResolveInfoAt(0)));
+ }
+
+ @Test
+ public void testWorkTab_crossProfileIntentsDisabled_personalToWork_emptyStateShown() {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets);
+ ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false;
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+ waitForIdle();
+ onView(withIdFromRuntimeResource("contentPanel"))
+ .perform(swipeUp());
+
+ onView(withTextFromRuntimeResource("resolver_cross_profile_blocked"))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testWorkTab_workProfileDisabled_emptyStateShown() {
+ // enable the work tab feature flag
+ markWorkProfileUserAvailable();
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets);
+ ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true;
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("contentPanel"))
+ .perform(swipeUp());
+ onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+ waitForIdle();
+
+ onView(withTextFromRuntimeResource("resolver_turn_on_work_apps"))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testWorkTab_noWorkAppsAvailable_emptyStateShown() {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(0);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("contentPanel"))
+ .perform(swipeUp());
+ onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+ waitForIdle();
+
+ onView(withTextFromRuntimeResource("resolver_no_work_apps_available"))
+ .check(matches(isDisplayed()));
+ }
+
+ @Ignore // b/220067877
+ @Test
+ public void testWorkTab_xProfileOff_noAppsAvailable_workOff_xProfileOffEmptyStateShown() {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(0);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true;
+ ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false;
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("contentPanel"))
+ .perform(swipeUp());
+ onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+ waitForIdle();
+
+ onView(withTextFromRuntimeResource("resolver_cross_profile_blocked"))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testWorkTab_noAppsAvailable_workOff_noAppsAvailableEmptyStateShown() {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(0);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true;
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("contentPanel"))
+ .perform(swipeUp());
+ onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+ waitForIdle();
+
+ onView(withTextFromRuntimeResource("resolver_no_work_apps_available"))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test @Ignore("b/222124533")
+ public void testAppTargetLogging() throws InterruptedException {
+ Intent sendIntent = createSendTextIntent();
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(resolvedComponentInfos);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ // TODO(b/222124533): other test cases use a timeout to make sure that the UI is fully
+ // populated; without one, this test flakes. Ideally we should address the need for a
+ // timeout everywhere instead of introducing one to fix this particular test.
+
+ assertThat(activity.getAdapter().getCount(), is(2));
+ onView(withIdFromRuntimeResource("profile_button")).check(doesNotExist());
+
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ ResolveInfo toChoose = resolvedComponentInfos.get(0).getResolveInfoAt(0);
+ onView(withText(toChoose.activityInfo.name))
+ .perform(click());
+ waitForIdle();
+
+ ChooserActivityLoggerFake logger =
+ (ChooserActivityLoggerFake) activity.getChooserActivityLogger();
+
+ // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
+ logger.removeCallsForUiEventsOfType(
+ ChooserActivityLogger.SharesheetStandardEvent
+ .SHARESHEET_DIRECT_LOAD_COMPLETE.getId());
+
+ // SHARESHEET_TRIGGERED:
+ assertThat(logger.event(0).getId(),
+ is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId()));
+
+ // SHARESHEET_STARTED:
+ assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED));
+ assertThat(logger.get(1).intent, is(Intent.ACTION_SEND));
+ assertThat(logger.get(1).mimeType, is("text/plain"));
+ assertThat(logger.get(1).packageName, is(
+ InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()));
+ assertThat(logger.get(1).appProvidedApp, is(0));
+ assertThat(logger.get(1).appProvidedDirect, is(0));
+ assertThat(logger.get(1).isWorkprofile, is(false));
+ assertThat(logger.get(1).previewType, is(3));
+
+ // SHARESHEET_APP_LOAD_COMPLETE:
+ assertThat(logger.event(2).getId(),
+ is(ChooserActivityLogger
+ .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId()));
+
+ // Next are just artifacts of test set-up:
+ assertThat(logger.event(3).getId(),
+ is(ChooserActivityLogger
+ .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId()));
+ assertThat(logger.event(4).getId(),
+ is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_EXPANDED.getId()));
+
+ // SHARESHEET_APP_TARGET_SELECTED:
+ assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED));
+ assertThat(logger.get(5).targetType,
+ is(ChooserActivityLogger
+ .SharesheetTargetSelectedEvent.SHARESHEET_APP_TARGET_SELECTED.getId()));
+
+ // No more events.
+ assertThat(logger.numCalls(), is(6));
+ }
+
+ @Test @Ignore
+ public void testDirectTargetLogging() throws InterruptedException {
+ 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);
+
+ // Create direct share target
+ List<ChooserTarget> serviceTargets = createDirectShareTargets(1,
+ resolvedComponentInfos.get(0).getResolveInfoAt(0).activityInfo.packageName);
+ ResolveInfo ri = ResolverDataProvider.createResolveInfo(3, 0);
+
+ // Start activity
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+
+ // Insert the direct share target
+ Map<ChooserTarget, ShortcutInfo> directShareToShortcutInfos = new HashMap<>();
+ directShareToShortcutInfos.put(serviceTargets.get(0), null);
+ InstrumentationRegistry.getInstrumentation().runOnMainSync(
+ () -> activity.getAdapter().addServiceResults(
+ activity.createTestDisplayResolveInfo(sendIntent,
+ ri,
+ "testLabel",
+ "testInfo",
+ sendIntent,
+ /* resolveInfoPresentationGetter */ null),
+ serviceTargets,
+ TARGET_TYPE_CHOOSER_TARGET,
+ directShareToShortcutInfos)
+ );
+ // Thread.sleep shouldn't be a thing in an integration test but it's
+ // necessary here because of the way the code is structured
+ // TODO: restructure the tests b/129870719
+ Thread.sleep(((ChooserActivity) activity).mListViewUpdateDelayMs);
+
+ assertThat("Chooser should have 3 targets (2 apps, 1 direct)",
+ activity.getAdapter().getCount(), is(3));
+ assertThat("Chooser should have exactly one selectable direct target",
+ activity.getAdapter().getSelectableServiceTargetCount(), is(1));
+ assertThat("The resolver info must match the resolver info used to create the target",
+ activity.getAdapter().getItem(0).getResolveInfo(), is(ri));
+
+ // Click on the direct target
+ String name = serviceTargets.get(0).getTitle().toString();
+ onView(withText(name))
+ .perform(click());
+ waitForIdle();
+
+ ChooserActivityLoggerFake logger =
+ (ChooserActivityLoggerFake) activity.getChooserActivityLogger();
+ assertThat(logger.numCalls(), is(6));
+ // first one should be SHARESHEET_TRIGGERED uievent
+ assertThat(logger.get(0).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED));
+ assertThat(logger.get(0).event.getId(),
+ is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId()));
+ // second one should be SHARESHEET_STARTED event
+ assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED));
+ assertThat(logger.get(1).intent, is(Intent.ACTION_SEND));
+ assertThat(logger.get(1).mimeType, is("text/plain"));
+ assertThat(logger.get(1).packageName, is(
+ InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()));
+ assertThat(logger.get(1).appProvidedApp, is(0));
+ assertThat(logger.get(1).appProvidedDirect, is(0));
+ assertThat(logger.get(1).isWorkprofile, is(false));
+ assertThat(logger.get(1).previewType, is(3));
+ // third one should be SHARESHEET_APP_LOAD_COMPLETE uievent
+ assertThat(logger.get(2).atomId, is(FrameworkStatsLog.UI_EVENT_REPORTED));
+ assertThat(logger.get(2).event.getId(),
+ is(ChooserActivityLogger
+ .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId()));
+ // fourth and fifth are just artifacts of test set-up
+ // sixth one should be ranking atom with SHARESHEET_COPY_TARGET_SELECTED event
+ assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED));
+ assertThat(logger.get(5).targetType,
+ is(ChooserActivityLogger
+ .SharesheetTargetSelectedEvent.SHARESHEET_SERVICE_TARGET_SELECTED.getId()));
+ }
+
+ @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);
+
+ // Start activity
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+
+ // Thread.sleep shouldn't be a thing in an integration test but it's
+ // necessary here because of the way the code is structured
+ Thread.sleep(3000);
+
+ assertThat("Chooser should have 2 app targets",
+ activity.getAdapter().getCount(), is(2));
+ assertThat("Chooser should have no direct targets",
+ activity.getAdapter().getSelectableServiceTargetCount(), is(0));
+
+ ChooserActivityLoggerFake logger =
+ (ChooserActivityLoggerFake) activity.getChooserActivityLogger();
+
+ // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
+ logger.removeCallsForUiEventsOfType(
+ ChooserActivityLogger.SharesheetStandardEvent
+ .SHARESHEET_DIRECT_LOAD_COMPLETE.getId());
+
+ // SHARESHEET_TRIGGERED:
+ assertThat(logger.event(0).getId(),
+ is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId()));
+
+ // SHARESHEET_STARTED:
+ assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED));
+ assertThat(logger.get(1).intent, is(Intent.ACTION_SEND));
+ assertThat(logger.get(1).mimeType, is("text/plain"));
+ assertThat(logger.get(1).packageName, is(
+ InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()));
+ assertThat(logger.get(1).appProvidedApp, is(0));
+ assertThat(logger.get(1).appProvidedDirect, is(0));
+ assertThat(logger.get(1).isWorkprofile, is(false));
+ assertThat(logger.get(1).previewType, is(3));
+
+ // SHARESHEET_APP_LOAD_COMPLETE:
+ assertThat(logger.event(2).getId(),
+ is(ChooserActivityLogger
+ .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId()));
+
+ // SHARESHEET_EMPTY_DIRECT_SHARE_ROW:
+ assertThat(logger.event(3).getId(),
+ is(ChooserActivityLogger
+ .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId()));
+
+ // Next is just an artifact of test set-up:
+ assertThat(logger.event(4).getId(),
+ is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_EXPANDED.getId()));
+
+ assertThat(logger.numCalls(), is(5));
+ }
+
+ @Ignore // b/220067877
+ @Test
+ public void testCopyTextToClipboardLogging() 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);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
+ waitForIdle();
+
+ onView(withIdFromRuntimeResource("chooser_copy_button")).check(matches(isDisplayed()));
+ onView(withIdFromRuntimeResource("chooser_copy_button")).perform(click());
+
+ ChooserActivityLoggerFake logger =
+ (ChooserActivityLoggerFake) activity.getChooserActivityLogger();
+
+ // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
+ logger.removeCallsForUiEventsOfType(
+ ChooserActivityLogger.SharesheetStandardEvent
+ .SHARESHEET_DIRECT_LOAD_COMPLETE.getId());
+
+ // SHARESHEET_TRIGGERED:
+ assertThat(logger.event(0).getId(),
+ is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId()));
+
+ // SHARESHEET_STARTED:
+ assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED));
+ assertThat(logger.get(1).intent, is(Intent.ACTION_SEND));
+ assertThat(logger.get(1).mimeType, is("text/plain"));
+ assertThat(logger.get(1).packageName, is(
+ InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()));
+ assertThat(logger.get(1).appProvidedApp, is(0));
+ assertThat(logger.get(1).appProvidedDirect, is(0));
+ assertThat(logger.get(1).isWorkprofile, is(false));
+ assertThat(logger.get(1).previewType, is(3));
+
+ // SHARESHEET_APP_LOAD_COMPLETE:
+ assertThat(logger.event(2).getId(),
+ is(ChooserActivityLogger
+ .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId()));
+
+ // Next are just artifacts of test set-up:
+ assertThat(logger.event(3).getId(),
+ is(ChooserActivityLogger
+ .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId()));
+ assertThat(logger.event(4).getId(),
+ is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_EXPANDED.getId()));
+
+ // SHARESHEET_COPY_TARGET_SELECTED:
+ assertThat(logger.get(5).atomId, is(FrameworkStatsLog.RANKING_SELECTED));
+ assertThat(logger.get(5).targetType,
+ is(ChooserActivityLogger
+ .SharesheetTargetSelectedEvent.SHARESHEET_COPY_TARGET_SELECTED.getId()));
+
+ // No more events.
+ assertThat(logger.numCalls(), is(6));
+ }
+
+ @Test @Ignore("b/222124533")
+ public void testSwitchProfileLogging() throws InterruptedException {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ final IChooserWrapper activity = (IChooserWrapper)
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+ waitForIdle();
+ onView(withTextFromRuntimeResource("resolver_personal_tab")).perform(click());
+ waitForIdle();
+
+ ChooserActivityLoggerFake logger =
+ (ChooserActivityLoggerFake) activity.getChooserActivityLogger();
+
+ // TODO(b/211669337): Determine the expected SHARESHEET_DIRECT_LOAD_COMPLETE events.
+ logger.removeCallsForUiEventsOfType(
+ ChooserActivityLogger.SharesheetStandardEvent
+ .SHARESHEET_DIRECT_LOAD_COMPLETE.getId());
+
+ // SHARESHEET_TRIGGERED:
+ assertThat(logger.event(0).getId(),
+ is(ChooserActivityLogger.SharesheetStandardEvent.SHARESHEET_TRIGGERED.getId()));
+
+ // SHARESHEET_STARTED:
+ assertThat(logger.get(1).atomId, is(FrameworkStatsLog.SHARESHEET_STARTED));
+ assertThat(logger.get(1).intent, is(Intent.ACTION_SEND));
+ assertThat(logger.get(1).mimeType, is(TEST_MIME_TYPE));
+ assertThat(logger.get(1).packageName, is(
+ InstrumentationRegistry.getInstrumentation().getTargetContext().getPackageName()));
+ assertThat(logger.get(1).appProvidedApp, is(0));
+ assertThat(logger.get(1).appProvidedDirect, is(0));
+ assertThat(logger.get(1).isWorkprofile, is(false));
+ assertThat(logger.get(1).previewType, is(3));
+
+ // SHARESHEET_APP_LOAD_COMPLETE:
+ assertThat(logger.event(2).getId(),
+ is(ChooserActivityLogger
+ .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId()));
+
+ // Next is just an artifact of test set-up:
+ assertThat(logger.event(3).getId(),
+ is(ChooserActivityLogger
+ .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId()));
+
+ // SHARESHEET_PROFILE_CHANGED:
+ assertThat(logger.event(4).getId(),
+ is(ChooserActivityLogger.SharesheetStandardEvent
+ .SHARESHEET_PROFILE_CHANGED.getId()));
+
+ // Repeat the loading steps in the new profile:
+
+ // SHARESHEET_APP_LOAD_COMPLETE:
+ assertThat(logger.event(5).getId(),
+ is(ChooserActivityLogger
+ .SharesheetStandardEvent.SHARESHEET_APP_LOAD_COMPLETE.getId()));
+
+ // Next is again an artifact of test set-up:
+ assertThat(logger.event(6).getId(),
+ is(ChooserActivityLogger
+ .SharesheetStandardEvent.SHARESHEET_EMPTY_DIRECT_SHARE_ROW.getId()));
+
+ // SHARESHEET_PROFILE_CHANGED:
+ assertThat(logger.event(7).getId(),
+ is(ChooserActivityLogger.SharesheetStandardEvent
+ .SHARESHEET_PROFILE_CHANGED.getId()));
+
+ // No more events (this profile was already loaded).
+ assertThat(logger.numCalls(), is(8));
+ }
+
+ @Test
+ public void testAutolaunch_singleTarget_wifthWorkProfileAndTabbedViewOff_noAutolaunch() {
+ ResolverActivity.ENABLE_TABBED_VIEW = false;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10);
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+ waitForIdle();
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+
+ assertTrue(chosen[0] == null);
+ }
+
+ @Test
+ public void testAutolaunch_singleTarget_noWorkProfile_autolaunch() {
+ ResolverActivity.ENABLE_TABBED_VIEW = false;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(1);
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+ waitForIdle();
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+
+ assertThat(chosen[0], is(personalResolvedComponentInfos.get(0).getResolveInfoAt(0)));
+ }
+
+ @Test
+ public void testWorkTab_onePersonalTarget_emptyStateOnWorkTarget_autolaunch() {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets);
+ ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false;
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+
+ mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+
+ assertThat(chosen[0], is(personalResolvedComponentInfos.get(1).getResolveInfoAt(0)));
+ }
+
+ @Test
+ 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));
+ Intent chooserIntent = createChooserIntent(createSendTextIntent(),
+ new Intent[] {new Intent("action.fake")});
+ ResolveInfo[] chosen = new ResolveInfo[1];
+ ChooserActivityOverrideData.getInstance().onSafelyStartCallback = targetInfo -> {
+ chosen[0] = targetInfo.getResolveInfo();
+ return true;
+ };
+ ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class);
+ ResolveInfo ri = createFakeResolveInfo();
+ when(
+ ChooserActivityOverrideData
+ .getInstance().packageManager
+ .resolveActivity(any(Intent.class), anyInt()))
+ .thenReturn(ri);
+ waitForIdle();
+
+ IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent);
+ waitForIdle();
+
+ assertNull(chosen[0]);
+ assertThat(activity
+ .getPersonalListAdapter().getCallerTargetCount(), is(1));
+ }
+
+ @Test
+ public void testWorkTab_withInitialIntents_workTabDoesNotIncludePersonalInitialIntents() {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
+ int workProfileTargets = 1;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(2, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent[] initialIntents = {
+ new Intent("action.fake1"),
+ new Intent("action.fake2")
+ };
+ Intent chooserIntent = createChooserIntent(createSendTextIntent(), initialIntents);
+ ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class);
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .packageManager
+ .resolveActivity(any(Intent.class), anyInt()))
+ .thenReturn(createFakeResolveInfo());
+ waitForIdle();
+
+ IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent);
+ waitForIdle();
+
+ assertThat(activity.getPersonalListAdapter().getCallerTargetCount(), is(2));
+ assertThat(activity.getWorkListAdapter().getCallerTargetCount(), is(0));
+ }
+
+ @Test
+ public void testWorkTab_xProfileIntentsDisabled_personalToWork_nonSendIntent_emptyStateShown() {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
+ int workProfileTargets = 4;
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(workProfileTargets);
+ ChooserActivityOverrideData.getInstance().hasCrossProfileIntents = false;
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent[] initialIntents = {
+ new Intent("action.fake1"),
+ new Intent("action.fake2")
+ };
+ Intent chooserIntent = createChooserIntent(new Intent(), initialIntents);
+ ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class);
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .packageManager
+ .resolveActivity(any(Intent.class), anyInt()))
+ .thenReturn(createFakeResolveInfo());
+
+ mActivityRule.launchActivity(chooserIntent);
+ waitForIdle();
+ onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+ waitForIdle();
+ onView(withIdFromRuntimeResource("contentPanel"))
+ .perform(swipeUp());
+
+ onView(withTextFromRuntimeResource("resolver_cross_profile_blocked"))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testWorkTab_noWorkAppsAvailable_nonSendIntent_emptyStateShown() {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(0);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent[] initialIntents = {
+ new Intent("action.fake1"),
+ new Intent("action.fake2")
+ };
+ Intent chooserIntent = createChooserIntent(new Intent(), initialIntents);
+ ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class);
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .packageManager
+ .resolveActivity(any(Intent.class), anyInt()))
+ .thenReturn(createFakeResolveInfo());
+
+ mActivityRule.launchActivity(chooserIntent);
+ waitForIdle();
+ onView(withIdFromRuntimeResource("contentPanel"))
+ .perform(swipeUp());
+ onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+ waitForIdle();
+
+ onView(withTextFromRuntimeResource("resolver_no_work_apps_available"))
+ .check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testDeduplicateCallerTargetRankedTarget() {
+ // 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));
+ // Create caller target which is duplicate with one of app targets
+ Intent chooserIntent = createChooserIntent(createSendTextIntent(),
+ new Intent[] {new Intent("action.fake")});
+ ChooserActivityOverrideData.getInstance().packageManager = mock(PackageManager.class);
+ ResolveInfo ri = ResolverDataProvider.createResolveInfo(0,
+ UserHandle.USER_CURRENT);
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .packageManager
+ .resolveActivity(any(Intent.class), anyInt()))
+ .thenReturn(ri);
+ waitForIdle();
+
+ IChooserWrapper activity = (IChooserWrapper) mActivityRule.launchActivity(chooserIntent);
+ waitForIdle();
+
+ // Total 4 targets (1 caller target, 3 ranked targets)
+ assertThat(activity.getAdapter().getCount(), is(4));
+ assertThat(activity.getAdapter().getCallerTargetCount(), is(1));
+ assertThat(activity.getAdapter().getRankedTargetCount(), is(3));
+ }
+
+ @Test
+ public void testWorkTab_selectingWorkTabWithPausedWorkProfile_directShareTargetsNotQueried() {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ ChooserActivityOverrideData.getInstance().isQuietModeEnabled = true;
+ boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false };
+ ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets =
+ chooserListAdapter -> {
+ isQueryDirectShareCalledOnWorkProfile[0] =
+ (chooserListAdapter.getUserHandle().getIdentifier() == 10);
+ return null;
+ };
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("contentPanel"))
+ .perform(swipeUp());
+ onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+ waitForIdle();
+
+ assertFalse("Direct share targets were queried on a paused work profile",
+ isQueryDirectShareCalledOnWorkProfile[0]);
+ }
+
+ @Test
+ public void testWorkTab_selectingWorkTabWithNotRunningWorkUser_directShareTargetsNotQueried() {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ ChooserActivityOverrideData.getInstance().isWorkProfileUserRunning = false;
+ boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false };
+ ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets =
+ chooserListAdapter -> {
+ isQueryDirectShareCalledOnWorkProfile[0] =
+ (chooserListAdapter.getUserHandle().getIdentifier() == 10);
+ return null;
+ };
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("contentPanel"))
+ .perform(swipeUp());
+ onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+ waitForIdle();
+
+ assertFalse("Direct share targets were queried on a locked work profile user",
+ isQueryDirectShareCalledOnWorkProfile[0]);
+ }
+
+ @Test
+ public void testWorkTab_workUserNotRunning_workTargetsShown() {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+ ChooserActivityOverrideData.getInstance().isWorkProfileUserRunning = false;
+
+ final ChooserActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ final IChooserWrapper wrapper = (IChooserWrapper) activity;
+ waitForIdle();
+ onView(withIdFromRuntimeResource("contentPanel")).perform(swipeUp());
+ onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+ waitForIdle();
+
+ assertEquals(3, wrapper.getWorkListAdapter().getCount());
+ }
+
+ @Test
+ public void testWorkTab_selectingWorkTabWithLockedWorkUser_directShareTargetsNotQueried() {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ ChooserActivityOverrideData.getInstance().isWorkProfileUserUnlocked = false;
+ boolean[] isQueryDirectShareCalledOnWorkProfile = new boolean[] { false };
+ ChooserActivityOverrideData.getInstance().onQueryDirectShareTargets =
+ chooserListAdapter -> {
+ isQueryDirectShareCalledOnWorkProfile[0] =
+ (chooserListAdapter.getUserHandle().getIdentifier() == 10);
+ return null;
+ };
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ waitForIdle();
+ onView(withIdFromRuntimeResource("contentPanel"))
+ .perform(swipeUp());
+ onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+ waitForIdle();
+
+ assertFalse("Direct share targets were queried on a locked work profile user",
+ isQueryDirectShareCalledOnWorkProfile[0]);
+ }
+
+ @Test
+ public void testWorkTab_workUserLocked_workTargetsShown() {
+ // enable the work tab feature flag
+ ResolverActivity.ENABLE_TABBED_VIEW = true;
+ markWorkProfileUserAvailable();
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, /* userId */ 10);
+ List<ResolvedComponentInfo> workResolvedComponentInfos =
+ createResolvedComponentsForTest(3);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ Intent sendIntent = createSendTextIntent();
+ sendIntent.setType(TEST_MIME_TYPE);
+ ChooserActivityOverrideData.getInstance().isWorkProfileUserUnlocked = false;
+
+ final ChooserActivity activity =
+ mActivityRule.launchActivity(Intent.createChooser(sendIntent, "work tab test"));
+ final IChooserWrapper wrapper = (IChooserWrapper) activity;
+ waitForIdle();
+ onView(withIdFromRuntimeResource("contentPanel"))
+ .perform(swipeUp());
+ onView(withTextFromRuntimeResource("resolver_work_tab")).perform(click());
+ waitForIdle();
+
+ assertEquals(3, wrapper.getWorkListAdapter().getCount());
+ }
+
+ private Intent createChooserIntent(Intent intent, Intent[] initialIntents) {
+ Intent chooserIntent = new Intent();
+ chooserIntent.setAction(Intent.ACTION_CHOOSER);
+ chooserIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending");
+ chooserIntent.putExtra(Intent.EXTRA_TITLE, "some title");
+ chooserIntent.putExtra(Intent.EXTRA_INTENT, intent);
+ chooserIntent.setType("text/plain");
+ if (initialIntents != null) {
+ chooserIntent.putExtra(Intent.EXTRA_INITIAL_INTENTS, initialIntents);
+ }
+ return chooserIntent;
}
/* This is a "test of a test" to make sure that our inherited test class
@@ -105,7 +2913,326 @@ public class UnbundledChooserActivityTest extends ChooserActivityTest {
assertThat(activity).isInstanceOf(com.android.intentresolver.ChooserWrapperActivity.class);
}
+ private ResolveInfo createFakeResolveInfo() {
+ ResolveInfo ri = new ResolveInfo();
+ ri.activityInfo = new ActivityInfo();
+ ri.activityInfo.name = "FakeActivityName";
+ ri.activityInfo.packageName = "fake.package.name";
+ ri.activityInfo.applicationInfo = new ApplicationInfo();
+ ri.activityInfo.applicationInfo.packageName = "fake.package.name";
+ return ri;
+ }
+
+ private Intent createSendTextIntent() {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending");
+ sendIntent.setType("text/plain");
+ return sendIntent;
+ }
+
+ private Intent createSendImageIntent(Uri imageThumbnail) {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_STREAM, imageThumbnail);
+ sendIntent.setType("image/png");
+ if (imageThumbnail != null) {
+ ClipData.Item clipItem = new ClipData.Item(imageThumbnail);
+ sendIntent.setClipData(new ClipData("Clip Label", new String[]{"image/png"}, clipItem));
+ }
+
+ return sendIntent;
+ }
+
+ private Intent createSendTextIntentWithPreview(String title, Uri imageThumbnail) {
+ Intent sendIntent = new Intent();
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_TEXT, "testing intent sending");
+ sendIntent.putExtra(Intent.EXTRA_TITLE, title);
+ if (imageThumbnail != null) {
+ ClipData.Item clipItem = new ClipData.Item(imageThumbnail);
+ sendIntent.setClipData(new ClipData("Clip Label", new String[]{"image/png"}, clipItem));
+ }
+
+ return sendIntent;
+ }
+
+ private Intent createSendUriIntentWithPreview(ArrayList<Uri> uris) {
+ Intent sendIntent = new Intent();
+
+ if (uris.size() > 1) {
+ sendIntent.setAction(Intent.ACTION_SEND_MULTIPLE);
+ sendIntent.putExtra(Intent.EXTRA_STREAM, uris);
+ } else {
+ sendIntent.setAction(Intent.ACTION_SEND);
+ sendIntent.putExtra(Intent.EXTRA_STREAM, uris.get(0));
+ }
+
+ return sendIntent;
+ }
+
+ private Intent createViewTextIntent() {
+ Intent viewIntent = new Intent();
+ viewIntent.setAction(Intent.ACTION_VIEW);
+ viewIntent.putExtra(Intent.EXTRA_TEXT, "testing intent viewing");
+ return viewIntent;
+ }
+
+ private List<ResolvedComponentInfo> createResolvedComponentsForTest(int numberOfResults) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < numberOfResults; i++) {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfo(i));
+ }
+ return infoList;
+ }
+
+ private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile(
+ int numberOfResults) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < numberOfResults; i++) {
+ if (i == 0) {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i));
+ } else {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfo(i));
+ }
+ }
+ return infoList;
+ }
+
+ private List<ResolvedComponentInfo> createResolvedComponentsForTestWithOtherProfile(
+ int numberOfResults, int userId) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < numberOfResults; i++) {
+ if (i == 0) {
+ infoList.add(
+ ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId));
+ } else {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfo(i));
+ }
+ }
+ return infoList;
+ }
+
+ private List<ResolvedComponentInfo> createResolvedComponentsForTestWithUserId(
+ int numberOfResults, int userId) {
+ List<ResolvedComponentInfo> infoList = new ArrayList<>(numberOfResults);
+ for (int i = 0; i < numberOfResults; i++) {
+ infoList.add(ResolverDataProvider.createResolvedComponentInfoWithOtherId(i, userId));
+ }
+ return infoList;
+ }
+
+ private List<ChooserTarget> createDirectShareTargets(int numberOfResults, String packageName) {
+ Icon icon = Icon.createWithBitmap(createBitmap());
+ String testTitle = "testTitle";
+ List<ChooserTarget> targets = new ArrayList<>();
+ for (int i = 0; i < numberOfResults; i++) {
+ ComponentName componentName;
+ if (packageName.isEmpty()) {
+ componentName = ResolverDataProvider.createComponentName(i);
+ } else {
+ componentName = new ComponentName(packageName, packageName + ".class");
+ }
+ ChooserTarget tempTarget = new ChooserTarget(
+ testTitle + i,
+ icon,
+ (float) (1 - ((i + 1) / 10.0)),
+ componentName,
+ null);
+ targets.add(tempTarget);
+ }
+ return targets;
+ }
+
private void waitForIdle() {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
+
+ private Bitmap createBitmap() {
+ int width = 200;
+ int height = 200;
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas canvas = new Canvas(bitmap);
+
+ Paint paint = new Paint();
+ paint.setColor(Color.RED);
+ paint.setStyle(Paint.Style.FILL);
+ canvas.drawPaint(paint);
+
+ paint.setColor(Color.WHITE);
+ paint.setAntiAlias(true);
+ paint.setTextSize(14.f);
+ paint.setTextAlign(Paint.Align.CENTER);
+ canvas.drawText("Hi!", (width / 2.f), (height / 2.f), paint);
+
+ return bitmap;
+ }
+
+ private List<ShareShortcutInfo> createShortcuts(Context context) {
+ Intent testIntent = new Intent("TestIntent");
+
+ List<ShareShortcutInfo> shortcuts = new ArrayList<>();
+ shortcuts.add(new ShareShortcutInfo(
+ new ShortcutInfo.Builder(context, "shortcut1")
+ .setIntent(testIntent).setShortLabel("label1").setRank(3).build(), // 0 2
+ new ComponentName("package1", "class1")));
+ shortcuts.add(new ShareShortcutInfo(
+ new ShortcutInfo.Builder(context, "shortcut2")
+ .setIntent(testIntent).setShortLabel("label2").setRank(7).build(), // 1 3
+ new ComponentName("package2", "class2")));
+ shortcuts.add(new ShareShortcutInfo(
+ new ShortcutInfo.Builder(context, "shortcut3")
+ .setIntent(testIntent).setShortLabel("label3").setRank(1).build(), // 2 0
+ new ComponentName("package3", "class3")));
+ shortcuts.add(new ShareShortcutInfo(
+ new ShortcutInfo.Builder(context, "shortcut4")
+ .setIntent(testIntent).setShortLabel("label4").setRank(3).build(), // 3 2
+ new ComponentName("package4", "class4")));
+
+ return shortcuts;
+ }
+
+ private void assertCorrectShortcutToChooserTargetConversion(List<ShareShortcutInfo> shortcuts,
+ List<ChooserTarget> chooserTargets, int[] expectedOrder, float[] expectedScores) {
+ assertEquals(expectedOrder.length, chooserTargets.size());
+ for (int i = 0; i < chooserTargets.size(); i++) {
+ ChooserTarget ct = chooserTargets.get(i);
+ ShortcutInfo si = shortcuts.get(expectedOrder[i]).getShortcutInfo();
+ ComponentName cn = shortcuts.get(expectedOrder[i]).getTargetComponent();
+
+ assertEquals(si.getId(), ct.getIntentExtras().getString(Intent.EXTRA_SHORTCUT_ID));
+ assertEquals(si.getShortLabel(), ct.getTitle());
+ assertThat(Math.abs(expectedScores[i] - ct.getScore()) < 0.000001, is(true));
+ assertEquals(cn.flattenToString(), ct.getComponentName().flattenToString());
+ }
+ }
+
+ private void markWorkProfileUserAvailable() {
+ ChooserActivityOverrideData.getInstance().workProfileUserHandle = UserHandle.of(10);
+ }
+
+ private void setupResolverControllers(
+ List<ResolvedComponentInfo> personalResolvedComponentInfos,
+ List<ResolvedComponentInfo> workResolvedComponentInfos) {
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resolverListController
+ .getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .workResolverListController
+ .getResolversForIntent(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class)))
+ .thenReturn(new ArrayList<>(workResolvedComponentInfos));
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .workResolverListController
+ .getResolversForIntentAsUser(
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.anyBoolean(),
+ Mockito.isA(List.class),
+ eq(UserHandle.SYSTEM)))
+ .thenReturn(new ArrayList<>(personalResolvedComponentInfos));
+ }
+
+ private Matcher<View> withIdFromRuntimeResource(String id) {
+ return withId(getRuntimeResourceId(id, "id"));
+ }
+
+ private Matcher<View> withTextFromRuntimeResource(String id) {
+ return withText(getRuntimeResourceId(id, "string"));
+ }
+
+ private static GridRecyclerSpanCountMatcher withGridColumnCount(int columnCount) {
+ return new GridRecyclerSpanCountMatcher(Matchers.is(columnCount));
+ }
+
+ private static class GridRecyclerSpanCountMatcher extends
+ BoundedDiagnosingMatcher<View, RecyclerView> {
+
+ private final Matcher<Integer> mIntegerMatcher;
+
+ private GridRecyclerSpanCountMatcher(Matcher<Integer> integerMatcher) {
+ super(RecyclerView.class);
+ this.mIntegerMatcher = integerMatcher;
+ }
+
+ @Override
+ protected void describeMoreTo(Description description) {
+ description.appendText("RecyclerView grid layout span count to match: ");
+ this.mIntegerMatcher.describeTo(description);
+ }
+
+ @Override
+ protected boolean matchesSafely(RecyclerView view, Description mismatchDescription) {
+ int spanCount = ((GridLayoutManager) view.getLayoutManager()).getSpanCount();
+ if (this.mIntegerMatcher.matches(spanCount)) {
+ return true;
+ } else {
+ mismatchDescription.appendText("RecyclerView grid layout span count was ")
+ .appendValue(spanCount);
+ return false;
+ }
+ }
+ }
+
+ 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);
+ }
+
+ private void updateMaxTargetsPerRowResource(int targetsPerRow) {
+ ChooserActivityOverrideData.getInstance().resources = Mockito.spy(
+ InstrumentationRegistry.getInstrumentation().getContext().getResources());
+ when(
+ ChooserActivityOverrideData
+ .getInstance()
+ .resources
+ .getInteger(R.integer.config_chooser_max_targets_per_row))
+ .thenReturn(targetsPerRow);
+ }
+
+ // ChooserWrapperActivity inherits from the framework ChooserActivity, so if the framework
+ // resources have been updated since the framework was last built/pushed, the inherited behavior
+ // (which is the focus of our testing) will still be implemented in terms of the old resource
+ // IDs; then when we try to assert those IDs in tests (e.g. `onView(withText(R.string.foo))`),
+ // the expected values won't match. The tests can instead call this method (with the same
+ // general semantics as Resources#getIdentifier() e.g. `getRuntimeResourceId("foo", "string")`)
+ // to refer to the resource by that name in the runtime chooser, regardless of whether the
+ // framework code on the device is up-to-date.
+ // TODO: is there a better way to do this? (Other than abandoning inheritance-based DI wrapper?)
+ private int getRuntimeResourceId(String name, String defType) {
+ int id = -1;
+ if (ChooserActivityOverrideData.getInstance().resources != null) {
+ id = ChooserActivityOverrideData.getInstance().resources.getIdentifier(
+ name, defType, "android");
+ } else {
+ id = mActivityRule.getActivity().getResources().getIdentifier(name, defType, "android");
+ }
+ assertThat(id, greaterThan(0));
+
+ return id;
+ }
}