diff options
Diffstat (limited to 'java')
47 files changed, 891 insertions, 571 deletions
diff --git a/java/res/drawable/chevron_right.xml b/java/res/drawable/chevron_right.xml index 9fd27a6b..3388821b 100644 --- a/java/res/drawable/chevron_right.xml +++ b/java/res/drawable/chevron_right.xml @@ -28,5 +28,5 @@ android:tint="?android:attr/textColorPrimary"> <path android:fillColor="@android:color/white" - android:pathData="M10,6L8.59,7.41 13.17,12l-4.58,4.59L10,18l6,-6 -6,-6z"/> + android:pathData="M10,4.5L8.59,5.91 13.17,10.5l-4.58,4.59L10,16.5l6,-6 -6,-6z"/> </vector> diff --git a/java/res/layout/chooser_action_button.xml b/java/res/layout/chooser_action_button.xml deleted file mode 100644 index 2b68ccca..00000000 --- a/java/res/layout/chooser_action_button.xml +++ /dev/null @@ -1,31 +0,0 @@ -<!-- - ~ Copyright (C) 2019 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License - --> - -<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 index 303426c4..f77b0e68 100644 --- a/java/res/layout/chooser_action_row.xml +++ b/java/res/layout/chooser_action_row.xml @@ -16,7 +16,8 @@ <merge xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> <View android:layout_width="match_parent" @@ -29,6 +30,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" + app:horizontalActions="@bool/horizontal_actions" android:gravity="center"/> <View diff --git a/java/res/layout/chooser_grid_preview_files_text.xml b/java/res/layout/chooser_grid_preview_files_text.xml new file mode 100644 index 00000000..d46da2c0 --- /dev/null +++ b/java/res/layout/chooser_grid_preview_files_text.xml @@ -0,0 +1,62 @@ +<?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:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" + android:background="?android:attr/colorBackground"> + + <include layout="@layout/chooser_headline_row" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal" + android:gravity="center_horizontal" + android:layout_marginBottom="@dimen/chooser_view_spacing" + android:paddingStart="@dimen/chooser_edge_margin_normal_half" + android:paddingEnd="@dimen/chooser_edge_margin_normal_half" + android:background="@drawable/chooser_content_preview_rounded"> + + <com.android.intentresolver.widget.RoundedRectImageView + android:id="@+id/image_view" + android:layout_width="100dp" + android:layout_height="100dp" + android:scaleType="centerCrop" + app:radius="@dimen/chooser_corner_radius_small" + /> + + <TextView + android:id="@+id/content_preview_text" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" + android:layout_gravity="center_vertical" + android:layout_marginStart="@dimen/chooser_edge_margin_normal_half" + android:layout_marginEnd="@dimen/chooser_edge_margin_normal_half" + android:maxLines="@integer/text_preview_lines" + android:ellipsize="end" + android:linksClickable="false" + android:textAppearance="@style/TextAppearance.ChooserDefault"/> + </LinearLayout> + + <include layout="@layout/chooser_action_row"/> +</LinearLayout> diff --git a/java/res/layout/chooser_grid_preview_image.xml b/java/res/layout/chooser_grid_preview_image.xml index 68b07846..12848a50 100644 --- a/java/res/layout/chooser_grid_preview_image.xml +++ b/java/res/layout/chooser_grid_preview_image.xml @@ -18,48 +18,24 @@ --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:background="?android:attr/colorBackground"> - <include layout="@layout/chooser_headline_row" /> + <include layout="@layout/chooser_headline_row"/> - <LinearLayout - android:layout_width="match_parent" - android:layout_height="wrap_content" - android:orientation="horizontal" - android:gravity="center_horizontal" - android:layout_marginBottom="@dimen/chooser_view_spacing" - android:paddingStart="@dimen/chooser_edge_margin_normal_half" - android:paddingEnd="@dimen/chooser_edge_margin_normal_half"> + <com.android.intentresolver.widget.ScrollableImagePreviewView + android:id="@+id/scrollable_image_preview" + android:layout_width="wrap_content" + android:layout_height="@dimen/chooser_preview_image_height_tall" + android:layout_gravity="center_horizontal" + android:layout_marginBottom="@dimen/chooser_view_spacing" + android:layout_marginHorizontal="@dimen/chooser_edge_margin_normal_half" + android:background="?android:attr/colorBackground" + app:itemInnerSpacing="3dp" + app:itemOuterSpacing="@dimen/chooser_edge_margin_normal_half"/> - <com.android.intentresolver.widget.ScrollableImagePreviewView - android:id="@+id/scrollable_image_preview" - android:layout_width="wrap_content" - android:layout_height="@dimen/chooser_preview_image_height_tall" - android:layout_gravity="center_horizontal" - android:background="?android:attr/colorBackground" - app:itemInnerSpacing="3dp" - app:itemOuterSpacing="@dimen/chooser_edge_margin_normal_half" - app:maxWidthHint="@dimen/chooser_width" /> - - <TextView - android:id="@androidprv:id/content_preview_text" - android:layout_width="0dp" - android:layout_height="wrap_content" - android:layout_weight="1" - android:layout_gravity="center_vertical" - android:layout_marginStart="@dimen/chooser_edge_margin_normal_half" - android:layout_marginEnd="@dimen/chooser_edge_margin_normal_half" - android:maxLines="6" - android:ellipsize="end" - android:linksClickable="false" - android:visibility="gone" - android:textAppearance="@style/TextAppearance.ChooserDefault" /> - </LinearLayout> - - <include layout="@layout/chooser_action_row" /> + <include layout="@layout/chooser_action_row"/> </LinearLayout> diff --git a/java/res/layout/chooser_grid_preview_text.xml b/java/res/layout/chooser_grid_preview_text.xml index 89e961e8..e22cfbd2 100644 --- a/java/res/layout/chooser_grid_preview_text.xml +++ b/java/res/layout/chooser_grid_preview_text.xml @@ -80,7 +80,7 @@ android:textColor="?android:attr/textColorPrimary" android:textAlignment="gravity" android:textDirection="locale" - android:maxLines="3" + android:maxLines="@integer/text_preview_lines" android:focusable="true"/> </RelativeLayout> diff --git a/java/res/values-iw/strings.xml b/java/res/values-iw/strings.xml index bbdefa35..62ff1d89 100644 --- a/java/res/values-iw/strings.xml +++ b/java/res/values-iw/strings.xml @@ -16,7 +16,7 @@ <resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"> - <string name="whichApplication" msgid="2309561338625872614">"השלמת הפעולה באמצעות"</string> + <string name="whichApplication" msgid="2309561338625872614">"השלמת הפעולה עם"</string> <string name="whichApplicationNamed" msgid="8514249643796783492">"השלמת הפעולה באמצעות <xliff:g id="APP">%1$s</xliff:g>"</string> <string name="whichApplicationLabel" msgid="4312929689807826793">"להשלמת הפעולה"</string> <string name="whichViewApplication" msgid="7660051361612888119">"פתיחה באמצעות"</string> diff --git a/java/res/values-h480dp/bools.xml b/java/res/values-land/bools.xml index 7896d9bf..761de1ea 100644 --- a/java/res/values-h480dp/bools.xml +++ b/java/res/values-land/bools.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <!-- - ~ Copyright (C) 2020 The Android Open Source Project + ~ Copyright (C) 2023 The Android Open Source Project ~ ~ Licensed under the Apache License, Version 2.0 (the "License"); ~ you may not use this file except in compliance with the License. @@ -16,5 +16,5 @@ --> <resources> - <bool name="resolver_landscape_phone">false</bool> -</resources>
\ No newline at end of file + <bool name="horizontal_actions">false</bool> +</resources> diff --git a/java/res/values-land/dimens.xml b/java/res/values-land/dimens.xml index 7e3fb9cb..e3169efd 100644 --- a/java/res/values-land/dimens.xml +++ b/java/res/values-land/dimens.xml @@ -20,4 +20,8 @@ <resources> <dimen name="chooser_preview_width">412dp</dimen> + <dimen name="chooser_preview_image_height_tall">64dp</dimen> + <dimen name="chooser_view_spacing">8dp</dimen> + <integer name="text_preview_lines">1</integer> + <bool name="horizontal_actions">true</bool> </resources> diff --git a/java/res/values-land/integers.xml b/java/res/values-land/integers.xml new file mode 100644 index 00000000..2e310d87 --- /dev/null +++ b/java/res/values-land/integers.xml @@ -0,0 +1,19 @@ +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <integer name="text_preview_lines">1</integer> +</resources> diff --git a/java/res/values-sw600dp/bools.xml b/java/res/values-sw600dp/bools.xml new file mode 100644 index 00000000..761de1ea --- /dev/null +++ b/java/res/values-sw600dp/bools.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources> + <bool name="horizontal_actions">false</bool> +</resources> diff --git a/java/res/values-sw600dp/dimens.xml b/java/res/values-sw600dp/dimens.xml index b397630e..6cd29747 100644 --- a/java/res/values-sw600dp/dimens.xml +++ b/java/res/values-sw600dp/dimens.xml @@ -20,5 +20,8 @@ <resources> <dimen name="chooser_width">624dp</dimen> - + <dimen name="chooser_preview_image_height_tall">192dp</dimen> + <dimen name="chooser_view_spacing">18dp</dimen> + <integer name="text_preview_lines">3</integer> + <bool name="horizontal_actions">false</bool> </resources> diff --git a/java/res/values-sw600dp/integers.xml b/java/res/values-sw600dp/integers.xml new file mode 100644 index 00000000..c1693057 --- /dev/null +++ b/java/res/values-sw600dp/integers.xml @@ -0,0 +1,19 @@ +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <integer name="text_preview_lines">3</integer> +</resources> diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml index 67acb3ae..c96221ec 100644 --- a/java/res/values/attrs.xml +++ b/java/res/values/attrs.xml @@ -51,4 +51,8 @@ <attr name="itemOuterSpacing" format="dimension" /> <attr name="maxWidthHint" format="dimension" /> </declare-styleable> + + <declare-styleable name="ScrollableActionRow"> + <attr name="horizontalActions" format="boolean" /> + </declare-styleable> </resources> diff --git a/java/res/values/bools.xml b/java/res/values/bools.xml index a84081b6..641620d7 100644 --- a/java/res/values/bools.xml +++ b/java/res/values/bools.xml @@ -15,5 +15,5 @@ --> <resources> - <bool name="resolver_landscape_phone">@*android:bool/resolver_landscape_phone</bool> + <bool name="horizontal_actions">false</bool> </resources> diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml index 8636f742..947351d8 100644 --- a/java/res/values/dimens.xml +++ b/java/res/values/dimens.xml @@ -28,7 +28,6 @@ <dimen name="chooser_preview_image_font_size">20sp</dimen> <dimen name="chooser_preview_image_border">1dp</dimen> <dimen name="chooser_preview_image_width">120dp</dimen> - <dimen name="chooser_preview_image_height">104dp</dimen> <dimen name="chooser_preview_image_height_tall">192dp</dimen> <dimen name="chooser_preview_image_max_dimen">200dp</dimen> <dimen name="chooser_preview_width">-1px</dimen> @@ -54,7 +53,6 @@ <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> <dimen name="chooser_action_view_icon_size">22dp</dimen> <dimen name="chooser_action_margin">0dp</dimen> </resources> diff --git a/java/res/values/integers.xml b/java/res/values/integers.xml new file mode 100644 index 00000000..c1693057 --- /dev/null +++ b/java/res/values/integers.xml @@ -0,0 +1,19 @@ +<!-- + ~ Copyright (C) 2023 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + <integer name="text_preview_lines">3</integer> +</resources> diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml index 546ef249..1648831c 100644 --- a/java/res/values/strings.xml +++ b/java/res/values/strings.xml @@ -160,12 +160,41 @@ other {Sharing # items} } </string> - <!-- Title atop a sharing UI indicating that an image is being shared with text attached. - [CHAR_LIMIT=50] --> - <string name="sharing_image_with_text">Sharing image with text</string> - <!-- Title atop a sharing UI indicating that an image is being shared with a link (URL) - attached. [CHAR_LIMIT=50] --> - <string name="sharing_image_with_link">Sharing image with link</string> + <string name="sharing_images_with_text">{count, plural, + =1 {Sharing image with text} + other {Sharing # images with text} + } + </string> + + <string name="sharing_images_with_link">{count, plural, + =1 {Sharing image with link} + other {Sharing # images with link} + } + </string> + + <string name="sharing_videos_with_text">{count, plural, + =1 {Sharing video with text} + other {Sharing # videos with text} + } + </string> + + <string name="sharing_videos_with_link">{count, plural, + =1 {Sharing video with link} + other {Sharing # videos with link} + } + </string> + + <string name="sharing_files_with_text">{count, plural, + =1 {Sharing file with text} + other {Sharing # files with text} + } + </string> + + <string name="sharing_files_with_link">{count, plural, + =1 {Sharing file with link} + other {Sharing # files with link} + } + </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> @@ -202,9 +231,9 @@ <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> + <string name="resolver_turn_on_work_apps">Work apps are paused</string> + <!-- Button text. This button unpauses a user's work apps and data. [CHAR LIMIT=NONE] --> + <string name="resolver_switch_on_work">Unpause</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> diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java index 23e04560..f355d9d4 100644 --- a/java/src/com/android/intentresolver/ChooserActionFactory.java +++ b/java/src/com/android/intentresolver/ChooserActionFactory.java @@ -26,12 +26,9 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; -import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; -import android.content.res.Resources; import android.graphics.drawable.Drawable; import android.net.Uri; -import android.os.Bundle; import android.service.chooser.ChooserAction; import android.text.TextUtils; import android.util.Log; @@ -92,8 +89,6 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio private final Runnable mOnCopyButtonClicked; private final TargetInfo mEditSharingTarget; private final Runnable mOnEditButtonClicked; - private final TargetInfo mNearbySharingTarget; - private final Runnable mOnNearbyButtonClicked; private final ImmutableList<ChooserAction> mCustomActions; private final @Nullable ChooserAction mModifyShareAction; private final Consumer<Boolean> mExcludeSharedTextAction; @@ -144,18 +139,6 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio firstVisibleImageQuery, activityStarter, logger), - getNearbySharingTarget( - context, - chooserRequest.getTargetIntent(), - integratedDeviceComponents), - makeOnNearbyShareRunnable( - getNearbySharingTarget( - context, - chooserRequest.getTargetIntent(), - integratedDeviceComponents), - activityStarter, - finishCallback, - logger), chooserRequest.getChooserActions(), chooserRequest.getModifyShareAction(), onUpdateSharedTextIsExcluded, @@ -171,8 +154,6 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio Runnable onCopyButtonClicked, TargetInfo editSharingTarget, Runnable onEditButtonClicked, - TargetInfo nearbySharingTarget, - Runnable onNearbyButtonClicked, List<ChooserAction> customActions, @Nullable ChooserAction modifyShareAction, Consumer<Boolean> onUpdateSharedTextIsExcluded, @@ -184,8 +165,6 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio mOnCopyButtonClicked = onCopyButtonClicked; mEditSharingTarget = editSharingTarget; mOnEditButtonClicked = onEditButtonClicked; - mNearbySharingTarget = nearbySharingTarget; - mOnNearbyButtonClicked = onNearbyButtonClicked; mCustomActions = ImmutableList.copyOf(customActions); mModifyShareAction = modifyShareAction; mExcludeSharedTextAction = onUpdateSharedTextIsExcluded; @@ -218,21 +197,6 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio mOnEditButtonClicked); } - /** Create a "Share to Nearby" action. */ - @Override - @Nullable - public ActionRow.Action createNearbyButton() { - if (mNearbySharingTarget == null) { - return null; - } - - return new ActionRow.Action( - com.android.internal.R.id.chooser_nearby_button, - mNearbySharingTarget.getDisplayLabel(), - mNearbySharingTarget.getDisplayIconHolder().getDisplayIcon(), - mOnNearbyButtonClicked); - } - /** Create custom actions */ @Override public List<ActionRow.Action> createCustomActions() { @@ -403,64 +367,6 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio }; } - private static TargetInfo getNearbySharingTarget( - Context context, - Intent originalIntent, - ChooserIntegratedDeviceComponents integratedComponents) { - final ComponentName cn = integratedComponents.getNearbySharingComponent(); - if (cn == null) { - return null; - } - - final Intent resolveIntent = new Intent(originalIntent); - resolveIntent.setComponent(cn); - final ResolveInfo ri = context.getPackageManager().resolveActivity( - resolveIntent, PackageManager.GET_META_DATA); - if (ri == null || ri.activityInfo == null) { - Log.e(TAG, "Device-specified nearby sharing component (" + cn - + ") not available"); - return null; - } - - // Allow the nearby sharing component to provide a more appropriate icon and label - // for the chip. - CharSequence name = null; - Drawable icon = null; - final Bundle metaData = ri.activityInfo.metaData; - if (metaData != null) { - try { - final Resources pkgRes = context.getPackageManager().getResourcesForActivity(cn); - final int nameResId = metaData.getInt(CHIP_LABEL_METADATA_KEY); - name = pkgRes.getString(nameResId); - final int resId = metaData.getInt(CHIP_ICON_METADATA_KEY); - icon = pkgRes.getDrawable(resId); - } catch (NameNotFoundException | Resources.NotFoundException ex) { /* ignore */ } - } - if (TextUtils.isEmpty(name)) { - name = ri.loadLabel(context.getPackageManager()); - } - if (icon == null) { - icon = ri.loadIcon(context.getPackageManager()); - } - - final DisplayResolveInfo dri = DisplayResolveInfo.newDisplayResolveInfo( - originalIntent, ri, name, "", resolveIntent, null); - dri.getDisplayIconHolder().setDisplayIcon(icon); - return dri; - } - - private static Runnable makeOnNearbyShareRunnable( - TargetInfo nearbyShareTarget, - ActionActivityStarter activityStarter, - Consumer<Integer> finishCallback, - ChooserActivityLogger logger) { - return () -> { - logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_NEARBY); - // Action bar is user-independent; always start as primary. - activityStarter.safelyStartActivityAsPersonalProfileUser(nearbyShareTarget); - }; - } - @Nullable private static ActionRow.Action createCustomAction( Context context, diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java index 404d6da3..97161452 100644 --- a/java/src/com/android/intentresolver/ChooserActivity.java +++ b/java/src/com/android/intentresolver/ChooserActivity.java @@ -84,6 +84,7 @@ import com.android.intentresolver.chooser.MultiDisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.contentpreview.ChooserContentPreviewUi; import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl; +import com.android.intentresolver.contentpreview.ImageLoader; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.flags.FeatureFlagRepositoryFactory; import com.android.intentresolver.grid.ChooserGridAdapter; @@ -239,7 +240,6 @@ public class ChooserActivity extends ResolverActivity implements getIntent(), getReferrerPackageName(), getReferrer(), - mIntegratedDeviceComponents, mFeatureFlagRepository); } catch (IllegalArgumentException e) { Log.e(TAG, "Caller provided invalid Chooser request parameters", e); @@ -1261,7 +1261,8 @@ public class ChooserActivity extends ResolverActivity implements AbstractResolverComparator resolverComparator; if (appPredictor != null) { resolverComparator = new AppPredictionServiceResolverComparator(this, getTargetIntent(), - getReferrerPackageName(), appPredictor, userHandle, getChooserActivityLogger()); + getReferrerPackageName(), appPredictor, userHandle, getChooserActivityLogger(), + getIntegratedDeviceComponents().getNearbySharingComponent()); } else { resolverComparator = new ResolverRankerServiceResolverComparator( @@ -1270,7 +1271,8 @@ public class ChooserActivity extends ResolverActivity implements getReferrerPackageName(), null, getChooserActivityLogger(), - getResolverRankerServiceUserHandleList(userHandle)); + getResolverRankerServiceUserHandleList(userHandle), + getIntegratedDeviceComponents().getNearbySharingComponent()); } return new ChooserListController( @@ -1287,9 +1289,9 @@ public class ChooserActivity extends ResolverActivity implements protected ImageLoader createPreviewImageLoader() { final int cacheSize; float chooserWidth = getResources().getDimension(R.dimen.chooser_width); - // imageWidth = imagePreviewHeight / minAspectRatio (see ScrollableImagePreviewView) + // imageWidth = imagePreviewHeight * minAspectRatio (see ScrollableImagePreviewView) float imageWidth = - getResources().getDimension(R.dimen.chooser_preview_image_height_tall) * 5 / 2; + getResources().getDimension(R.dimen.chooser_preview_image_height_tall) * 2 / 5; cacheSize = (int) (Math.ceil(chooserWidth / imageWidth) + 2); return new ImagePreviewImageLoader(this, getLifecycle(), cacheSize); } @@ -1623,8 +1625,7 @@ public class ChooserActivity extends ResolverActivity implements * we instead show the content preview as a regular list item. */ private boolean shouldShowStickyContentPreview() { - return shouldShowStickyContentPreviewNoOrientationCheck() - && !getResources().getBoolean(R.bool.resolver_landscape_phone); + return shouldShowStickyContentPreviewNoOrientationCheck(); } private boolean shouldShowStickyContentPreviewNoOrientationCheck() { diff --git a/java/src/com/android/intentresolver/ChooserRequestParameters.java b/java/src/com/android/intentresolver/ChooserRequestParameters.java index f9004a9b..039f50e9 100644 --- a/java/src/com/android/intentresolver/ChooserRequestParameters.java +++ b/java/src/com/android/intentresolver/ChooserRequestParameters.java @@ -69,7 +69,6 @@ public class ChooserRequestParameters { Intent.FLAG_ACTIVITY_NEW_DOCUMENT | Intent.FLAG_ACTIVITY_MULTIPLE_TASK; private final Intent mTarget; - private final ChooserIntegratedDeviceComponents mIntegratedDeviceComponents; private final String mReferrerPackageName; private final Pair<CharSequence, Integer> mTitleSpec; private final Intent mReferrerFillInIntent; @@ -104,14 +103,11 @@ public class ChooserRequestParameters { final Intent clientIntent, String referrerPackageName, final Uri referrer, - ChooserIntegratedDeviceComponents integratedDeviceComponents, FeatureFlagRepository featureFlags) { final Intent requestedTarget = parseTargetIntentExtra( clientIntent.getParcelableExtra(Intent.EXTRA_INTENT)); mTarget = intentWithModifiedLaunchFlags(requestedTarget); - mIntegratedDeviceComponents = integratedDeviceComponents; - mReferrerPackageName = referrerPackageName; mAdditionalTargets = intentsWithModifiedLaunchFlagsFromExtraIfPresent( @@ -133,8 +129,11 @@ public class ChooserRequestParameters { mRefinementIntentSender = clientIntent.getParcelableExtra( Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER); - mFilteredComponentNames = getFilteredComponentNames( - clientIntent, mIntegratedDeviceComponents.getNearbySharingComponent()); + ComponentName[] filteredComponents = clientIntent.getParcelableArrayExtra( + Intent.EXTRA_EXCLUDE_COMPONENTS, ComponentName.class); + mFilteredComponentNames = filteredComponents != null + ? ImmutableList.copyOf(filteredComponents) + : ImmutableList.of(); mCallerChooserTargets = parseCallerTargetsFromClientIntent(clientIntent); @@ -252,10 +251,6 @@ public class ChooserRequestParameters { return mTargetIntentFilter; } - public ChooserIntegratedDeviceComponents getIntegratedDeviceComponents() { - return mIntegratedDeviceComponents; - } - private static boolean isSendAction(@Nullable String action) { return (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)); } @@ -309,23 +304,6 @@ public class ChooserRequestParameters { return Pair.create(requestedTitle, defaultTitleRes); } - private static ImmutableList<ComponentName> getFilteredComponentNames( - Intent clientIntent, @Nullable ComponentName nearbySharingComponent) { - Stream<ComponentName> filteredComponents = streamParcelableArrayExtra( - clientIntent, Intent.EXTRA_EXCLUDE_COMPONENTS, ComponentName.class, true, true); - - if (nearbySharingComponent != null) { - // Exclude Nearby from main list if chip is present, to avoid duplication. - // TODO: we don't have an explicit guarantee that the chip will be displayed just - // because we have a non-null component; that's ultimately determined by the preview - // layout. Maybe we can make that decision further upstream? - filteredComponents = Stream.concat( - filteredComponents, Stream.of(nearbySharingComponent)); - } - - return filteredComponents.collect(toImmutableList()); - } - private static ImmutableList<ChooserTarget> parseCallerTargetsFromClientIntent( Intent clientIntent) { return diff --git a/java/src/com/android/intentresolver/ImageLoader.kt b/java/src/com/android/intentresolver/ImageLoader.kt deleted file mode 100644 index 0ed8b122..00000000 --- a/java/src/com/android/intentresolver/ImageLoader.kt +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver - -import android.graphics.Bitmap -import android.net.Uri -import java.util.function.Consumer - -interface ImageLoader : suspend (Uri) -> Bitmap? { - fun loadImage(uri: Uri, callback: Consumer<Bitmap?>) - fun prePopulate(uris: List<Uri>) -} diff --git a/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt index 9650403e..c97efdd1 100644 --- a/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt +++ b/java/src/com/android/intentresolver/ImagePreviewImageLoader.kt @@ -26,8 +26,11 @@ import androidx.annotation.VisibleForTesting import androidx.collection.LruCache import androidx.lifecycle.Lifecycle import androidx.lifecycle.coroutineScope +import com.android.intentresolver.contentpreview.ImageLoader +import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.isActive import kotlinx.coroutines.launch @@ -35,6 +38,10 @@ import java.util.function.Consumer private const val TAG = "ImagePreviewImageLoader" +/** + * Implements preview image loading for the content preview UI. Provides requests deduplication and + * image caching. + */ @VisibleForTesting class ImagePreviewImageLoader @JvmOverloads constructor( private val context: Context, @@ -48,14 +55,17 @@ class ImagePreviewImageLoader @JvmOverloads constructor( Size(it, it) } - @GuardedBy("self") - private val cache = LruCache<Uri, CompletableDeferred<Bitmap?>>(cacheSize) + private val lock = Any() + @GuardedBy("lock") + private val cache = LruCache<Uri, RequestRecord>(cacheSize) + @GuardedBy("lock") + private val runningRequests = HashMap<Uri, RequestRecord>() - override suspend fun invoke(uri: Uri): Bitmap? = loadImageAsync(uri) + override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = loadImageAsync(uri, caching) override fun loadImage(uri: Uri, callback: Consumer<Bitmap?>) { lifecycle.coroutineScope.launch { - val image = loadImageAsync(uri) + val image = loadImageAsync(uri, caching = true) if (isActive) { callback.accept(image) } @@ -65,23 +75,44 @@ class ImagePreviewImageLoader @JvmOverloads constructor( override fun prePopulate(uris: List<Uri>) { uris.asSequence().take(cache.maxSize()).forEach { uri -> lifecycle.coroutineScope.launch { - loadImageAsync(uri) + loadImageAsync(uri, caching = true) } } } - private suspend fun loadImageAsync(uri: Uri): Bitmap? { - return synchronized(cache) { - cache.get(uri) ?: CompletableDeferred<Bitmap?>().also { result -> - cache.put(uri, result) - lifecycle.coroutineScope.launch(dispatcher) { - result.loadBitmap(uri) + private suspend fun loadImageAsync(uri: Uri, caching: Boolean): Bitmap? { + return getRequestDeferred(uri, caching) + .await() + } + + private fun getRequestDeferred(uri: Uri, caching: Boolean): Deferred<Bitmap?> { + var shouldLaunchImageLoading = false + val request = synchronized(lock) { + cache[uri] + ?: runningRequests.getOrPut(uri) { + shouldLaunchImageLoading = true + RequestRecord(uri, CompletableDeferred(), caching) + }.apply { + this.caching = this.caching || caching } + } + if (shouldLaunchImageLoading) { + request.loadBitmapAsync() + } + return request.deferred + } + + private fun RequestRecord.loadBitmapAsync() { + lifecycle.coroutineScope.launch(dispatcher) { + loadBitmap() + }.invokeOnCompletion { cause -> + if (cause is CancellationException) { + cancel() } - }.await() + } } - private fun CompletableDeferred<Bitmap?>.loadBitmap(uri: Uri) { + private fun RequestRecord.loadBitmap() { val bitmap = try { context.contentResolver.loadThumbnail(uri, thumbnailSize, null) } catch (t: Throwable) { @@ -90,4 +121,27 @@ class ImagePreviewImageLoader @JvmOverloads constructor( } complete(bitmap) } + + private fun RequestRecord.cancel() { + synchronized(lock) { + runningRequests.remove(uri) + deferred.cancel() + } + } + + private fun RequestRecord.complete(bitmap: Bitmap?) { + deferred.complete(bitmap) + synchronized(lock) { + runningRequests.remove(uri) + if (bitmap != null && caching) { + cache.put(uri, this) + } + } + } + + private class RequestRecord( + val uri: Uri, + val deferred: CompletableDeferred<Bitmap?>, + @GuardedBy("lock") var caching: Boolean + ) } diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java index 3b9d2a53..66eae92d 100644 --- a/java/src/com/android/intentresolver/ResolverActivity.java +++ b/java/src/com/android/intentresolver/ResolverActivity.java @@ -871,7 +871,8 @@ public class ResolverActivity extends FragmentActivity implements getReferrerPackageName(), null, null, - getResolverRankerServiceUserHandleList(userHandle)); + getResolverRankerServiceUserHandleList(userHandle), + null); return new ResolverListController( this, mPm, diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java index 56027a16..69d8c49f 100644 --- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java @@ -38,7 +38,6 @@ import android.view.ViewGroup; import androidx.annotation.Nullable; -import com.android.intentresolver.ImageLoader; import com.android.intentresolver.widget.ActionRow; import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; @@ -66,10 +65,6 @@ public final class ChooserContentPreviewUi { @Nullable ActionRow.Action createEditButton(); - /** Create an "Share to Nearby" action. */ - @Nullable - ActionRow.Action createNearbyButton(); - /** Create custom actions */ List<ActionRow.Action> createCustomActions(); @@ -151,6 +146,15 @@ public final class ChooserContentPreviewUi { } ArrayList<FileInfo> files = new ArrayList<>(uris.size()); int previewCount = readFileInfo(contentResolver, typeClassifier, uris, files); + CharSequence text = targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT); + if (!TextUtils.isEmpty(text)) { + return new FilesPlusTextContentPreviewUi(files, + targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT), + actionFactory, + imageLoader, + typeClassifier, + headlineGenerator); + } if (previewCount == 0) { return new FileContentPreviewUi( files, @@ -159,7 +163,6 @@ public final class ChooserContentPreviewUi { } return new UnifiedContentPreviewUi( files, - targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT), actionFactory, imageLoader, typeClassifier, diff --git a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java index e9d65eed..cae9403a 100644 --- a/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/FileContentPreviewUi.java @@ -103,21 +103,8 @@ class FileContentPreviewUi extends ContentPreviewUi { final ActionRow actionRow = contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row); actionRow.setActions( - createActions( - createFilePreviewActions(), - mActionFactory.createCustomActions())); + createActions(new ArrayList<>(), mActionFactory.createCustomActions())); return contentPreviewLayout; } - - private List<ActionRow.Action> createFilePreviewActions() { - List<ActionRow.Action> actions = new ArrayList<>(1); - //TODO(b/120417119): - // add action buttonFactory.createCopyButton() - ActionRow.Action action = mActionFactory.createNearbyButton(); - if (action != null) { - actions.add(action); - } - return actions; - } } diff --git a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java new file mode 100644 index 00000000..5174234a --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java @@ -0,0 +1,193 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.contentpreview; + +import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE; +import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE; + +import android.content.res.Resources; +import android.text.util.Linkify; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.TextView; + +import com.android.intentresolver.R; +import com.android.intentresolver.widget.ActionRow; +import com.android.intentresolver.widget.ScrollableImagePreviewView; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +/** + * FilesPlusTextContentPreviewUi is shown when the user is sending 1 or more files along with + * non-empty EXTRA_TEXT. The text can be toggled with a checkbox. If a single image file is being + * shared, it is shown in a preview (otherwise the headline summary is the sole indication of the + * file content). + */ +class FilesPlusTextContentPreviewUi extends ContentPreviewUi { + private final List<FileInfo> mFiles; + private final CharSequence mText; + private final ChooserContentPreviewUi.ActionFactory mActionFactory; + private final ImageLoader mImageLoader; + private final MimeTypeClassifier mTypeClassifier; + private final HeadlineGenerator mHeadlineGenerator; + private final boolean mAllImages; + private final boolean mAllVideos; + + FilesPlusTextContentPreviewUi( + List<FileInfo> files, + CharSequence text, + ChooserContentPreviewUi.ActionFactory actionFactory, + ImageLoader imageLoader, + MimeTypeClassifier typeClassifier, + HeadlineGenerator headlineGenerator) { + mFiles = files; + mText = text; + mActionFactory = actionFactory; + mImageLoader = imageLoader; + mTypeClassifier = typeClassifier; + mHeadlineGenerator = headlineGenerator; + + boolean allImages = true; + boolean allVideos = true; + for (FileInfo fileInfo : mFiles) { + ScrollableImagePreviewView.PreviewType previewType = + getPreviewType(fileInfo.getMimeType()); + allImages = allImages && previewType == ScrollableImagePreviewView.PreviewType.Image; + allVideos = allVideos && previewType == ScrollableImagePreviewView.PreviewType.Video; + } + mAllImages = allImages; + mAllVideos = allVideos; + } + + @Override + public int getType() { + return shouldShowPreview() ? CONTENT_PREVIEW_IMAGE : CONTENT_PREVIEW_FILE; + } + + @Override + public ViewGroup display(Resources resources, LayoutInflater layoutInflater, ViewGroup parent) { + ViewGroup layout = displayInternal(layoutInflater, parent); + displayModifyShareAction(layout, mActionFactory); + return layout; + } + + private ViewGroup displayInternal(LayoutInflater layoutInflater, ViewGroup parent) { + ViewGroup contentPreviewLayout = (ViewGroup) layoutInflater.inflate( + R.layout.chooser_grid_preview_files_text, parent, false); + ImageView imagePreview = + contentPreviewLayout.findViewById(R.id.image_view); + + final ActionRow actionRow = + contentPreviewLayout.findViewById(com.android.internal.R.id.chooser_action_row); + actionRow.setActions(createActions( + createImagePreviewActions(), + mActionFactory.createCustomActions())); + + if (shouldShowPreview()) { + mImageLoader.loadImage(mFiles.get(0).getPreviewUri(), bitmap -> { + if (bitmap == null) { + imagePreview.setVisibility(View.GONE); + } else { + imagePreview.setImageBitmap(bitmap); + } + }); + } else { + imagePreview.setVisibility(View.GONE); + } + + prepareTextPreview(contentPreviewLayout, mActionFactory); + updateHeadline(contentPreviewLayout); + + return contentPreviewLayout; + } + + private boolean shouldShowPreview() { + return mAllImages && mFiles.size() == 1 && mFiles.get(0).getPreviewUri() != null; + } + + private List<ActionRow.Action> createImagePreviewActions() { + ArrayList<ActionRow.Action> actions = new ArrayList<>(2); + //TODO: add copy action; + if (mFiles.size() == 1 && mAllImages) { + ActionRow.Action action = mActionFactory.createEditButton(); + if (action != null) { + actions.add(action); + } + } + return actions; + } + + private void updateHeadline(ViewGroup contentPreview) { + CheckBox includeText = contentPreview.requireViewById(R.id.include_text_action); + String headline; + if (includeText.getVisibility() == View.VISIBLE && includeText.isChecked()) { + if (mAllImages) { + headline = mHeadlineGenerator.getImagesWithTextHeadline(mText, mFiles.size()); + } else if (mAllVideos) { + headline = mHeadlineGenerator.getVideosWithTextHeadline(mText, mFiles.size()); + } else { + headline = mHeadlineGenerator.getFilesWithTextHeadline(mText, mFiles.size()); + } + } else { + if (mAllImages) { + headline = mHeadlineGenerator.getImagesHeadline(mFiles.size()); + } else if (mAllVideos) { + headline = mHeadlineGenerator.getVideosHeadline(mFiles.size()); + } else { + headline = mHeadlineGenerator.getItemsHeadline(mFiles.size()); + } + } + + displayHeadline(contentPreview, headline); + } + + private void prepareTextPreview( + ViewGroup contentPreview, + ChooserContentPreviewUi.ActionFactory actionFactory) { + final TextView textView = contentPreview.requireViewById(R.id.content_preview_text); + CheckBox includeText = contentPreview.requireViewById(R.id.include_text_action); + boolean isLink = HttpUriMatcher.isHttpUri(mText.toString()); + textView.setAutoLinkMask(isLink ? Linkify.WEB_URLS : 0); + textView.setText(mText); + + final Consumer<Boolean> shareTextAction = actionFactory.getExcludeSharedTextAction(); + includeText.setChecked(true); + includeText.setText(isLink ? R.string.include_link : R.string.include_text); + shareTextAction.accept(false); + includeText.setOnCheckedChangeListener((view, isChecked) -> { + textView.setEnabled(isChecked); + shareTextAction.accept(!isChecked); + updateHeadline(contentPreview); + }); + includeText.setVisibility(View.VISIBLE); + } + + private ScrollableImagePreviewView.PreviewType getPreviewType(String mimeType) { + if (mTypeClassifier.isImageType(mimeType)) { + return ScrollableImagePreviewView.PreviewType.Image; + } + if (mTypeClassifier.isVideoType(mimeType)) { + return ScrollableImagePreviewView.PreviewType.Video; + } + return ScrollableImagePreviewView.PreviewType.File; + } +} diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt index e32bb5c4..ad2a7ada 100644 --- a/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt +++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt @@ -25,7 +25,11 @@ private const val PLURALS_COUNT = "count" interface HeadlineGenerator { fun getTextHeadline(text: CharSequence): String - fun getImageWithTextHeadline(text: CharSequence): String + fun getImagesWithTextHeadline(text: CharSequence, count: Int): String + + fun getVideosWithTextHeadline(text: CharSequence, count: Int): String + + fun getFilesWithTextHeadline(text: CharSequence, count: Int): String fun getImagesHeadline(count: Int): String diff --git a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt index ae44294c..a6b782ad 100644 --- a/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt +++ b/java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt @@ -16,6 +16,7 @@ package com.android.intentresolver.contentpreview +import android.annotation.StringRes import android.content.Context import com.android.intentresolver.R import android.util.PluralsMessageFormatter @@ -28,40 +29,49 @@ private const val PLURALS_COUNT = "count" */ class HeadlineGeneratorImpl(private val context: Context) : HeadlineGenerator { override fun getTextHeadline(text: CharSequence): String { - if (text.toString().isHttpUri()) { - return context.getString(R.string.sharing_link) - } - return context.getString(R.string.sharing_text) + return context.getString( + getTemplateResource(text, R.string.sharing_link, R.string.sharing_text)) } - override fun getImageWithTextHeadline(text: CharSequence): String { - if (text.toString().isHttpUri()) { - return context.getString(R.string.sharing_image_with_link) - } - return context.getString(R.string.sharing_image_with_text) + override fun getImagesWithTextHeadline(text: CharSequence, count: Int): String { + return getPluralString(getTemplateResource( + text, R.string.sharing_images_with_link, R.string.sharing_images_with_text), count) + } + + override fun getVideosWithTextHeadline(text: CharSequence, count: Int): String { + return getPluralString(getTemplateResource( + text, R.string.sharing_videos_with_link, R.string.sharing_videos_with_text), count) + } + + override fun getFilesWithTextHeadline(text: CharSequence, count: Int): String { + return getPluralString(getTemplateResource( + text, R.string.sharing_files_with_link, R.string.sharing_files_with_text), count) } override fun getImagesHeadline(count: Int): String { - return PluralsMessageFormatter.format( - context.resources, - mapOf(PLURALS_COUNT to count), - R.string.sharing_images - ) + return getPluralString(R.string.sharing_images, count) } override fun getVideosHeadline(count: Int): String { - return PluralsMessageFormatter.format( - context.resources, - mapOf(PLURALS_COUNT to count), - R.string.sharing_videos - ) + return getPluralString(R.string.sharing_videos, count) } override fun getItemsHeadline(count: Int): String { + return getPluralString(R.string.sharing_items, count) + } + + private fun getPluralString(@StringRes templateResource: Int, count: Int): String { return PluralsMessageFormatter.format( context.resources, mapOf(PLURALS_COUNT to count), - R.string.sharing_items + templateResource ) } + + @StringRes + private fun getTemplateResource( + text: CharSequence, @StringRes linkResource: Int, @StringRes nonLinkResource: Int + ): Int { + return if (text.toString().isHttpUri()) linkResource else nonLinkResource + } } diff --git a/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt new file mode 100644 index 00000000..225807ee --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/ImageLoader.kt @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.contentpreview + +import android.graphics.Bitmap +import android.net.Uri +import java.util.function.Consumer + +/** + * A content preview image loader. + */ +interface ImageLoader : suspend (Uri) -> Bitmap?, suspend (Uri, Boolean) -> Bitmap? { + /** + * Load preview image asynchronously; caching is allowed. + * @param uri content URI + * @param callback a callback that will be invoked with the loaded image or null if loading has + * failed. + */ + fun loadImage(uri: Uri, callback: Consumer<Bitmap?>) + + /** + * Prepopulate the image loader cache. + */ + fun prePopulate(uris: List<Uri>) + + /** + * Load preview image; caching is allowed. + */ + override suspend fun invoke(uri: Uri) = invoke(uri, true) + + /** + * Load preview image. + * @param uri content URI + * @param caching indicates if the loaded image could be cached. + */ + override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? +} diff --git a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java index ece0c312..746da49e 100644 --- a/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java @@ -27,7 +27,6 @@ import android.widget.TextView; import androidx.annotation.Nullable; -import com.android.intentresolver.ImageLoader; import com.android.intentresolver.R; import com.android.intentresolver.widget.ActionRow; @@ -125,10 +124,6 @@ class TextContentPreviewUi extends ContentPreviewUi { private List<ActionRow.Action> createTextPreviewActions() { ArrayList<ActionRow.Action> actions = new ArrayList<>(2); actions.add(mActionFactory.createCopyButton()); - ActionRow.Action nearbyAction = mActionFactory.createNearbyButton(); - if (nearbyAction != null) { - actions.add(nearbyAction); - } return actions; } } diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java index 9ce875c8..f52d233d 100644 --- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java +++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java @@ -19,19 +19,13 @@ package com.android.intentresolver.contentpreview; import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE; import android.content.res.Resources; -import android.text.TextUtils; -import android.text.util.Linkify; -import android.transition.TransitionManager; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.CheckBox; -import android.widget.TextView; import androidx.annotation.Nullable; -import com.android.intentresolver.ImageLoader; import com.android.intentresolver.R; import com.android.intentresolver.widget.ActionRow; import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback; @@ -40,12 +34,10 @@ import com.android.intentresolver.widget.ScrollableImagePreviewView; import java.util.ArrayList; import java.util.List; import java.util.Objects; -import java.util.function.Consumer; class UnifiedContentPreviewUi extends ContentPreviewUi { private final List<FileInfo> mFiles; @Nullable - private final CharSequence mText; private final ChooserContentPreviewUi.ActionFactory mActionFactory; private final ImageLoader mImageLoader; private final MimeTypeClassifier mTypeClassifier; @@ -54,14 +46,12 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { UnifiedContentPreviewUi( List<FileInfo> files, - @Nullable CharSequence text, ChooserContentPreviewUi.ActionFactory actionFactory, ImageLoader imageLoader, MimeTypeClassifier typeClassifier, TransitionElementStatusCallback transitionElementStatusCallback, HeadlineGenerator headlineGenerator) { mFiles = files; - mText = text; mActionFactory = actionFactory; mImageLoader = imageLoader; mTypeClassifier = typeClassifier; @@ -132,34 +122,25 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { mFiles.size() - previews.size(), mImageLoader); - if (!TextUtils.isEmpty(mText) && mFiles.size() == 1 && allImages) { - setTextInImagePreviewVisibility(contentPreviewLayout, imagePreview, mActionFactory); - updateTextWithImageHeadline(contentPreviewLayout); + if (allImages) { + displayHeadline( + contentPreviewLayout, mHeadlineGenerator.getImagesHeadline(mFiles.size())); + } else if (allVideos) { + displayHeadline( + contentPreviewLayout, mHeadlineGenerator.getVideosHeadline(mFiles.size())); } else { - if (allImages) { - displayHeadline( - contentPreviewLayout, mHeadlineGenerator.getImagesHeadline(mFiles.size())); - } else if (allVideos) { - displayHeadline( - contentPreviewLayout, mHeadlineGenerator.getVideosHeadline(mFiles.size())); - } else { - displayHeadline( - contentPreviewLayout, mHeadlineGenerator.getItemsHeadline(mFiles.size())); - } + displayHeadline( + contentPreviewLayout, mHeadlineGenerator.getItemsHeadline(mFiles.size())); } return contentPreviewLayout; } private List<ActionRow.Action> createImagePreviewActions() { - ArrayList<ActionRow.Action> actions = new ArrayList<>(2); + ArrayList<ActionRow.Action> actions = new ArrayList<>(1); //TODO: add copy action; - ActionRow.Action action = mActionFactory.createNearbyButton(); - if (action != null) { - actions.add(action); - } if (mFiles.size() == 1 && mTypeClassifier.isImageType(mFiles.get(0).getMimeType())) { - action = mActionFactory.createEditButton(); + ActionRow.Action action = mActionFactory.createEditButton(); if (action != null) { actions.add(action); } @@ -167,50 +148,6 @@ class UnifiedContentPreviewUi extends ContentPreviewUi { return actions; } - private void updateTextWithImageHeadline(ViewGroup contentPreview) { - CheckBox actionView = contentPreview.requireViewById(R.id.include_text_action); - if (actionView.getVisibility() == View.VISIBLE && actionView.isChecked()) { - displayHeadline(contentPreview, mHeadlineGenerator.getImageWithTextHeadline(mText)); - } else { - displayHeadline( - contentPreview, mHeadlineGenerator.getImagesHeadline(mFiles.size())); - } - } - - private void setTextInImagePreviewVisibility( - ViewGroup contentPreview, - ScrollableImagePreviewView imagePreview, - ChooserContentPreviewUi.ActionFactory actionFactory) { - final TextView textView = contentPreview - .requireViewById(com.android.internal.R.id.content_preview_text); - CheckBox actionView = contentPreview - .requireViewById(R.id.include_text_action); - textView.setVisibility(View.VISIBLE); - boolean isLink = HttpUriMatcher.isHttpUri(mText.toString()); - textView.setAutoLinkMask(isLink ? Linkify.WEB_URLS : 0); - textView.setText(mText); - - final int[] actionLabels = isLink - ? new int[] { R.string.include_link, R.string.exclude_link } - : new int[] { R.string.include_text, R.string.exclude_text }; - final Consumer<Boolean> shareTextAction = actionFactory.getExcludeSharedTextAction(); - actionView.setChecked(true); - actionView.setText(actionLabels[1]); - shareTextAction.accept(false); - actionView.setOnCheckedChangeListener((view, isChecked) -> { - view.setText(actionLabels[isChecked ? 1 : 0]); - textView.setEnabled(isChecked); - if (imagePreview.getVisibility() == View.VISIBLE) { - // animate only only if we have preview - TransitionManager.beginDelayedTransition((ViewGroup) textView.getParent()); - textView.setVisibility(isChecked ? View.VISIBLE : View.GONE); - } - shareTextAction.accept(!isChecked); - updateTextWithImageHeadline(contentPreview); - }); - actionView.setVisibility(View.VISIBLE); - } - private ScrollableImagePreviewView.PreviewType getPreviewType(String mimeType) { if (mTypeClassifier.isImageType(mimeType)) { return ScrollableImagePreviewView.PreviewType.Image; diff --git a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java index 7357fde9..bc54e01e 100644 --- a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java +++ b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java @@ -16,6 +16,7 @@ package com.android.intentresolver.model; +import android.annotation.Nullable; import android.app.usage.UsageStatsManager; import android.content.ComponentName; import android.content.Context; @@ -34,8 +35,6 @@ import com.android.intentresolver.ResolvedComponentInfo; import com.android.intentresolver.ResolverActivity; import com.android.intentresolver.chooser.TargetInfo; -import com.google.android.collect.Lists; - import java.text.Collator; import java.util.ArrayList; import java.util.Comparator; @@ -59,6 +58,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC protected final Map<UserHandle, UsageStatsManager> mUsmMap = new HashMap<>(); protected String[] mAnnotations; protected String mContentType; + protected final ComponentName mPromoteToFirst; // True if the current share is a link. private final boolean mHttp; @@ -109,32 +109,18 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC * Constructor to initialize the comparator. * @param launchedFromContext the activity calling this comparator * @param intent original intent - * @param resolvedActivityUserSpace refers to the userSpace used by the comparator for - * fetching activity stats and recording activity selection. - * The latter could be different from the userSpace provided by - * context. - */ - public AbstractResolverComparator( - Context launchedFromContext, - Intent intent, - UserHandle resolvedActivityUserSpace) { - this(launchedFromContext, intent, Lists.newArrayList(resolvedActivityUserSpace)); - } - - - /** - * Constructor to initialize the comparator. - * @param launchedFromContext the activity calling this comparator - * @param intent original intent * @param resolvedActivityUserSpaceList refers to the userSpace(s) used by the comparator for * fetching activity stats and recording activity * selection. The latter could be different from the * userSpace provided by context. + * @param promoteToFirst a component to be moved to the front of the app list if it's being + * ranked. Unlike pinned apps, this cannot be modified by the user. */ public AbstractResolverComparator( Context launchedFromContext, Intent intent, - List<UserHandle> resolvedActivityUserSpaceList) { + List<UserHandle> resolvedActivityUserSpaceList, + @Nullable ComponentName promoteToFirst) { String scheme = intent.getScheme(); mHttp = "http".equals(scheme) || "https".equals(scheme); mContentType = intent.getType(); @@ -147,6 +133,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC (UsageStatsManager) userContext.getSystemService(Context.USAGE_STATS_SERVICE)); } mAzComparator = new AzInfoComparator(launchedFromContext); + mPromoteToFirst = promoteToFirst; } // get annotations of content from intent. @@ -202,6 +189,16 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC return -1; } + if (mPromoteToFirst != null) { + // A single component can be cemented to the front of the list. If it is seen, let it + // always get priority. + if (mPromoteToFirst.equals(lhs.activityInfo.getComponentName())) { + return -1; + } else if (mPromoteToFirst.equals(rhs.activityInfo.getComponentName())) { + return 1; + } + } + if (mHttp) { final boolean lhsSpecific = ResolverActivity.isSpecificUriMatch(lhs.match); final boolean rhsSpecific = ResolverActivity.isSpecificUriMatch(rhs.match); diff --git a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java index 84dca3ff..ba054731 100644 --- a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java +++ b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java @@ -72,8 +72,9 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp String referrerPackage, AppPredictor appPredictor, UserHandle user, - ChooserActivityLogger chooserActivityLogger) { - super(context, intent, Lists.newArrayList(user)); + ChooserActivityLogger chooserActivityLogger, + @Nullable ComponentName promoteToFirst) { + super(context, intent, Lists.newArrayList(user), promoteToFirst); mContext = context; mIntent = intent; mAppPredictor = appPredictor; @@ -116,7 +117,8 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp mReferrerPackage, () -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT), getChooserActivityLogger(), - mUser); + mUser, + mPromoteToFirst); mComparatorModel = buildUpdatedModel(); mResolverRankerService.compute(targets); } else { diff --git a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java index 725212e4..ebaffc36 100644 --- a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java +++ b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java @@ -102,9 +102,10 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom */ public ResolverRankerServiceResolverComparator(Context launchedFromContext, Intent intent, String referrerPackage, Runnable afterCompute, - ChooserActivityLogger chooserActivityLogger, UserHandle targetUserSpace) { + ChooserActivityLogger chooserActivityLogger, UserHandle targetUserSpace, + ComponentName promoteToFirst) { this(launchedFromContext, intent, referrerPackage, afterCompute, chooserActivityLogger, - Lists.newArrayList(targetUserSpace)); + Lists.newArrayList(targetUserSpace), promoteToFirst); } /** @@ -117,8 +118,9 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom */ public ResolverRankerServiceResolverComparator(Context launchedFromContext, Intent intent, String referrerPackage, Runnable afterCompute, - ChooserActivityLogger chooserActivityLogger, List<UserHandle> targetUserSpaceList) { - super(launchedFromContext, intent, targetUserSpaceList); + ChooserActivityLogger chooserActivityLogger, List<UserHandle> targetUserSpaceList, + @Nullable ComponentName promoteToFirst) { + super(launchedFromContext, intent, targetUserSpaceList, promoteToFirst); mCollator = Collator.getInstance( launchedFromContext.getResources().getConfiguration().locale); mReferrerPackage = referrerPackage; diff --git a/java/src/com/android/intentresolver/widget/ChooserActionRow.kt b/java/src/com/android/intentresolver/widget/ChooserActionRow.kt deleted file mode 100644 index a4656bb5..00000000 --- a/java/src/com/android/intentresolver/widget/ChooserActionRow.kt +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver.widget - -import android.annotation.LayoutRes -import android.content.Context -import android.os.Parcelable -import android.util.AttributeSet -import android.view.LayoutInflater -import android.widget.Button -import android.widget.LinearLayout -import com.android.intentresolver.R -import com.android.intentresolver.widget.ActionRow.Action - -class ChooserActionRow : LinearLayout, ActionRow { - constructor(context: Context) : this(context, null) - constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) - constructor( - context: Context, attrs: AttributeSet?, defStyleAttr: Int - ) : this(context, attrs, defStyleAttr, 0) - - constructor( - context: Context, attrs: AttributeSet?, defStyleAttr: Int, defStyleRes: Int - ) : super(context, attrs, defStyleAttr, defStyleRes) { - orientation = HORIZONTAL - } - - @LayoutRes - private val itemLayout = R.layout.chooser_action_button - private val itemMargin = - context.resources.getDimensionPixelSize(R.dimen.resolver_icon_margin) / 2 - private var actions: List<Action> = emptyList() - - override fun onRestoreInstanceState(state: Parcelable?) { - super.onRestoreInstanceState(state) - setActions(actions) - } - - override fun setActions(actions: List<Action>) { - removeAllViews() - this.actions = ArrayList(actions) - for (action in actions) { - addAction(action) - } - } - - private fun addAction(action: Action) { - val b = LayoutInflater.from(context).inflate(itemLayout, null) as Button - if (action.icon != null) { - val size = resources - .getDimensionPixelSize(R.dimen.chooser_action_button_icon_size) - action.icon.setBounds(0, 0, size, size) - b.setCompoundDrawablesRelative(action.icon, null, null, null) - } - b.text = action.label ?: "" - b.setOnClickListener { - action.onClicked.run() - } - b.id = action.id - addView(b) - } - - override fun generateDefaultLayoutParams(): LayoutParams = - super.generateDefaultLayoutParams().apply { - setMarginsRelative(itemMargin, 0, itemMargin, 0) - } -} diff --git a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt index 5f92b149..3f0458ee 100644 --- a/java/src/com/android/intentresolver/widget/ImagePreviewView.kt +++ b/java/src/com/android/intentresolver/widget/ImagePreviewView.kt @@ -16,12 +16,8 @@ package com.android.intentresolver.widget -import android.graphics.Bitmap -import android.net.Uri import android.view.View -internal typealias ImageLoader = suspend (Uri) -> Bitmap? - interface ImagePreviewView { fun setTransitionElementStatusCallback(callback: TransitionElementStatusCallback?) fun getTransitionView(): View? diff --git a/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt b/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt index f2a8b9e8..8fdb609e 100644 --- a/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt +++ b/java/src/com/android/intentresolver/widget/ScrollableActionRow.kt @@ -35,9 +35,16 @@ class ScrollableActionRow : RecyclerView, ActionRow { ) : super(context, attrs, defStyleAttr) { layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) adapter = Adapter(context) + + context.obtainStyledAttributes( + attrs, R.styleable.ScrollableActionRow, defStyleAttr, 0 + ).use { a -> + horizontalActions = a.getBoolean(R.styleable.ScrollableActionRow_horizontalActions, false) + } } private val actionsAdapter get() = adapter as Adapter + private val horizontalActions: Boolean override fun setActions(actions: List<ActionRow.Action>) { actionsAdapter.setActions(actions) @@ -50,7 +57,7 @@ class ScrollableActionRow : RecyclerView, ActionRow { ) } - private class Adapter(private val context: Context) : RecyclerView.Adapter<ViewHolder>() { + private inner class Adapter(private val context: Context) : RecyclerView.Adapter<ViewHolder>() { private val iconSize: Int = context.resources.getDimensionPixelSize(R.dimen.chooser_action_view_icon_size) private val itemLayout = R.layout.chooser_action_view @@ -59,7 +66,7 @@ class ScrollableActionRow : RecyclerView, ActionRow { override fun onCreateViewHolder(parent: ViewGroup, type: Int): ViewHolder = ViewHolder( LayoutInflater.from(context).inflate(itemLayout, null) as TextView, - iconSize + iconSize, ) override fun onBindViewHolder(holder: ViewHolder, position: Int) { @@ -83,8 +90,8 @@ class ScrollableActionRow : RecyclerView, ActionRow { } } - private class ViewHolder( - private val view: TextView, private val iconSize: Int + private inner class ViewHolder( + private val view: TextView, private val iconSize: Int, ) : RecyclerView.ViewHolder(view) { fun bind(action: ActionRow.Action) { @@ -93,7 +100,11 @@ class ScrollableActionRow : RecyclerView, ActionRow { // some drawables (edit) does not gets tinted when set to the top of the text // with TextView#setCompoundDrawableRelative tintIcon(icon, view) - view.setCompoundDrawablesRelative(null, icon, null, null) + if (horizontalActions) { + view.setCompoundDrawablesRelative(icon, null, null, null) + } else { + view.setCompoundDrawablesRelative(null, icon, null, null) + } } view.text = action.label ?: "" view.setOnClickListener { diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt index 7755610d..e760e6d0 100644 --- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt +++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt @@ -56,6 +56,8 @@ private const val MIN_ASPECT_RATIO_STRING = "2:5" private const val MAX_ASPECT_RATIO = 2.5f private const val MAX_ASPECT_RATIO_STRING = "5:2" +private typealias CachingImageLoader = suspend (Uri, Boolean) -> Bitmap? + class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { constructor(context: Context) : this(context, null) constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0) @@ -76,7 +78,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { TypedValue.COMPLEX_UNIT_DIP, 3f, context.resources.displayMetrics ).toInt() } - var outerSpacing = a.getDimensionPixelSize( + outerSpacing = a.getDimensionPixelSize( R.styleable.ScrollableImagePreviewView_itemOuterSpacing, -1 ) if (outerSpacing < 0) { @@ -102,12 +104,24 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { var maxWidthHint: Int = -1 private var requestedHeight: Int = 0 private var isMeasured = false + private var maxAspectRatio = MAX_ASPECT_RATIO + private var maxAspectRatioString = MAX_ASPECT_RATIO_STRING + private var outerSpacing: Int = 0 override fun onMeasure(widthSpec: Int, heightSpec: Int) { super.onMeasure(widthSpec, heightSpec) if (!isMeasured) { isMeasured = true - batchLoader?.loadAspectRatios(getMaxWidth(), this::calcPreviewWidth) + updateMaxWidthHint(widthSpec) + updateMaxAspectRatio() + batchLoader?.loadAspectRatios(getMaxWidth(), this::updatePreviewSize) + } + } + + private fun updateMaxWidthHint(widthSpec: Int) { + if (maxWidthHint > 0) return + if (View.MeasureSpec.getMode(widthSpec) != View.MeasureSpec.UNSPECIFIED) { + maxWidthHint = View.MeasureSpec.getSize(widthSpec) } } @@ -131,7 +145,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { return null } - fun setPreviews(previews: List<Preview>, otherItemCount: Int, imageLoader: ImageLoader) { + fun setPreviews(previews: List<Preview>, otherItemCount: Int, imageLoader: CachingImageLoader) { previewAdapter.reset(0, imageLoader) batchLoader?.cancel() batchLoader = BatchPreviewLoader( @@ -144,7 +158,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { } .apply { if (isMeasured) { - loadAspectRatios(getMaxWidth(), this@ScrollableImagePreviewView::calcPreviewWidth) + loadAspectRatios(getMaxWidth(), this@ScrollableImagePreviewView::updatePreviewSize) } } } @@ -158,14 +172,39 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { else -> measuredWidth } - private fun calcPreviewWidth(bitmap: Bitmap): Int { + private fun updateMaxAspectRatio() { + val padding = outerSpacing * 2 + val w = maxOf(padding, getMaxWidth() - padding) + val h = if (isLaidOut) height else measuredHeight + if (w > 0 && h > 0) { + maxAspectRatio = (w.toFloat() / h.toFloat()) + .coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO) + maxAspectRatioString = when { + maxAspectRatio <= MIN_ASPECT_RATIO -> MIN_ASPECT_RATIO_STRING + maxAspectRatio >= MAX_ASPECT_RATIO -> MAX_ASPECT_RATIO_STRING + else -> "$w:$h" + } + } + } + + /** + * Sets [preview]'s aspect ratio based on the preview image size. + * @return adjusted preview width + */ + private fun updatePreviewSize(preview: Preview, width: Int, height: Int): Int { val effectiveHeight = if (isLaidOut) height else measuredHeight - return if (bitmap.width <= 0 || bitmap.height <= 0) { + return if (width <= 0 || height <= 0) { + preview.aspectRatioString = "1:1" effectiveHeight } else { - val ar = (bitmap.width.toFloat() / bitmap.height.toFloat()) - .coerceIn(MIN_ASPECT_RATIO, MAX_ASPECT_RATIO) - (effectiveHeight * ar).roundToInt() + val aspectRatio = (width.toFloat() / height.toFloat()) + .coerceIn(MIN_ASPECT_RATIO, maxAspectRatio) + preview.aspectRatioString = when { + aspectRatio <= MIN_ASPECT_RATIO -> MIN_ASPECT_RATIO_STRING + aspectRatio >= maxAspectRatio -> maxAspectRatioString + else -> "$width:$height" + } + (effectiveHeight * aspectRatio).toInt() } } @@ -175,18 +214,6 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { internal var aspectRatioString: String ) { constructor(type: PreviewType, uri: Uri) : this(type, uri, "1:1") - - internal var bitmap: Bitmap? = null - - internal fun updateAspectRatio(width: Int, height: Int) { - if (width <= 0 || height <= 0) return - val aspectRatio = width.toFloat() / height.toFloat() - aspectRatioString = when { - aspectRatio <= MIN_ASPECT_RATIO -> MIN_ASPECT_RATIO_STRING - aspectRatio >= MAX_ASPECT_RATIO -> MAX_ASPECT_RATIO_STRING - else -> "$width:$height" - } - } } enum class PreviewType { @@ -197,7 +224,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { private val context: Context ) : RecyclerView.Adapter<ViewHolder>() { private val previews = ArrayList<Preview>() - private var imageLoader: ImageLoader? = null + private var imageLoader: CachingImageLoader? = null private var firstImagePos = -1 private var totalItemCount: Int = 0 @@ -206,7 +233,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { var transitionStatusElementCallback: TransitionElementStatusCallback? = null - fun reset(totalItemCount: Int, imageLoader: ImageLoader) { + fun reset(totalItemCount: Int, imageLoader: CachingImageLoader) { this.imageLoader = imageLoader firstImagePos = -1 previews.clear() @@ -299,7 +326,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { fun bind( preview: Preview, - imageLoader: ImageLoader, + imageLoader: CachingImageLoader, isSharedTransitionElement: Boolean, previewReadyCallback: ((String) -> Unit)? ) { @@ -334,11 +361,11 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { } } - private suspend fun loadImage(preview: Preview, imageLoader: ImageLoader) { - val bitmap = preview.bitmap ?: runCatching { + private suspend fun loadImage(preview: Preview, imageLoader: CachingImageLoader) { + val bitmap = runCatching { // it's expected for all loading/caching optimizations to be implemented by the // loader - imageLoader(preview.uri) + imageLoader(preview.uri, true) }.getOrNull() image.setImageBitmap(bitmap) } @@ -384,7 +411,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { private class BatchPreviewLoader( private val adapter: Adapter, - private val imageLoader: ImageLoader, + private val imageLoader: CachingImageLoader, previews: List<Preview>, otherItemCount: Int, private val onNoPreviewCallback: (() -> Unit) @@ -398,7 +425,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { scope = null } - fun loadAspectRatios(maxWidth: Int, previewWidthCalculator: (Bitmap) -> Int) { + fun loadAspectRatios(maxWidth: Int, previewSizeUpdater: (Preview, Int, Int) -> Int) { val scope = this.scope ?: return val updates = ArrayDeque<Preview>(pendingPreviews.size) // replay 2 items to guarantee that we'd get at least one update @@ -435,18 +462,16 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView { launch { while (pendingPreviews.isNotEmpty()) { val preview = pendingPreviews.poll() ?: continue + val isVisible = loadedPreviewWidth < maxWidth val bitmap = runCatching { // TODO: decide on adding a timeout - imageLoader(preview.uri) + imageLoader(preview.uri, isVisible) }.getOrNull() ?: continue - preview.updateAspectRatio(bitmap.width, bitmap.height) + val previewWidth = + previewSizeUpdater(preview, bitmap.width, bitmap.height) updates.add(preview) - if (loadedPreviewWidth < maxWidth) { - loadedPreviewWidth += previewWidthCalculator(bitmap) - // cache bitmaps for the first preview items to aovid potential - // double-loading (in case those values are evicted from the image - // loader's cache) - preview.bitmap = bitmap + if (isVisible) { + loadedPreviewWidth += previewWidth if (loadedPreviewWidth >= maxWidth) { // notify that the preview now can be displayed reportFlow.emit(updateEvent) diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java index 2a4d654a..9ebeb79d 100644 --- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java +++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java @@ -28,6 +28,7 @@ import android.os.UserHandle; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.contentpreview.ImageLoader; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.shortcuts.ShortcutLoader; diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java index dc9baade..d23e4a66 100644 --- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java +++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -35,6 +35,7 @@ import android.os.UserHandle; import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.contentpreview.ImageLoader; import com.android.intentresolver.flags.FeatureFlagRepository; import com.android.intentresolver.grid.ChooserGridAdapter; import com.android.intentresolver.shortcuts.ShortcutLoader; diff --git a/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt b/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt index f327e19e..3c399cc4 100644 --- a/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt +++ b/java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt @@ -19,11 +19,18 @@ package com.android.intentresolver import android.content.ContentResolver import android.content.Context import android.content.res.Resources +import android.graphics.Bitmap import android.net.Uri import android.util.Size import androidx.lifecycle.Lifecycle +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CoroutineStart.UNDISPATCHED import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.resetMain @@ -41,7 +48,10 @@ class ImagePreviewImageLoaderTest { private val imageSize = Size(300, 300) private val uriOne = Uri.parse("content://org.package.app/image-1.png") private val uriTwo = Uri.parse("content://org.package.app/image-2.png") - private val contentResolver = mock<ContentResolver>() + private val bitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + private val contentResolver = mock<ContentResolver> { + whenever(loadThumbnail(any(), any(), anyOrNull())).thenReturn(bitmap) + } private val resources = mock<Resources> { whenever(getDimensionPixelSize(R.dimen.chooser_preview_image_max_dimen)) .thenReturn(imageSize.width) @@ -70,7 +80,7 @@ class ImagePreviewImageLoaderTest { } @Test - fun test_prePopulate() = runTest { + fun prePopulate_cachesImagesUpToTheCacheSize() = runTest { testSubject.prePopulate(listOf(uriOne, uriTwo)) verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) @@ -81,7 +91,7 @@ class ImagePreviewImageLoaderTest { } @Test - fun test_invoke_return_cached_image() = runTest { + fun invoke_returnCachedImageWhenCalledTwice() = runTest { testSubject(uriOne) testSubject(uriOne) @@ -89,7 +99,33 @@ class ImagePreviewImageLoaderTest { } @Test - fun test_invoke_old_records_evicted_from_the_cache() = runTest { + fun invoke_whenInstructed_doesNotCache() = runTest { + testSubject(uriOne, false) + testSubject(uriOne, false) + + verify(contentResolver, times(2)).loadThumbnail(any(), any(), anyOrNull()) + } + + @Test + fun invoke_overlappedRequests_Deduplicate() = runTest { + val scheduler = TestCoroutineScheduler() + val dispatcher = StandardTestDispatcher(scheduler) + val testSubject = ImagePreviewImageLoader(context, lifecycleOwner.lifecycle, 1, dispatcher) + coroutineScope { + launch(start = UNDISPATCHED) { + testSubject(uriOne, false) + } + launch(start = UNDISPATCHED) { + testSubject(uriOne, false) + } + scheduler.advanceUntilIdle() + } + + verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull()) + } + + @Test + fun invoke_oldRecordsEvictedFromTheCache() = runTest { testSubject(uriOne) testSubject(uriTwo) testSubject(uriTwo) @@ -98,4 +134,53 @@ class ImagePreviewImageLoaderTest { verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) verify(contentResolver, times(1)).loadThumbnail(uriTwo, imageSize, null) } + + @Test + fun invoke_doNotCacheNulls() = runTest { + whenever(contentResolver.loadThumbnail(any(), any(), anyOrNull())).thenReturn(null) + testSubject(uriOne) + testSubject(uriOne) + + verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) + } + + @Test(expected = CancellationException::class) + fun invoke_onClosedImageLoaderScope_throwsCancellationException() = runTest { + lifecycleOwner.state = Lifecycle.State.DESTROYED + testSubject(uriOne) + } + + @Test(expected = CancellationException::class) + fun invoke_imageLoaderScopeClosedMidflight_throwsCancellationException() = runTest { + val scheduler = TestCoroutineScheduler() + val dispatcher = StandardTestDispatcher(scheduler) + val testSubject = ImagePreviewImageLoader(context, lifecycleOwner.lifecycle, 1, dispatcher) + coroutineScope { + val deferred = async(start = UNDISPATCHED) { + testSubject(uriOne, false) + } + lifecycleOwner.state = Lifecycle.State.DESTROYED + scheduler.advanceUntilIdle() + deferred.await() + } + } + + @Test + fun invoke_multipleCallsWithDifferentCacheInstructions_cachingPrevails() = runTest { + val scheduler = TestCoroutineScheduler() + val dispatcher = StandardTestDispatcher(scheduler) + val testSubject = ImagePreviewImageLoader(context, lifecycleOwner.lifecycle, 1, dispatcher) + coroutineScope { + launch(start = UNDISPATCHED) { + testSubject(uriOne, false) + } + launch(start = UNDISPATCHED) { + testSubject(uriOne, true) + } + scheduler.advanceUntilIdle() + } + testSubject(uriOne, true) + + verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) + } } diff --git a/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt b/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt index 2f240d58..74a253b8 100644 --- a/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt +++ b/java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt @@ -18,6 +18,7 @@ package com.android.intentresolver import android.graphics.Bitmap import android.net.Uri +import com.android.intentresolver.contentpreview.ImageLoader import java.util.function.Consumer internal class TestPreviewImageLoader( @@ -27,6 +28,7 @@ internal class TestPreviewImageLoader( callback.accept(bitmaps[uri]) } - override suspend fun invoke(uri: Uri): Bitmap? = bitmaps[uri] + override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = bitmaps[uri] + override fun prePopulate(uris: List<Uri>) = Unit } diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java index 0a60b8c7..385f9fd8 100644 --- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -81,6 +81,7 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; +import android.graphics.Rect; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; @@ -92,6 +93,7 @@ import android.util.HashedStringCache; import android.util.Pair; import android.util.SparseArray; import android.view.View; +import android.view.WindowManager; import androidx.annotation.CallSuper; import androidx.annotation.NonNull; @@ -104,6 +106,7 @@ import androidx.test.platform.app.InstrumentationRegistry; import androidx.test.rule.ActivityTestRule; import com.android.intentresolver.chooser.DisplayResolveInfo; +import com.android.intentresolver.contentpreview.ImageLoader; import com.android.intentresolver.shortcuts.ShortcutLoader; import com.android.internal.config.sysui.SystemUiDeviceConfigFlags; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; @@ -854,9 +857,9 @@ public class UnbundledChooserActivityTest { .perform(click()); waitForIdle(); - onView(withId(R.id.scrollable_image_preview)) + onView(withId(R.id.image_view)) .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); - onView(withId(com.android.internal.R.id.content_preview_text)) + onView(withId(R.id.content_preview_text)) .check(matches(allOf(isDisplayed(), not(isEnabled())))); } @@ -958,7 +961,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendUriIntentWithPreview(uris); ChooserActivityOverrideData.getInstance().imageLoader = - createImageLoader(uri, createBitmap()); + createImageLoader(uri, createWideBitmap()); ChooserActivityOverrideData.getInstance().isImageType = true; List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -974,9 +977,14 @@ public class UnbundledChooserActivityTest { RecyclerView recyclerView = (RecyclerView) view; assertThat(recyclerView.getAdapter().getItemCount(), is(1)); assertThat(recyclerView.getChildCount(), is(1)); + View imageView = recyclerView.getChildAt(0); + Rect rect = new Rect(); + boolean isPartiallyVisible = imageView.getGlobalVisibleRect(rect); assertThat( - "image preview view is fully visible", - isDisplayed().matches(recyclerView.getChildAt(0))); + "image preview view is not fully visible", + isPartiallyVisible + && rect.width() == imageView.getWidth() + && rect.height() == imageView.getHeight()); }); } @@ -1066,7 +1074,7 @@ public class UnbundledChooserActivityTest { } @Test - public void testNoTextPreviewWhenTextIsSharedWithMultipleImages() { + public void testTextPreviewWhenTextIsSharedWithMultipleImages() { final Uri uri = Uri.parse("android.resource://com.android.frameworks.coretests/" + R.drawable.test320x240); final String sharedText = "text-" + System.currentTimeMillis(); @@ -1096,8 +1104,7 @@ public class UnbundledChooserActivityTest { .thenReturn(resolvedComponentInfos); mActivityRule.launchActivity(Intent.createChooser(sendIntent, null)); waitForIdle(); - onView(withId(com.android.internal.R.id.content_preview_text)) - .check(matches(withEffectiveVisibility(ViewMatchers.Visibility.GONE))); + onView(withText(sharedText)).check(matches(isDisplayed())); } @Test @@ -2784,8 +2791,22 @@ public class UnbundledChooserActivityTest { } private Bitmap createBitmap() { - int width = 200; - int height = 200; + return createBitmap(200, 200); + } + + private Bitmap createWideBitmap() { + WindowManager windowManager = InstrumentationRegistry.getInstrumentation() + .getTargetContext() + .getSystemService(WindowManager.class); + int width = 3000; + if (windowManager != null) { + Rect bounds = windowManager.getMaximumWindowMetrics().getBounds(); + width = bounds.width() + 200; + } + return createBitmap(width, 100); + } + + private Bitmap createBitmap(int width, int height) { Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(bitmap); diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt index 7b9a0ce6..f29fac84 100644 --- a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt +++ b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt @@ -21,7 +21,6 @@ import android.content.ContentInterface import android.content.Intent import android.graphics.Bitmap import android.net.Uri -import com.android.intentresolver.ImageLoader import com.android.intentresolver.any import com.android.intentresolver.anyOrNull import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory @@ -48,12 +47,11 @@ class ChooserContentPreviewUiTest { callback.accept(null) } override fun prePopulate(uris: List<Uri>) = Unit - override suspend fun invoke(uri: Uri): Bitmap? = null + override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = null } private val actionFactory = object : ActionFactory { override fun createCopyButton() = ActionRow.Action(label = "Copy", icon = null) {} override fun createEditButton(): ActionRow.Action? = null - override fun createNearbyButton(): ActionRow.Action? = null override fun createCustomActions(): List<ActionRow.Action> = emptyList() override fun getModifyShareAction(): ActionRow.Action? = null override fun getExcludeSharedTextAction(): Consumer<Boolean> = Consumer<Boolean> {} diff --git a/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt index 9becce99..aac6caa7 100644 --- a/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt +++ b/java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt @@ -28,14 +28,26 @@ class HeadlineGeneratorImplTest { fun testHeadlineGeneration() { val generator = HeadlineGeneratorImpl( InstrumentationRegistry.getInstrumentation().getTargetContext()) - val str = "Some sting" + val str = "Some string" val url = "http://www.google.com" assertThat(generator.getTextHeadline(str)).isEqualTo("Sharing text") assertThat(generator.getTextHeadline(url)).isEqualTo("Sharing link") - assertThat(generator.getImageWithTextHeadline(str)).isEqualTo("Sharing image with text") - assertThat(generator.getImageWithTextHeadline(url)).isEqualTo("Sharing image with link") + assertThat(generator.getImagesWithTextHeadline(str, 1)).isEqualTo("Sharing image with text") + assertThat(generator.getImagesWithTextHeadline(url, 1)).isEqualTo("Sharing image with link") + assertThat(generator.getImagesWithTextHeadline(str, 5)).isEqualTo("Sharing 5 images with text") + assertThat(generator.getImagesWithTextHeadline(url, 5)).isEqualTo("Sharing 5 images with link") + + assertThat(generator.getVideosWithTextHeadline(str, 1)).isEqualTo("Sharing video with text") + assertThat(generator.getVideosWithTextHeadline(url, 1)).isEqualTo("Sharing video with link") + assertThat(generator.getVideosWithTextHeadline(str, 5)).isEqualTo("Sharing 5 videos with text") + assertThat(generator.getVideosWithTextHeadline(url, 5)).isEqualTo("Sharing 5 videos with link") + + assertThat(generator.getFilesWithTextHeadline(str, 1)).isEqualTo("Sharing file with text") + assertThat(generator.getFilesWithTextHeadline(url, 1)).isEqualTo("Sharing file with link") + assertThat(generator.getFilesWithTextHeadline(str, 5)).isEqualTo("Sharing 5 files with text") + assertThat(generator.getFilesWithTextHeadline(url, 5)).isEqualTo("Sharing 5 files with link") assertThat(generator.getImagesHeadline(1)).isEqualTo("Sharing image") assertThat(generator.getImagesHeadline(4)).isEqualTo("Sharing 4 images") diff --git a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java index 892a2e28..5f0ead7b 100644 --- a/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java +++ b/java/tests/src/com/android/intentresolver/model/AbstractResolverComparatorTest.java @@ -40,52 +40,82 @@ public class AbstractResolverComparatorTest { @Test public void testPinned() { - ResolvedComponentInfo r1 = new ResolvedComponentInfo( - new ComponentName("package", "class"), new Intent(), new ResolveInfo() - ); + ResolvedComponentInfo r1 = createResolvedComponentInfo( + new ComponentName("package", "class")); r1.setPinned(true); - ResolvedComponentInfo r2 = new ResolvedComponentInfo( - new ComponentName("zackage", "zlass"), new Intent(), new ResolveInfo() - ); + ResolvedComponentInfo r2 = createResolvedComponentInfo( + new ComponentName("zackage", "zlass")); Context context = InstrumentationRegistry.getTargetContext(); - AbstractResolverComparator comparator = getTestComparator(context); + AbstractResolverComparator comparator = getTestComparator(context, null); assertEquals("Pinned ranks over unpinned", -1, comparator.compare(r1, r2)); assertEquals("Unpinned ranks under pinned", 1, comparator.compare(r2, r1)); } - @Test public void testBothPinned() { - ResolveInfo pmInfo1 = new ResolveInfo(); - pmInfo1.activityInfo = new ActivityInfo(); - pmInfo1.activityInfo.packageName = "aaa"; - - ResolvedComponentInfo r1 = new ResolvedComponentInfo( - new ComponentName("package", "class"), new Intent(), pmInfo1); + ResolvedComponentInfo r1 = createResolvedComponentInfo( + new ComponentName("package", "class")); r1.setPinned(true); - ResolveInfo pmInfo2 = new ResolveInfo(); - pmInfo2.activityInfo = new ActivityInfo(); - pmInfo2.activityInfo.packageName = "zzz"; - ResolvedComponentInfo r2 = new ResolvedComponentInfo( - new ComponentName("zackage", "zlass"), new Intent(), pmInfo2); + ResolvedComponentInfo r2 = createResolvedComponentInfo( + new ComponentName("zackage", "zlass")); r2.setPinned(true); Context context = InstrumentationRegistry.getTargetContext(); - AbstractResolverComparator comparator = getTestComparator(context); + AbstractResolverComparator comparator = getTestComparator(context, null); assertEquals("Both pinned should rank alphabetically", -1, comparator.compare(r1, r2)); } - private AbstractResolverComparator getTestComparator(Context context) { + @Test + public void testPromoteToFirst() { + ComponentName promoteToFirst = new ComponentName("promoted-package", "class"); + ResolvedComponentInfo r1 = createResolvedComponentInfo(promoteToFirst); + + ResolvedComponentInfo r2 = createResolvedComponentInfo( + new ComponentName("package", "class")); + + Context context = InstrumentationRegistry.getTargetContext(); + AbstractResolverComparator comparator = getTestComparator(context, promoteToFirst); + + assertEquals("PromoteToFirst ranks over non-cemented", -1, comparator.compare(r1, r2)); + assertEquals("Non-cemented ranks under PromoteToFirst", 1, comparator.compare(r2, r1)); + } + + @Test + public void testPromoteToFirstOverPinned() { + ComponentName cementedComponent = new ComponentName("promoted-package", "class"); + ResolvedComponentInfo r1 = createResolvedComponentInfo(cementedComponent); + + ResolvedComponentInfo r2 = createResolvedComponentInfo( + new ComponentName("package", "class")); + r2.setPinned(true); + + Context context = InstrumentationRegistry.getTargetContext(); + AbstractResolverComparator comparator = getTestComparator(context, cementedComponent); + + assertEquals("PromoteToFirst ranks over pinned", -1, comparator.compare(r1, r2)); + assertEquals("Pinned ranks under PromoteToFirst", 1, comparator.compare(r2, r1)); + } + + private ResolvedComponentInfo createResolvedComponentInfo(ComponentName component) { + ResolveInfo info = new ResolveInfo(); + info.activityInfo = new ActivityInfo(); + info.activityInfo.packageName = component.getPackageName(); + info.activityInfo.name = component.getClassName(); + return new ResolvedComponentInfo(component, new Intent(), info); + } + + private AbstractResolverComparator getTestComparator( + Context context, ComponentName promoteToFirst) { Intent intent = new Intent(); AbstractResolverComparator testComparator = new AbstractResolverComparator(context, intent, - Lists.newArrayList(context.getUser())) { + Lists.newArrayList(context.getUser()), promoteToFirst) { @Override int compare(ResolveInfo lhs, ResolveInfo rhs) { |