summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp12
-rw-r--r--AndroidManifest-app.xml (renamed from AndroidManifest.xml)62
-rw-r--r--AndroidManifest-lib.xml34
-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_loading_item.xml32
-rw-r--r--java/res/values/styles.xml3
-rw-r--r--java/src/com/android/intentresolver/ApplicationComponentOwner.kt15
-rw-r--r--java/src/com/android/intentresolver/ChooserActionFactory.java17
-rw-r--r--java/src/com/android/intentresolver/ChooserActivity.java129
-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/IntentForwarderActivity.java7
-rw-r--r--java/src/com/android/intentresolver/IntentResolverApplication.kt30
-rw-r--r--java/src/com/android/intentresolver/ResolverActivity.java128
-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/dagger/ActivityBinderModule.kt33
-rw-r--r--java/src/com/android/intentresolver/dagger/ActivityComponent.kt21
-rw-r--r--java/src/com/android/intentresolver/dagger/ActivityModule.kt6
-rw-r--r--java/src/com/android/intentresolver/dagger/ApplicationComponent.kt21
-rw-r--r--java/src/com/android/intentresolver/dagger/ApplicationModule.kt29
-rw-r--r--java/src/com/android/intentresolver/dagger/CoroutinesModule.kt51
-rw-r--r--java/src/com/android/intentresolver/dagger/InjectedAppComponentFactory.kt92
-rw-r--r--java/src/com/android/intentresolver/dagger/InjectedViewModelFactory.kt84
-rw-r--r--java/src/com/android/intentresolver/dagger/ReceiverBinderModule.kt12
-rw-r--r--java/src/com/android/intentresolver/dagger/ViewModelBinderModule.kt34
-rw-r--r--java/src/com/android/intentresolver/dagger/ViewModelComponent.kt57
-rw-r--r--java/src/com/android/intentresolver/dagger/ViewModelModule.kt6
-rw-r--r--java/src/com/android/intentresolver/dagger/qualifiers/Qualifiers.kt37
-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/ui/ChooserViewModel.kt34
-rw-r--r--java/src/com/android/intentresolver/widget/ScrollableImagePreviewView.kt261
-rw-r--r--java/tests/Android.bp11
-rw-r--r--java/tests/AndroidManifest.xml8
-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.java15
-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/ResolverWrapperActivity.java3
-rw-r--r--java/tests/src/com/android/intentresolver/TestApplication.kt7
-rw-r--r--java/tests/src/com/android/intentresolver/TestContentProvider.kt32
-rw-r--r--java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt2
-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/PreviewDataProviderTest.kt212
-rw-r--r--java/tests/src/com/android/intentresolver/contentpreview/UnifiedContentPreviewUiTest.kt166
-rw-r--r--java/tests/src/com/android/intentresolver/dagger/TestActivityBinderModule.kt40
-rw-r--r--java/tests/src/com/android/intentresolver/dagger/TestActivityComponent.kt30
-rw-r--r--java/tests/src/com/android/intentresolver/dagger/TestApplicationComponent.kt34
-rw-r--r--java/tests/src/com/android/intentresolver/dagger/TestApplicationModule.kt38
-rw-r--r--java/tests/src/com/android/intentresolver/dagger/TestViewModelComponent.kt29
-rw-r--r--java/tests/src/com/android/intentresolver/dagger/TestViewModelModule.kt6
-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/widget/BatchPreviewLoaderTest.kt24
-rw-r--r--proguard.flags2
70 files changed, 2218 insertions, 716 deletions
diff --git a/Android.bp b/Android.bp
index bab33509..6aad4419 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",
@@ -75,6 +75,9 @@ android_library {
"androidx.lifecycle_lifecycle-extensions",
"androidx.lifecycle_lifecycle-runtime-ktx",
"androidx.lifecycle_lifecycle-viewmodel-ktx",
+ "androidx.savedstate_savedstate-ktx",
+ "dagger2",
+ "jsr330",
"kotlin-stdlib",
"kotlinx_coroutines",
"kotlinx-coroutines-android",
@@ -83,9 +86,15 @@ android_library {
"SystemUIFlagsLib",
],
+ plugins: ["dagger2-compiler"],
+
lint: {
strict_updatability_linting: false,
},
+
+ optimize: {
+ proguard_flags_files: ["proguard.flags"],
+ },
}
android_app {
@@ -93,6 +102,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..ba9afe28 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest-app.xml
@@ -17,44 +17,29 @@
*/
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.intentresolver"
- android:versionCode="0"
- 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" />
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.intentresolver"
+ android:versionCode="0"
+ android:versionName="2021-11"
+ coreApp="true">
<application
+ android:name=".IntentResolverApplication"
android:hardwareAccelerated="true"
android:label="@string/app_label"
android:directBootAware="true"
android:forceQueryable="true"
android:requiredForAllUsers="true"
- android:supportsRtl="true">
+ android:supportsRtl="true"
+ tools:replace="android:appComponentFactory"
+ android:appComponentFactory=".dagger.InjectedAppComponentFactory">
- <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 +50,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/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_loading_item.xml b/java/res/layout/image_preview_loading_item.xml
new file mode 100644
index 00000000..85020e9a
--- /dev/null
+++ b/java/res/layout/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/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/ApplicationComponentOwner.kt b/java/src/com/android/intentresolver/ApplicationComponentOwner.kt
new file mode 100644
index 00000000..fb39814c
--- /dev/null
+++ b/java/src/com/android/intentresolver/ApplicationComponentOwner.kt
@@ -0,0 +1,15 @@
+package com.android.intentresolver
+
+import com.android.intentresolver.dagger.ApplicationComponent
+
+/**
+ * Interface that should be implemented by the [Application][android.app.Application] object as the
+ * owner of the [ApplicationComponent].
+ */
+interface ApplicationComponentOwner {
+ /**
+ * Invokes the given [action] when the [ApplicationComponent] has been created. If it has
+ * already been created, then it invokes [action] immediately.
+ */
+ fun doWhenApplicationComponentReady(action: (ApplicationComponent) -> Unit)
+}
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..8edbba08 100644
--- a/java/src/com/android/intentresolver/ChooserActivity.java
+++ b/java/src/com/android/intentresolver/ChooserActivity.java
@@ -72,6 +72,7 @@ import android.view.animation.LinearInterpolator;
import android.widget.TextView;
import androidx.annotation.MainThread;
+import androidx.lifecycle.HasDefaultViewModelProviderFactory;
import androidx.lifecycle.ViewModelProvider;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
@@ -87,22 +88,28 @@ import com.android.intentresolver.contentpreview.BasePreviewViewModel;
import com.android.intentresolver.contentpreview.ChooserContentPreviewUi;
import com.android.intentresolver.contentpreview.HeadlineGeneratorImpl;
import com.android.intentresolver.contentpreview.PreviewViewModel;
+import com.android.intentresolver.dagger.InjectedViewModelFactory;
+import com.android.intentresolver.dagger.ViewModelComponent;
import com.android.intentresolver.flags.FeatureFlagRepository;
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;
import com.android.intentresolver.model.ResolverRankerServiceResolverComparator;
import com.android.intentresolver.shortcuts.AppPredictorFactory;
import com.android.intentresolver.shortcuts.ShortcutLoader;
+import com.android.intentresolver.ui.ChooserViewModel;
import com.android.intentresolver.widget.ImagePreviewView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.content.PackageMonitor;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import org.jetbrains.annotations.NotNull;
+
import java.io.File;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -118,13 +125,15 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.function.Consumer;
+import javax.inject.Inject;
+
/**
* The Chooser Activity handles intent resolution specifically for sharing intents -
* for example, as generated by {@see android.content.Intent#createChooser(Intent, CharSequence)}.
*
*/
public class ChooserActivity extends ResolverActivity implements
- ResolverListAdapter.ResolverListCommunicator {
+ ResolverListAdapter.ResolverListCommunicator, HasDefaultViewModelProviderFactory {
private static final String TAG = "ChooserActivity";
/**
@@ -164,6 +173,11 @@ public class ChooserActivity extends ResolverActivity implements
private static final int SCROLL_STATUS_SCROLLING_VERTICAL = 1;
private static final int SCROLL_STATUS_SCROLLING_HORIZONTAL = 2;
+ private ViewModelProvider.Factory mViewModelFactory;
+ private final ViewModelComponent.Builder mViewModelComponentBuilder;
+
+ private ChooserViewModel mViewModel;
+
@IntDef(flag = false, prefix = { "TARGET_TYPE_" }, value = {
TARGET_TYPE_DEFAULT,
TARGET_TYPE_CHOOSER_TARGET,
@@ -191,7 +205,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;
@@ -225,15 +239,32 @@ public class ChooserActivity extends ResolverActivity implements
private boolean mExcludeSharedText = false;
- public ChooserActivity() {}
+ @Inject
+ public ChooserActivity(ViewModelComponent.Builder builder) {
+ mViewModelComponentBuilder = builder;
+ }
+
+ @NotNull
+ @Override
+ public final ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
+ if (mViewModelFactory == null) {
+ mViewModelFactory = new InjectedViewModelFactory(mViewModelComponentBuilder,
+ getDefaultViewModelCreationExtras(),
+ getReferrer());
+ }
+ return mViewModelFactory;
+ }
@Override
protected void onCreate(Bundle savedInstanceState) {
+ Log.d(TAG, "onCreate");
Tracer.INSTANCE.markLaunched();
final long intentReceivedTime = System.currentTimeMillis();
mLatencyTracker.onActionStart(ACTION_LOAD_SHARE_SHEET);
- getChooserActivityLogger().logSharesheetTriggered();
+ getEventLog().logSharesheetTriggered();
+
+ mViewModel = new ViewModelProvider(this).get(ChooserViewModel.class);
mFeatureFlagRepository = createFeatureFlagRepository();
mIntegratedDeviceComponents = getIntegratedDeviceComponents();
@@ -251,7 +282,9 @@ public class ChooserActivity extends ResolverActivity implements
return;
}
- mRefinementManager = new ViewModelProvider(this).get(ChooserRefinementManager.class);
+ // Note: Uses parent ViewModelProvider.Factory because RefinementManager is not injectable
+ mRefinementManager = new ViewModelProvider(this, super.getDefaultViewModelProviderFactory())
+ .get(ChooserRefinementManager.class);
mRefinementManager.getRefinementCompletion().observe(this, completion -> {
if (completion.consume()) {
@@ -273,7 +306,7 @@ public class ChooserActivity extends ResolverActivity implements
BasePreviewViewModel previewViewModel =
new ViewModelProvider(this, createPreviewViewModelFactory())
- .get(BasePreviewViewModel.class);
+ .get(PreviewViewModel.class);
mChooserContentPreviewUi = new ChooserContentPreviewUi(
getLifecycle(),
previewViewModel.createOrReuseProvider(mChooserRequest),
@@ -283,10 +316,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 +333,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 +353,7 @@ public class ChooserActivity extends ResolverActivity implements
mResolverDrawerLayout.setOnCollapsedChangedListener(
isCollapsed -> {
mChooserMultiProfilePagerAdapter.setIsCollapsed(isCollapsed);
- getChooserActivityLogger().logSharesheetExpansionChanged(isCollapsed);
+ getEventLog().logSharesheetExpansionChanged(isCollapsed);
});
}
@@ -330,7 +361,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 +580,7 @@ public class ChooserActivity extends ResolverActivity implements
if (shouldShowStickyContentPreview()
|| mChooserMultiProfilePagerAdapter
.getCurrentRootAdapter().getSystemRowCount() != 0) {
- getChooserActivityLogger().logActionShareWithPreview(
+ getEventLog().logActionShareWithPreview(
mChooserContentPreviewUi.getPreferredContentPreview());
}
return postRebuildListInternal(rebuildCompleted);
@@ -848,9 +879,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 +912,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 +920,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 +954,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 +971,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 +1024,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 +1148,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 +1158,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 +1288,7 @@ public class ChooserActivity extends ResolverActivity implements
targetIntent,
this,
context.getPackageManager(),
- getChooserActivityLogger(),
+ getEventLog(),
chooserRequest,
maxTargetsPerRow,
initialIntentsUserSpace,
@@ -1279,7 +1312,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 +1321,7 @@ public class ChooserActivity extends ResolverActivity implements
getTargetIntent(),
getReferrerPackageName(),
null,
- getChooserActivityLogger(),
+ getEventLog(),
getResolverRankerServiceUserHandleList(userHandle),
getIntegratedDeviceComponents().getNearbySharingComponent());
}
@@ -1313,7 +1346,7 @@ public class ChooserActivity extends ResolverActivity implements
this,
mChooserRequest,
mIntegratedDeviceComponents,
- getChooserActivityLogger(),
+ getEventLog(),
(isExcluded) -> mExcludeSharedText = isExcluded,
this::getFirstVisibleImgPreviewView,
new ChooserActionFactory.ActionActivityStarter() {
@@ -1528,7 +1561,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 +1608,7 @@ public class ChooserActivity extends ResolverActivity implements
}
logDirectShareTargetReceived(userHandle);
sendVoiceChoicesIfNeeded();
- getChooserActivityLogger().logSharesheetDirectLoadComplete();
+ getEventLog().logSharesheetDirectLoadComplete();
}
private void setupScrollListener() {
@@ -1881,7 +1914,7 @@ public class ChooserActivity extends ResolverActivity implements
@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/IntentForwarderActivity.java b/java/src/com/android/intentresolver/IntentForwarderActivity.java
index 5e8945f1..d69a6c71 100644
--- a/java/src/com/android/intentresolver/IntentForwarderActivity.java
+++ b/java/src/com/android/intentresolver/IntentForwarderActivity.java
@@ -57,6 +57,8 @@ import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
+import javax.inject.Inject;
+
/**
* This is used in conjunction with
* {@link DevicePolicyManager#addCrossProfileIntentFilter} to enable intents to
@@ -84,6 +86,11 @@ public class IntentForwarderActivity extends Activity {
private MetricsLogger mMetricsLogger;
protected ExecutorService mExecutorService;
+ @Inject
+ public IntentForwarderActivity() {
+ super();
+ }
+
@Override
protected void onDestroy() {
super.onDestroy();
diff --git a/java/src/com/android/intentresolver/IntentResolverApplication.kt b/java/src/com/android/intentresolver/IntentResolverApplication.kt
new file mode 100644
index 00000000..61df7fff
--- /dev/null
+++ b/java/src/com/android/intentresolver/IntentResolverApplication.kt
@@ -0,0 +1,30 @@
+package com.android.intentresolver
+
+import android.app.Application
+import com.android.intentresolver.dagger.ApplicationComponent
+import com.android.intentresolver.dagger.DaggerApplicationComponent
+
+/** [Application] that maintains the [ApplicationComponent]. */
+open class IntentResolverApplication : Application(), ApplicationComponentOwner {
+
+ private lateinit var applicationComponent: ApplicationComponent
+
+ private val pendingDaggerActions = mutableSetOf<(ApplicationComponent) -> Unit>()
+
+ open fun createApplicationComponentBuilder() = DaggerApplicationComponent.builder()
+
+ override fun onCreate() {
+ super.onCreate()
+ applicationComponent = createApplicationComponentBuilder().application(this).build()
+ pendingDaggerActions.forEach { it.invoke(applicationComponent) }
+ pendingDaggerActions.clear()
+ }
+
+ override fun doWhenApplicationComponentReady(action: (ApplicationComponent) -> Unit) {
+ if (this::applicationComponent.isInitialized) {
+ action.invoke(applicationComponent)
+ } else {
+ pendingDaggerActions.add(action)
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/ResolverActivity.java b/java/src/com/android/intentresolver/ResolverActivity.java
index 57871532..252d0a41 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,12 +120,15 @@ 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;
import java.util.Set;
import java.util.function.Supplier;
+import javax.inject.Inject;
+
/**
* This is a copy of ResolverActivity to support IntentResolver's ChooserActivity. This code is
* *not* the resolver that is actually triggered by the system right now (you want
@@ -135,6 +139,7 @@ import java.util.function.Supplier;
public class ResolverActivity extends FragmentActivity implements
ResolverListAdapter.ResolverListCommunicator {
+ @Inject
public ResolverActivity() {
mIsIntentPicker = getClass().equals(ResolverActivity.class);
}
@@ -143,7 +148,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 +344,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 +410,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 +433,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 +489,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 +1077,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 +1088,7 @@ public class ResolverActivity extends FragmentActivity implements
context,
payloadIntents,
initialIntents,
- rList,
+ resolutionList,
filterLastUsed,
createListController(userHandle),
userHandle,
@@ -1127,6 +1161,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 +1182,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 +1210,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 +1237,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 +1247,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 +1405,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 +1465,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 +1665,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 +2282,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 +2420,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/dagger/ActivityBinderModule.kt b/java/src/com/android/intentresolver/dagger/ActivityBinderModule.kt
new file mode 100644
index 00000000..7c997ef7
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/ActivityBinderModule.kt
@@ -0,0 +1,33 @@
+package com.android.intentresolver.dagger
+
+import android.app.Activity
+import com.android.intentresolver.ChooserActivity
+import com.android.intentresolver.IntentForwarderActivity
+import com.android.intentresolver.ResolverActivity
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+/** Injection instructions for injectable [Activities][Activity]. */
+@Module
+interface ActivityBinderModule {
+
+ @Binds
+ @IntoMap
+ @ClassKey(ChooserActivity::class)
+ @ActivityScope
+ fun bindChooserActivity(activity: ChooserActivity): Activity
+
+ @Binds
+ @IntoMap
+ @ClassKey(ResolverActivity::class)
+ @ActivityScope
+ fun bindResolverActivity(activity: ResolverActivity): Activity
+
+ @Binds
+ @IntoMap
+ @ClassKey(IntentForwarderActivity::class)
+ @ActivityScope
+ fun bindIntentForwarderActivity(activity: IntentForwarderActivity): Activity
+}
diff --git a/java/src/com/android/intentresolver/dagger/ActivityComponent.kt b/java/src/com/android/intentresolver/dagger/ActivityComponent.kt
new file mode 100644
index 00000000..bf5ff761
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/ActivityComponent.kt
@@ -0,0 +1,21 @@
+package com.android.intentresolver.dagger
+
+import android.app.Activity
+import dagger.Subcomponent
+import javax.inject.Provider
+import javax.inject.Scope
+
+@MustBeDocumented @Retention(AnnotationRetention.RUNTIME) @Scope annotation class ActivityScope
+
+/** Subcomponent for injections across the life of an Activity. */
+@ActivityScope
+@Subcomponent(modules = [ActivityModule::class, ActivityBinderModule::class])
+interface ActivityComponent {
+
+ @Subcomponent.Factory
+ interface Factory {
+ fun create(): ActivityComponent
+ }
+
+ fun activities(): Map<Class<*>, @JvmSuppressWildcards Provider<Activity>>
+}
diff --git a/java/src/com/android/intentresolver/dagger/ActivityModule.kt b/java/src/com/android/intentresolver/dagger/ActivityModule.kt
new file mode 100644
index 00000000..f6a2229d
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/ActivityModule.kt
@@ -0,0 +1,6 @@
+package com.android.intentresolver.dagger
+
+import dagger.Module
+
+/** Bindings provided to [@ActivityScope][ActivityScope]. */
+@Module interface ActivityModule
diff --git a/java/src/com/android/intentresolver/dagger/ApplicationComponent.kt b/java/src/com/android/intentresolver/dagger/ApplicationComponent.kt
new file mode 100644
index 00000000..9fc57712
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/ApplicationComponent.kt
@@ -0,0 +1,21 @@
+package com.android.intentresolver.dagger
+
+import android.app.Application
+import dagger.BindsInstance
+import dagger.Component
+import javax.inject.Singleton
+
+/** Top level component for injections across the life of the process. */
+@Singleton
+@Component(modules = [ApplicationModule::class])
+interface ApplicationComponent {
+
+ @Component.Builder
+ interface Builder {
+ @BindsInstance fun application(application: Application): Builder
+
+ fun build(): ApplicationComponent
+ }
+
+ fun inject(appComponentFactory: InjectedAppComponentFactory)
+}
diff --git a/java/src/com/android/intentresolver/dagger/ApplicationModule.kt b/java/src/com/android/intentresolver/dagger/ApplicationModule.kt
new file mode 100644
index 00000000..4986d7e1
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/ApplicationModule.kt
@@ -0,0 +1,29 @@
+package com.android.intentresolver.dagger
+
+import android.app.Application
+import android.content.Context
+import com.android.intentresolver.dagger.qualifiers.App
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+
+/**
+ * Bindings provided to [ApplicationComponent] and children.
+ *
+ * These are all @Singleton scope, one for the duration of the process.
+ */
+@Module(
+ subcomponents = [ActivityComponent::class, ViewModelComponent::class],
+ includes = [ReceiverBinderModule::class, CoroutinesModule::class],
+)
+interface ApplicationModule {
+
+ companion object {
+
+ @JvmStatic
+ @Provides
+ @Singleton
+ @App
+ fun applicationContext(app: Application): Context = app.applicationContext
+ }
+}
diff --git a/java/src/com/android/intentresolver/dagger/CoroutinesModule.kt b/java/src/com/android/intentresolver/dagger/CoroutinesModule.kt
new file mode 100644
index 00000000..5fda2c30
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/CoroutinesModule.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver.dagger
+
+import com.android.intentresolver.dagger.qualifiers.Background
+import com.android.intentresolver.dagger.qualifiers.Main
+import dagger.Module
+import dagger.Provides
+import javax.inject.Singleton
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+
+@Module
+interface CoroutinesModule {
+ companion object {
+ @JvmStatic
+ @Provides
+ @Singleton
+ @Main
+ fun mainDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
+
+ @JvmStatic
+ @Provides
+ @Singleton
+ @Main
+ fun mainCoroutineScope(@Main mainDispatcher: CoroutineDispatcher) =
+ CoroutineScope(SupervisorJob() + mainDispatcher)
+
+ @JvmStatic
+ @Provides
+ @Singleton
+ @Background
+ fun backgroundDispatcher(): CoroutineDispatcher = Dispatchers.IO
+ }
+}
diff --git a/java/src/com/android/intentresolver/dagger/InjectedAppComponentFactory.kt b/java/src/com/android/intentresolver/dagger/InjectedAppComponentFactory.kt
new file mode 100644
index 00000000..db209ef0
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/InjectedAppComponentFactory.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.dagger
+
+import android.app.Activity
+import android.app.Application
+import android.content.BroadcastReceiver
+import android.content.Intent
+import android.util.Log
+import androidx.core.app.AppComponentFactory
+import com.android.intentresolver.ApplicationComponentOwner
+import javax.inject.Inject
+import javax.inject.Provider
+
+/** Provides instances of application components, delegates construction to Dagger. */
+class InjectedAppComponentFactory : AppComponentFactory() {
+
+ @set:Inject lateinit var activityComponentBuilder: ActivityComponent.Factory
+
+ @set:Inject
+ lateinit var receivers: Map<Class<*>, @JvmSuppressWildcards Provider<BroadcastReceiver>>
+
+ override fun instantiateApplicationCompat(cl: ClassLoader, className: String): Application {
+ val app = super.instantiateApplicationCompat(cl, className)
+ if (app !is ApplicationComponentOwner) {
+ throw RuntimeException("App must be ApplicationComponentOwner")
+ }
+ app.doWhenApplicationComponentReady { it.inject(this) }
+ return app
+ }
+
+ override fun instantiateActivityCompat(
+ cl: ClassLoader,
+ className: String,
+ intent: Intent?,
+ ): Activity {
+ return runCatching {
+ val activities = activityComponentBuilder.create().activities()
+ instantiate(className, activities)
+ }
+ .onFailure {
+ if (it is UninitializedPropertyAccessException) {
+ // This should never happen but if it did it would cause errors that could
+ // be very difficult to identify, so we log it out of an abundance of
+ // caution.
+ Log.e(TAG, "Tried to instantiate $className before AppComponent", it)
+ }
+ }
+ .getOrNull()
+ ?: super.instantiateActivityCompat(cl, className, intent)
+ }
+
+ override fun instantiateReceiverCompat(
+ cl: ClassLoader,
+ className: String,
+ intent: Intent?,
+ ): BroadcastReceiver {
+ return instantiate(className, receivers)
+ ?: super.instantiateReceiverCompat(cl, className, intent)
+ }
+
+ private fun <T> instantiate(className: String, providers: Map<Class<*>, Provider<T>>): T? {
+ return runCatching { providers[Class.forName(className)]?.get() }
+ .onFailure {
+ if (it is UninitializedPropertyAccessException) {
+ // This should never happen but if it did it would cause errors that could
+ // be very difficult to identify, so we log it out of an abundance of
+ // caution.
+ Log.e(TAG, "Tried to instantiate $className before AppComponent", it)
+ }
+ }
+ .getOrNull()
+ }
+
+ companion object {
+ private const val TAG = "AppComponentFactory"
+ }
+}
diff --git a/java/src/com/android/intentresolver/dagger/InjectedViewModelFactory.kt b/java/src/com/android/intentresolver/dagger/InjectedViewModelFactory.kt
new file mode 100644
index 00000000..f0906d3e
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/InjectedViewModelFactory.kt
@@ -0,0 +1,84 @@
+/*
+ * 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.dagger
+
+import android.net.Uri
+import android.os.Bundle
+import androidx.lifecycle.DEFAULT_ARGS_KEY
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import androidx.lifecycle.viewmodel.CreationExtras
+import java.io.Closeable
+import javax.inject.Provider
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.isActive
+
+/** Instantiates new ViewModel instances using Dagger. */
+class InjectedViewModelFactory(
+ private val viewModelComponentBuilder: ViewModelComponent.Builder,
+ creationExtras: CreationExtras,
+ private val referrer: Uri,
+) : ViewModelProvider.Factory {
+
+ private val defaultArgs = creationExtras[DEFAULT_ARGS_KEY] ?: Bundle()
+
+ private fun viewModelScope(viewModelClass: Class<*>) =
+ CloseableCoroutineScope(
+ SupervisorJob() + CoroutineName(viewModelClass.simpleName) + Dispatchers.Main.immediate
+ )
+
+ private fun <T> newViewModel(
+ providerMap: Map<Class<*>, Provider<ViewModel>>,
+ modelClass: Class<T>
+ ): T {
+ val provider =
+ providerMap[modelClass]
+ ?: error(
+ "Unable to create an instance of $modelClass. " +
+ "Does the class have a binding in ViewModelComponent?"
+ )
+ return modelClass.cast(provider.get())
+ }
+
+ override fun <T : ViewModel> create(modelClass: Class<T>): T {
+ val viewModelScope = viewModelScope(modelClass)
+ val viewModelComponent =
+ viewModelComponentBuilder
+ .coroutineScope(viewModelScope)
+ .intentExtras(defaultArgs)
+ .referrer(referrer)
+ .build()
+ val viewModel = newViewModel(viewModelComponent.viewModels(), modelClass)
+ viewModel.addCloseable(viewModelScope)
+ return viewModel
+ }
+}
+
+internal class CloseableCoroutineScope(context: CoroutineContext) : Closeable, CoroutineScope {
+ override val coroutineContext: CoroutineContext = context
+
+ override fun close() {
+ if (isActive) {
+ coroutineContext.cancel()
+ }
+ }
+}
diff --git a/java/src/com/android/intentresolver/dagger/ReceiverBinderModule.kt b/java/src/com/android/intentresolver/dagger/ReceiverBinderModule.kt
new file mode 100644
index 00000000..32ce2f45
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/ReceiverBinderModule.kt
@@ -0,0 +1,12 @@
+package com.android.intentresolver.dagger
+
+import android.content.BroadcastReceiver
+import dagger.Module
+import dagger.multibindings.Multibinds
+
+/** Injection instructions for injectable [BroadcastReceivers][BroadcastReceiver] */
+@Module
+interface ReceiverBinderModule {
+
+ @Multibinds fun bindReceivers(): Map<Class<*>, @JvmSuppressWildcards BroadcastReceiver>
+}
diff --git a/java/src/com/android/intentresolver/dagger/ViewModelBinderModule.kt b/java/src/com/android/intentresolver/dagger/ViewModelBinderModule.kt
new file mode 100644
index 00000000..91ba039c
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/ViewModelBinderModule.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.dagger
+
+import androidx.lifecycle.ViewModel
+import com.android.intentresolver.ui.ChooserViewModel
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+/** Defines a map of injectable ViewModel classes. */
+@Module
+interface ViewModelBinderModule {
+ @Binds
+ @IntoMap
+ @ClassKey(ChooserViewModel::class)
+ @ViewModelScope
+ fun chooserViewModel(viewModel: ChooserViewModel): ViewModel
+}
diff --git a/java/src/com/android/intentresolver/dagger/ViewModelComponent.kt b/java/src/com/android/intentresolver/dagger/ViewModelComponent.kt
new file mode 100644
index 00000000..3e2e2681
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/ViewModelComponent.kt
@@ -0,0 +1,57 @@
+/*
+ * 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.dagger
+
+import android.net.Uri
+import android.os.Bundle
+import com.android.intentresolver.dagger.qualifiers.Referrer
+import com.android.intentresolver.dagger.qualifiers.ViewModel
+import dagger.BindsInstance
+import dagger.Subcomponent
+import javax.inject.Provider
+import javax.inject.Scope
+import kotlin.annotation.AnnotationRetention.RUNTIME
+import kotlinx.coroutines.CoroutineScope
+
+@Scope @Retention(RUNTIME) @MustBeDocumented annotation class ViewModelScope
+
+/**
+ * Provides dependencies within [ViewModelScope] within a [ViewModel].
+ *
+ * @see InjectedViewModelFactory
+ */
+@ViewModelScope
+@Subcomponent(modules = [ViewModelModule::class, ViewModelBinderModule::class])
+interface ViewModelComponent {
+
+ /**
+ * Binds instance values from the creating Activity to make them available for injection within
+ * [ViewModelScope].
+ */
+ @Subcomponent.Builder
+ interface Builder {
+ @BindsInstance fun intentExtras(@ViewModel intentExtras: Bundle): Builder
+
+ @BindsInstance fun referrer(@Referrer uri: Uri): Builder
+
+ @BindsInstance fun coroutineScope(@ViewModel scope: CoroutineScope): Builder
+
+ fun build(): ViewModelComponent
+ }
+
+ fun viewModels(): Map<Class<*>, @JvmSuppressWildcards Provider<androidx.lifecycle.ViewModel>>
+}
diff --git a/java/src/com/android/intentresolver/dagger/ViewModelModule.kt b/java/src/com/android/intentresolver/dagger/ViewModelModule.kt
new file mode 100644
index 00000000..23320311
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/ViewModelModule.kt
@@ -0,0 +1,6 @@
+package com.android.intentresolver.dagger
+
+import dagger.Module
+
+/** Provides bindings shared among components within [@ViewModelScope][ViewModelScope]. */
+@Module abstract class ViewModelModule
diff --git a/java/src/com/android/intentresolver/dagger/qualifiers/Qualifiers.kt b/java/src/com/android/intentresolver/dagger/qualifiers/Qualifiers.kt
new file mode 100644
index 00000000..fa50170e
--- /dev/null
+++ b/java/src/com/android/intentresolver/dagger/qualifiers/Qualifiers.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.dagger.qualifiers
+
+import javax.inject.Qualifier
+
+// Note: 'qualifiers' package avoids name collisions in Dagger code.
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class App
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Activity
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ViewModel
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Main
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Background
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Delegate
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Default
+
+@Qualifier @MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class Referrer
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/ui/ChooserViewModel.kt b/java/src/com/android/intentresolver/ui/ChooserViewModel.kt
new file mode 100644
index 00000000..817f0b6c
--- /dev/null
+++ b/java/src/com/android/intentresolver/ui/ChooserViewModel.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.ui
+
+import android.util.Log
+import androidx.lifecycle.ViewModel
+import com.android.intentresolver.dagger.qualifiers.ViewModel as ViewModelQualifier
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.launch
+
+const val TAG = "ChooserViewModel"
+
+/** The primary container for ViewModelScope dependencies. */
+class ChooserViewModel
+@Inject
+constructor(
+ @ViewModelQualifier val viewModelScope: CoroutineScope,
+) : ViewModel() \ No newline at end of file
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..bb287eb2 100644
--- a/java/tests/Android.bp
+++ b/java/tests/Android.bp
@@ -19,18 +19,21 @@ android_test {
static_libs: [
"IntentResolver-core",
- "androidx.test.rules",
+ "androidx.test.core",
"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",
"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/AndroidManifest.xml b/java/tests/AndroidManifest.xml
index 05830c4c..9f8dd41c 100644
--- a/java/tests/AndroidManifest.xml
+++ b/java/tests/AndroidManifest.xml
@@ -15,7 +15,8 @@
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
- package="com.android.intentresolver.tests">
+ xmlns:tools="http://schemas.android.com/tools"
+ package="com.android.intentresolver.tests">
<uses-sdk android:minSdkVersion="21" android:targetSdkVersion="30" />
@@ -25,7 +26,10 @@
<uses-permission android:name="android.permission.WRITE_DEVICE_CONFIG"/>
<uses-permission android:name="android.permission.READ_DEVICE_CONFIG" />
- <application android:name="com.android.intentresolver.TestApplication">
+ <application
+ android:name="com.android.intentresolver.TestApplication"
+ tools:replace="android:appComponentFactory"
+ android:appComponentFactory="com.android.intentresolver.dagger.InjectedAppComponentFactory">
<uses-library android:name="android.test.runner" />
<activity android:name="com.android.intentresolver.ChooserWrapperActivity" />
<activity android:name="com.android.intentresolver.ResolverWrapperActivity" />
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..49305a6c 100644
--- a/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
+++ b/java/tests/src/com/android/intentresolver/ChooserWrapperActivity.java
@@ -37,15 +37,19 @@ import androidx.lifecycle.ViewModelProvider;
import com.android.intentresolver.AbstractMultiProfilePagerAdapter.CrossProfileIntentsChecker;
import com.android.intentresolver.chooser.DisplayResolveInfo;
import com.android.intentresolver.chooser.TargetInfo;
+import com.android.intentresolver.dagger.TestViewModelComponent;
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;
import java.util.List;
import java.util.function.Consumer;
+import javax.inject.Inject;
+
/**
* Simple wrapper around chooser activity to be able to initiate it under test. For more
* information, see {@code com.android.internal.app.ChooserWrapperActivity}.
@@ -55,6 +59,11 @@ public class ChooserWrapperActivity
static final ChooserActivityOverrideData sOverrides = ChooserActivityOverrideData.getInstance();
private UsageStatsManager mUsm;
+ @Inject
+ public ChooserWrapperActivity(TestViewModelComponent.Builder builder) {
+ super(builder);
+ }
+
// ResolverActivity (the base class of ChooserActivity) inspects the launched-from UID at
// onCreate and needs to see some non-negative value in the test.
@Override
@@ -89,7 +98,7 @@ public class ChooserWrapperActivity
targetIntent,
this,
packageManager,
- getChooserActivityLogger(),
+ getEventLog(),
chooserRequest,
maxTargetsPerRow,
userHandle,
@@ -205,8 +214,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/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/ResolverWrapperActivity.java b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java
index 401ede26..11e7dffd 100644
--- a/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java
+++ b/java/tests/src/com/android/intentresolver/ResolverWrapperActivity.java
@@ -44,6 +44,8 @@ import java.util.List;
import java.util.function.Consumer;
import java.util.function.Function;
+import javax.inject.Inject;
+
/*
* Simple wrapper around chooser activity to be able to initiate it under test
*/
@@ -53,6 +55,7 @@ public class ResolverWrapperActivity extends ResolverActivity {
private final CountingIdlingResource mLabelIdlingResource =
new CountingIdlingResource("LoadLabelTask");
+ @Inject
public ResolverWrapperActivity() {
super(/* isIntentPicker= */ true);
}
diff --git a/java/tests/src/com/android/intentresolver/TestApplication.kt b/java/tests/src/com/android/intentresolver/TestApplication.kt
index 849cfbab..4f5aefb9 100644
--- a/java/tests/src/com/android/intentresolver/TestApplication.kt
+++ b/java/tests/src/com/android/intentresolver/TestApplication.kt
@@ -16,12 +16,13 @@
package com.android.intentresolver
-import android.app.Application
import android.content.Context
import android.os.UserHandle
+import com.android.intentresolver.dagger.DaggerTestApplicationComponent
-class TestApplication : Application() {
+class TestApplication : IntentResolverApplication() {
+ override fun createApplicationComponentBuilder() = DaggerTestApplicationComponent.builder()
// return the current context as a work profile doesn't really exist in these tests
override fun createContextAsUser(user: UserHandle, flags: Int): Context = this
-} \ No newline at end of file
+}
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
index f47e343f..7e588f98 100644
--- a/java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt
+++ b/java/tests/src/com/android/intentresolver/TestLifecycleOwner.kt
@@ -23,7 +23,7 @@ import androidx.lifecycle.LifecycleRegistry
internal class TestLifecycleOwner : LifecycleOwner {
private val lifecycleRegistry = LifecycleRegistry.createUnsafe(this)
- override fun getLifecycle(): Lifecycle = lifecycleRegistry
+ override val lifecycle: Lifecycle get() = lifecycleRegistry
var state: Lifecycle.State
get() = lifecycle.currentState
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..008cc162 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 com.android.intentresolver.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..06ade1ce
--- /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.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation
+import com.android.intentresolver.R
+import com.android.intentresolver.TestLifecycleOwner
+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/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/dagger/TestActivityBinderModule.kt b/java/tests/src/com/android/intentresolver/dagger/TestActivityBinderModule.kt
new file mode 100644
index 00000000..c08bc3b2
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/dagger/TestActivityBinderModule.kt
@@ -0,0 +1,40 @@
+/*
+ * 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.dagger
+
+import android.app.Activity
+import com.android.intentresolver.ChooserWrapperActivity
+import com.android.intentresolver.ResolverWrapperActivity
+import dagger.Binds
+import dagger.Module
+import dagger.multibindings.ClassKey
+import dagger.multibindings.IntoMap
+
+@Module
+interface TestActivityBinderModule {
+ @Binds
+ @IntoMap
+ @ClassKey(ResolverWrapperActivity::class)
+ @ActivityScope
+ fun resolverWrapperActivity(activity: ResolverWrapperActivity): Activity
+
+ @Binds
+ @IntoMap
+ @ClassKey(ChooserWrapperActivity::class)
+ @ActivityScope
+ fun chooserWrapperActivity(activity: ChooserWrapperActivity): Activity
+}
diff --git a/java/tests/src/com/android/intentresolver/dagger/TestActivityComponent.kt b/java/tests/src/com/android/intentresolver/dagger/TestActivityComponent.kt
new file mode 100644
index 00000000..4416c852
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/dagger/TestActivityComponent.kt
@@ -0,0 +1,30 @@
+/*
+ * 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.dagger
+
+import dagger.Subcomponent
+
+@ActivityScope
+@Subcomponent(
+ modules = [ActivityModule::class, ActivityBinderModule::class, TestActivityBinderModule::class]
+)
+interface TestActivityComponent : ActivityComponent {
+ @Subcomponent.Factory
+ interface Factory : ActivityComponent.Factory {
+ override fun create(): TestActivityComponent
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/dagger/TestApplicationComponent.kt b/java/tests/src/com/android/intentresolver/dagger/TestApplicationComponent.kt
new file mode 100644
index 00000000..224c46c6
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/dagger/TestApplicationComponent.kt
@@ -0,0 +1,34 @@
+/*
+ * 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.dagger
+
+import android.app.Application
+import dagger.BindsInstance
+import dagger.Component
+import javax.inject.Singleton
+
+@Singleton
+@Component(modules = [TestApplicationModule::class])
+interface TestApplicationComponent : ApplicationComponent {
+ @Component.Builder
+ interface Builder : ApplicationComponent.Builder {
+ @BindsInstance
+ override fun application(application: Application): TestApplicationComponent.Builder
+
+ override fun build(): TestApplicationComponent
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/dagger/TestApplicationModule.kt b/java/tests/src/com/android/intentresolver/dagger/TestApplicationModule.kt
new file mode 100644
index 00000000..714748d2
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/dagger/TestApplicationModule.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.dagger
+
+import dagger.Binds
+import dagger.Module
+import javax.inject.Singleton
+
+@Module(
+ subcomponents = [TestActivityComponent::class, TestViewModelComponent::class],
+ includes = [ReceiverBinderModule::class, CoroutinesModule::class]
+)
+interface TestApplicationModule : ApplicationModule {
+
+ /** Required to support field injection of [InjectedAppComponentFactory] */
+ @Binds
+ @Singleton
+ fun activityComponent(component: TestActivityComponent.Factory): ActivityComponent.Factory
+
+ /** Required to support injection into Activity instances */
+ @Binds
+ @Singleton
+ fun viewModelComponent(component: TestViewModelComponent.Builder): ViewModelComponent.Builder
+}
diff --git a/java/tests/src/com/android/intentresolver/dagger/TestViewModelComponent.kt b/java/tests/src/com/android/intentresolver/dagger/TestViewModelComponent.kt
new file mode 100644
index 00000000..539b3f36
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/dagger/TestViewModelComponent.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.dagger
+
+import dagger.Subcomponent
+
+/** A ViewModelComponent for tests which replaces ViewModelModule -> TestViewModelModule */
+@ViewModelScope
+@Subcomponent(modules = [TestViewModelModule::class, ViewModelBinderModule::class])
+interface TestViewModelComponent : ViewModelComponent {
+ @Subcomponent.Builder
+ interface Builder : ViewModelComponent.Builder {
+ override fun build(): TestViewModelComponent
+ }
+}
diff --git a/java/tests/src/com/android/intentresolver/dagger/TestViewModelModule.kt b/java/tests/src/com/android/intentresolver/dagger/TestViewModelModule.kt
new file mode 100644
index 00000000..28f4fa73
--- /dev/null
+++ b/java/tests/src/com/android/intentresolver/dagger/TestViewModelModule.kt
@@ -0,0 +1,6 @@
+package com.android.intentresolver.dagger
+
+import dagger.Module
+
+/** Provides bindings shared among components within [@ViewModelScope][ViewModelScope]. */
+@Module abstract class TestViewModelModule
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/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