summaryrefslogtreecommitdiff
path: root/java
diff options
context:
space:
mode:
Diffstat (limited to 'java')
-rw-r--r--java/res/drawable/chevron_right.xml2
-rw-r--r--java/res/layout/chooser_action_button.xml31
-rw-r--r--java/res/layout/chooser_action_row.xml4
-rw-r--r--java/res/layout/chooser_grid_preview_files_text.xml62
-rw-r--r--java/res/layout/chooser_grid_preview_image.xml48
-rw-r--r--java/res/layout/chooser_grid_preview_text.xml2
-rw-r--r--java/res/values-iw/strings.xml2
-rw-r--r--java/res/values-land/bools.xml (renamed from java/res/values-h480dp/bools.xml)6
-rw-r--r--java/res/values-land/dimens.xml4
-rw-r--r--java/res/values-land/integers.xml19
-rw-r--r--java/res/values-sw600dp/bools.xml20
-rw-r--r--java/res/values-sw600dp/dimens.xml5
-rw-r--r--java/res/values-sw600dp/integers.xml19
-rw-r--r--java/res/values/attrs.xml4
-rw-r--r--java/res/values/bools.xml2
-rw-r--r--java/res/values/dimens.xml2
-rw-r--r--java/res/values/integers.xml19
-rw-r--r--java/res/values/strings.xml47
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java8
-rw-r--r--java/src/com/android/intentresolver/ImageLoader.kt26
-rw-r--r--java/src/com/android/intentresolver/ImagePreviewImageLoader.kt80
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java11
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java193
-rw-r--r--java/src/com/android/intentresolver/contentpreview/HeadlineGenerator.kt6
-rw-r--r--java/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImpl.kt50
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ImageLoader.kt51
-rw-r--r--java/src/com/android/intentresolver/contentpreview/TextContentPreviewUi.java1
-rw-r--r--java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java75
-rw-r--r--java/src/com/android/intentresolver/widget/ChooserActionRow.kt81
-rw-r--r--java/src/com/android/intentresolver/widget/ImagePreviewView.kt4
-rw-r--r--java/src/com/android/intentresolver/widget/ScrollableActionRow.kt21
-rw-r--r--java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt99
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java1
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java1
-rw-r--r--java/tests/src/com/android/intentresolver/ImagePreviewImageLoaderTest.kt93
-rw-r--r--java/tests/src/com/android/intentresolver/TestPreviewImageLoader.kt4
-rw-r--r--java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java41
-rw-r--r--java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt3
-rw-r--r--java/tests/src/com/android/intentresolver/contentpreview/HeadlineGeneratorImplTest.kt18
39 files changed, 797 insertions, 368 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/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index ab2ba91e..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;
@@ -1288,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);
}
@@ -1624,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/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/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
index 3c2ee343..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;
@@ -147,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,
@@ -155,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/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 44a9e654..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;
diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
index ebf9bf11..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,20 +122,15 @@ 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;
@@ -163,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/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 a7273a86..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,7 +47,7 @@ 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) {}
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")