summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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;
+ }
}