summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp7
-rw-r--r--AndroidManifest-app.xml (renamed from AndroidManifest.xml)48
-rw-r--r--AndroidManifest-lib.xml34
-rw-r--r--java/res/drawable/ic_file_video.xml3
-rw-r--r--java/res/layout-h480dp/image_preview_loading_item.xml32
-rw-r--r--java/res/layout-h480dp/image_preview_other_item.xml31
-rw-r--r--java/res/layout/chooser_action_row.xml5
-rw-r--r--java/res/layout/chooser_action_view.xml2
-rw-r--r--java/res/layout/image_preview_image_item.xml4
-rw-r--r--java/res/layout/image_preview_loading_item.xml33
-rw-r--r--java/res/layout/image_preview_other_item.xml10
-rw-r--r--java/res/values/styles.xml3
-rw-r--r--java/src/com/android/intentresolver/ChooserActionFactory.java17
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java195
-rw-r--r--java/src/com/android/intentresolver/ChooserActivityReEnabler.kt39
-rw-r--r--java/src/com/android/intentresolver/ChooserListAdapter.java12
-rw-r--r--java/src/com/android/intentresolver/ResolverActivity.java125
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java17
-rw-r--r--java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java2
-rw-r--r--java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java46
-rw-r--r--java/src/com/android/intentresolver/contentpreview/JavaFlowHelper.kt51
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt128
-rw-r--r--java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt17
-rw-r--r--java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java53
-rw-r--r--java/src/com/android/intentresolver/flags/Flags.kt7
-rw-r--r--java/src/com/android/intentresolver/icons/LoadIconTask.java4
-rw-r--r--java/src/com/android/intentresolver/logging/EventLog.java (renamed from java/src/com/android/intentresolver/ChooserActivityLogger.java)11
-rw-r--r--java/src/com/android/intentresolver/model/AbstractResolverComparator.java16
-rw-r--r--java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java8
-rw-r--r--java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java10
-rw-r--r--java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt3
-rw-r--r--java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt261
-rw-r--r--java/tests/Android.bp11
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt5
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java5
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt5
-rw-r--r--java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java7
-rw-r--r--java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt38
-rw-r--r--java/tests/src/com/android/intentresolver/IChooserWrapper.java3
-rw-r--r--java/tests/src/com/android/intentresolver/ResolverDataProvider.java28
-rw-r--r--java/tests/src/com/android/intentresolver/TestContentProvider.kt32
-rw-r--r--java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt33
-rw-r--r--java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java253
-rw-r--r--java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt19
-rw-r--r--java/tests/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt225
-rw-r--r--java/tests/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt10
-rw-r--r--java/tests/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt212
-rw-r--r--java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt166
-rw-r--r--java/tests/src/com/android/intentresolver/logging/EventLogTest.java (renamed from java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java)54
-rw-r--r--java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt289
-rw-r--r--java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt24
-rw-r--r--proguard.flags2
52 files changed, 1664 insertions, 991 deletions
diff --git a/Android.bp b/Android.bp
index bab33509..9d0a8ee6 100644
--- a/Android.bp
+++ b/Android.bp
@@ -63,7 +63,7 @@ android_library {
"java/res",
],
- manifest: "AndroidManifest.xml",
+ manifest: "AndroidManifest-lib.xml",
static_libs: [
"androidx.annotation_annotation",
@@ -86,6 +86,10 @@ android_library {
lint: {
strict_updatability_linting: false,
},
+
+ optimize: {
+ proguard_flags_files: ["proguard.flags"],
+ },
}
android_app {
@@ -93,6 +97,7 @@ android_app {
min_sdk_version: "current",
certificate: "platform",
privileged: true,
+ manifest: "AndroidManifest-app.xml",
required: [
"privapp_whitelist_com.android.intentresolver",
],
diff --git a/AndroidManifest.xml b/AndroidManifest-app.xml
index da781a22..57ea497b 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest-app.xml
@@ -22,22 +22,6 @@
android:versionName="2021-11"
coreApp="true">
-
- <uses-permission android:name="android.permission.ACCESS_SHORTCUTS" />
- <uses-permission android:name="android.permission.BIND_RESOLVER_RANKER_SERVICE" />
- <uses-permission android:name="android.permission.GET_ANY_PROVIDER_TYPE" />
- <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
- <uses-permission android:name="android.permission.MANAGE_APP_PREDICTIONS" />
- <uses-permission android:name="android.permission.MANAGE_USERS" />
- <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
- <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
- <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
- <uses-permission android:name="android.permission.SET_CLIP_SOURCE" />
- <uses-permission android:name="android.permission.START_ACTIVITY_AS_CALLER" />
- <uses-permission android:name="android.permission.UNLIMITED_SHORTCUTS_API_CALLS" />
- <uses-permission android:name="android.permission.QUERY_CLONED_APPS" />
- <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
-
<application
android:hardwareAccelerated="true"
android:label="@string/app_label"
@@ -46,15 +30,12 @@
android:requiredForAllUsers="true"
android:supportsRtl="true">
- <activity android:name=".ChooserActivity"
- android:theme="@style/Theme.DeviceDefault.Chooser"
- android:finishOnCloseSystemDialogs="true"
- android:excludeFromRecents="true"
- android:documentLaunchMode="never"
- android:relinquishTaskIdentity="true"
- android:configChanges="screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
- android:visibleToInstantApps="true"
- android:exported="true">
+ <!-- This alias needs to be maintained until there are no more devices that could be
+ upgrading from T QPR3. (b/283722356) -->
+ <activity-alias
+ android:name=".ChooserActivityLauncher"
+ android:targetActivity=".ChooserActivity"
+ android:exported="true">
<!-- This intent filter is assigned a priority greater than 100 so
that it will take precedence over the framework ChooserActivity
@@ -65,14 +46,17 @@
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.VOICE" />
</intent-filter>
- </activity>
+ </activity-alias>
- <receiver android:name=".ChooserActivityReEnabler"
- android:exported="true">
- <intent-filter>
- <action android:name="android.intent.action.BOOT_COMPLETED" />
- </intent-filter>
- </receiver>
+ <activity android:name=".ChooserActivity"
+ android:theme="@style/Theme.DeviceDefault.Chooser"
+ android:finishOnCloseSystemDialogs="true"
+ android:excludeFromRecents="true"
+ android:documentLaunchMode="never"
+ android:relinquishTaskIdentity="true"
+ android:configChanges="screenSize|smallestScreenSize|screenLayout|keyboard|keyboardHidden"
+ android:visibleToInstantApps="true"
+ android:exported="false"/>
</application>
diff --git a/AndroidManifest-lib.xml b/AndroidManifest-lib.xml
new file mode 100644
index 00000000..509d46a5
--- /dev/null
+++ b/AndroidManifest-lib.xml
@@ -0,0 +1,34 @@
+<?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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.intentresolver" >
+ <uses-permission android:name="android.permission.ACCESS_SHORTCUTS" />
+ <uses-permission android:name="android.permission.BIND_RESOLVER_RANKER_SERVICE" />
+ <uses-permission android:name="android.permission.GET_ANY_PROVIDER_TYPE" />
+ <uses-permission android:name="android.permission.INTERACT_ACROSS_USERS_FULL" />
+ <uses-permission android:name="android.permission.MANAGE_APP_PREDICTIONS" />
+ <uses-permission android:name="android.permission.MANAGE_USERS" />
+ <uses-permission android:name="android.permission.PACKAGE_USAGE_STATS" />
+ <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES" />
+ <uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
+ <uses-permission android:name="android.permission.SET_CLIP_SOURCE" />
+ <uses-permission android:name="android.permission.START_ACTIVITY_AS_CALLER" />
+ <uses-permission android:name="android.permission.UNLIMITED_SHORTCUTS_API_CALLS" />
+ <uses-permission android:name="android.permission.QUERY_CLONED_APPS" />
+ <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
+</manifest>
diff --git a/java/res/drawable/ic_file_video.xml b/java/res/drawable/ic_file_video.xml
index ec6e290b..3156c55a 100644
--- a/java/res/drawable/ic_file_video.xml
+++ b/java/res/drawable/ic_file_video.xml
@@ -23,5 +23,6 @@
<path
android:fillColor="@android:color/white"
- android:pathData="m4,20c-0.55,0 -1.02,-0.19 -1.42,-0.57c-0.39,-0.4 -0.58,-0.88 -0.58,-1.43l0,-12c0,-0.55 0.19,-1.02 0.58,-1.4c0.39,-0.4 0.87,-0.6 1.42,-0.6l12,0c0.55,0 1.02,0.2 1.4,0.6c0.4,0.38 0.6,0.85 0.6,1.4l0,4.5l4,-4l0,11l-4,-4l0,4.5c0,0.55 -0.2,1.03 -0.6,1.43c-0.38,0.38 -0.85,0.57 -1.4,0.57l-12,0zm0,-2l12,0l0,-12l-12,0l0,12zm0,0l0,-12l0,12z"/>
+ android:pathData="M2,12C2,6.48 6.48,2 12,2C17.52,2 22,6.48 22,12C22,17.52 17.52,22 12,22C6.48,22 2,17.52 2,12ZM16,12L10,7.5V16.5L16,12Z"
+ android:fillType="evenOdd"/>
</vector>
diff --git a/java/res/layout-h480dp/image_preview_loading_item.xml b/java/res/layout-h480dp/image_preview_loading_item.xml
new file mode 100644
index 00000000..85020e9a
--- /dev/null
+++ b/java/res/layout-h480dp/image_preview_loading_item.xml
@@ -0,0 +1,32 @@
+<!--
+ ~ 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.
+ -->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="@dimen/chooser_preview_image_width"
+ android:layout_height="@dimen/chooser_preview_image_height_tall">
+
+ <ProgressBar
+ android:id="@+id/loading_indicator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:indeterminate="true"
+ android:indeterminateTint="?androidprv:attr/materialColorPrimary"
+ android:indeterminateTintMode="src_in" />
+
+</FrameLayout>
diff --git a/java/res/layout-h480dp/image_preview_other_item.xml b/java/res/layout-h480dp/image_preview_other_item.xml
new file mode 100644
index 00000000..470f105a
--- /dev/null
+++ b/java/res/layout-h480dp/image_preview_other_item.xml
@@ -0,0 +1,31 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/chooser_preview_image_width"
+ android:layout_height="@dimen/chooser_preview_image_height_tall">
+
+ <TextView
+ android:id="@+id/label"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:drawableTop="@drawable/ic_file_copy"
+ android:drawablePadding="8dp"
+ android:textAppearance="@style/TextAppearance.ChooserDefault" />
+
+</FrameLayout> \ No newline at end of file
diff --git a/java/res/layout/chooser_action_row.xml b/java/res/layout/chooser_action_row.xml
index 55d6adf7..7bce113e 100644
--- a/java/res/layout/chooser_action_row.xml
+++ b/java/res/layout/chooser_action_row.xml
@@ -20,10 +20,9 @@
<com.android.intentresolver.widget.ScrollableActionRow
android:id="@androidprv:id/chooser_action_row"
- android:layout_width="match_parent"
+ android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_gravity="center_horizontal"
- android:gravity="center"/>
+ android:layout_gravity="center_horizontal" />
<View
android:layout_width="match_parent"
diff --git a/java/res/layout/chooser_action_view.xml b/java/res/layout/chooser_action_view.xml
index ba9134cc..e17dce0e 100644
--- a/java/res/layout/chooser_action_view.xml
+++ b/java/res/layout/chooser_action_view.xml
@@ -18,7 +18,7 @@
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
style="?android:attr/borderlessButtonStyle"
android:background="@drawable/chooser_action_button_bg"
- android:paddingBottom="8dp"
+ android:paddingVertical="15dp"
android:paddingHorizontal="@dimen/chooser_edge_margin_normal_half"
android:clickable="true"
android:drawablePadding="6dp"
diff --git a/java/res/layout/image_preview_image_item.xml b/java/res/layout/image_preview_image_item.xml
index 3f534831..442e9345 100644
--- a/java/res/layout/image_preview_image_item.xml
+++ b/java/res/layout/image_preview_image_item.xml
@@ -18,8 +18,8 @@
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
- android:layout_width="46dp"
- android:layout_height="46dp">
+ android:layout_width="@dimen/chooser_preview_image_height_tall"
+ android:layout_height="@dimen/chooser_preview_image_height_tall">
<com.android.intentresolver.widget.RoundedRectImageView
android:id="@+id/image"
diff --git a/java/res/layout/image_preview_loading_item.xml b/java/res/layout/image_preview_loading_item.xml
new file mode 100644
index 00000000..a8a8f264
--- /dev/null
+++ b/java/res/layout/image_preview_loading_item.xml
@@ -0,0 +1,33 @@
+<!--
+ ~ Copyright (C) 2023 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/chooser_preview_image_height_tall"
+ android:padding="8dp">
+
+ <ProgressBar
+ android:id="@+id/loading_indicator"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:indeterminate="true"
+ android:indeterminateTint="?androidprv:attr/materialColorPrimary"
+ android:indeterminateTintMode="src_in" />
+
+</FrameLayout>
diff --git a/java/res/layout/image_preview_other_item.xml b/java/res/layout/image_preview_other_item.xml
index 07f87e3a..db458656 100644
--- a/java/res/layout/image_preview_other_item.xml
+++ b/java/res/layout/image_preview_other_item.xml
@@ -16,16 +16,18 @@
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
- android:layout_width="@dimen/chooser_preview_image_width"
- android:layout_height="@dimen/chooser_preview_image_height_tall">
+ android:layout_width="wrap_content"
+ android:layout_height="@dimen/chooser_preview_image_height_tall"
+ android:paddingHorizontal="@dimen/chooser_edge_margin_normal_half">
<TextView
android:id="@+id/label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
- android:drawableTop="@drawable/ic_file_copy"
- android:drawablePadding="8dp"
+ android:drawableStart="@drawable/ic_file_copy"
+ android:drawablePadding="4dp"
+ android:gravity="bottom"
android:textAppearance="@style/TextAppearance.ChooserDefault" />
</FrameLayout>
diff --git a/java/res/values/styles.xml b/java/res/values/styles.xml
index 9a31a141..0ccab4c0 100644
--- a/java/res/values/styles.xml
+++ b/java/res/values/styles.xml
@@ -23,7 +23,7 @@
<item name="android:taskOpenExitAnimation">@anim/resolver_close_anim</item>
</style>
<style name="Theme.DeviceDefault.ResolverCommon"
- parent="@android:style/Theme.DeviceDefault.DayNight">
+ parent="@android:style/Theme.DeviceDefault.DayNight">
<item name="android:windowAnimationStyle">@style/ResolverAnimation</item>
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowNoTitle">true</item>
@@ -45,6 +45,7 @@
<style name="Theme.DeviceDefault.Chooser" parent="Theme.DeviceDefault.Resolver">
<item name="*android:iconfactoryIconSize">@dimen/chooser_icon_size</item>
<item name="*android:iconfactoryBadgeSize">@dimen/chooser_badge_size</item>
+ <item name="android:windowLayoutInDisplayCutoutMode">shortEdges</item>
</style>
<style name="TextAppearance.ChooserDefault"
diff --git a/java/src/com/android/intentresolver/ChooserActionFactory.java b/java/src/com/android/intentresolver/ChooserActionFactory.java
index 06c7e8d7..a54e8c62 100644
--- a/java/src/com/android/intentresolver/ChooserActionFactory.java
+++ b/java/src/com/android/intentresolver/ChooserActionFactory.java
@@ -37,6 +37,7 @@ import android.view.View;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
+import com.android.intentresolver.logging.EventLog;
import com.android.intentresolver.widget.ActionRow;
import com.android.internal.annotations.VisibleForTesting;
@@ -97,7 +98,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
private final @Nullable ChooserAction mModifyShareAction;
private final Consumer<Boolean> mExcludeSharedTextAction;
private final Consumer</* @Nullable */ Integer> mFinishCallback;
- private final ChooserActivityLogger mLogger;
+ private final EventLog mLogger;
/**
* @param context
@@ -116,7 +117,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
Context context,
ChooserRequestParameters chooserRequest,
ChooserIntegratedDeviceComponents integratedDeviceComponents,
- ChooserActivityLogger logger,
+ EventLog logger,
Consumer<Boolean> onUpdateSharedTextIsExcluded,
Callable</* @Nullable */ View> firstVisibleImageQuery,
ActionActivityStarter activityStarter,
@@ -152,7 +153,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
List<ChooserAction> customActions,
@Nullable ChooserAction modifyShareAction,
Consumer<Boolean> onUpdateSharedTextIsExcluded,
- ChooserActivityLogger logger,
+ EventLog logger,
Consumer</* @Nullable */ Integer> finishCallback) {
mContext = context;
mCopyButtonRunnable = copyButtonRunnable;
@@ -208,7 +209,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
mModifyShareAction,
mFinishCallback,
() -> {
- mLogger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_MODIFY_SHARE);
+ mLogger.logActionSelected(EventLog.SELECTION_TYPE_MODIFY_SHARE);
});
}
@@ -232,7 +233,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
Intent targetIntent,
String referrerPackageName,
Consumer<Integer> finishCallback,
- ChooserActivityLogger logger) {
+ EventLog logger) {
final ClipData clipData;
try {
clipData = extractTextToCopy(targetIntent);
@@ -248,7 +249,7 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
Context.CLIPBOARD_SERVICE);
clipboardManager.setPrimaryClipAsPackage(clipData, referrerPackageName);
- logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_COPY);
+ logger.logActionSelected(EventLog.SELECTION_TYPE_COPY);
finishCallback.accept(Activity.RESULT_OK);
};
}
@@ -327,10 +328,10 @@ public final class ChooserActionFactory implements ChooserContentPreviewUi.Actio
TargetInfo editSharingTarget,
Callable</* @Nullable */ View> firstVisibleImageQuery,
ActionActivityStarter activityStarter,
- ChooserActivityLogger logger) {
+ EventLog logger) {
return () -> {
// Log share completion via edit.
- logger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_EDIT);
+ logger.logActionSelected(EventLog.SELECTION_TYPE_EDIT);
View firstImageView = null;
try {
diff --git a/java/src/com/android/intentresolver/ChooserActivity.java b/java/src/com/android/intentresolver/ChooserActivity.java
index 63ac6435..b27f054e 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -27,7 +27,6 @@ import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_S
import static com.android.internal.util.LatencyTracker.ACTION_LOAD_SHARE_SHEET;
import android.annotation.IntDef;
-import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
import android.app.ActivityManager;
@@ -66,9 +65,6 @@ import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
-import android.view.animation.AlphaAnimation;
-import android.view.animation.Animation;
-import android.view.animation.LinearInterpolator;
import android.widget.TextView;
import androidx.annotation.MainThread;
@@ -92,6 +88,7 @@ import com.android.intentresolver.flags.FeatureFlagRepositoryFactory;
import com.android.intentresolver.grid.ChooserGridAdapter;
import com.android.intentresolver.icons.DefaultTargetDataLoader;
import com.android.intentresolver.icons.TargetDataLoader;
+import com.android.intentresolver.logging.EventLog;
import com.android.intentresolver.measurements.Tracer;
import com.android.intentresolver.model.AbstractResolverComparator;
import com.android.intentresolver.model.AppPredictionServiceResolverComparator;
@@ -191,7 +188,7 @@ public class ChooserActivity extends ResolverActivity implements
private boolean mShouldDisplayLandscape;
// statsd logger wrapper
- protected ChooserActivityLogger mChooserActivityLogger;
+ protected EventLog mEventLog;
private long mChooserShownTime;
protected boolean mIsSuccessfullySelected;
@@ -224,6 +221,13 @@ public class ChooserActivity extends ResolverActivity implements
private final SparseArray<ProfileRecord> mProfileRecords = new SparseArray<>();
private boolean mExcludeSharedText = false;
+ /**
+ * When we intend to finish the activity with a shared element transition, we can't immediately
+ * finish() when the transition is invoked, as the receiving end may not be able to start the
+ * animation and the UI breaks if this takes too long. Instead we defer finishing until onStop
+ * in order to wait for the transition to begin.
+ */
+ private boolean mFinishWhenStopped = false;
public ChooserActivity() {}
@@ -233,7 +237,7 @@ public class ChooserActivity extends ResolverActivity implements
final long intentReceivedTime = System.currentTimeMillis();
mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
- getChooserActivityLogger().logSharesheetTriggered();
+ getEventLog().logSharesheetTriggered();
mFeatureFlagRepository = createFeatureFlagRepository();
mIntegratedDeviceComponents = getIntegratedDeviceComponents();
@@ -283,10 +287,6 @@ public class ChooserActivity extends ResolverActivity implements
mEnterTransitionAnimationDelegate,
new HeadlineGeneratorImpl(this));
- setAdditionalTargets(mChooserRequest.getAdditionalTargets());
-
- setSafeForwardingMode(true);
-
mPinnedSharedPrefs = getPinnedSharedPrefs(this);
mMaxTargetsPerRow = getResources().getInteger(R.integer.config_chooser_max_targets_per_row);
@@ -304,16 +304,18 @@ public class ChooserActivity extends ResolverActivity implements
super.onCreate(
savedInstanceState,
mChooserRequest.getTargetIntent(),
+ mChooserRequest.getAdditionalTargets(),
mChooserRequest.getTitle(),
mChooserRequest.getDefaultTitleResource(),
mChooserRequest.getInitialIntents(),
- /* rList: List<ResolveInfo> = */ null,
- /* supportsAlwaysUseOption = */ false,
- new DefaultTargetDataLoader(this, getLifecycle(), false));
+ /* resolutionList= */ null,
+ /* supportsAlwaysUseOption= */ false,
+ new DefaultTargetDataLoader(this, getLifecycle(), false),
+ /* safeForwardingMode= */ true);
mChooserShownTime = System.currentTimeMillis();
final long systemCost = mChooserShownTime - intentReceivedTime;
- getChooserActivityLogger().logChooserActivityShown(
+ getEventLog().logChooserActivityShown(
isWorkProfile(), mChooserRequest.getTargetType(), systemCost);
if (mResolverDrawerLayout != null) {
@@ -322,7 +324,7 @@ public class ChooserActivity extends ResolverActivity implements
mResolverDrawerLayout.setOnCollapsedChangedListener(
isCollapsed -> {
mChooserMultiProfilePagerAdapter.setIsCollapsed(isCollapsed);
- getChooserActivityLogger().logSharesheetExpansionChanged(isCollapsed);
+ getEventLog().logSharesheetExpansionChanged(isCollapsed);
});
}
@@ -330,7 +332,7 @@ public class ChooserActivity extends ResolverActivity implements
Log.d(TAG, "System Time Cost is " + systemCost);
}
- getChooserActivityLogger().logShareStarted(
+ getEventLog().logShareStarted(
getReferrerPackageName(),
mChooserRequest.getTargetType(),
mChooserRequest.getCallerChooserTargets().size(),
@@ -549,7 +551,7 @@ public class ChooserActivity extends ResolverActivity implements
if (shouldShowStickyContentPreview()
|| mChooserMultiProfilePagerAdapter
.getCurrentRootAdapter().getSystemRowCount() != 0) {
- getChooserActivityLogger().logActionShareWithPreview(
+ getEventLog().logActionShareWithPreview(
mChooserContentPreviewUi.getPreferredContentPreview());
}
return postRebuildListInternal(rebuildCompleted);
@@ -614,8 +616,7 @@ public class ChooserActivity extends ResolverActivity implements
protected void onResume() {
super.onResume();
Log.d(TAG, "onResume: " + getComponentName().flattenToShortString());
- maybeCancelFinishAnimation();
-
+ mFinishWhenStopped = false;
mRefinementManager.onActivityResume();
}
@@ -716,7 +717,8 @@ public class ChooserActivity extends ResolverActivity implements
super.onStop();
mRefinementManager.onActivityStop(isChangingConfigurations());
- if (maybeCancelFinishAnimation()) {
+ if (mFinishWhenStopped) {
+ mFinishWhenStopped = false;
finish();
}
}
@@ -848,9 +850,7 @@ public class ChooserActivity extends ResolverActivity implements
targetList,
// Adding userHandle from ResolveInfo allows the app icon in Dialog Box to be
// resolved correctly within the same tab.
- getResolveInfoUserHandle(
- targetInfo.getResolveInfo(),
- mChooserMultiProfilePagerAdapter.getCurrentUserHandle()),
+ targetInfo.getResolveInfo().userHandle,
shortcutIdKey,
shortcutTitle,
isShortcutPinned,
@@ -883,7 +883,7 @@ public class ChooserActivity extends ResolverActivity implements
final long selectionCost = System.currentTimeMillis() - mChooserShownTime;
- if (targetInfo.isMultiDisplayResolveInfo()) {
+ if ((targetInfo != null) && targetInfo.isMultiDisplayResolveInfo()) {
MultiDisplayResolveInfo mti = (MultiDisplayResolveInfo) targetInfo;
if (!mti.hasSelected()) {
// Add userHandle based badge to the stackedAppDialogBox.
@@ -891,20 +891,28 @@ public class ChooserActivity extends ResolverActivity implements
getSupportFragmentManager(),
mti,
which,
- getResolveInfoUserHandle(
- targetInfo.getResolveInfo(),
- mChooserMultiProfilePagerAdapter.getCurrentUserHandle()));
+ targetInfo.getResolveInfo().userHandle);
return;
}
}
super.startSelected(which, always, filtered);
- if (currentListAdapter.getCount() > 0) {
+ // TODO: both of the conditions around this switch logic *should* be redundant, and
+ // can be removed if certain invariants can be guaranteed. In particular, it seems
+ // like targetInfo (from `ChooserListAdapter.targetInfoForPosition()`) is *probably*
+ // expected to be null only at out-of-bounds indexes where `getPositionTargetType()`
+ // returns TARGET_BAD; then the switch falls through to a default no-op, and we don't
+ // need to null-check targetInfo. We only need the null check if it's possible that
+ // the ChooserListAdapter contains null elements "in the middle" of its list data,
+ // such that they're classified as belonging to one of the real target types. That
+ // should probably never happen. But why would this method ever be invoked with a
+ // null target at all? Even an out-of-bounds index should never be "selected"...
+ if ((currentListAdapter.getCount() > 0) && (targetInfo != null)) {
switch (currentListAdapter.getPositionTargetType(which)) {
case ChooserListAdapter.TARGET_SERVICE:
- getChooserActivityLogger().logShareTargetSelected(
- ChooserActivityLogger.SELECTION_TYPE_SERVICE,
+ getEventLog().logShareTargetSelected(
+ EventLog.SELECTION_TYPE_SERVICE,
targetInfo.getResolveInfo().activityInfo.processName,
which,
/* directTargetAlsoRanked= */ getRankedPosition(targetInfo),
@@ -917,8 +925,8 @@ public class ChooserActivity extends ResolverActivity implements
return;
case ChooserListAdapter.TARGET_CALLER:
case ChooserListAdapter.TARGET_STANDARD:
- getChooserActivityLogger().logShareTargetSelected(
- ChooserActivityLogger.SELECTION_TYPE_APP,
+ getEventLog().logShareTargetSelected(
+ EventLog.SELECTION_TYPE_APP,
targetInfo.getResolveInfo().activityInfo.processName,
(which - currentListAdapter.getSurfacedTargetInfo().size()),
/* directTargetAlsoRanked= */ -1,
@@ -934,8 +942,8 @@ public class ChooserActivity extends ResolverActivity implements
// they are from the alphabetical pool.
// TODO: why do we log a different selection type if the -1 value already
// designates the same condition?
- getChooserActivityLogger().logShareTargetSelected(
- ChooserActivityLogger.SELECTION_TYPE_STANDARD,
+ getEventLog().logShareTargetSelected(
+ EventLog.SELECTION_TYPE_STANDARD,
targetInfo.getResolveInfo().activityInfo.processName,
/* value= */ -1,
/* directTargetAlsoRanked= */ -1,
@@ -987,7 +995,7 @@ public class ChooserActivity extends ResolverActivity implements
if (profileRecord == null) {
return;
}
- getChooserActivityLogger().logDirectShareTargetReceived(
+ getEventLog().logDirectShareTargetReceived(
MetricsEvent.ACTION_DIRECT_SHARE_TARGETS_LOADED_SHORTCUT_MANAGER,
(int) (SystemClock.elapsedRealtime() - profileRecord.loadingStartTime));
}
@@ -1111,11 +1119,7 @@ public class ChooserActivity extends ResolverActivity implements
// Adding two stage comparator, first stage compares using displayLabel, next stage
// compares using resolveInfo.userHandle
mComparator = Comparator.comparing(DisplayResolveInfo::getDisplayLabel, collator)
- .thenComparingInt(displayResolveInfo ->
- getResolveInfoUserHandle(
- displayResolveInfo.getResolveInfo(),
- // TODO: User resolveInfo.userHandle, once its available.
- UserHandle.SYSTEM).getIdentifier());
+ .thenComparingInt(target -> target.getResolveInfo().userHandle.getIdentifier());
}
@Override
@@ -1125,11 +1129,11 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- protected ChooserActivityLogger getChooserActivityLogger() {
- if (mChooserActivityLogger == null) {
- mChooserActivityLogger = new ChooserActivityLogger();
+ protected EventLog getEventLog() {
+ if (mEventLog == null) {
+ mEventLog = new EventLog();
}
- return mChooserActivityLogger;
+ return mEventLog;
}
public class ChooserListController extends ResolverListController {
@@ -1255,7 +1259,7 @@ public class ChooserActivity extends ResolverActivity implements
targetIntent,
this,
context.getPackageManager(),
- getChooserActivityLogger(),
+ getEventLog(),
chooserRequest,
maxTargetsPerRow,
initialIntentsUserSpace,
@@ -1279,7 +1283,7 @@ public class ChooserActivity extends ResolverActivity implements
AbstractResolverComparator resolverComparator;
if (appPredictor != null) {
resolverComparator = new AppPredictionServiceResolverComparator(this, getTargetIntent(),
- getReferrerPackageName(), appPredictor, userHandle, getChooserActivityLogger(),
+ getReferrerPackageName(), appPredictor, userHandle, getEventLog(),
getIntegratedDeviceComponents().getNearbySharingComponent());
} else {
resolverComparator =
@@ -1288,7 +1292,7 @@ public class ChooserActivity extends ResolverActivity implements
getTargetIntent(),
getReferrerPackageName(),
null,
- getChooserActivityLogger(),
+ getEventLog(),
getResolverRankerServiceUserHandleList(userHandle),
getIntegratedDeviceComponents().getNearbySharingComponent());
}
@@ -1313,7 +1317,7 @@ public class ChooserActivity extends ResolverActivity implements
this,
mChooserRequest,
mIntegratedDeviceComponents,
- getChooserActivityLogger(),
+ getEventLog(),
(isExcluded) -> mExcludeSharedText = isExcluded,
this::getFirstVisibleImgPreviewView,
new ChooserActionFactory.ActionActivityStarter() {
@@ -1330,7 +1334,10 @@ public class ChooserActivity extends ResolverActivity implements
ChooserActivity.this, sharedElement, sharedElementName);
safelyStartActivityAsUser(
targetInfo, getPersonalProfileUserHandle(), options.toBundle());
- startFinishAnimation();
+ // Can't finish right away because the shared element transition may not
+ // be ready to start.
+ mFinishWhenStopped = true;
+
}
},
(status) -> {
@@ -1528,7 +1535,7 @@ public class ChooserActivity extends ResolverActivity implements
Log.d(TAG, "app target loading time " + duration + " ms");
}
addCallerChooserTargets();
- getChooserActivityLogger().logSharesheetAppLoadComplete();
+ getEventLog().logSharesheetAppLoadComplete();
maybeQueryAdditionalPostProcessingTargets(chooserListAdapter);
mLatencyTracker.onActionEnd(ACTION_LOAD_SHARE_SHEET);
}
@@ -1575,7 +1582,7 @@ public class ChooserActivity extends ResolverActivity implements
}
logDirectShareTargetReceived(userHandle);
sendVoiceChoicesIfNeeded();
- getChooserActivityLogger().logSharesheetDirectLoadComplete();
+ getEventLog().logSharesheetDirectLoadComplete();
}
private void setupScrollListener() {
@@ -1715,25 +1722,6 @@ public class ChooserActivity extends ResolverActivity implements
contentPreviewContainer.setVisibility(View.GONE);
}
- private void startFinishAnimation() {
- View rootView = findRootView();
- if (rootView != null) {
- rootView.startAnimation(new FinishAnimation(this, rootView));
- }
- }
-
- private boolean maybeCancelFinishAnimation() {
- View rootView = findRootView();
- Animation animation = (rootView == null) ? null : rootView.getAnimation();
- if (animation instanceof FinishAnimation) {
- boolean hasEnded = animation.hasEnded();
- animation.cancel();
- rootView.clearAnimation();
- return !hasEnded;
- }
- return false;
- }
-
private View findRootView() {
if (mContentView == null) {
mContentView = findViewById(android.R.id.content);
@@ -1814,74 +1802,9 @@ public class ChooserActivity extends ResolverActivity implements
}
}
- /**
- * Used in combination with the scene transition when launching the image editor
- */
- private static class FinishAnimation extends AlphaAnimation implements
- Animation.AnimationListener {
- @Nullable
- private Activity mActivity;
- @Nullable
- private View mRootView;
- private final float mFromAlpha;
-
- FinishAnimation(@NonNull Activity activity, @NonNull View rootView) {
- super(rootView.getAlpha(), 0.0f);
- mActivity = activity;
- mRootView = rootView;
- mFromAlpha = rootView.getAlpha();
- setInterpolator(new LinearInterpolator());
- long duration = activity.getWindow().getTransitionBackgroundFadeDuration();
- setDuration(duration);
- // The scene transition animation looks better when it's not overlapped with this
- // fade-out animation thus the delay.
- // It is most likely that the image editor will cause this activity to stop and this
- // animation will be cancelled in the background without running (i.e. we'll animate
- // only when this activity remains partially visible after the image editor launch).
- setStartOffset(duration);
- super.setAnimationListener(this);
- }
-
- @Override
- public void setAnimationListener(AnimationListener listener) {
- throw new UnsupportedOperationException();
- }
-
- @Override
- public void cancel() {
- if (mRootView != null) {
- mRootView.setAlpha(mFromAlpha);
- }
- cleanup();
- super.cancel();
- }
-
- @Override
- public void onAnimationStart(Animation animation) {
- }
-
- @Override
- public void onAnimationEnd(Animation animation) {
- Activity activity = mActivity;
- cleanup();
- if (activity != null) {
- activity.finish();
- }
- }
-
- @Override
- public void onAnimationRepeat(Animation animation) {
- }
-
- private void cleanup() {
- mActivity = null;
- mRootView = null;
- }
- }
-
@Override
protected void maybeLogProfileChange() {
- getChooserActivityLogger().logSharesheetProfileChanged();
+ getEventLog().logSharesheetProfileChanged();
}
private static class ProfileRecord {
diff --git a/java/src/com/android/intentresolver/ChooserActivityReEnabler.kt b/java/src/com/android/intentresolver/ChooserActivityReEnabler.kt
deleted file mode 100644
index 3236c1be..00000000
--- a/java/src/com/android/intentresolver/ChooserActivityReEnabler.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package com.android.intentresolver
-
-import android.content.BroadcastReceiver
-import android.content.ComponentName
-import android.content.Context
-import android.content.Intent
-import android.content.pm.PackageManager
-
-/**
- * Ensures that the unbundled version of [ChooserActivity] does not get stuck in a disabled state.
- */
-class ChooserActivityReEnabler : BroadcastReceiver() {
-
- override fun onReceive(context: Context, intent: Intent) {
- if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
- context.packageManager.setComponentEnabledSetting(
- CHOOSER_COMPONENT,
- PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
- /* flags = */ 0,
- )
-
- // This only needs to be run once, so we disable ourself to avoid additional startup
- // process on future boots
- context.packageManager.setComponentEnabledSetting(
- SELF_COMPONENT,
- PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
- /* flags = */ 0,
- )
- }
- }
-
- companion object {
- private const val CHOOSER_PACKAGE = "com.android.intentresolver"
- private val CHOOSER_COMPONENT =
- ComponentName(CHOOSER_PACKAGE, "$CHOOSER_PACKAGE.ChooserActivity")
- private val SELF_COMPONENT =
- ComponentName(CHOOSER_PACKAGE, "$CHOOSER_PACKAGE.ChooserActivityReEnabler")
- }
-}
diff --git a/java/src/com/android/intentresolver/ChooserListAdapter.java b/java/src/com/android/intentresolver/ChooserListAdapter.java
index b1fa16b0..e6d6dbf4 100644
--- a/java/src/com/android/intentresolver/ChooserListAdapter.java
+++ b/java/src/com/android/intentresolver/ChooserListAdapter.java
@@ -49,6 +49,7 @@ import com.android.intentresolver.chooser.NotSelectableTargetInfo;
import com.android.intentresolver.chooser.SelectableTargetInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.icons.TargetDataLoader;
+import com.android.intentresolver.logging.EventLog;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
@@ -80,7 +81,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
private final ChooserRequestParameters mChooserRequest;
private final int mMaxRankedTargets;
- private final ChooserActivityLogger mChooserActivityLogger;
+ private final EventLog mEventLog;
private final Set<TargetInfo> mRequestedIcons = new HashSet<>();
@@ -139,7 +140,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
Intent targetIntent,
ResolverListCommunicator resolverListCommunicator,
PackageManager packageManager,
- ChooserActivityLogger chooserActivityLogger,
+ EventLog eventLog,
ChooserRequestParameters chooserRequest,
int maxRankedTargets,
UserHandle initialIntentsUserSpace,
@@ -165,7 +166,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
mPlaceHolderTargetInfo = NotSelectableTargetInfo.newPlaceHolderTargetInfo(context);
mTargetDataLoader = targetDataLoader;
createPlaceHolders();
- mChooserActivityLogger = chooserActivityLogger;
+ mEventLog = eventLog;
mShortcutSelectionLogic = new ShortcutSelectionLogic(
context.getResources().getInteger(R.integer.config_maxShortcutTargetsPerApp),
DeviceConfig.getBoolean(
@@ -384,8 +385,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
.collect(Collectors.groupingBy(target ->
target.getResolvedComponentName().getPackageName()
+ "#" + target.getDisplayLabel()
- + '#' + ResolverActivity.getResolveInfoUserHandle(
- target.getResolveInfo(), getUserHandle()).getIdentifier()
+ + '#' + target.getResolveInfo().userHandle.getIdentifier()
))
.values()
.stream()
@@ -634,7 +634,7 @@ public class ChooserListAdapter extends ResolverListAdapter {
mServiceTargets.removeIf(o -> o.isPlaceHolderTargetInfo());
if (mServiceTargets.isEmpty()) {
mServiceTargets.add(NotSelectableTargetInfo.newEmptyTargetInfo());
- mChooserActivityLogger.logSharesheetEmptyDirectShareRow();
+ mEventLog.logSharesheetEmptyDirectShareRow();
}
notifyDataSetChanged();
}
diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java
index 57871532..35c7e897 100644
--- a/java/src/com/android/intentresolver/ResolverActivity.java
+++ b/java/src/com/android/intentresolver/ResolverActivity.java
@@ -27,6 +27,7 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_PERS
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_PROFILE_NOT_SUPPORTED;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB;
import static android.app.admin.DevicePolicyResources.Strings.Core.RESOLVER_WORK_TAB_ACCESSIBILITY;
+import static android.content.Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.PermissionChecker.PID_UNKNOWN;
import static android.stats.devicepolicy.nano.DevicePolicyEnums.RESOLVER_EMPTY_STATE_NO_SHARING_TO_PERSONAL;
@@ -119,6 +120,7 @@ import com.android.internal.util.LatencyTracker;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
@@ -143,7 +145,14 @@ public class ResolverActivity extends FragmentActivity implements
mIsIntentPicker = isIntentPicker;
}
+ /**
+ * Whether to enable a launch mode that is safe to use when forwarding intents received from
+ * applications and running in system processes. This mode uses Activity.startActivityAsCaller
+ * instead of the normal Activity.startActivity for launching the activity selected
+ * by the user.
+ */
private boolean mSafeForwardingMode;
+
private Button mAlwaysButton;
private Button mOnceButton;
protected View mProfileView;
@@ -332,38 +341,55 @@ public class ResolverActivity extends FragmentActivity implements
mResolvingHome = true;
}
- setSafeForwardingMode(true);
-
- onCreate(savedInstanceState, intent, null, 0, null, null, true, createIconLoader());
+ onCreate(
+ savedInstanceState,
+ intent,
+ /* additionalTargets= */ null,
+ /* title= */ null,
+ /* defaultTitleRes= */ 0,
+ /* initialIntents= */ null,
+ /* resolutionList= */ null,
+ /* supportsAlwaysUseOption= */ true,
+ createIconLoader(),
+ /* safeForwardingMode= */ true);
}
/**
* Compatibility version for other bundled services that use this overload without
* a default title resource
*/
- protected void onCreate(Bundle savedInstanceState, Intent intent,
- CharSequence title, Intent[] initialIntents,
- List<ResolveInfo> rList, boolean supportsAlwaysUseOption) {
+ protected void onCreate(
+ Bundle savedInstanceState,
+ Intent intent,
+ CharSequence title,
+ Intent[] initialIntents,
+ List<ResolveInfo> resolutionList,
+ boolean supportsAlwaysUseOption,
+ boolean safeForwardingMode) {
onCreate(
savedInstanceState,
intent,
+ null,
title,
0,
initialIntents,
- rList,
+ resolutionList,
supportsAlwaysUseOption,
- createIconLoader());
+ createIconLoader(),
+ safeForwardingMode);
}
protected void onCreate(
Bundle savedInstanceState,
Intent intent,
+ Intent[] additionalTargets,
CharSequence title,
int defaultTitleRes,
Intent[] initialIntents,
- List<ResolveInfo> rList,
+ List<ResolveInfo> resolutionList,
boolean supportsAlwaysUseOption,
- TargetDataLoader targetDataLoader) {
+ TargetDataLoader targetDataLoader,
+ boolean safeForwardingMode) {
setTheme(appliedThemeResId());
super.onCreate(savedInstanceState);
@@ -381,12 +407,17 @@ public class ResolverActivity extends FragmentActivity implements
mReferrerPackage = getReferrerPackageName();
- // Add our initial intent as the first item, regardless of what else has already been added.
+ // The initial intent must come before any other targets that are to be added.
mIntents.add(0, new Intent(intent));
+ if (additionalTargets != null) {
+ Collections.addAll(mIntents, additionalTargets);
+ }
+
mTitle = title;
mDefaultTitleResId = defaultTitleRes;
mSupportsAlwaysUseOption = supportsAlwaysUseOption;
+ mSafeForwardingMode = safeForwardingMode;
// The last argument of createResolverListAdapter is whether to do special handling
// of the last used choice to highlight it in the list. We need to always
@@ -399,7 +430,7 @@ public class ResolverActivity extends FragmentActivity implements
boolean filterLastUsed = mSupportsAlwaysUseOption && !isVoiceInteraction()
&& !shouldShowTabs() && !hasCloneProfile();
mMultiProfilePagerAdapter = createMultiProfilePagerAdapter(
- initialIntents, rList, filterLastUsed, targetDataLoader);
+ initialIntents, resolutionList, filterLastUsed, targetDataLoader);
if (configureContentView(targetDataLoader)) {
return;
}
@@ -455,17 +486,17 @@ public class ResolverActivity extends FragmentActivity implements
protected AbstractMultiProfilePagerAdapter createMultiProfilePagerAdapter(
Intent[] initialIntents,
- List<ResolveInfo> rList,
+ List<ResolveInfo> resolutionList,
boolean filterLastUsed,
TargetDataLoader targetDataLoader) {
AbstractMultiProfilePagerAdapter resolverMultiProfilePagerAdapter = null;
if (shouldShowTabs()) {
resolverMultiProfilePagerAdapter =
createResolverMultiProfilePagerAdapterForTwoProfiles(
- initialIntents, rList, filterLastUsed, targetDataLoader);
+ initialIntents, resolutionList, filterLastUsed, targetDataLoader);
} else {
resolverMultiProfilePagerAdapter = createResolverMultiProfilePagerAdapterForOneProfile(
- initialIntents, rList, filterLastUsed, targetDataLoader);
+ initialIntents, resolutionList, filterLastUsed, targetDataLoader);
}
return resolverMultiProfilePagerAdapter;
}
@@ -1043,7 +1074,7 @@ public class ResolverActivity extends FragmentActivity implements
Context context,
List<Intent> payloadIntents,
Intent[] initialIntents,
- List<ResolveInfo> rList,
+ List<ResolveInfo> resolutionList,
boolean filterLastUsed,
UserHandle userHandle,
TargetDataLoader targetDataLoader) {
@@ -1054,7 +1085,7 @@ public class ResolverActivity extends FragmentActivity implements
context,
payloadIntents,
initialIntents,
- rList,
+ resolutionList,
filterLastUsed,
createListController(userHandle),
userHandle,
@@ -1127,6 +1158,12 @@ public class ResolverActivity extends FragmentActivity implements
// flag set, we are now losing it. That should be a very rare case
// and we can live with this.
intent.setFlags(intent.getFlags() & ~Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
+
+ // If FLAG_ACTIVITY_LAUNCH_ADJACENT was set, ResolverActivity was opened in the alternate
+ // side, which means we want to open the target app on the same side as ResolverActivity.
+ if ((intent.getFlags() & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0) {
+ intent.setFlags(intent.getFlags() & ~FLAG_ACTIVITY_LAUNCH_ADJACENT);
+ }
return intent;
}
@@ -1142,14 +1179,14 @@ public class ResolverActivity extends FragmentActivity implements
private ResolverMultiProfilePagerAdapter
createResolverMultiProfilePagerAdapterForOneProfile(
Intent[] initialIntents,
- List<ResolveInfo> rList,
+ List<ResolveInfo> resolutionList,
boolean filterLastUsed,
TargetDataLoader targetDataLoader) {
ResolverListAdapter adapter = createResolverListAdapter(
/* context */ this,
/* payloadIntents */ mIntents,
initialIntents,
- rList,
+ resolutionList,
filterLastUsed,
/* userHandle */ getPersonalProfileUserHandle(),
targetDataLoader);
@@ -1170,7 +1207,7 @@ public class ResolverActivity extends FragmentActivity implements
private ResolverMultiProfilePagerAdapter createResolverMultiProfilePagerAdapterForTwoProfiles(
Intent[] initialIntents,
- List<ResolveInfo> rList,
+ List<ResolveInfo> resolutionList,
boolean filterLastUsed,
TargetDataLoader targetDataLoader) {
// In the edge case when we have 0 apps in the current profile and >1 apps in the other,
@@ -1197,7 +1234,7 @@ public class ResolverActivity extends FragmentActivity implements
/* context */ this,
/* payloadIntents */ mIntents,
selectedProfile == PROFILE_PERSONAL ? initialIntents : null,
- rList,
+ resolutionList,
(filterLastUsed && UserHandle.myUserId()
== getPersonalProfileUserHandle().getIdentifier()),
/* userHandle */ getPersonalProfileUserHandle(),
@@ -1207,7 +1244,7 @@ public class ResolverActivity extends FragmentActivity implements
/* context */ this,
/* payloadIntents */ mIntents,
selectedProfile == PROFILE_WORK ? initialIntents : null,
- rList,
+ resolutionList,
(filterLastUsed && UserHandle.myUserId()
== workProfileUserHandle.getIdentifier()),
/* userHandle */ workProfileUserHandle,
@@ -1365,14 +1402,6 @@ public class ResolverActivity extends FragmentActivity implements
return new Option(target.getDisplayLabel(), index);
}
- protected final void setAdditionalTargets(Intent[] intents) {
- if (intents != null) {
- for (Intent intent : intents) {
- mIntents.add(intent);
- }
- }
- }
-
public final Intent getTargetIntent() {
return mIntents.isEmpty() ? null : mIntents.get(0);
}
@@ -1433,22 +1462,6 @@ public class ResolverActivity extends FragmentActivity implements
() -> getString(R.string.forward_intent_to_work));
}
- /**
- * Turn on launch mode that is safe to use when forwarding intents received from
- * applications and running in system processes. This mode uses Activity.startActivityAsCaller
- * instead of the normal Activity.startActivity for launching the activity selected
- * by the user.
- *
- * <p>This mode is set to true by default if the activity is initialized through
- * {@link #onCreate(android.os.Bundle)}. If a subclass calls one of the other onCreate
- * methods, it is set to false by default. You must set it before calling one of the
- * more detailed onCreate methods, so that it will be set correctly in the case where
- * there is only one intent to resolve and it is thus started immediately.</p>
- */
- public final void setSafeForwardingMode(boolean safeForwarding) {
- mSafeForwardingMode = safeForwarding;
- }
-
protected final CharSequence getTitleForAction(Intent intent, int defaultTitleRes) {
final ActionTitle title = mResolvingHome
? ActionTitle.HOME
@@ -1649,10 +1662,9 @@ public class ResolverActivity extends FragmentActivity implements
/** Start the activity specified by the {@link TargetInfo}.*/
public final void safelyStartActivity(TargetInfo cti) {
// In case cloned apps are present, we would want to start those apps in cloned user
- // space, which will not be same as adaptor's userHandle. resolveInfo.userHandle
+ // space, which will not be same as the adapter's userHandle. resolveInfo.userHandle
// identifies the correct user space in such cases.
- UserHandle activityUserHandle = getResolveInfoUserHandle(
- cti.getResolveInfo(), mMultiProfilePagerAdapter.getCurrentUserHandle());
+ UserHandle activityUserHandle = cti.getResolveInfo().userHandle;
safelyStartActivityAsUser(cti, activityUserHandle, null);
}
@@ -2267,11 +2279,7 @@ public class ResolverActivity extends FragmentActivity implements
&& Objects.equals(lhs.activityInfo.packageName, rhs.activityInfo.packageName)
// Comparing against resolveInfo.userHandle in case cloned apps are present,
// as they will have the same activityInfo.
- && Objects.equals(
- getResolveInfoUserHandle(lhs,
- mMultiProfilePagerAdapter.getActiveListAdapter().getUserHandle()),
- getResolveInfoUserHandle(rhs,
- mMultiProfilePagerAdapter.getActiveListAdapter().getUserHandle()));
+ && Objects.equals(lhs.userHandle, rhs.userHandle);
}
private boolean inactiveListAdapterHasItems() {
@@ -2409,13 +2417,4 @@ public class ResolverActivity extends FragmentActivity implements
}
return userList;
}
-
- /**
- * This function is temporary in nature, and its usages will be replaced with just
- * resolveInfo.userHandle, once it is available, once sharesheet is stable.
- */
- public static UserHandle getResolveInfoUserHandle(ResolveInfo resolveInfo,
- UserHandle predictedHandle) {
- return resolveInfo.userHandle;
- }
}
diff --git a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
index e8367c4e..d279f11f 100644
--- a/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUi.java
@@ -16,6 +16,8 @@
package com.android.intentresolver.contentpreview;
+import static androidx.lifecycle.LifecycleKt.getCoroutineScope;
+
import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_FILE;
import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_IMAGE;
import static com.android.intentresolver.contentpreview.ContentPreviewType.CONTENT_PREVIEW_TEXT;
@@ -150,26 +152,31 @@ public final class ChooserContentPreviewUi {
isSingleImageShare,
previewData.getUriCount(),
targetIntent.getCharSequenceExtra(Intent.EXTRA_TEXT),
+ targetIntent.getType(),
actionFactory,
imageLoader,
typeClassifier,
headlineGenerator);
if (previewData.getUriCount() > 0) {
- previewData.getFileMetadataForImagePreview(
- mLifecycle, previewUi::updatePreviewMetadata);
+ JavaFlowHelper.collectToList(
+ getCoroutineScope(mLifecycle),
+ previewData.getImagePreviewFileInfoFlow(),
+ previewUi::updatePreviewMetadata);
}
return previewUi;
}
- UnifiedContentPreviewUi unifiedContentPreviewUi = new UnifiedContentPreviewUi(
+ return new UnifiedContentPreviewUi(
+ getCoroutineScope(mLifecycle),
isSingleImageShare,
+ targetIntent.getType(),
actionFactory,
imageLoader,
typeClassifier,
transitionElementStatusCallback,
+ previewData.getImagePreviewFileInfoFlow(),
+ previewData.getUriCount(),
headlineGenerator);
- previewData.getFileMetadataForImagePreview(mLifecycle, unifiedContentPreviewUi::setFiles);
- return unifiedContentPreviewUi;
}
public int getPreferredContentPreview() {
diff --git a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
index 07071236..2d81794e 100644
--- a/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/ContentPreviewUi.java
@@ -85,7 +85,7 @@ abstract class ContentPreviewUi {
}
}
- protected static ScrollableImagePreviewView.PreviewType getPreviewType(
+ static ScrollableImagePreviewView.PreviewType getPreviewType(
MimeTypeClassifier typeClassifier, String mimeType) {
if (mimeType == null) {
return ScrollableImagePreviewView.PreviewType.File;
diff --git a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java
index 35990990..6e1212e9 100644
--- a/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUi.java
@@ -49,6 +49,8 @@ import java.util.function.Consumer;
*/
class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
private final Lifecycle mLifecycle;
+ @Nullable
+ private final String mIntentMimeType;
private final CharSequence mText;
private final ChooserContentPreviewUi.ActionFactory mActionFactory;
private final ImageLoader mImageLoader;
@@ -70,15 +72,17 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
boolean isSingleImage,
int fileCount,
CharSequence text,
+ @Nullable String intentMimeType,
ChooserContentPreviewUi.ActionFactory actionFactory,
ImageLoader imageLoader,
MimeTypeClassifier typeClassifier,
HeadlineGenerator headlineGenerator) {
- mLifecycle = lifecycle;
if (isSingleImage && fileCount != 1) {
throw new IllegalArgumentException(
"fileCount = " + fileCount + " and isSingleImage = true");
}
+ mLifecycle = lifecycle;
+ mIntentMimeType = intentMimeType;
mFileCount = fileCount;
mIsSingleImage = isSingleImage;
mText = text;
@@ -127,18 +131,25 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
List<ActionRow.Action> actions = mActionFactory.createCustomActions();
actionRow.setActions(actions);
+ if (!mIsSingleImage) {
+ mContentPreviewView.requireViewById(R.id.image_view).setVisibility(View.GONE);
+ }
+ prepareTextPreview(mContentPreviewView, mActionFactory);
if (mIsMetadataUpdated) {
updateUiWithMetadata(mContentPreviewView);
- } else if (!mIsSingleImage) {
- mContentPreviewView.requireViewById(R.id.image_view).setVisibility(View.GONE);
+ } else {
+ updateHeadline(
+ mContentPreviewView,
+ mFileCount,
+ mTypeClassifier.isImageType(mIntentMimeType),
+ mTypeClassifier.isVideoType(mIntentMimeType));
}
return mContentPreviewView;
}
private void updateUiWithMetadata(ViewGroup contentPreviewView) {
- prepareTextPreview(contentPreviewView, mActionFactory);
- updateHeadline(contentPreviewView);
+ updateHeadline(contentPreviewView, mFileCount, mAllImages, mAllVideos);
ImageView imagePreview = mContentPreviewView.requireViewById(R.id.image_view);
if (mIsSingleImage && mFirstFilePreviewUri != null) {
@@ -157,24 +168,25 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
}
}
- private void updateHeadline(ViewGroup contentPreview) {
+ private void updateHeadline(
+ ViewGroup contentPreview, int fileCount, boolean allImages, boolean allVideos) {
CheckBox includeText = contentPreview.requireViewById(R.id.include_text_action);
String headline;
if (includeText.getVisibility() == View.VISIBLE && includeText.isChecked()) {
- if (mAllImages) {
- headline = mHeadlineGenerator.getImagesWithTextHeadline(mText, mFileCount);
- } else if (mAllVideos) {
- headline = mHeadlineGenerator.getVideosWithTextHeadline(mText, mFileCount);
+ if (allImages) {
+ headline = mHeadlineGenerator.getImagesWithTextHeadline(mText, fileCount);
+ } else if (allVideos) {
+ headline = mHeadlineGenerator.getVideosWithTextHeadline(mText, fileCount);
} else {
- headline = mHeadlineGenerator.getFilesWithTextHeadline(mText, mFileCount);
+ headline = mHeadlineGenerator.getFilesWithTextHeadline(mText, fileCount);
}
} else {
- if (mAllImages) {
- headline = mHeadlineGenerator.getImagesHeadline(mFileCount);
- } else if (mAllVideos) {
- headline = mHeadlineGenerator.getVideosHeadline(mFileCount);
+ if (allImages) {
+ headline = mHeadlineGenerator.getImagesHeadline(fileCount);
+ } else if (allVideos) {
+ headline = mHeadlineGenerator.getVideosHeadline(fileCount);
} else {
- headline = mHeadlineGenerator.getFilesHeadline(mFileCount);
+ headline = mHeadlineGenerator.getFilesHeadline(fileCount);
}
}
@@ -201,7 +213,7 @@ class FilesPlusTextContentPreviewUi extends ContentPreviewUi {
textView.setText(getNoTextString(contentPreview.getResources()));
}
shareTextAction.accept(!isChecked);
- updateHeadline(contentPreview);
+ updateHeadline(contentPreview, mFileCount, mAllImages, mAllVideos);
});
if (SHOW_TOGGLE_CHECKMARK) {
includeText.setVisibility(View.VISIBLE);
diff --git a/java/src/com/android/intentresolver/contentpreview/JavaFlowHelper.kt b/java/src/com/android/intentresolver/contentpreview/JavaFlowHelper.kt
new file mode 100644
index 00000000..b29c5774
--- /dev/null
+++ b/java/src/com/android/intentresolver/contentpreview/JavaFlowHelper.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.
+ */
+
+@file:JvmName("JavaFlowHelper")
+
+package com.android.intentresolver.contentpreview
+
+import com.android.intentresolver.widget.ScrollableImagePreviewView.Preview
+import java.util.function.Consumer
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.launch
+
+internal fun mapFileIntoToPreview(
+ flow: Flow<FileInfo>,
+ typeClassifier: MimeTypeClassifier,
+ editAction: Runnable?
+): Flow<Preview> =
+ flow
+ .filter { it.previewUri != null }
+ .map { fileInfo ->
+ Preview(
+ ContentPreviewUi.getPreviewType(typeClassifier, fileInfo.mimeType),
+ requireNotNull(fileInfo.previewUri),
+ editAction
+ )
+ }
+
+internal fun <T> collectToList(
+ clientScope: CoroutineScope,
+ flow: Flow<T>,
+ callback: Consumer<List<T>>
+) {
+ clientScope.launch { callback.accept(flow.toList()) }
+}
diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt
index 8ab3a272..9f1cc6c1 100644
--- a/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt
+++ b/java/src/com/android/intentresolver/contentpreview/PreviewDataProvider.kt
@@ -38,14 +38,18 @@ import com.android.intentresolver.measurements.runTracing
import com.android.intentresolver.util.ownedByCurrentUser
import java.util.concurrent.atomic.AtomicInteger
import java.util.function.Consumer
+import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.SharedFlow
+import kotlinx.coroutines.flow.take
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withContext
import kotlinx.coroutines.withTimeoutOrNull
/**
@@ -68,31 +72,45 @@ private const val TIMEOUT_MS = 1_000L
*/
@OpenForTesting
open class PreviewDataProvider
-@VisibleForTesting
+@JvmOverloads
constructor(
+ private val scope: CoroutineScope,
private val targetIntent: Intent,
private val contentResolver: ContentInterface,
- private val typeClassifier: MimeTypeClassifier,
- private val dispatcher: CoroutineDispatcher,
+ private val typeClassifier: MimeTypeClassifier = DefaultMimeTypeClassifier,
) {
- constructor(
- targetIntent: Intent,
- contentResolver: ContentInterface,
- ) : this(
- targetIntent,
- contentResolver,
- DefaultMimeTypeClassifier,
- Dispatchers.IO,
- )
private val records = targetIntent.contentUris.map { UriRecord(it) }
+ private val fileInfoSharedFlow: SharedFlow<FileInfo> by lazy {
+ // Alternatively, we could just use [shareIn()] on a [flow] -- and it would be, arguably,
+ // cleaner -- but we'd lost the ability to trace the traverse as [runTracing] does not
+ // generally work over suspend function invocations.
+ MutableSharedFlow<FileInfo>(replay = records.size).apply {
+ scope.launch {
+ runTracing("image-preview-metadata") {
+ for (record in records) {
+ tryEmit(FileInfo.Builder(record.uri).readFromRecord(record).build())
+ }
+ }
+ }
+ }
+ }
+
/** returns number of shared URIs, see [Intent.EXTRA_STREAM] */
@get:OpenForTesting
open val uriCount: Int
get() = records.size
/**
+ * Returns a [Flow] of [FileInfo], for each shared URI in order, with [FileInfo.mimeType] and
+ * [FileInfo.previewUri] set (a data projection tailored for the image preview UI).
+ */
+ @get:OpenForTesting
+ open val imagePreviewFileInfoFlow: Flow<FileInfo>
+ get() = fileInfoSharedFlow.take(records.size)
+
+ /**
* Preview type to use. The type is determined asynchronously with a timeout; the fall-back
* values is [ContentPreviewType.CONTENT_PREVIEW_FILE]
*/
@@ -107,10 +125,18 @@ constructor(
if (!targetIntent.isSend || records.isEmpty()) {
CONTENT_PREVIEW_TEXT
} else {
- runBlocking(dispatcher) {
- withTimeoutOrNull(TIMEOUT_MS) {
- loadPreviewType()
- } ?: CONTENT_PREVIEW_FILE
+ try {
+ runBlocking(scope.coroutineContext) {
+ withTimeoutOrNull(TIMEOUT_MS) { scope.async { loadPreviewType() }.await() }
+ ?: CONTENT_PREVIEW_FILE
+ }
+ } catch (e: CancellationException) {
+ Log.w(
+ ContentPreviewUi.TAG,
+ "An attempt to read preview type from a cancelled scope",
+ e
+ )
+ CONTENT_PREVIEW_FILE
}
}
}
@@ -123,46 +149,24 @@ constructor(
open val firstFileInfo: FileInfo? by lazy {
runTracing("first-uri-metadata") {
records.firstOrNull()?.let { record ->
- runBlocking(dispatcher) {
- val builder = FileInfo.Builder(record.uri)
- withTimeoutOrNull(TIMEOUT_MS) {
- builder.readFromRecord(record)
+ val builder = FileInfo.Builder(record.uri)
+ try {
+ runBlocking(scope.coroutineContext) {
+ withTimeoutOrNull(TIMEOUT_MS) {
+ scope.async { builder.readFromRecord(record) }.await()
+ }
}
- builder.build()
- }
- }
- }
- }
-
- /**
- * Returns a collection of [FileInfo], for each shared URI in order, with [FileInfo.mimeType]
- * and [FileInfo.previewUri] set (a data projection tailored for the image preview UI).
- */
- @OpenForTesting
- open fun getFileMetadataForImagePreview(
- callerLifecycle: Lifecycle,
- callback: Consumer<List<FileInfo>>,
- ) {
- callerLifecycle.coroutineScope.launch {
- val result = withContext(dispatcher) {
- getFileMetadataForImagePreview()
- }
- callback.accept(result)
- }
- }
-
- private fun getFileMetadataForImagePreview(): List<FileInfo> =
- runTracing("image-preview-metadata") {
- ArrayList<FileInfo>(records.size).also { result ->
- for (record in records) {
- result.add(
- FileInfo.Builder(record.uri)
- .readFromRecord(record)
- .build()
+ } catch (e: CancellationException) {
+ Log.w(
+ ContentPreviewUi.TAG,
+ "An attempt to read first file info from a cancelled scope",
+ e
)
}
+ builder.build()
}
}
+ }
private fun FileInfo.Builder.readFromRecord(record: UriRecord): FileInfo.Builder {
withMimeType(record.mimeType)
@@ -186,9 +190,7 @@ constructor(
throw IndexOutOfBoundsException("There are no shared URIs")
}
callerLifecycle.coroutineScope.launch {
- val result = withContext(dispatcher) {
- getFirstFileName()
- }
+ val result = scope.async { getFirstFileName() }.await()
callback.accept(result)
}
}
@@ -237,8 +239,7 @@ constructor(
}
resultDeferred.complete(CONTENT_PREVIEW_FILE)
}
- resultDeferred.await()
- .also { job.cancel() }
+ resultDeferred.await().also { job.cancel() }
}
}
@@ -251,8 +252,8 @@ constructor(
val isImageType: Boolean
get() = typeClassifier.isImageType(mimeType)
val supportsImageType: Boolean by lazy {
- contentResolver.getStreamTypesSafe(uri)
- ?.firstOrNull(typeClassifier::isImageType) != null
+ contentResolver.getStreamTypesSafe(uri)?.firstOrNull(typeClassifier::isImageType) !=
+ null
}
val supportsThumbnail: Boolean
get() = query.supportsThumbnail
@@ -264,9 +265,8 @@ constructor(
private val query by lazy { readQueryResult() }
private fun readQueryResult(): QueryResult {
- val cursor = contentResolver.querySafe(uri)
- ?.takeIf { it.moveToFirst() }
- ?: return QueryResult()
+ val cursor =
+ contentResolver.querySafe(uri)?.takeIf { it.moveToFirst() } ?: return QueryResult()
var flagColIdx = -1
var displayIconUriColIdx = -1
diff --git a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
index 331b0cb6..6013f5a0 100644
--- a/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
+++ b/java/src/com/android/intentresolver/contentpreview/PreviewViewModel.kt
@@ -25,11 +25,15 @@ import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.CreationExtras
import com.android.intentresolver.ChooserRequestParameters
import com.android.intentresolver.R
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.plus
/** A trivial view model to keep a [PreviewDataProvider] instance over a configuration change */
-class PreviewViewModel(private val application: Application) : BasePreviewViewModel() {
+class PreviewViewModel(
+ private val application: Application,
+ private val dispatcher: CoroutineDispatcher = Dispatchers.IO,
+) : BasePreviewViewModel() {
private var previewDataProvider: PreviewDataProvider? = null
private var imageLoader: ImagePreviewImageLoader? = null
@@ -38,15 +42,18 @@ class PreviewViewModel(private val application: Application) : BasePreviewViewMo
chooserRequest: ChooserRequestParameters
): PreviewDataProvider =
previewDataProvider
- ?: PreviewDataProvider(chooserRequest.targetIntent, application.contentResolver).also {
- previewDataProvider = it
- }
+ ?: PreviewDataProvider(
+ viewModelScope + dispatcher,
+ chooserRequest.targetIntent,
+ application.contentResolver
+ )
+ .also { previewDataProvider = it }
@MainThread
override fun createOrReuseImageLoader(): ImageLoader =
imageLoader
?: ImagePreviewImageLoader(
- viewModelScope + Dispatchers.IO,
+ viewModelScope + dispatcher,
thumbnailSize =
application.resources.getDimensionPixelSize(
R.dimen.chooser_preview_image_max_dimen
diff --git a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
index 6385f2b6..8e635aba 100644
--- a/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
+++ b/java/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUi.java
@@ -31,35 +31,50 @@ import com.android.intentresolver.widget.ActionRow;
import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback;
import com.android.intentresolver.widget.ScrollableImagePreviewView;
-import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
+import kotlinx.coroutines.CoroutineScope;
+import kotlinx.coroutines.flow.Flow;
+
class UnifiedContentPreviewUi extends ContentPreviewUi {
private final boolean mShowEditAction;
+ @Nullable
+ private final String mIntentMimeType;
private final ChooserContentPreviewUi.ActionFactory mActionFactory;
private final ImageLoader mImageLoader;
private final MimeTypeClassifier mTypeClassifier;
private final TransitionElementStatusCallback mTransitionElementStatusCallback;
private final HeadlineGenerator mHeadlineGenerator;
+ private final Flow<FileInfo> mFileInfoFlow;
+ private final int mItemCount;
@Nullable
private List<FileInfo> mFiles;
@Nullable
private ViewGroup mContentPreviewView;
UnifiedContentPreviewUi(
+ CoroutineScope scope,
boolean isSingleImage,
+ @Nullable String intentMimeType,
ChooserContentPreviewUi.ActionFactory actionFactory,
ImageLoader imageLoader,
MimeTypeClassifier typeClassifier,
TransitionElementStatusCallback transitionElementStatusCallback,
+ Flow<FileInfo> fileInfoFlow,
+ int itemCount,
HeadlineGenerator headlineGenerator) {
mShowEditAction = isSingleImage;
+ mIntentMimeType = intentMimeType;
mActionFactory = actionFactory;
mImageLoader = imageLoader;
mTypeClassifier = typeClassifier;
mTransitionElementStatusCallback = transitionElementStatusCallback;
+ mFileInfoFlow = fileInfoFlow;
+ mItemCount = itemCount;
mHeadlineGenerator = headlineGenerator;
+
+ JavaFlowHelper.collectToList(scope, fileInfoFlow, this::setFiles);
}
@Override
@@ -74,7 +89,7 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
return layout;
}
- public void setFiles(List<FileInfo> files) {
+ private void setFiles(List<FileInfo> files) {
mImageLoader.prePopulate(files.stream()
.map(FileInfo::getPreviewUri)
.filter(Objects::nonNull)
@@ -96,11 +111,25 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
ScrollableImagePreviewView imagePreview =
mContentPreviewView.requireViewById(R.id.scrollable_image_preview);
+ imagePreview.setImageLoader(mImageLoader);
imagePreview.setOnNoPreviewCallback(() -> imagePreview.setVisibility(View.GONE));
imagePreview.setTransitionElementStatusCallback(mTransitionElementStatusCallback);
+ imagePreview.setPreviews(
+ JavaFlowHelper.mapFileIntoToPreview(
+ mFileInfoFlow,
+ mTypeClassifier,
+ mShowEditAction ? mActionFactory.getEditButtonRunnable() : null),
+ mItemCount);
if (mFiles != null) {
updatePreviewWithFiles(mContentPreviewView, mFiles);
+ } else {
+ displayHeadline(
+ mContentPreviewView,
+ mItemCount,
+ mTypeClassifier.isImageType(mIntentMimeType),
+ mTypeClassifier.isVideoType(mIntentMimeType));
+ imagePreview.setLoading(mItemCount);
}
return mContentPreviewView;
@@ -120,7 +149,6 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
return;
}
- List<ScrollableImagePreviewView.Preview> previews = new ArrayList<>();
boolean allImages = true;
boolean allVideos = true;
for (FileInfo fileInfo : files) {
@@ -128,24 +156,19 @@ class UnifiedContentPreviewUi extends ContentPreviewUi {
getPreviewType(mTypeClassifier, fileInfo.getMimeType());
allImages = allImages && previewType == ScrollableImagePreviewView.PreviewType.Image;
allVideos = allVideos && previewType == ScrollableImagePreviewView.PreviewType.Video;
-
- if (fileInfo.getPreviewUri() != null) {
- Runnable editAction =
- mShowEditAction ? mActionFactory.getEditButtonRunnable() : null;
- previews.add(
- new ScrollableImagePreviewView.Preview(
- previewType, fileInfo.getPreviewUri(), editAction));
- }
}
- imagePreview.setPreviews(previews, count - previews.size(), mImageLoader);
+ displayHeadline(contentPreviewView, count, allImages, allVideos);
+ }
+ private void displayHeadline(
+ ViewGroup layout, int count, boolean allImages, boolean allVideos) {
if (allImages) {
- displayHeadline(contentPreviewView, mHeadlineGenerator.getImagesHeadline(count));
+ displayHeadline(layout, mHeadlineGenerator.getImagesHeadline(count));
} else if (allVideos) {
- displayHeadline(contentPreviewView, mHeadlineGenerator.getVideosHeadline(count));
+ displayHeadline(layout, mHeadlineGenerator.getVideosHeadline(count));
} else {
- displayHeadline(contentPreviewView, mHeadlineGenerator.getFilesHeadline(count));
+ displayHeadline(layout, mHeadlineGenerator.getFilesHeadline(count));
}
}
}
diff --git a/java/src/com/android/intentresolver/flags/Flags.kt b/java/src/com/android/intentresolver/flags/Flags.kt
index b303dd1a..2c20d341 100644
--- a/java/src/com/android/intentresolver/flags/Flags.kt
+++ b/java/src/com/android/intentresolver/flags/Flags.kt
@@ -23,9 +23,8 @@ import com.android.systemui.flags.UnreleasedFlag
// make the flags available in the flag flipper app (see go/sysui-flags).
// All flags added should be included in UnbundledChooserActivityTest.ALL_FLAGS.
object Flags {
- private fun releasedFlag(id: Int, name: String) =
- ReleasedFlag(id, name, "systemui")
+ private fun releasedFlag(name: String) = ReleasedFlag(name, "systemui")
- private fun unreleasedFlag(id: Int, name: String, teamfood: Boolean = false) =
- UnreleasedFlag(id, name, "systemui", teamfood)
+ private fun unreleasedFlag(name: String, teamfood: Boolean = false) =
+ UnreleasedFlag(name, "systemui", teamfood)
}
diff --git a/java/src/com/android/intentresolver/icons/LoadIconTask.java b/java/src/com/android/intentresolver/icons/LoadIconTask.java
index 37ce4093..75132208 100644
--- a/java/src/com/android/intentresolver/icons/LoadIconTask.java
+++ b/java/src/com/android/intentresolver/icons/LoadIconTask.java
@@ -24,7 +24,6 @@ import android.os.Trace;
import android.os.UserHandle;
import android.util.Log;
-import com.android.intentresolver.ResolverActivity;
import com.android.intentresolver.TargetPresentationGetter;
import com.android.intentresolver.chooser.DisplayResolveInfo;
@@ -64,8 +63,7 @@ class LoadIconTask extends BaseLoadIconTask {
protected final Drawable loadIconForResolveInfo(ResolveInfo ri) {
// Load icons based on userHandle from ResolveInfo. If in work profile/clone profile, icons
// should be badged.
- return mPresentationFactory.makePresentationGetter(ri)
- .getIcon(ResolverActivity.getResolveInfoUserHandle(ri, mUserHandle));
+ return mPresentationFactory.makePresentationGetter(ri).getIcon(ri.userHandle);
}
}
diff --git a/java/src/com/android/intentresolver/ChooserActivityLogger.java b/java/src/com/android/intentresolver/logging/EventLog.java
index 1f606f26..b30e825b 100644
--- a/java/src/com/android/intentresolver/ChooserActivityLogger.java
+++ b/java/src/com/android/intentresolver/logging/EventLog.java
@@ -1,5 +1,5 @@
/*
- * 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.intentresolver;
+package com.android.intentresolver.logging;
import android.annotation.Nullable;
import android.content.Intent;
@@ -24,6 +24,7 @@ import android.provider.MediaStore;
import android.util.HashedStringCache;
import android.util.Log;
+import com.android.intentresolver.ChooserActivity;
import com.android.intentresolver.contentpreview.ContentPreviewType;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.InstanceId;
@@ -39,7 +40,7 @@ import com.android.internal.util.FrameworkStatsLog;
* Helper for writing Sharesheet atoms to statsd log.
* @hide
*/
-public class ChooserActivityLogger {
+public class EventLog {
private static final String TAG = "ChooserActivity";
private static final boolean DEBUG = true;
@@ -94,12 +95,12 @@ public class ChooserActivityLogger {
private final FrameworkStatsLogger mFrameworkStatsLogger;
private final MetricsLogger mMetricsLogger;
- public ChooserActivityLogger() {
+ public EventLog() {
this(new UiEventLoggerImpl(), new DefaultFrameworkStatsLogger(), new MetricsLogger());
}
@VisibleForTesting
- ChooserActivityLogger(
+ EventLog(
UiEventLogger uiEventLogger,
FrameworkStatsLogger frameworkLogger,
MetricsLogger metricsLogger) {
diff --git a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
index bc54e01e..ff2d6a0f 100644
--- a/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AbstractResolverComparator.java
@@ -30,7 +30,7 @@ import android.os.Message;
import android.os.UserHandle;
import android.util.Log;
-import com.android.intentresolver.ChooserActivityLogger;
+import com.android.intentresolver.logging.EventLog;
import com.android.intentresolver.ResolvedComponentInfo;
import com.android.intentresolver.ResolverActivity;
import com.android.intentresolver.chooser.TargetInfo;
@@ -72,7 +72,7 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
private static final int WATCHDOG_TIMEOUT_MILLIS = 500;
private final Comparator<ResolveInfo> mAzComparator;
- private ChooserActivityLogger mChooserActivityLogger;
+ private EventLog mEventLog;
protected final Handler mHandler = new Handler(Looper.getMainLooper()) {
public void handleMessage(Message msg) {
@@ -94,8 +94,8 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
}
mHandler.removeMessages(RANKER_SERVICE_RESULT);
afterCompute();
- if (mChooserActivityLogger != null) {
- mChooserActivityLogger.logSharesheetAppShareRankingTimeout();
+ if (mEventLog != null) {
+ mEventLog.logSharesheetAppShareRankingTimeout();
}
break;
@@ -161,12 +161,12 @@ public abstract class AbstractResolverComparator implements Comparator<ResolvedC
mAfterCompute = afterCompute;
}
- void setChooserActivityLogger(ChooserActivityLogger chooserActivityLogger) {
- mChooserActivityLogger = chooserActivityLogger;
+ void setEventLog(EventLog eventLog) {
+ mEventLog = eventLog;
}
- ChooserActivityLogger getChooserActivityLogger() {
- return mChooserActivityLogger;
+ EventLog getEventLog() {
+ return mEventLog;
}
protected final void afterCompute() {
diff --git a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
index ba054731..621ae306 100644
--- a/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/AppPredictionServiceResolverComparator.java
@@ -31,7 +31,7 @@ import android.os.Message;
import android.os.UserHandle;
import android.util.Log;
-import com.android.intentresolver.ChooserActivityLogger;
+import com.android.intentresolver.logging.EventLog;
import com.android.intentresolver.ResolvedComponentInfo;
import com.android.intentresolver.chooser.TargetInfo;
@@ -72,7 +72,7 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp
String referrerPackage,
AppPredictor appPredictor,
UserHandle user,
- ChooserActivityLogger chooserActivityLogger,
+ EventLog eventLog,
@Nullable ComponentName promoteToFirst) {
super(context, intent, Lists.newArrayList(user), promoteToFirst);
mContext = context;
@@ -80,7 +80,7 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp
mAppPredictor = appPredictor;
mUser = user;
mReferrerPackage = referrerPackage;
- setChooserActivityLogger(chooserActivityLogger);
+ setEventLog(eventLog);
mComparatorModel = buildUpdatedModel();
}
@@ -116,7 +116,7 @@ public class AppPredictionServiceResolverComparator extends AbstractResolverComp
mIntent,
mReferrerPackage,
() -> mHandler.sendEmptyMessage(RANKER_SERVICE_RESULT),
- getChooserActivityLogger(),
+ getEventLog(),
mUser,
mPromoteToFirst);
mComparatorModel = buildUpdatedModel();
diff --git a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
index ebaffc36..7d473660 100644
--- a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
+++ b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java
@@ -39,7 +39,7 @@ import android.service.resolver.ResolverRankerService;
import android.service.resolver.ResolverTarget;
import android.util.Log;
-import com.android.intentresolver.ChooserActivityLogger;
+import com.android.intentresolver.logging.EventLog;
import com.android.intentresolver.ResolvedComponentInfo;
import com.android.intentresolver.chooser.TargetInfo;
import com.android.internal.logging.MetricsLogger;
@@ -102,9 +102,9 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom
*/
public ResolverRankerServiceResolverComparator(Context launchedFromContext, Intent intent,
String referrerPackage, Runnable afterCompute,
- ChooserActivityLogger chooserActivityLogger, UserHandle targetUserSpace,
+ EventLog eventLog, UserHandle targetUserSpace,
ComponentName promoteToFirst) {
- this(launchedFromContext, intent, referrerPackage, afterCompute, chooserActivityLogger,
+ this(launchedFromContext, intent, referrerPackage, afterCompute, eventLog,
Lists.newArrayList(targetUserSpace), promoteToFirst);
}
@@ -118,7 +118,7 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom
*/
public ResolverRankerServiceResolverComparator(Context launchedFromContext, Intent intent,
String referrerPackage, Runnable afterCompute,
- ChooserActivityLogger chooserActivityLogger, List<UserHandle> targetUserSpaceList,
+ EventLog eventLog, List<UserHandle> targetUserSpaceList,
@Nullable ComponentName promoteToFirst) {
super(launchedFromContext, intent, targetUserSpaceList, promoteToFirst);
mCollator = Collator.getInstance(
@@ -139,7 +139,7 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom
mAction = intent.getAction();
mRankerServiceName = new ComponentName(mContext, this.getClass());
setCallBack(afterCompute);
- setChooserActivityLogger(chooserActivityLogger);
+ setEventLog(eventLog);
mComparatorModel = buildUpdatedModel();
}
diff --git a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
index 3ffbe039..f05542e2 100644
--- a/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
+++ b/java/src/com/android/intentresolver/shortcuts/ShortcutLoader.kt
@@ -136,7 +136,8 @@ constructor(
}
/** Clear application targets (see [updateAppTargets] and initiate shrtcuts loading. */
- fun reset() {
+ @OpenForTesting
+ open fun reset() {
Log.d(TAG, "reset shortcut loader for user $userHandle")
appTargetSource.tryEmit(null)
shortcutSource.tryEmit(null)
diff --git a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
index 583a2887..3bbafc40 100644
--- a/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
+++ b/java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt
@@ -39,14 +39,12 @@ import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatu
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
-import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.onCompletion
import kotlinx.coroutines.flow.takeWhile
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
-import kotlinx.coroutines.plus
private const val TRANSITION_NAME = "screenshot_preview_image"
private const val PLURALS_COUNT = "count"
@@ -127,7 +125,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
isMeasured = true
updateMaxWidthHint(widthSpec)
updateMaxAspectRatio()
- batchLoader?.loadAspectRatios(getMaxWidth(), this::updatePreviewSize)
+ maybeLoadAspectRatios()
}
}
@@ -145,6 +143,17 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
)
}
+ override fun onAttachedToWindow() {
+ super.onAttachedToWindow()
+ batchLoader?.totalItemCount?.let(previewAdapter::reset)
+ maybeLoadAspectRatios()
+ }
+
+ override fun onDetachedFromWindow() {
+ batchLoader?.cancel()
+ super.onDetachedFromWindow()
+ }
+
override fun setTransitionElementStatusCallback(callback: TransitionElementStatusCallback?) {
previewAdapter.transitionStatusElementCallback = callback
}
@@ -158,32 +167,38 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
return null
}
- fun setPreviews(previews: List<Preview>, otherItemCount: Int, imageLoader: CachingImageLoader) {
- previewAdapter.reset(0, imageLoader)
+ fun setImageLoader(imageLoader: CachingImageLoader) {
+ previewAdapter.imageLoader = imageLoader
+ }
+
+ fun setLoading(totalItemCount: Int) {
+ previewAdapter.reset(totalItemCount)
+ }
+
+ fun setPreviews(previews: Flow<Preview>, totalItemCount: Int) {
+ previewAdapter.reset(totalItemCount)
batchLoader?.cancel()
batchLoader =
BatchPreviewLoader(
- imageLoader,
- previews,
- otherItemCount,
- onReset = { totalItemCount ->
- previewAdapter.reset(totalItemCount, imageLoader)
- },
- onUpdate = previewAdapter::addPreviews,
- onCompletion = {
- if (!previewAdapter.hasPreviews) {
- onNoPreviewCallback?.run()
- }
- }
- )
- .apply {
- if (isMeasured) {
- loadAspectRatios(
- getMaxWidth(),
- this@ScrollableImagePreviewView::updatePreviewSize
- )
+ previewAdapter.imageLoader ?: error("Image loader is not set"),
+ previews,
+ totalItemCount,
+ onUpdate = previewAdapter::addPreviews,
+ onCompletion = {
+ batchLoader = null
+ if (!previewAdapter.hasPreviews) {
+ onNoPreviewCallback?.run()
}
+ previewAdapter.markLoaded()
}
+ )
+ maybeLoadAspectRatios()
+ }
+
+ private fun maybeLoadAspectRatios() {
+ if (isMeasured && isAttachedToWindow()) {
+ batchLoader?.let { it.loadAspectRatios(getMaxWidth(), this::updatePreviewSize) }
+ }
}
var onNoPreviewCallback: Runnable? = null
@@ -262,10 +277,11 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
context.resources.getString(R.string.video_preview_a11y_description)
private val filePreviewDescription =
context.resources.getString(R.string.file_preview_a11y_description)
- private var imageLoader: CachingImageLoader? = null
+ var imageLoader: CachingImageLoader? = null
private var firstImagePos = -1
private var totalItemCount: Int = 0
+ private var isLoading = false
private val hasOtherItem
get() = previews.size < totalItemCount
val hasPreviews: Boolean
@@ -273,61 +289,79 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
var transitionStatusElementCallback: TransitionElementStatusCallback? = null
- fun reset(totalItemCount: Int, imageLoader: CachingImageLoader) {
- this.imageLoader = imageLoader
+ fun reset(totalItemCount: Int) {
firstImagePos = -1
previews.clear()
this.totalItemCount = maxOf(0, totalItemCount)
+ isLoading = this.totalItemCount > 0
notifyDataSetChanged()
}
+ fun markLoaded() {
+ if (!isLoading) return
+ isLoading = false
+ if (hasOtherItem) {
+ notifyItemChanged(previews.size)
+ } else {
+ notifyItemRemoved(previews.size)
+ }
+ }
+
fun addPreviews(newPreviews: Collection<Preview>) {
if (newPreviews.isEmpty()) return
val insertPos = previews.size
val hadOtherItem = hasOtherItem
+ val wasEmpty = previews.isEmpty()
previews.addAll(newPreviews)
if (firstImagePos < 0) {
val pos = newPreviews.indexOfFirst { it.type == PreviewType.Image }
if (pos >= 0) firstImagePos = insertPos + pos
}
- notifyItemRangeInserted(insertPos, newPreviews.size)
- when {
- hadOtherItem && previews.size >= totalItemCount -> {
- notifyItemRemoved(previews.size)
- }
- !hadOtherItem && previews.size < totalItemCount -> {
- notifyItemInserted(previews.size)
+ if (wasEmpty) {
+ // we don't want any item animation in that case
+ notifyDataSetChanged()
+ } else {
+ notifyItemRangeInserted(insertPos, newPreviews.size)
+ when {
+ hadOtherItem && !hasOtherItem -> {
+ notifyItemRemoved(previews.size)
+ }
+ !hadOtherItem && hasOtherItem -> {
+ notifyItemInserted(previews.size)
+ }
+ else -> notifyItemChanged(previews.size)
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, itemType: Int): ViewHolder {
val view = LayoutInflater.from(context).inflate(itemType, parent, false)
- return if (itemType == R.layout.image_preview_other_item) {
- OtherItemViewHolder(view)
- } else {
- PreviewViewHolder(
- view,
- imagePreviewDescription,
- videoPreviewDescription,
- filePreviewDescription,
- )
+ return when (itemType) {
+ R.layout.image_preview_other_item -> OtherItemViewHolder(view)
+ R.layout.image_preview_loading_item -> LoadingItemViewHolder(view)
+ else ->
+ PreviewViewHolder(
+ view,
+ imagePreviewDescription,
+ videoPreviewDescription,
+ filePreviewDescription,
+ )
}
}
- override fun getItemCount(): Int = previews.size + if (hasOtherItem) 1 else 0
+ override fun getItemCount(): Int = previews.size + if (isLoading || hasOtherItem) 1 else 0
- override fun getItemViewType(position: Int): Int {
- return if (position == previews.size) {
- R.layout.image_preview_other_item
- } else {
- R.layout.image_preview_image_item
+ override fun getItemViewType(position: Int): Int =
+ when {
+ position == previews.size && isLoading -> R.layout.image_preview_loading_item
+ position == previews.size -> R.layout.image_preview_other_item
+ else -> R.layout.image_preview_image_item
}
- }
override fun onBindViewHolder(vh: ViewHolder, position: Int) {
when (vh) {
is OtherItemViewHolder -> vh.bind(totalItemCount - previews.size)
+ is LoadingItemViewHolder -> vh.bind()
is PreviewViewHolder ->
vh.bind(
previews[position],
@@ -440,7 +474,7 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
}
private fun resetScope(): CoroutineScope =
- (MainScope() + Dispatchers.Main.immediate).also {
+ CoroutineScope(Dispatchers.Main.immediate).also {
scope?.cancel()
scope = it
}
@@ -466,6 +500,11 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
override fun unbind() = Unit
}
+ private class LoadingItemViewHolder(view: View) : ViewHolder(view) {
+ fun bind() = Unit
+ override fun unbind() = Unit
+ }
+
private class SpacingDecoration(private val innerSpacing: Int, private val outerSpacing: Int) :
ItemDecoration() {
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: State) {
@@ -485,27 +524,22 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
@VisibleForTesting
class BatchPreviewLoader(
private val imageLoader: CachingImageLoader,
- previews: List<Preview>,
- otherItemCount: Int,
- private val onReset: (Int) -> Unit,
+ private val previews: Flow<Preview>,
+ val totalItemCount: Int,
private val onUpdate: (List<Preview>) -> Unit,
private val onCompletion: () -> Unit,
) {
- private val previews: List<Preview> =
- if (previews is RandomAccess) previews else ArrayList(previews)
- private val totalItemCount = previews.size + otherItemCount
- private var scope: CoroutineScope? = MainScope() + Dispatchers.Main.immediate
+ private var scope: CoroutineScope = createScope()
+
+ private fun createScope() = CoroutineScope(Dispatchers.Main.immediate)
fun cancel() {
- scope?.cancel()
- scope = null
+ scope.cancel()
+ scope = createScope()
}
fun loadAspectRatios(maxWidth: Int, previewSizeUpdater: (Preview, Int, Int) -> Int) {
- val scope = this.scope ?: return
- // -1 encodes that the preview has not been processed,
- // 0 means failed, > 0 is a preview width
- val previewWidths = IntArray(previews.size) { -1 }
+ val previewInfos = ArrayList<PreviewWidthInfo>(totalItemCount)
var blockStart = 0 // inclusive
var blockEnd = 0 // exclusive
@@ -514,26 +548,16 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
val updateEvent = Any()
val completedEvent = Any()
- // throttle adapter updates using flow; the flow first emits when enough preview
- // elements is loaded to fill the viewport and then each time a subsequent block of
- // previews is loaded
+ // collects updates from [reportFlow] throttling adapter updates;
scope.launch(Dispatchers.Main) {
reportFlow
.takeWhile { it !== completedEvent }
.throttle(ADAPTER_UPDATE_INTERVAL_MS)
- .onCompletion { cause ->
- if (cause == null) {
- onCompletion()
- }
- }
.collect {
- if (blockStart == 0) {
- onReset(totalItemCount)
- }
val updates = ArrayList<Preview>(blockEnd - blockStart)
while (blockStart < blockEnd) {
- if (previewWidths[blockStart] > 0) {
- updates.add(previews[blockStart])
+ if (previewInfos[blockStart].width > 0) {
+ updates.add(previewInfos[blockStart].preview)
}
blockStart++
}
@@ -541,57 +565,64 @@ class ScrollableImagePreviewView : RecyclerView, ImagePreviewView {
onUpdate(updates)
}
}
+ onCompletion()
}
+ // Collects [previews] flow and loads aspect ratios, emits updates into [reportFlow]
+ // when a next sequential block of preview aspect ratios is loaded: initially emits when
+ // enough preview elements is loaded to fill the viewport.
scope.launch {
var blockWidth = 0
var isFirstBlock = true
- var nextIdx = 0
- List<Job>(4) {
- launch {
- while (true) {
- val i = nextIdx++
- if (i >= previews.size) break
- val preview = previews[i]
-
- previewWidths[i] =
- runCatching {
- // TODO: decide on adding a timeout
- imageLoader(preview.uri, isFirstBlock)?.let { bitmap ->
- previewSizeUpdater(
- preview,
- bitmap.width,
- bitmap.height
- )
- }
- ?: 0
- }
- .getOrDefault(0)
-
- if (blockEnd != i) continue
- while (
- blockEnd < previewWidths.size && previewWidths[blockEnd] >= 0
- ) {
- blockWidth += previewWidths[blockEnd]
- blockEnd++
- }
- if (isFirstBlock) {
- if (blockWidth >= maxWidth) {
- isFirstBlock = false
- // notify that the preview now can be displayed
- reportFlow.emit(updateEvent)
+
+ val jobs = ArrayList<Job>()
+ previews.collect { preview ->
+ val i = previewInfos.size
+ val pair = PreviewWidthInfo(preview)
+ previewInfos.add(pair)
+
+ val job = launch {
+ pair.width =
+ runCatching {
+ // TODO: decide on adding a timeout. The worst case I can
+ // imagine is one of the first images never loads so we never
+ // fill the initial viewport and does not show the previews at
+ // all.
+ imageLoader(preview.uri, isFirstBlock)?.let { bitmap ->
+ previewSizeUpdater(preview, bitmap.width, bitmap.height)
}
- } else {
- reportFlow.emit(updateEvent)
+ ?: 0
}
+ .getOrDefault(0)
+
+ if (i == blockEnd) {
+ while (
+ blockEnd < previewInfos.size && previewInfos[blockEnd].width >= 0
+ ) {
+ blockWidth += previewInfos[blockEnd].width
+ blockEnd++
+ }
+ if (isFirstBlock && blockWidth >= maxWidth) {
+ isFirstBlock = false
+ }
+ if (!isFirstBlock) {
+ reportFlow.emit(updateEvent)
}
}
}
- .joinAll()
+ jobs.add(job)
+ }
+ jobs.joinAll()
// in case all previews have failed to load
reportFlow.emit(updateEvent)
reportFlow.emit(completedEvent)
}
}
}
+
+ private class PreviewWidthInfo(val preview: Preview) {
+ // -1 encodes that the preview has not been processed,
+ // 0 means failed, > 0 is a preview width
+ var width: Int = -1
+ }
}
diff --git a/java/tests/Android.bp b/java/tests/Android.bp
index c381d0a8..90c7fb7a 100644
--- a/java/tests/Android.bp
+++ b/java/tests/Android.bp
@@ -19,18 +19,23 @@ android_test {
static_libs: [
"IntentResolver-core",
+ "androidx.test.core",
"androidx.test.rules",
"androidx.test.ext.junit",
+ "androidx.test.ext.truth",
"androidx.test.espresso.contrib",
- "mockito-target-minus-junit4",
"androidx.test.espresso.core",
+ "androidx.test.rules",
"androidx.lifecycle_lifecycle-common-java8",
"androidx.lifecycle_lifecycle-extensions",
"androidx.lifecycle_lifecycle-runtime-ktx",
- "truth-prebuilt",
- "testables",
+ "androidx.lifecycle_lifecycle-runtime-testing",
"kotlinx_coroutines_test",
+ "mockito-target-minus-junit4",
+ "testables",
+ "truth-prebuilt",
],
+ plugins: ["dagger2-compiler"],
test_suites: ["general-tests"],
sdk_version: "core_platform",
compile_multilib: "both",
diff --git a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt
index 8d994f08..af6e5f16 100644
--- a/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt
+++ b/java/tests/src/com/android/intentresolver/ChooserActionFactoryTest.kt
@@ -27,6 +27,7 @@ import android.graphics.drawable.Icon
import android.service.chooser.ChooserAction
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
+import com.android.intentresolver.logging.EventLog
import com.google.common.collect.ImmutableList
import com.google.common.truth.Truth.assertThat
import java.util.concurrent.CountDownLatch
@@ -43,7 +44,7 @@ import org.mockito.Mockito
class ChooserActionFactoryTest {
private val context = InstrumentationRegistry.getInstrumentation().getContext()
- private val logger = mock<ChooserActivityLogger>()
+ private val logger = mock<EventLog>()
private val actionLabel = "Action label"
private val modifyShareLabel = "Modify share"
private val testAction = "com.android.intentresolver.testaction"
@@ -107,7 +108,7 @@ class ChooserActionFactoryTest {
action.onClicked.run()
Mockito.verify(logger)
- .logActionSelected(eq(ChooserActivityLogger.SELECTION_TYPE_MODIFY_SHARE))
+ .logActionSelected(eq(EventLog.SELECTION_TYPE_MODIFY_SHARE))
assertEquals(Activity.RESULT_OK, resultConsumer.latestReturn)
// Verify the pending intent has been called
countdown.await(500, TimeUnit.MILLISECONDS)
diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
index ce96ef63..84f5124c 100644
--- a/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
+++ b/java/tests/src/com/android/intentresolver/ChooserActivityOverrideData.java
@@ -30,6 +30,7 @@ import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileI
import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.contentpreview.ImageLoader;
import com.android.intentresolver.flags.FeatureFlagRepository;
+import com.android.intentresolver.logging.EventLog;
import com.android.intentresolver.shortcuts.ShortcutLoader;
import java.util.function.Consumer;
@@ -64,7 +65,7 @@ public class ChooserActivityOverrideData {
public Cursor resolverCursor;
public boolean resolverForceException;
public ImageLoader imageLoader;
- public ChooserActivityLogger chooserActivityLogger;
+ public EventLog mEventLog;
public int alternateProfileSetting;
public Resources resources;
public UserHandle workProfileUserHandle;
@@ -87,7 +88,7 @@ public class ChooserActivityOverrideData {
resolverForceException = false;
resolverListController = mock(ChooserActivity.ChooserListController.class);
workResolverListController = mock(ChooserActivity.ChooserListController.class);
- chooserActivityLogger = mock(ChooserActivityLogger.class);
+ mEventLog = mock(EventLog.class);
alternateProfileSetting = 0;
resources = null;
workProfileUserHandle = null;
diff --git a/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt b/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt
index 4612b430..c8cb4b9b 100644
--- a/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt
+++ b/java/tests/src/com/android/intentresolver/ChooserListAdapterTest.kt
@@ -31,6 +31,7 @@ import com.android.intentresolver.chooser.DisplayResolveInfo
import com.android.intentresolver.chooser.SelectableTargetInfo
import com.android.intentresolver.chooser.TargetInfo
import com.android.intentresolver.icons.TargetDataLoader
+import com.android.intentresolver.logging.EventLog
import com.android.internal.R
import org.junit.Before
import org.junit.Test
@@ -49,7 +50,7 @@ class ChooserListAdapterTest {
}
private val context = InstrumentationRegistry.getInstrumentation().context
private val resolverListController = mock<ResolverListController>()
- private val chooserActivityLogger = mock<ChooserActivityLogger>()
+ private val mEventLog = mock<EventLog>()
private val mTargetDataLoader = mock<TargetDataLoader>()
private val testSubject by lazy {
@@ -64,7 +65,7 @@ class ChooserListAdapterTest {
Intent(),
mock(),
packageManager,
- chooserActivityLogger,
+ mEventLog,
mock(),
0,
null,
diff --git a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
index 6ac6b6d3..8608cf72 100644
--- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
+++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
@@ -40,6 +40,7 @@ import com.android.intentresolver.chooser.TargetInfo;
import com.android.intentresolver.flags.FeatureFlagRepository;
import com.android.intentresolver.grid.ChooserGridAdapter;
import com.android.intentresolver.icons.TargetDataLoader;
+import com.android.intentresolver.logging.EventLog;
import com.android.intentresolver.shortcuts.ShortcutLoader;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -89,7 +90,7 @@ public class ChooserWrapperActivity
targetIntent,
this,
packageManager,
- getChooserActivityLogger(),
+ getEventLog(),
chooserRequest,
maxTargetsPerRow,
userHandle,
@@ -205,8 +206,8 @@ public class ChooserWrapperActivity
}
@Override
- public ChooserActivityLogger getChooserActivityLogger() {
- return sOverrides.chooserActivityLogger;
+ public EventLog getEventLog() {
+ return sOverrides.mEventLog;
}
@Override
diff --git a/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt b/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt
index 9ea9dfa7..c7d20000 100644
--- a/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt
+++ b/java/tests/src/com/android/intentresolver/EnterTransitionAnimationDelegateTest.kt
@@ -21,6 +21,7 @@ import android.view.View
import android.view.Window
import androidx.activity.ComponentActivity
import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.testing.TestLifecycleOwner
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
@@ -44,35 +45,34 @@ class EnterTransitionAnimationDelegateTest {
private val dispatcher = StandardTestDispatcher(scheduler)
private val lifecycleOwner = TestLifecycleOwner()
- private val transitionTargetView = mock<View> {
- // avoid the request-layout path in the delegate
- whenever(isInLayout).thenReturn(true)
- }
+ private val transitionTargetView =
+ mock<View> {
+ // avoid the request-layout path in the delegate
+ whenever(isInLayout).thenReturn(true)
+ }
private val windowMock = mock<Window>()
- private val resourcesMock = mock<Resources> {
- whenever(getInteger(anyInt())).thenReturn(TIMEOUT_MS)
- }
- private val activity = mock<ComponentActivity> {
- whenever(lifecycle).thenReturn(lifecycleOwner.lifecycle)
- whenever(resources).thenReturn(resourcesMock)
- whenever(isActivityTransitionRunning).thenReturn(true)
- whenever(window).thenReturn(windowMock)
- }
-
- private val testSubject = EnterTransitionAnimationDelegate(activity) {
- transitionTargetView
- }
+ private val resourcesMock =
+ mock<Resources> { whenever(getInteger(anyInt())).thenReturn(TIMEOUT_MS) }
+ private val activity =
+ mock<ComponentActivity> {
+ whenever(lifecycle).thenReturn(lifecycleOwner.lifecycle)
+ whenever(resources).thenReturn(resourcesMock)
+ whenever(isActivityTransitionRunning).thenReturn(true)
+ whenever(window).thenReturn(windowMock)
+ }
+
+ private val testSubject = EnterTransitionAnimationDelegate(activity) { transitionTargetView }
@Before
fun setup() {
Dispatchers.setMain(dispatcher)
- lifecycleOwner.state = Lifecycle.State.CREATED
+ lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
@After
fun cleanup() {
- lifecycleOwner.state = Lifecycle.State.DESTROYED
+ lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
Dispatchers.resetMain()
}
diff --git a/java/tests/src/com/android/intentresolver/IChooserWrapper.java b/java/tests/src/com/android/intentresolver/IChooserWrapper.java
index af897a47..3326d7f2 100644
--- a/java/tests/src/com/android/intentresolver/IChooserWrapper.java
+++ b/java/tests/src/com/android/intentresolver/IChooserWrapper.java
@@ -23,6 +23,7 @@ import android.content.pm.ResolveInfo;
import android.os.UserHandle;
import com.android.intentresolver.chooser.DisplayResolveInfo;
+import com.android.intentresolver.logging.EventLog;
import java.util.concurrent.Executor;
@@ -41,6 +42,6 @@ public interface IChooserWrapper {
CharSequence pLabel, CharSequence pInfo, Intent replacementIntent,
@Nullable TargetPresentationGetter resolveInfoPresentationGetter);
UserHandle getCurrentUserHandle();
- ChooserActivityLogger getChooserActivityLogger();
+ EventLog getEventLog();
Executor getMainExecutor();
}
diff --git a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
index 688dd867..1f8d9bee 100644
--- a/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
+++ b/java/tests/src/com/android/intentresolver/ResolverDataProvider.java
@@ -103,30 +103,26 @@ public class ResolverDataProvider {
}
public static ResolveInfo createResolveInfo(int i, int userId) {
- final ResolveInfo resolveInfo = new ResolveInfo();
- resolveInfo.activityInfo = createActivityInfo(i);
- resolveInfo.targetUserId = userId;
- return resolveInfo;
+ return createResolveInfo(i, userId, UserHandle.of(userId));
}
+
public static ResolveInfo createResolveInfo(int i, int userId, UserHandle resolvedForUser) {
- final ResolveInfo resolveInfo = new ResolveInfo();
- resolveInfo.activityInfo = createActivityInfo(i);
- resolveInfo.targetUserId = userId;
- resolveInfo.userHandle = resolvedForUser;
- return resolveInfo;
+ return createResolveInfo(createActivityInfo(i), userId, resolvedForUser);
}
public static ResolveInfo createResolveInfo(ComponentName componentName, int userId) {
- final ResolveInfo resolveInfo = new ResolveInfo();
- resolveInfo.activityInfo = createActivityInfo(componentName);
- resolveInfo.targetUserId = userId;
- return resolveInfo;
+ return createResolveInfo(componentName, userId, UserHandle.of(userId));
}
- public static ResolveInfo createResolveInfo(ComponentName componentName, int userId,
- UserHandle resolvedForUser) {
+ public static ResolveInfo createResolveInfo(
+ ComponentName componentName, int userId, UserHandle resolvedForUser) {
+ return createResolveInfo(createActivityInfo(componentName), userId, resolvedForUser);
+ }
+
+ public static ResolveInfo createResolveInfo(
+ ActivityInfo activityInfo, int userId, UserHandle resolvedForUser) {
final ResolveInfo resolveInfo = new ResolveInfo();
- resolveInfo.activityInfo = createActivityInfo(componentName);
+ resolveInfo.activityInfo = activityInfo;
resolveInfo.targetUserId = userId;
resolveInfo.userHandle = resolvedForUser;
return resolveInfo;
diff --git a/java/tests/src/com/android/intentresolver/TestContentProvider.kt b/java/tests/src/com/android/intentresolver/TestContentProvider.kt
index b3b53baa..426f9af2 100644
--- a/java/tests/src/com/android/intentresolver/TestContentProvider.kt
+++ b/java/tests/src/com/android/intentresolver/TestContentProvider.kt
@@ -30,15 +30,23 @@ class TestContentProvider : ContentProvider() {
sortOrder: String?
): Cursor? = null
- override fun getType(uri: Uri): String?
- = runCatching {
- uri.getQueryParameter("mimeType")
- }.getOrNull()
+ override fun getType(uri: Uri): String? =
+ runCatching { uri.getQueryParameter(PARAM_MIME_TYPE) }.getOrNull()
- override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>?
- = runCatching {
- uri.getQueryParameter("streamType")?.let { arrayOf(it) }
- }.getOrNull()
+ override fun getStreamTypes(uri: Uri, mimeTypeFilter: String): Array<String>? {
+ val delay =
+ runCatching { uri.getQueryParameter(PARAM_STREAM_TYPE_TIMEOUT)?.toLong() ?: 0L }
+ .getOrDefault(0L)
+ if (delay > 0) {
+ try {
+ Thread.sleep(delay)
+ } catch (e: InterruptedException) {
+ Thread.currentThread().interrupt()
+ }
+ }
+ return runCatching { uri.getQueryParameter(PARAM_STREAM_TYPE)?.let { arrayOf(it) } }
+ .getOrNull()
+ }
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
@@ -52,4 +60,10 @@ class TestContentProvider : ContentProvider() {
): Int = 0
override fun onCreate(): Boolean = true
-} \ No newline at end of file
+
+ companion object {
+ const val PARAM_MIME_TYPE = "mimeType"
+ const val PARAM_STREAM_TYPE = "streamType"
+ const val PARAM_STREAM_TYPE_TIMEOUT = "streamTypeTo"
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt b/java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt
deleted file mode 100644
index 7e588f98..00000000
--- a/java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.intentresolver
-
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.LifecycleOwner
-import androidx.lifecycle.LifecycleRegistry
-
-internal class TestLifecycleOwner : LifecycleOwner {
- private val lifecycleRegistry = LifecycleRegistry.createUnsafe(this)
-
- override val lifecycle: Lifecycle get() = lifecycleRegistry
-
- var state: Lifecycle.State
- get() = lifecycle.currentState
- set(value) {
- lifecycleRegistry.currentState = value
- }
-} \ No newline at end of file
diff --git a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
index 3ddd4394..b8b57403 100644
--- a/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
+++ b/java/tests/src/com/android/intentresolver/UnbundledChooserActivityTest.java
@@ -52,6 +52,7 @@ import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -107,6 +108,7 @@ import androidx.test.rule.ActivityTestRule;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.contentpreview.ImageLoader;
+import com.android.intentresolver.logging.EventLog;
import com.android.intentresolver.shortcuts.ShortcutLoader;
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
@@ -134,6 +136,10 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Function;
@@ -862,7 +868,7 @@ public class UnbundledChooserActivityTest {
}
@Test
- public void copyTextToClipboard() throws Exception {
+ public void copyTextToClipboard() {
Intent sendIntent = createSendTextIntent();
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -877,7 +883,8 @@ public class UnbundledChooserActivityTest {
ClipboardManager clipboard = (ClipboardManager) activity.getSystemService(
Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
- assertThat("testing intent sending", is(clipData.getItemAt(0).getText()));
+ assertThat(clipData).isNotNull();
+ assertThat(clipData.getItemAt(0).getText()).isEqualTo("testing intent sending");
ClipDescription clipDescription = clipData.getDescription();
assertThat("text/plain", is(clipDescription.getMimeType(0)));
@@ -899,8 +906,8 @@ public class UnbundledChooserActivityTest {
onView(withId(R.id.copy)).check(matches(isDisplayed()));
onView(withId(R.id.copy)).perform(click());
- ChooserActivityLogger logger = activity.getChooserActivityLogger();
- verify(logger, times(1)).logActionSelected(eq(ChooserActivityLogger.SELECTION_TYPE_COPY));
+ EventLog logger = activity.getEventLog();
+ verify(logger, times(1)).logActionSelected(eq(EventLog.SELECTION_TYPE_COPY));
}
@Test
@@ -1003,6 +1010,55 @@ public class UnbundledChooserActivityTest {
}
@Test
+ public void testSlowUriMetadata_fallbackToFilePreview() throws InterruptedException {
+ Uri uri = createTestContentProviderUri(
+ "application/pdf", "image/png", /*streamTypeTimeout=*/4_000);
+ ArrayList<Uri> uris = new ArrayList<>(1);
+ uris.add(uri);
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(uri, createBitmap());
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+
+ setupResolverControllers(resolvedComponentInfos);
+ assertThat(launchActivityWithTimeout(Intent.createChooser(sendIntent, null), 2_000))
+ .isTrue();
+ waitForIdle();
+
+ onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed()));
+ onView(withId(R.id.content_preview_filename)).check(matches(withText("image.png")));
+ onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed()));
+ }
+
+ @Test
+ public void testSendManyFilesWithSmallMetadataDelayAndOneImage_fallbackToFilePreviewUi()
+ throws InterruptedException {
+ Uri fileUri = createTestContentProviderUri(
+ "application/pdf", "application/pdf", /*streamTypeTimeout=*/150);
+ Uri imageUri = createTestContentProviderUri("application/pdf", "image/png");
+ ArrayList<Uri> uris = new ArrayList<>(50);
+ for (int i = 0; i < 49; i++) {
+ uris.add(fileUri);
+ }
+ uris.add(imageUri);
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ createImageLoader(imageUri, createBitmap());
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+ assertThat(launchActivityWithTimeout(Intent.createChooser(sendIntent, null), 2_000))
+ .isTrue();
+
+ waitForIdle();
+
+ onView(withId(R.id.content_preview_filename)).check(matches(isDisplayed()));
+ onView(withId(R.id.content_preview_filename)).check(matches(withText("image.png")));
+ onView(withId(R.id.content_preview_file_icon)).check(matches(isDisplayed()));
+ }
+
+ @Test
public void testManyVisibleImagePreview_ScrollableImagePreview() {
Uri uri = createTestContentProviderUri("image/png", null);
@@ -1039,6 +1095,63 @@ public class UnbundledChooserActivityTest {
}
@Test
+ public void testPartiallyLoadedMetadata_previewIsShownForTheLoadedPart()
+ throws InterruptedException {
+ Uri imgOneUri = createTestContentProviderUri("image/png", null);
+ Uri imgTwoUri = createTestContentProviderUri("image/png", null)
+ .buildUpon()
+ .path("image-2.png")
+ .build();
+ Uri docUri = createTestContentProviderUri("application/pdf", "image/png", 3_000);
+ ArrayList<Uri> uris = new ArrayList<>(2);
+ // two large previews to fill the screen and be presented right away and one
+ // document that would be delayed by the URI metadata reading
+ uris.add(imgOneUri);
+ uris.add(imgTwoUri);
+ uris.add(docUri);
+
+ Intent sendIntent = createSendUriIntentWithPreview(uris);
+ Map<Uri, Bitmap> bitmaps = new HashMap<>();
+ bitmaps.put(imgOneUri, createWideBitmap(Color.RED));
+ bitmaps.put(imgTwoUri, createWideBitmap(Color.GREEN));
+ bitmaps.put(docUri, createWideBitmap(Color.BLUE));
+ ChooserActivityOverrideData.getInstance().imageLoader =
+ new TestPreviewImageLoader(bitmaps);
+
+ List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
+ setupResolverControllers(resolvedComponentInfos);
+
+ assertThat(launchActivityWithTimeout(Intent.createChooser(sendIntent, null), 1_000))
+ .isTrue();
+ waitForIdle();
+
+ onView(withId(R.id.scrollable_image_preview))
+ .check((view, exception) -> {
+ if (exception != null) {
+ throw exception;
+ }
+ RecyclerView recyclerView = (RecyclerView) view;
+ assertThat(recyclerView.getChildCount()).isAtLeast(1);
+ // the first view is a preview
+ View imageView = recyclerView.getChildAt(0).findViewById(R.id.image);
+ assertThat(imageView).isNotNull();
+ })
+ .perform(RecyclerViewActions.scrollToLastPosition())
+ .check((view, exception) -> {
+ if (exception != null) {
+ throw exception;
+ }
+ RecyclerView recyclerView = (RecyclerView) view;
+ assertThat(recyclerView.getChildCount()).isAtLeast(1);
+ // check that the last view is a loading indicator
+ View loadingIndicator =
+ recyclerView.getChildAt(recyclerView.getChildCount() - 1);
+ assertThat(loadingIndicator).isNotNull();
+ });
+ waitForIdle();
+ }
+
+ @Test
public void testImageAndTextPreview() {
final Uri uri = createTestContentProviderUri("image/png", null);
final String sharedText = "text-" + System.currentTimeMillis();
@@ -1099,7 +1212,7 @@ public class UnbundledChooserActivityTest {
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
- ChooserActivityLogger logger = activity.getChooserActivityLogger();
+ EventLog logger = activity.getEventLog();
waitForIdle();
verify(logger).logChooserActivityShown(eq(false), eq(TEST_MIME_TYPE), anyLong());
@@ -1114,7 +1227,7 @@ public class UnbundledChooserActivityTest {
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, "logger test"));
- ChooserActivityLogger logger = activity.getChooserActivityLogger();
+ EventLog logger = activity.getEventLog();
waitForIdle();
verify(logger).logChooserActivityShown(eq(true), eq(TEST_MIME_TYPE), anyLong());
@@ -1127,7 +1240,7 @@ public class UnbundledChooserActivityTest {
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(
Intent.createChooser(sendIntent, "empty preview logger test"));
- ChooserActivityLogger logger = activity.getChooserActivityLogger();
+ EventLog logger = activity.getEventLog();
waitForIdle();
verify(logger).logChooserActivityShown(eq(false), eq(null), anyLong());
@@ -1146,7 +1259,7 @@ public class UnbundledChooserActivityTest {
waitForIdle();
// Second invocation is from onCreate
- ChooserActivityLogger logger = activity.getChooserActivityLogger();
+ EventLog logger = activity.getEventLog();
Mockito.verify(logger, times(1)).logActionShareWithPreview(eq(CONTENT_PREVIEW_TEXT));
}
@@ -1168,7 +1281,7 @@ public class UnbundledChooserActivityTest {
final IChooserWrapper activity = (IChooserWrapper)
mActivityRule.launchActivity(Intent.createChooser(sendIntent, null));
waitForIdle();
- ChooserActivityLogger logger = activity.getChooserActivityLogger();
+ EventLog logger = activity.getEventLog();
Mockito.verify(logger, times(1)).logActionShareWithPreview(eq(CONTENT_PREVIEW_IMAGE));
}
@@ -1370,8 +1483,8 @@ public class UnbundledChooserActivityTest {
ArgumentCaptor<HashedStringCache.HashResult> hashCaptor =
ArgumentCaptor.forClass(HashedStringCache.HashResult.class);
- verify(activity.getChooserActivityLogger(), times(1)).logShareTargetSelected(
- eq(ChooserActivityLogger.SELECTION_TYPE_SERVICE),
+ verify(activity.getEventLog(), times(1)).logShareTargetSelected(
+ eq(EventLog.SELECTION_TYPE_SERVICE),
/* packageName= */ any(),
/* positionPicked= */ anyInt(),
/* directTargetAlsoRanked= */ eq(-1),
@@ -1451,8 +1564,8 @@ public class UnbundledChooserActivityTest {
.perform(click());
waitForIdle();
- verify(activity.getChooserActivityLogger(), times(1)).logShareTargetSelected(
- eq(ChooserActivityLogger.SELECTION_TYPE_SERVICE),
+ verify(activity.getEventLog(), times(1)).logShareTargetSelected(
+ eq(EventLog.SELECTION_TYPE_SERVICE),
/* packageName= */ any(),
/* positionPicked= */ anyInt(),
/* directTargetAlsoRanked= */ eq(0),
@@ -1466,14 +1579,10 @@ public class UnbundledChooserActivityTest {
@Test
public void testShortcutTargetWithApplyAppLimits() {
// Set up resources
- ChooserActivityOverrideData.getInstance().resources = Mockito.spy(
+ Resources resources = Mockito.spy(
InstrumentationRegistry.getInstrumentation().getContext().getResources());
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resources
- .getInteger(R.integer.config_maxShortcutTargetsPerApp))
- .thenReturn(1);
+ ChooserActivityOverrideData.getInstance().resources = resources;
+ doReturn(1).when(resources).getInteger(R.integer.config_maxShortcutTargetsPerApp);
Intent sendIntent = createSendTextIntent();
// We need app targets for direct targets to get displayed
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -1541,14 +1650,10 @@ public class UnbundledChooserActivityTest {
SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
Boolean.toString(false));
// Set up resources
- ChooserActivityOverrideData.getInstance().resources = Mockito.spy(
+ Resources resources = Mockito.spy(
InstrumentationRegistry.getInstrumentation().getContext().getResources());
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resources
- .getInteger(R.integer.config_maxShortcutTargetsPerApp))
- .thenReturn(1);
+ ChooserActivityOverrideData.getInstance().resources = resources;
+ doReturn(1).when(resources).getInteger(R.integer.config_maxShortcutTargetsPerApp);
Intent sendIntent = createSendTextIntent();
// We need app targets for direct targets to get displayed
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -1620,14 +1725,10 @@ public class UnbundledChooserActivityTest {
SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI,
Boolean.toString(false));
// Set up resources
- ChooserActivityOverrideData.getInstance().resources = Mockito.spy(
+ Resources resources = Mockito.spy(
InstrumentationRegistry.getInstrumentation().getContext().getResources());
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resources
- .getInteger(R.integer.config_maxShortcutTargetsPerApp))
- .thenReturn(1);
+ ChooserActivityOverrideData.getInstance().resources = resources;
+ doReturn(1).when(resources).getInteger(R.integer.config_maxShortcutTargetsPerApp);
// We need app targets for direct targets to get displayed
List<ResolvedComponentInfo> resolvedComponentInfos = createResolvedComponentsForTest(2);
@@ -1823,14 +1924,10 @@ public class UnbundledChooserActivityTest {
.getResources().getConfiguration());
configuration.orientation = orientation;
- ChooserActivityOverrideData.getInstance().resources = Mockito.spy(
+ Resources resources = Mockito.spy(
InstrumentationRegistry.getInstrumentation().getContext().getResources());
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resources
- .getConfiguration())
- .thenReturn(configuration);
+ ChooserActivityOverrideData.getInstance().resources = resources;
+ doReturn(configuration).when(resources).getConfiguration();
Intent sendIntent = createSendTextIntent();
// We need app targets for direct targets to get displayed
@@ -1877,9 +1974,9 @@ public class UnbundledChooserActivityTest {
.perform(click());
waitForIdle();
- ChooserActivityLogger logger = wrapper.getChooserActivityLogger();
+ EventLog logger = wrapper.getEventLog();
verify(logger, times(1)).logShareTargetSelected(
- eq(ChooserActivityLogger.SELECTION_TYPE_SERVICE),
+ eq(EventLog.SELECTION_TYPE_SERVICE),
/* packageName= */ any(),
/* positionPicked= */ anyInt(),
// The packages sholdn't match for app target and direct target:
@@ -2209,10 +2306,10 @@ public class UnbundledChooserActivityTest {
.perform(click());
waitForIdle();
- ChooserActivityLogger logger = activity.getChooserActivityLogger();
+ EventLog logger = activity.getEventLog();
ArgumentCaptor<Integer> typeCaptor = ArgumentCaptor.forClass(Integer.class);
verify(logger, times(1)).logShareTargetSelected(
- eq(ChooserActivityLogger.SELECTION_TYPE_SERVICE),
+ eq(EventLog.SELECTION_TYPE_SERVICE),
/* packageName= */ any(),
/* positionPicked= */ anyInt(),
/* directTargetAlsoRanked= */ anyInt(),
@@ -2654,15 +2751,25 @@ public class UnbundledChooserActivityTest {
private Uri createTestContentProviderUri(
@Nullable String mimeType, @Nullable String streamType) {
+ return createTestContentProviderUri(mimeType, streamType, 0);
+ }
+
+ private Uri createTestContentProviderUri(
+ @Nullable String mimeType, @Nullable String streamType, long streamTypeTimeout) {
String packageName =
InstrumentationRegistry.getInstrumentation().getContext().getPackageName();
Uri.Builder builder = Uri.parse("content://" + packageName + "/image.png")
.buildUpon();
if (mimeType != null) {
- builder.appendQueryParameter("mimeType", mimeType);
+ builder.appendQueryParameter(TestContentProvider.PARAM_MIME_TYPE, mimeType);
}
if (streamType != null) {
- builder.appendQueryParameter("streamType", streamType);
+ builder.appendQueryParameter(TestContentProvider.PARAM_STREAM_TYPE, streamType);
+ }
+ if (streamTypeTimeout > 0) {
+ builder.appendQueryParameter(
+ TestContentProvider.PARAM_STREAM_TYPE_TIMEOUT,
+ Long.toString(streamTypeTimeout));
}
return builder.build();
}
@@ -2792,11 +2899,44 @@ public class UnbundledChooserActivityTest {
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
}
+ private boolean launchActivityWithTimeout(Intent intent, long timeout)
+ throws InterruptedException {
+ final int initialState = 0;
+ final int completedState = 1;
+ final int timeoutState = 2;
+ final AtomicInteger state = new AtomicInteger(initialState);
+ final CountDownLatch cdl = new CountDownLatch(1);
+
+ ScheduledExecutorService executor = Executors.newScheduledThreadPool(2);
+ try {
+ executor.execute(() -> {
+ mActivityRule.launchActivity(intent);
+ state.compareAndSet(initialState, completedState);
+ cdl.countDown();
+ });
+ executor.schedule(
+ () -> {
+ state.compareAndSet(initialState, timeoutState);
+ cdl.countDown();
+ },
+ timeout,
+ TimeUnit.MILLISECONDS);
+ cdl.await();
+ return state.get() == completedState;
+ } finally {
+ executor.shutdownNow();
+ }
+ }
+
private Bitmap createBitmap() {
return createBitmap(200, 200);
}
private Bitmap createWideBitmap() {
+ return createWideBitmap(Color.RED);
+ }
+
+ private Bitmap createWideBitmap(int bgColor) {
WindowManager windowManager = InstrumentationRegistry.getInstrumentation()
.getTargetContext()
.getSystemService(WindowManager.class);
@@ -2805,15 +2945,19 @@ public class UnbundledChooserActivityTest {
Rect bounds = windowManager.getMaximumWindowMetrics().getBounds();
width = bounds.width() + 200;
}
- return createBitmap(width, 100);
+ return createBitmap(width, 100, bgColor);
}
private Bitmap createBitmap(int width, int height) {
+ return createBitmap(width, height, Color.RED);
+ }
+
+ private Bitmap createBitmap(int width, int height, int bgColor) {
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Paint paint = new Paint();
- paint.setColor(Color.RED);
+ paint.setColor(bgColor);
paint.setStyle(Paint.Style.FILL);
canvas.drawPaint(paint);
@@ -2941,14 +3085,11 @@ public class UnbundledChooserActivityTest {
}
private void updateMaxTargetsPerRowResource(int targetsPerRow) {
- ChooserActivityOverrideData.getInstance().resources = Mockito.spy(
+ Resources resources = Mockito.spy(
InstrumentationRegistry.getInstrumentation().getContext().getResources());
- when(
- ChooserActivityOverrideData
- .getInstance()
- .resources
- .getInteger(R.integer.config_chooser_max_targets_per_row))
- .thenReturn(targetsPerRow);
+ ChooserActivityOverrideData.getInstance().resources = resources;
+ doReturn(targetsPerRow).when(resources).getInteger(
+ R.integer.config_chooser_max_targets_per_row);
}
private SparseArray<Pair<ShortcutLoader, Consumer<ShortcutLoader.Result>>>
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt
index 9bfd2052..dab1a956 100644
--- a/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt
+++ b/java/tests/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt
@@ -20,7 +20,7 @@ import android.content.Intent
import android.graphics.Bitmap
import android.net.Uri
import androidx.lifecycle.Lifecycle
-import com.android.intentresolver.any
+import androidx.lifecycle.testing.TestLifecycleOwner
import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory
import com.android.intentresolver.mock
import com.android.intentresolver.whenever
@@ -28,13 +28,14 @@ import com.android.intentresolver.widget.ActionRow
import com.android.intentresolver.widget.ImagePreviewView
import com.google.common.truth.Truth.assertThat
import java.util.function.Consumer
+import kotlinx.coroutines.flow.MutableSharedFlow
import org.junit.Test
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
class ChooserContentPreviewUiTest {
- private val lifecycle = mock<Lifecycle>()
+ private val lifecycleOwner = TestLifecycleOwner()
private val previewData = mock<PreviewDataProvider>()
private val headlineGenerator = mock<HeadlineGenerator>()
private val imageLoader =
@@ -64,7 +65,7 @@ class ChooserContentPreviewUiTest {
whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_TEXT)
val testSubject =
ChooserContentPreviewUi(
- lifecycle,
+ lifecycleOwner.lifecycle,
previewData,
Intent(Intent.ACTION_VIEW),
imageLoader,
@@ -83,7 +84,7 @@ class ChooserContentPreviewUiTest {
whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_FILE)
val testSubject =
ChooserContentPreviewUi(
- lifecycle,
+ lifecycleOwner.lifecycle,
previewData,
Intent(Intent.ACTION_SEND),
imageLoader,
@@ -104,9 +105,10 @@ class ChooserContentPreviewUiTest {
whenever(previewData.uriCount).thenReturn(2)
whenever(previewData.firstFileInfo)
.thenReturn(FileInfo.Builder(uri).withPreviewUri(uri).withMimeType("image/png").build())
+ whenever(previewData.imagePreviewFileInfoFlow).thenReturn(MutableSharedFlow())
val testSubject =
ChooserContentPreviewUi(
- lifecycle,
+ lifecycleOwner.lifecycle,
previewData,
Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_TEXT, "Shared text") },
imageLoader,
@@ -116,7 +118,7 @@ class ChooserContentPreviewUiTest {
)
assertThat(testSubject.mContentPreviewUi)
.isInstanceOf(FilesPlusTextContentPreviewUi::class.java)
- verify(previewData, times(1)).getFileMetadataForImagePreview(any(), any())
+ verify(previewData, times(1)).imagePreviewFileInfoFlow
verify(transitionCallback, times(1)).onAllTransitionElementsReady()
}
@@ -127,9 +129,10 @@ class ChooserContentPreviewUiTest {
whenever(previewData.uriCount).thenReturn(2)
whenever(previewData.firstFileInfo)
.thenReturn(FileInfo.Builder(uri).withPreviewUri(uri).withMimeType("image/png").build())
+ whenever(previewData.imagePreviewFileInfoFlow).thenReturn(MutableSharedFlow())
val testSubject =
ChooserContentPreviewUi(
- lifecycle,
+ lifecycleOwner.lifecycle,
previewData,
Intent(Intent.ACTION_SEND),
imageLoader,
@@ -140,7 +143,7 @@ class ChooserContentPreviewUiTest {
assertThat(testSubject.preferredContentPreview)
.isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
assertThat(testSubject.mContentPreviewUi).isInstanceOf(UnifiedContentPreviewUi::class.java)
- verify(previewData, times(1)).getFileMetadataForImagePreview(any(), any())
+ verify(previewData, times(1)).imagePreviewFileInfoFlow
verify(transitionCallback, never()).onAllTransitionElementsReady()
}
}
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt
new file mode 100644
index 00000000..fe13a215
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/contentpreview/FilesPlusTextContentPreviewUiTest.kt
@@ -0,0 +1,225 @@
+/*
+ * 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.net.Uri
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import android.widget.TextView
+import androidx.lifecycle.testing.TestLifecycleOwner
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.intentresolver.R
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import com.android.intentresolver.widget.ActionRow
+import com.google.common.truth.Truth.assertThat
+import java.util.function.Consumer
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.never
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+private const val HEADLINE_IMAGES = "Image Headline"
+private const val HEADLINE_VIDEOS = "Video Headline"
+private const val HEADLINE_FILES = "Files Headline"
+private const val SHARED_TEXT = "Some text to share"
+
+@RunWith(AndroidJUnit4::class)
+class FilesPlusTextContentPreviewUiTest {
+ private val lifecycleOwner = TestLifecycleOwner()
+ private val actionFactory =
+ object : ChooserContentPreviewUi.ActionFactory {
+ override fun getEditButtonRunnable(): Runnable? = null
+ override fun getCopyButtonRunnable(): Runnable? = null
+ override fun createCustomActions(): List<ActionRow.Action> = emptyList()
+ override fun getModifyShareAction(): ActionRow.Action? = null
+ override fun getExcludeSharedTextAction(): Consumer<Boolean> = Consumer<Boolean> {}
+ }
+ private val imageLoader = mock<ImageLoader>()
+ private val headlineGenerator =
+ mock<HeadlineGenerator> {
+ whenever(getImagesHeadline(anyInt())).thenReturn(HEADLINE_IMAGES)
+ whenever(getVideosHeadline(anyInt())).thenReturn(HEADLINE_VIDEOS)
+ whenever(getFilesHeadline(anyInt())).thenReturn(HEADLINE_FILES)
+ }
+
+ private val context
+ get() = getInstrumentation().getContext()
+
+ @Test
+ fun test_displayImagesPlusTextWithoutUriMetadata_showImagesHeadline() {
+ val sharedFileCount = 2
+ val previewView = testLoadingHeadline("image/*", sharedFileCount)
+
+ verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_IMAGES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayVideosPlusTextWithoutUriMetadata_showVideosHeadline() {
+ val sharedFileCount = 2
+ val previewView = testLoadingHeadline("video/*", sharedFileCount)
+
+ verify(headlineGenerator, times(1)).getVideosHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_VIDEOS)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayDocsPlusTextWithoutUriMetadata_showFilesHeadline() {
+ val sharedFileCount = 2
+ val previewView = testLoadingHeadline("application/pdf", sharedFileCount)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_FILES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayMixedContentPlusTextWithoutUriMetadata_showFilesHeadline() {
+ val sharedFileCount = 2
+ val previewView = testLoadingHeadline("*/*", sharedFileCount)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_FILES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayImagesPlusTextWithUriMetadataSet_showImagesHeadline() {
+ val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "image/jpeg")
+ val sharedFileCount = loadedFileMetadata.size
+ val previewView = testLoadingHeadline("image/*", sharedFileCount, loadedFileMetadata)
+
+ verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_IMAGES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayVideosPlusTextWithUriMetadataSet_showVideosHeadline() {
+ val loadedFileMetadata = createFileInfosWithMimeTypes("video/mp4", "video/mp4")
+ val sharedFileCount = loadedFileMetadata.size
+ val previewView = testLoadingHeadline("video/*", sharedFileCount, loadedFileMetadata)
+
+ verify(headlineGenerator, times(1)).getVideosHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_VIDEOS)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayImagesAndVideosPlusTextWithUriMetadataSet_showFilesHeadline() {
+ val loadedFileMetadata = createFileInfosWithMimeTypes("image/png", "video/mp4")
+ val sharedFileCount = loadedFileMetadata.size
+ val previewView = testLoadingHeadline("*/*", sharedFileCount, loadedFileMetadata)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_FILES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_displayDocsPlusTextWithUriMetadataSet_showFilesHeadline() {
+ val loadedFileMetadata = createFileInfosWithMimeTypes("application/pdf", "application/pdf")
+ val sharedFileCount = loadedFileMetadata.size
+ val previewView =
+ testLoadingHeadline("application/pdf", sharedFileCount, loadedFileMetadata)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_FILES)
+ verifySharedText(previewView)
+ }
+
+ @Test
+ fun test_uriMetadataIsMoreSpecificThanIntentMimeType_headlineGetsUpdated() {
+ val sharedFileCount = 2
+ val testSubject =
+ FilesPlusTextContentPreviewUi(
+ lifecycleOwner.lifecycle,
+ /*isSingleImage=*/ false,
+ sharedFileCount,
+ SHARED_TEXT,
+ /*intentMimeType=*/ "*/*",
+ actionFactory,
+ imageLoader,
+ DefaultMimeTypeClassifier,
+ headlineGenerator
+ )
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup
+
+ val previewView =
+ testSubject.display(context.resources, LayoutInflater.from(context), gridLayout)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verify(headlineGenerator, never()).getImagesHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_FILES)
+
+ testSubject.updatePreviewMetadata(createFileInfosWithMimeTypes("image/png", "image/jpg"))
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(sharedFileCount)
+ verify(headlineGenerator, times(1)).getImagesHeadline(sharedFileCount)
+ verifyPreviewHeadline(previewView, HEADLINE_IMAGES)
+ }
+
+ private fun testLoadingHeadline(
+ intentMimeType: String,
+ sharedFileCount: Int,
+ loadedFileMetadata: List<FileInfo>? = null
+ ): ViewGroup? {
+ val testSubject =
+ FilesPlusTextContentPreviewUi(
+ lifecycleOwner.lifecycle,
+ /*isSingleImage=*/ false,
+ sharedFileCount,
+ SHARED_TEXT,
+ intentMimeType,
+ actionFactory,
+ imageLoader,
+ DefaultMimeTypeClassifier,
+ headlineGenerator
+ )
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout = layoutInflater.inflate(R.layout.chooser_grid, null, false) as ViewGroup
+
+ loadedFileMetadata?.let(testSubject::updatePreviewMetadata)
+ return testSubject.display(context.resources, LayoutInflater.from(context), gridLayout)
+ }
+
+ private fun createFileInfosWithMimeTypes(vararg mimeTypes: String): List<FileInfo> {
+ val uri = Uri.parse("content://pkg.app/file")
+ return mimeTypes.map { mimeType -> FileInfo.Builder(uri).withMimeType(mimeType).build() }
+ }
+
+ private fun verifyPreviewHeadline(previewView: ViewGroup?, expectedText: String) {
+ assertThat(previewView).isNotNull()
+ val headlineView = previewView?.findViewById<TextView>(R.id.headline)
+ assertThat(headlineView).isNotNull()
+ assertThat(headlineView?.text).isEqualTo(expectedText)
+ }
+
+ private fun verifySharedText(previewView: ViewGroup?) {
+ assertThat(previewView).isNotNull()
+ val textContentView = previewView?.findViewById<TextView>(R.id.content_preview_text)
+ assertThat(textContentView).isNotNull()
+ assertThat(textContentView?.text).isEqualTo(SHARED_TEXT)
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt
index 6e57c289..b5fd1fa6 100644
--- a/java/tests/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt
+++ b/java/tests/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt
@@ -22,7 +22,7 @@ import android.net.Uri
import android.util.Size
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope
-import com.android.intentresolver.TestLifecycleOwner
+import androidx.lifecycle.testing.TestLifecycleOwner
import com.android.intentresolver.any
import com.android.intentresolver.anyOrNull
import com.android.intentresolver.mock
@@ -78,7 +78,7 @@ class ImagePreviewImageLoaderTest {
@Before
fun setup() {
Dispatchers.setMain(dispatcher)
- lifecycleOwner.state = Lifecycle.State.CREATED
+ lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
// create test subject after we've updated the lifecycle dispatcher
testSubject =
ImagePreviewImageLoader(
@@ -91,7 +91,7 @@ class ImagePreviewImageLoaderTest {
@After
fun cleanup() {
- lifecycleOwner.state = Lifecycle.State.DESTROYED
+ lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
Dispatchers.resetMain()
}
@@ -164,7 +164,7 @@ class ImagePreviewImageLoaderTest {
@Test(expected = CancellationException::class)
fun invoke_onClosedImageLoaderScope_throwsCancellationException() = runTest {
- lifecycleOwner.state = Lifecycle.State.DESTROYED
+ lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
testSubject(uriOne)
}
@@ -181,7 +181,7 @@ class ImagePreviewImageLoaderTest {
)
coroutineScope {
val deferred = async(start = UNDISPATCHED) { testSubject(uriOne, false) }
- lifecycleOwner.state = Lifecycle.State.DESTROYED
+ lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
scheduler.advanceUntilIdle()
deferred.await()
}
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt
index 145b89ad..6599baa9 100644
--- a/java/tests/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt
+++ b/java/tests/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt
@@ -22,18 +22,15 @@ import android.database.MatrixCursor
import android.media.MediaMetadata
import android.net.Uri
import android.provider.DocumentsContract
-import androidx.lifecycle.Lifecycle
-import com.android.intentresolver.TestLifecycleOwner
import com.android.intentresolver.mock
import com.android.intentresolver.whenever
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
+import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.toList
+import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.UnconfinedTestDispatcher
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.setMain
-import org.junit.After
-import org.junit.Before
+import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.mockito.Mockito.any
import org.mockito.Mockito.never
@@ -44,27 +41,13 @@ import org.mockito.Mockito.verify
class PreviewDataProviderTest {
private val contentResolver = mock<ContentInterface>()
private val mimeTypeClassifier = DefaultMimeTypeClassifier
-
- private val lifecycleOwner = TestLifecycleOwner()
- private val dispatcher = UnconfinedTestDispatcher()
-
- @Before
- fun setup() {
- Dispatchers.setMain(dispatcher)
- lifecycleOwner.state = Lifecycle.State.CREATED
- }
-
- @After
- fun cleanup() {
- lifecycleOwner.state = Lifecycle.State.DESTROYED
- Dispatchers.resetMain()
- }
+ private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher())
@Test
fun test_nonSendIntentAction_resolvesToTextPreviewUiSynchronously() {
val targetIntent = Intent(Intent.ACTION_VIEW)
val testSubject =
- PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher)
+ PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT)
verify(contentResolver, never()).getType(any())
@@ -73,14 +56,14 @@ class PreviewDataProviderTest {
@Test
fun test_sendSingleTextFileWithoutPreview_resolvesToFilePreviewUi() {
val uri = Uri.parse("content://org.pkg.app/notes.txt")
- val targetIntent = Intent(Intent.ACTION_SEND)
- .apply {
+ val targetIntent =
+ Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
type = "text/plain"
}
whenever(contentResolver.getType(uri)).thenReturn("text/plain")
val testSubject =
- PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher)
+ PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
assertThat(testSubject.uriCount).isEqualTo(1)
@@ -90,12 +73,9 @@ class PreviewDataProviderTest {
@Test
fun test_sendIntentWithoutUris_resolvesToTextPreviewUiSynchronously() {
- val targetIntent = Intent(Intent.ACTION_SEND)
- .apply {
- type = "image/png"
- }
+ val targetIntent = Intent(Intent.ACTION_SEND).apply { type = "image/png" }
val testSubject =
- PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher)
+ PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT)
verify(contentResolver, never()).getType(any())
@@ -104,13 +84,10 @@ class PreviewDataProviderTest {
@Test
fun test_sendSingleImage_resolvesToImagePreviewUi() {
val uri = Uri.parse("content://org.pkg.app/image.png")
- val targetIntent = Intent(Intent.ACTION_SEND)
- .apply {
- putExtra(Intent.EXTRA_STREAM, uri)
- }
+ val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
whenever(contentResolver.getType(uri)).thenReturn("image/png")
val testSubject =
- PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher)
+ PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
assertThat(testSubject.uriCount).isEqualTo(1)
@@ -122,13 +99,10 @@ class PreviewDataProviderTest {
@Test
fun test_sendSingleNonImage_resolvesToFilePreviewUi() {
val uri = Uri.parse("content://org.pkg.app/paper.pdf")
- val targetIntent = Intent(Intent.ACTION_SEND)
- .apply {
- putExtra(Intent.EXTRA_STREAM, uri)
- }
+ val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
whenever(contentResolver.getType(uri)).thenReturn("application/pdf")
val testSubject =
- PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher)
+ PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
assertThat(testSubject.uriCount).isEqualTo(1)
@@ -141,14 +115,13 @@ class PreviewDataProviderTest {
fun test_sendSingleImageWithFailingGetType_resolvesToFilePreviewUi() {
val uri = Uri.parse("content://org.pkg.app/image.png")
val targetIntent =
- Intent(Intent.ACTION_SEND)
- .apply {
- type = "image/png"
- putExtra(Intent.EXTRA_STREAM, uri)
- }
+ Intent(Intent.ACTION_SEND).apply {
+ type = "image/png"
+ putExtra(Intent.EXTRA_STREAM, uri)
+ }
whenever(contentResolver.getType(uri)).thenThrow(SecurityException("test failure"))
val testSubject =
- PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher)
+ PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
assertThat(testSubject.uriCount).isEqualTo(1)
@@ -161,17 +134,16 @@ class PreviewDataProviderTest {
fun test_sendSingleImageWithFailingMetadata_resolvesToFilePreviewUi() {
val uri = Uri.parse("content://org.pkg.app/image.png")
val targetIntent =
- Intent(Intent.ACTION_SEND)
- .apply {
- type = "image/png"
- putExtra(Intent.EXTRA_STREAM, uri)
- }
+ Intent(Intent.ACTION_SEND).apply {
+ type = "image/png"
+ putExtra(Intent.EXTRA_STREAM, uri)
+ }
whenever(contentResolver.getStreamTypes(uri, "*/*"))
.thenThrow(SecurityException("test failure"))
whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null))
.thenThrow(SecurityException("test failure"))
val testSubject =
- PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher)
+ PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
assertThat(testSubject.uriCount).isEqualTo(1)
@@ -183,14 +155,11 @@ class PreviewDataProviderTest {
@Test
fun test_SingleNonImageUriWithImageTypeInGetStreamTypes_useImagePreviewUi() {
val uri = Uri.parse("content://org.pkg.app/paper.pdf")
- val targetIntent = Intent(Intent.ACTION_SEND)
- .apply {
- putExtra(Intent.EXTRA_STREAM, uri)
- }
+ val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
whenever(contentResolver.getStreamTypes(uri, "*/*"))
.thenReturn(arrayOf("application/pdf", "image/png"))
val testSubject =
- PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher)
+ PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
assertThat(testSubject.uriCount).isEqualTo(1)
@@ -221,15 +190,12 @@ class PreviewDataProviderTest {
private fun testMetadataToImagePreview(columns: Array<String>, values: Array<Any>) {
val uri = Uri.parse("content://org.pkg.app/test.pdf")
- val targetIntent = Intent(Intent.ACTION_SEND)
- .apply {
- putExtra(Intent.EXTRA_STREAM, uri)
- }
+ val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) }
whenever(contentResolver.getType(uri)).thenReturn("application/pdf")
whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null))
.thenReturn(MatrixCursor(columns).apply { addRow(values) })
val testSubject =
- PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher)
+ PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
assertThat(testSubject.uriCount).isEqualTo(1)
@@ -243,20 +209,19 @@ class PreviewDataProviderTest {
val uri1 = Uri.parse("content://org.pkg.app/test.png")
val uri2 = Uri.parse("content://org.pkg.app/test.jpg")
val targetIntent =
- Intent(Intent.ACTION_SEND_MULTIPLE)
- .apply {
- putExtra(
- Intent.EXTRA_STREAM,
- ArrayList<Uri>().apply {
- add(uri1)
- add(uri2)
- }
- )
- }
+ Intent(Intent.ACTION_SEND_MULTIPLE).apply {
+ putExtra(
+ Intent.EXTRA_STREAM,
+ ArrayList<Uri>().apply {
+ add(uri1)
+ add(uri2)
+ }
+ )
+ }
whenever(contentResolver.getType(uri1)).thenReturn("image/png")
whenever(contentResolver.getType(uri2)).thenReturn("image/jpeg")
val testSubject =
- PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher)
+ PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
assertThat(testSubject.uriCount).isEqualTo(2)
@@ -273,18 +238,17 @@ class PreviewDataProviderTest {
whenever(contentResolver.getType(uri1)).thenReturn("image/png")
whenever(contentResolver.getType(uri2)).thenReturn("application/pdf")
val targetIntent =
- Intent(Intent.ACTION_SEND_MULTIPLE)
- .apply {
- putExtra(
- Intent.EXTRA_STREAM,
- ArrayList<Uri>().apply {
- add(uri1)
- add(uri2)
- }
- )
- }
+ Intent(Intent.ACTION_SEND_MULTIPLE).apply {
+ putExtra(
+ Intent.EXTRA_STREAM,
+ ArrayList<Uri>().apply {
+ add(uri1)
+ add(uri2)
+ }
+ )
+ }
val testSubject =
- PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher)
+ PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
assertThat(testSubject.uriCount).isEqualTo(2)
@@ -299,21 +263,20 @@ class PreviewDataProviderTest {
val uri1 = Uri.parse("content://org.pkg.app/test.mp4")
val uri2 = Uri.parse("content://org.pkg.app/test.pdf")
val targetIntent =
- Intent(Intent.ACTION_SEND_MULTIPLE)
- .apply {
- putExtra(
- Intent.EXTRA_STREAM,
- ArrayList<Uri>().apply {
- add(uri1)
- add(uri2)
- }
- )
- }
+ Intent(Intent.ACTION_SEND_MULTIPLE).apply {
+ putExtra(
+ Intent.EXTRA_STREAM,
+ ArrayList<Uri>().apply {
+ add(uri1)
+ add(uri2)
+ }
+ )
+ }
whenever(contentResolver.getType(uri1)).thenReturn("video/mpeg4")
whenever(contentResolver.getStreamTypes(uri1, "*/*")).thenReturn(arrayOf("image/png"))
whenever(contentResolver.getType(uri2)).thenReturn("application/pdf")
val testSubject =
- PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher)
+ PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE)
assertThat(testSubject.uriCount).isEqualTo(2)
@@ -327,20 +290,19 @@ class PreviewDataProviderTest {
val uri1 = Uri.parse("content://org.pkg.app/test.html")
val uri2 = Uri.parse("content://org.pkg.app/test.pdf")
val targetIntent =
- Intent(Intent.ACTION_SEND_MULTIPLE)
- .apply {
- putExtra(
- Intent.EXTRA_STREAM,
- ArrayList<Uri>().apply {
- add(uri1)
- add(uri2)
- }
- )
- }
+ Intent(Intent.ACTION_SEND_MULTIPLE).apply {
+ putExtra(
+ Intent.EXTRA_STREAM,
+ ArrayList<Uri>().apply {
+ add(uri1)
+ add(uri2)
+ }
+ )
+ }
whenever(contentResolver.getType(uri1)).thenReturn("text/html")
whenever(contentResolver.getType(uri2)).thenReturn("application/pdf")
val testSubject =
- PreviewDataProvider(targetIntent, contentResolver, mimeTypeClassifier, dispatcher)
+ PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE)
assertThat(testSubject.uriCount).isEqualTo(2)
@@ -348,4 +310,40 @@ class PreviewDataProviderTest {
assertThat(testSubject.firstFileInfo?.previewUri).isNull()
verify(contentResolver, times(2)).getType(any())
}
+
+ @Test
+ fun test_imagePreviewFileInfoFlow_dataLoadedOnce() =
+ testScope.runTest {
+ val uri1 = Uri.parse("content://org.pkg.app/test.html")
+ val uri2 = Uri.parse("content://org.pkg.app/test.pdf")
+ val targetIntent =
+ Intent(Intent.ACTION_SEND_MULTIPLE).apply {
+ putExtra(
+ Intent.EXTRA_STREAM,
+ ArrayList<Uri>().apply {
+ add(uri1)
+ add(uri2)
+ }
+ )
+ }
+ whenever(contentResolver.getType(uri1)).thenReturn("text/html")
+ whenever(contentResolver.getType(uri2)).thenReturn("application/pdf")
+ whenever(contentResolver.getStreamTypes(uri1, "*/*"))
+ .thenReturn(arrayOf("text/html", "image/jpeg"))
+ whenever(contentResolver.getStreamTypes(uri2, "*/*"))
+ .thenReturn(arrayOf("application/pdf", "image/png"))
+ val testSubject =
+ PreviewDataProvider(testScope, targetIntent, contentResolver, mimeTypeClassifier)
+
+ val fileInfoListOne = testSubject.imagePreviewFileInfoFlow.toList()
+ val fileInfoListTwo = testSubject.imagePreviewFileInfoFlow.toList()
+
+ assertThat(fileInfoListOne).hasSize(2)
+ assertThat(fileInfoListOne).containsAtLeastElementsIn(fileInfoListTwo).inOrder()
+
+ verify(contentResolver, times(1)).getType(uri1)
+ verify(contentResolver, times(1)).getStreamTypes(uri1, "*/*")
+ verify(contentResolver, times(1)).getType(uri2)
+ verify(contentResolver, times(1)).getStreamTypes(uri2, "*/*")
+ }
}
diff --git a/java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt b/java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt
new file mode 100644
index 00000000..e7de0b7b
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.contentpreview
+
+import android.net.Uri
+import android.view.LayoutInflater
+import android.view.ViewGroup
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.intentresolver.R.layout.chooser_grid
+import com.android.intentresolver.mock
+import com.android.intentresolver.whenever
+import com.android.intentresolver.widget.ImagePreviewView.TransitionElementStatusCallback
+import kotlin.coroutines.EmptyCoroutineContext
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asFlow
+import kotlinx.coroutines.flow.takeWhile
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+
+@RunWith(AndroidJUnit4::class)
+class UnifiedContentPreviewUiTest {
+ private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher())
+ private val actionFactory =
+ mock<ChooserContentPreviewUi.ActionFactory> {
+ whenever(createCustomActions()).thenReturn(emptyList())
+ }
+ private val imageLoader = mock<ImageLoader>()
+ private val headlineGenerator =
+ mock<HeadlineGenerator> {
+ whenever(getImagesHeadline(anyInt())).thenReturn("Image Headline")
+ whenever(getVideosHeadline(anyInt())).thenReturn("Video Headline")
+ whenever(getFilesHeadline(anyInt())).thenReturn("Files Headline")
+ }
+
+ private val context
+ get() = getInstrumentation().getContext()
+
+ @Test
+ fun test_displayImagesWithoutUriMetadata_showImagesHeadline() {
+ testLoadingHeadline("image/*", files = null)
+
+ verify(headlineGenerator, times(1)).getImagesHeadline(2)
+ }
+
+ @Test
+ fun test_displayVideosWithoutUriMetadata_showImagesHeadline() {
+ testLoadingHeadline("video/*", files = null)
+
+ verify(headlineGenerator, times(1)).getVideosHeadline(2)
+ }
+
+ @Test
+ fun test_displayDocumentsWithoutUriMetadata_showImagesHeadline() {
+ testLoadingHeadline("application/pdf", files = null)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ }
+
+ @Test
+ fun test_displayMixedContentWithoutUriMetadata_showImagesHeadline() {
+ testLoadingHeadline("*/*", files = null)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ }
+
+ @Test
+ fun test_displayImagesWithUriMetadataSet_showImagesHeadline() {
+ val uri = Uri.parse("content://pkg.app/image.png")
+ val files =
+ listOf(
+ FileInfo.Builder(uri).withMimeType("image/png").build(),
+ FileInfo.Builder(uri).withMimeType("image/jpeg").build(),
+ )
+ testLoadingHeadline("image/*", files)
+
+ verify(headlineGenerator, times(1)).getImagesHeadline(2)
+ }
+
+ @Test
+ fun test_displayVideosWithUriMetadataSet_showImagesHeadline() {
+ val uri = Uri.parse("content://pkg.app/image.png")
+ val files =
+ listOf(
+ FileInfo.Builder(uri).withMimeType("video/mp4").build(),
+ FileInfo.Builder(uri).withMimeType("video/mp4").build(),
+ )
+ testLoadingHeadline("video/*", files)
+
+ verify(headlineGenerator, times(1)).getVideosHeadline(2)
+ }
+
+ @Test
+ fun test_displayImagesAndVideosWithUriMetadataSet_showImagesHeadline() {
+ val uri = Uri.parse("content://pkg.app/image.png")
+ val files =
+ listOf(
+ FileInfo.Builder(uri).withMimeType("image/png").build(),
+ FileInfo.Builder(uri).withMimeType("video/mp4").build(),
+ )
+ testLoadingHeadline("*/*", files)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ }
+
+ @Test
+ fun test_displayDocumentsWithUriMetadataSet_showImagesHeadline() {
+ val uri = Uri.parse("content://pkg.app/image.png")
+ val files =
+ listOf(
+ FileInfo.Builder(uri).withMimeType("application/pdf").build(),
+ FileInfo.Builder(uri).withMimeType("application/pdf").build(),
+ )
+ testLoadingHeadline("application/pdf", files)
+
+ verify(headlineGenerator, times(1)).getFilesHeadline(2)
+ }
+
+ private fun testLoadingHeadline(intentMimeType: String, files: List<FileInfo>?) {
+ testScope.runTest {
+ val endMarker = FileInfo.Builder(Uri.EMPTY).build()
+ val emptySourceFlow = MutableSharedFlow<FileInfo>(replay = 1)
+ val testSubject =
+ UnifiedContentPreviewUi(
+ testScope,
+ /*isSingleImage=*/ false,
+ intentMimeType,
+ actionFactory,
+ imageLoader,
+ DefaultMimeTypeClassifier,
+ object : TransitionElementStatusCallback {
+ override fun onTransitionElementReady(name: String) = Unit
+ override fun onAllTransitionElementsReady() = Unit
+ },
+ files?.let { it.asFlow() } ?: emptySourceFlow.takeWhile { it !== endMarker },
+ /*itemCount=*/ 2,
+ headlineGenerator
+ )
+ val layoutInflater = LayoutInflater.from(context)
+ val gridLayout = layoutInflater.inflate(chooser_grid, null, false) as ViewGroup
+
+ testSubject.display(context.resources, LayoutInflater.from(context), gridLayout)
+ emptySourceFlow.tryEmit(endMarker)
+ }
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java b/java/tests/src/com/android/intentresolver/logging/EventLogTest.java
index aa42c24c..17452774 100644
--- a/java/tests/src/com/android/intentresolver/ChooserActivityLoggerTest.java
+++ b/java/tests/src/com/android/intentresolver/logging/EventLogTest.java
@@ -1,5 +1,5 @@
/*
- * 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.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.intentresolver;
+package com.android.intentresolver.logging;
import static com.google.common.truth.Truth.assertThat;
@@ -32,10 +32,10 @@ import static org.mockito.Mockito.verifyNoMoreInteractions;
import android.content.Intent;
import android.metrics.LogMaker;
-import com.android.intentresolver.ChooserActivityLogger.FrameworkStatsLogger;
-import com.android.intentresolver.ChooserActivityLogger.SharesheetStandardEvent;
-import com.android.intentresolver.ChooserActivityLogger.SharesheetStartedEvent;
-import com.android.intentresolver.ChooserActivityLogger.SharesheetTargetSelectedEvent;
+import com.android.intentresolver.logging.EventLog.FrameworkStatsLogger;
+import com.android.intentresolver.logging.EventLog.SharesheetStandardEvent;
+import com.android.intentresolver.logging.EventLog.SharesheetStartedEvent;
+import com.android.intentresolver.logging.EventLog.SharesheetTargetSelectedEvent;
import com.android.intentresolver.contentpreview.ContentPreviewType;
import com.android.internal.logging.InstanceId;
import com.android.internal.logging.MetricsLogger;
@@ -53,17 +53,17 @@ import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
@RunWith(MockitoJUnitRunner.class)
-public final class ChooserActivityLoggerTest {
+public final class EventLogTest {
@Mock private UiEventLogger mUiEventLog;
@Mock private FrameworkStatsLogger mFrameworkLog;
@Mock private MetricsLogger mMetricsLogger;
- private ChooserActivityLogger mChooserLogger;
+ private EventLog mChooserLogger;
@Before
public void setUp() {
//Mockito.reset(mUiEventLog, mFrameworkLog, mMetricsLogger);
- mChooserLogger = new ChooserActivityLogger(mUiEventLog, mFrameworkLog, mMetricsLogger);
+ mChooserLogger = new EventLog(mUiEventLog, mFrameworkLog, mMetricsLogger);
}
@After
@@ -151,7 +151,7 @@ public final class ChooserActivityLoggerTest {
@Test
public void testLogShareTargetSelected() {
- final int targetType = ChooserActivityLogger.SELECTION_TYPE_SERVICE;
+ final int targetType = EventLog.SELECTION_TYPE_SERVICE;
final String packageName = "com.test.foo";
final int positionPicked = 123;
final int directTargetAlsoRanked = -1;
@@ -189,7 +189,7 @@ public final class ChooserActivityLoggerTest {
@Test
public void testLogActionSelected() {
- mChooserLogger.logActionSelected(ChooserActivityLogger.SELECTION_TYPE_COPY);
+ mChooserLogger.logActionSelected(EventLog.SELECTION_TYPE_COPY);
verify(mFrameworkLog).write(
eq(FrameworkStatsLog.RANKING_SELECTED),
@@ -320,10 +320,10 @@ public final class ChooserActivityLoggerTest {
@Test
public void testDifferentLoggerInstancesUseDifferentInstanceIds() {
ArgumentCaptor<Integer> idIntCaptor = ArgumentCaptor.forClass(Integer.class);
- ChooserActivityLogger chooserLogger2 =
- new ChooserActivityLogger(mUiEventLog, mFrameworkLog, mMetricsLogger);
+ EventLog chooserLogger2 =
+ new EventLog(mUiEventLog, mFrameworkLog, mMetricsLogger);
- final int targetType = ChooserActivityLogger.SELECTION_TYPE_COPY;
+ final int targetType = EventLog.SELECTION_TYPE_COPY;
final String packageName = "com.test.foo";
final int positionPicked = 123;
final int directTargetAlsoRanked = -1;
@@ -370,7 +370,7 @@ public final class ChooserActivityLoggerTest {
ArgumentCaptor<Integer> idIntCaptor = ArgumentCaptor.forClass(Integer.class);
ArgumentCaptor<InstanceId> idObjectCaptor = ArgumentCaptor.forClass(InstanceId.class);
- final int targetType = ChooserActivityLogger.SELECTION_TYPE_COPY;
+ final int targetType = EventLog.SELECTION_TYPE_COPY;
final String packageName = "com.test.foo";
final int positionPicked = 123;
final int directTargetAlsoRanked = -1;
@@ -403,20 +403,20 @@ public final class ChooserActivityLoggerTest {
@Test
public void testTargetSelectionCategories() {
- assertThat(ChooserActivityLogger.getTargetSelectionCategory(
- ChooserActivityLogger.SELECTION_TYPE_SERVICE))
+ assertThat(EventLog.getTargetSelectionCategory(
+ EventLog.SELECTION_TYPE_SERVICE))
.isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_SERVICE_TARGET);
- assertThat(ChooserActivityLogger.getTargetSelectionCategory(
- ChooserActivityLogger.SELECTION_TYPE_APP))
+ assertThat(EventLog.getTargetSelectionCategory(
+ EventLog.SELECTION_TYPE_APP))
.isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_APP_TARGET);
- assertThat(ChooserActivityLogger.getTargetSelectionCategory(
- ChooserActivityLogger.SELECTION_TYPE_STANDARD))
+ assertThat(EventLog.getTargetSelectionCategory(
+ EventLog.SELECTION_TYPE_STANDARD))
.isEqualTo(MetricsEvent.ACTION_ACTIVITY_CHOOSER_PICKED_STANDARD_TARGET);
- assertThat(ChooserActivityLogger.getTargetSelectionCategory(
- ChooserActivityLogger.SELECTION_TYPE_COPY)).isEqualTo(0);
- assertThat(ChooserActivityLogger.getTargetSelectionCategory(
- ChooserActivityLogger.SELECTION_TYPE_NEARBY)).isEqualTo(0);
- assertThat(ChooserActivityLogger.getTargetSelectionCategory(
- ChooserActivityLogger.SELECTION_TYPE_EDIT)).isEqualTo(0);
+ assertThat(EventLog.getTargetSelectionCategory(
+ EventLog.SELECTION_TYPE_COPY)).isEqualTo(0);
+ assertThat(EventLog.getTargetSelectionCategory(
+ EventLog.SELECTION_TYPE_NEARBY)).isEqualTo(0);
+ assertThat(EventLog.getTargetSelectionCategory(
+ EventLog.SELECTION_TYPE_EDIT)).isEqualTo(0);
}
}
diff --git a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
index 742aac71..9b4a8057 100644
--- a/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
+++ b/java/tests/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt
@@ -27,8 +27,8 @@ import android.content.pm.ShortcutManager
import android.os.UserHandle
import android.os.UserManager
import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.testing.TestLifecycleOwner
import androidx.test.filters.SmallTest
-import com.android.intentresolver.TestLifecycleOwner
import com.android.intentresolver.any
import com.android.intentresolver.argumentCaptor
import com.android.intentresolver.capture
@@ -38,6 +38,7 @@ import com.android.intentresolver.createShareShortcutInfo
import com.android.intentresolver.createShortcutInfo
import com.android.intentresolver.mock
import com.android.intentresolver.whenever
+import java.util.function.Consumer
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.TestCoroutineScheduler
@@ -56,28 +57,31 @@ import org.mockito.Mockito.atLeastOnce
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
-import java.util.function.Consumer
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
class ShortcutLoaderTest {
- private val appInfo = ApplicationInfo().apply {
- enabled = true
- flags = 0
- }
- private val pm = mock<PackageManager> {
- whenever(getApplicationInfo(any(), any<ApplicationInfoFlags>())).thenReturn(appInfo)
- }
- private val userManager = mock<UserManager> {
- whenever(isUserRunning(any<UserHandle>())).thenReturn(true)
- whenever(isUserUnlocked(any<UserHandle>())).thenReturn(true)
- whenever(isQuietModeEnabled(any<UserHandle>())).thenReturn(false)
- }
- private val context = mock<Context> {
- whenever(packageManager).thenReturn(pm)
- whenever(createContextAsUser(any(), anyInt())).thenReturn(this)
- whenever(getSystemService(Context.USER_SERVICE)).thenReturn(userManager)
- }
+ private val appInfo =
+ ApplicationInfo().apply {
+ enabled = true
+ flags = 0
+ }
+ private val pm =
+ mock<PackageManager> {
+ whenever(getApplicationInfo(any(), any<ApplicationInfoFlags>())).thenReturn(appInfo)
+ }
+ private val userManager =
+ mock<UserManager> {
+ whenever(isUserRunning(any<UserHandle>())).thenReturn(true)
+ whenever(isUserUnlocked(any<UserHandle>())).thenReturn(true)
+ whenever(isQuietModeEnabled(any<UserHandle>())).thenReturn(false)
+ }
+ private val context =
+ mock<Context> {
+ whenever(packageManager).thenReturn(pm)
+ whenever(createContextAsUser(any(), anyInt())).thenReturn(this)
+ whenever(getSystemService(Context.USER_SERVICE)).thenReturn(userManager)
+ }
private val scheduler = TestCoroutineScheduler()
private val dispatcher = UnconfinedTestDispatcher(scheduler)
private val lifecycleOwner = TestLifecycleOwner()
@@ -85,47 +89,48 @@ class ShortcutLoaderTest {
private val appPredictor = mock<ShortcutLoader.AppPredictorProxy>()
private val callback = mock<Consumer<ShortcutLoader.Result>>()
private val componentName = ComponentName("pkg", "Class")
- private val appTarget = mock<DisplayResolveInfo> {
- whenever(resolvedComponentName).thenReturn(componentName)
- }
+ private val appTarget =
+ mock<DisplayResolveInfo> { whenever(resolvedComponentName).thenReturn(componentName) }
private val appTargets = arrayOf(appTarget)
private val matchingShortcutInfo = createShortcutInfo("id-0", componentName, 1)
@Before
fun setup() {
Dispatchers.setMain(dispatcher)
- lifecycleOwner.state = Lifecycle.State.CREATED
+ lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE)
}
@After
fun cleanup() {
- lifecycleOwner.state = Lifecycle.State.DESTROYED
+ lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
Dispatchers.resetMain()
}
@Test
fun test_loadShortcutsWithAppPredictor_resultIntegrity() {
- val testSubject = ShortcutLoader(
- context,
- lifecycleOwner.lifecycle,
- appPredictor,
- UserHandle.of(0),
- true,
- intentFilter,
- dispatcher,
- callback
- )
+ val testSubject =
+ ShortcutLoader(
+ context,
+ lifecycleOwner.lifecycle,
+ appPredictor,
+ UserHandle.of(0),
+ true,
+ intentFilter,
+ dispatcher,
+ callback
+ )
testSubject.updateAppTargets(appTargets)
val matchingAppTarget = createAppTarget(matchingShortcutInfo)
- val shortcuts = listOf(
- matchingAppTarget,
- // an AppTarget that does not belong to any resolved application; should be ignored
- createAppTarget(
- createShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+ val shortcuts =
+ listOf(
+ matchingAppTarget,
+ // an AppTarget that does not belong to any resolved application; should be ignored
+ createAppTarget(
+ createShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+ )
)
- )
val appPredictorCallbackCaptor = argumentCaptor<AppPredictor.Callback>()
verify(appPredictor, atLeastOnce())
.registerPredictionUpdates(any(), capture(appPredictorCallbackCaptor))
@@ -155,25 +160,28 @@ class ShortcutLoaderTest {
@Test
fun test_loadShortcutsWithShortcutManager_resultIntegrity() {
- val shortcutManagerResult = listOf(
- ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
- // mismatching shortcut
- createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
- )
- val shortcutManager = mock<ShortcutManager> {
- whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
- }
+ val shortcutManagerResult =
+ listOf(
+ ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
+ // mismatching shortcut
+ createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+ )
+ val shortcutManager =
+ mock<ShortcutManager> {
+ whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
+ }
whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
- val testSubject = ShortcutLoader(
- context,
- lifecycleOwner.lifecycle,
- null,
- UserHandle.of(0),
- true,
- intentFilter,
- dispatcher,
- callback
- )
+ val testSubject =
+ ShortcutLoader(
+ context,
+ lifecycleOwner.lifecycle,
+ null,
+ UserHandle.of(0),
+ true,
+ intentFilter,
+ dispatcher,
+ callback
+ )
testSubject.updateAppTargets(appTargets)
@@ -200,25 +208,28 @@ class ShortcutLoaderTest {
@Test
fun test_appPredictorReturnsEmptyList_fallbackToShortcutManager() {
- val shortcutManagerResult = listOf(
- ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
- // mismatching shortcut
- createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
- )
- val shortcutManager = mock<ShortcutManager> {
- whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
- }
+ val shortcutManagerResult =
+ listOf(
+ ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
+ // mismatching shortcut
+ createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+ )
+ val shortcutManager =
+ mock<ShortcutManager> {
+ whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
+ }
whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
- val testSubject = ShortcutLoader(
- context,
- lifecycleOwner.lifecycle,
- appPredictor,
- UserHandle.of(0),
- true,
- intentFilter,
- dispatcher,
- callback
- )
+ val testSubject =
+ ShortcutLoader(
+ context,
+ lifecycleOwner.lifecycle,
+ appPredictor,
+ UserHandle.of(0),
+ true,
+ intentFilter,
+ dispatcher,
+ callback
+ )
testSubject.updateAppTargets(appTargets)
@@ -251,27 +262,30 @@ class ShortcutLoaderTest {
@Test
fun test_appPredictor_requestPredictionUpdateFailure_fallbackToShortcutManager() {
- val shortcutManagerResult = listOf(
- ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
- // mismatching shortcut
- createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
- )
- val shortcutManager = mock<ShortcutManager> {
- whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
- }
+ val shortcutManagerResult =
+ listOf(
+ ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
+ // mismatching shortcut
+ createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+ )
+ val shortcutManager =
+ mock<ShortcutManager> {
+ whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
+ }
whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
whenever(appPredictor.requestPredictionUpdate())
.thenThrow(IllegalStateException("Test exception"))
- val testSubject = ShortcutLoader(
- context,
- lifecycleOwner.lifecycle,
- appPredictor,
- UserHandle.of(0),
- true,
- intentFilter,
- dispatcher,
- callback
- )
+ val testSubject =
+ ShortcutLoader(
+ context,
+ lifecycleOwner.lifecycle,
+ appPredictor,
+ UserHandle.of(0),
+ true,
+ intentFilter,
+ dispatcher,
+ callback
+ )
testSubject.updateAppTargets(appTargets)
@@ -317,25 +331,28 @@ class ShortcutLoaderTest {
@Test
fun test_ShortcutLoader_noResultsWithoutAppTargets() {
- val shortcutManagerResult = listOf(
- ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
- // mismatching shortcut
- createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
- )
- val shortcutManager = mock<ShortcutManager> {
- whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
- }
+ val shortcutManagerResult =
+ listOf(
+ ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName),
+ // mismatching shortcut
+ createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1)
+ )
+ val shortcutManager =
+ mock<ShortcutManager> {
+ whenever(getShareTargets(intentFilter)).thenReturn(shortcutManagerResult)
+ }
whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager)
- val testSubject = ShortcutLoader(
- context,
- lifecycleOwner.lifecycle,
- null,
- UserHandle.of(0),
- true,
- intentFilter,
- dispatcher,
- callback
- )
+ val testSubject =
+ ShortcutLoader(
+ context,
+ lifecycleOwner.lifecycle,
+ null,
+ UserHandle.of(0),
+ true,
+ intentFilter,
+ dispatcher,
+ callback
+ )
verify(shortcutManager, times(1)).getShareTargets(any())
verify(callback, never()).accept(any())
@@ -366,7 +383,7 @@ class ShortcutLoaderTest {
verify(appPredictor, never()).unregisterPredictionUpdates(any())
- lifecycleOwner.state = Lifecycle.State.DESTROYED
+ lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY)
verify(appPredictor, times(1)).unregisterPredictionUpdates(any())
}
@@ -412,19 +429,20 @@ class ShortcutLoaderTest {
whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked)
whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled)
}
- whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager);
+ whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager)
val appPredictor = mock<ShortcutLoader.AppPredictorProxy>()
val callback = mock<Consumer<ShortcutLoader.Result>>()
- val testSubject = ShortcutLoader(
- context,
- lifecycleOwner.lifecycle,
- appPredictor,
- userHandle,
- false,
- intentFilter,
- dispatcher,
- callback
- )
+ val testSubject =
+ ShortcutLoader(
+ context,
+ lifecycleOwner.lifecycle,
+ appPredictor,
+ userHandle,
+ false,
+ intentFilter,
+ dispatcher,
+ callback
+ )
testSubject.updateAppTargets(arrayOf<DisplayResolveInfo>(mock()))
@@ -442,19 +460,20 @@ class ShortcutLoaderTest {
whenever(isUserUnlocked(userHandle)).thenReturn(isUserUnlocked)
whenever(isQuietModeEnabled(userHandle)).thenReturn(isQuietModeEnabled)
}
- whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager);
+ whenever(context.getSystemService(Context.USER_SERVICE)).thenReturn(userManager)
val appPredictor = mock<ShortcutLoader.AppPredictorProxy>()
val callback = mock<Consumer<ShortcutLoader.Result>>()
- val testSubject = ShortcutLoader(
- context,
- lifecycleOwner.lifecycle,
- appPredictor,
- userHandle,
- true,
- intentFilter,
- dispatcher,
- callback
- )
+ val testSubject =
+ ShortcutLoader(
+ context,
+ lifecycleOwner.lifecycle,
+ appPredictor,
+ userHandle,
+ true,
+ intentFilter,
+ dispatcher,
+ callback
+ )
testSubject.updateAppTargets(arrayOf<DisplayResolveInfo>(mock()))
diff --git a/java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt b/java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt
index e65cba5f..4f4223c0 100644
--- a/java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt
+++ b/java/tests/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt
@@ -31,6 +31,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.resetMain
@@ -47,7 +48,6 @@ class BatchPreviewLoaderTest {
private val dispatcher = UnconfinedTestDispatcher()
private val testScope = CoroutineScope(dispatcher)
private val onCompletion = mock<() -> Unit>()
- private val onReset = mock<(Int) -> Unit>()
private val onUpdate = mock<(List<Preview>) -> Unit>()
@Before
@@ -71,8 +71,7 @@ class BatchPreviewLoaderTest {
BatchPreviewLoader(
imageLoader,
previews(uriOne, uriTwo),
- 0,
- onReset,
+ totalItemCount = 2,
onUpdate,
onCompletion
)
@@ -80,7 +79,6 @@ class BatchPreviewLoaderTest {
dispatcher.scheduler.advanceUntilIdle()
verify(onCompletion, times(1)).invoke()
- verify(onReset, times(1)).invoke(2)
val list = withArgCaptor { verify(onUpdate, times(1)).invoke(capture()) }.map { it.uri }
assertThat(list).containsExactly(uriOne, uriTwo).inOrder()
}
@@ -96,8 +94,7 @@ class BatchPreviewLoaderTest {
BatchPreviewLoader(
imageLoader,
previews(uriOne, uriTwo, uriThree),
- 0,
- onReset,
+ totalItemCount = 3,
onUpdate,
onCompletion
)
@@ -105,7 +102,6 @@ class BatchPreviewLoaderTest {
dispatcher.scheduler.advanceUntilIdle()
verify(onCompletion, times(1)).invoke()
- verify(onReset, times(1)).invoke(3)
val list = withArgCaptor { verify(onUpdate, times(1)).invoke(capture()) }.map { it.uri }
assertThat(list).containsExactly(uriOne, uriThree).inOrder()
}
@@ -126,12 +122,11 @@ class BatchPreviewLoaderTest {
}
imageLoader.setUriLoadingOrder(*loadingOrder)
val testSubject =
- BatchPreviewLoader(imageLoader, previews(*uris), 0, onReset, onUpdate, onCompletion)
+ BatchPreviewLoader(imageLoader, previews(*uris), uris.size, onUpdate, onCompletion)
testSubject.loadAspectRatios(200) { _, _, _ -> 100 }
dispatcher.scheduler.advanceUntilIdle()
verify(onCompletion, times(1)).invoke()
- verify(onReset, times(1)).invoke(uris.size)
val list =
captureMany { verify(onUpdate, atLeast(1)).invoke(capture()) }
.fold(ArrayList<Preview>()) { acc, update -> acc.apply { addAll(update) } }
@@ -156,12 +151,11 @@ class BatchPreviewLoaderTest {
val expectedUris = Array(uris.size / 2) { createUri(it * 2 + 1) }
imageLoader.setUriLoadingOrder(*loadingOrder)
val testSubject =
- BatchPreviewLoader(imageLoader, previews(*uris), 0, onReset, onUpdate, onCompletion)
+ BatchPreviewLoader(imageLoader, previews(*uris), uris.size, onUpdate, onCompletion)
testSubject.loadAspectRatios(200) { _, _, _ -> 100 }
dispatcher.scheduler.advanceUntilIdle()
verify(onCompletion, times(1)).invoke()
- verify(onReset, times(1)).invoke(uris.size)
val list =
captureMany { verify(onUpdate, atLeast(1)).invoke(capture()) }
.fold(ArrayList<Preview>()) { acc, update -> acc.apply { addAll(update) } }
@@ -174,9 +168,11 @@ class BatchPreviewLoaderTest {
private fun fail(uri: Uri) = uri to false
private fun succeed(uri: Uri) = uri to true
private fun previews(vararg uris: Uri) =
- uris.fold(ArrayList<Preview>(uris.size)) { acc, uri ->
- acc.apply { add(Preview(PreviewType.Image, uri, editAction = null)) }
- }
+ uris
+ .fold(ArrayList<Preview>(uris.size)) { acc, uri ->
+ acc.apply { add(Preview(PreviewType.Image, uri, editAction = null)) }
+ }
+ .asFlow()
}
private class TestImageLoader(scope: CoroutineScope) : suspend (Uri, Boolean) -> Bitmap? {
diff --git a/proguard.flags b/proguard.flags
new file mode 100644
index 00000000..5541c3ff
--- /dev/null
+++ b/proguard.flags
@@ -0,0 +1,2 @@
+# Class referenced from xml drawable
+-keep class com.android.intentresolver.SimpleIconFactory$FixedScaleDrawable