From cd3c804563ea58c83c3dc1287c30acb557099cc7 Mon Sep 17 00:00:00 2001 From: Mark Renouf Date: Sun, 25 Feb 2024 21:46:03 -0500 Subject: Make 'ShortcutSelectionLogic' Injectable. This adds a module to provide the shortcut per-app limit and the existing DeviceConfig flag used to control whether that limit is applied. Once fully integrated, tests should be updated to simply increase the limit by injecting a different value, instead of altering the device state by modfying DeviceConfig values. To complete, inject into the caller of ChooserListAdapter.addServiceResults, moving usage injecting this component to the caller to use. Bug: 300157408 Test: (this change does not yet affect live code) Flag: n/a Change-Id: I63a661ca304c8d5653992761f10983b5776d50dd --- .../intentresolver/ShortcutSelectionLogic.java | 17 +++-- .../intentresolver/v2/ui/ShortcutPolicyModule.kt | 83 ++++++++++++++++++++++ 2 files changed, 95 insertions(+), 5 deletions(-) create mode 100644 java/src/com/android/intentresolver/v2/ui/ShortcutPolicyModule.kt (limited to 'java') diff --git a/java/src/com/android/intentresolver/ShortcutSelectionLogic.java b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java index efaaf894..12465184 100644 --- a/java/src/com/android/intentresolver/ShortcutSelectionLogic.java +++ b/java/src/com/android/intentresolver/ShortcutSelectionLogic.java @@ -30,13 +30,19 @@ import androidx.annotation.Nullable; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.SelectableTargetInfo; import com.android.intentresolver.chooser.TargetInfo; +import com.android.intentresolver.v2.ui.AppShortcutLimit; +import com.android.intentresolver.v2.ui.EnforceShortcutLimit; import java.util.Collections; import java.util.Comparator; import java.util.List; import java.util.Map; -class ShortcutSelectionLogic { +import javax.inject.Inject; +import javax.inject.Singleton; + +@Singleton +public class ShortcutSelectionLogic { private static final String TAG = "ShortcutSelectionLogic"; private static final boolean DEBUG = false; private static final float PINNED_SHORTCUT_TARGET_SCORE_BOOST = 1000.f; @@ -49,9 +55,10 @@ class ShortcutSelectionLogic { private final Comparator mBaseTargetComparator = (lhs, rhs) -> Float.compare(rhs.getScore(), lhs.getScore()); - ShortcutSelectionLogic( - int maxShortcutTargetsPerApp, - boolean applySharingAppLimits) { + @Inject + public ShortcutSelectionLogic( + @AppShortcutLimit int maxShortcutTargetsPerApp, + @EnforceShortcutLimit boolean applySharingAppLimits) { mMaxShortcutTargetsPerApp = maxShortcutTargetsPerApp; mApplySharingAppLimits = applySharingAppLimits; } @@ -78,7 +85,7 @@ class ShortcutSelectionLogic { + targets.size() + " targets"); } - if (targets.size() == 0) { + if (targets.isEmpty()) { return false; } Collections.sort(targets, mBaseTargetComparator); diff --git a/java/src/com/android/intentresolver/v2/ui/ShortcutPolicyModule.kt b/java/src/com/android/intentresolver/v2/ui/ShortcutPolicyModule.kt new file mode 100644 index 00000000..9ed5f9dd --- /dev/null +++ b/java/src/com/android/intentresolver/v2/ui/ShortcutPolicyModule.kt @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2024 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.v2.ui + +import android.content.res.Resources +import android.provider.DeviceConfig +import com.android.intentresolver.R +import com.android.intentresolver.inject.ApplicationOwned +import com.android.internal.config.sysui.SystemUiDeviceConfigFlags +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Qualifier +import javax.inject.Singleton + +@Qualifier +@MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class AppShortcutLimit +@Qualifier +@MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class EnforceShortcutLimit +@Qualifier +@MustBeDocumented @Retention(AnnotationRetention.RUNTIME) annotation class ShortcutRowLimit + +@Module +@InstallIn(SingletonComponent::class) +object ShortcutPolicyModule { + /** + * Defines the limit for the number of shortcut targets provided for any single app. + * + * This value applies to both results from Shortcut-service and app-provided targets on + * a per-package basis. + */ + @Provides + @Singleton + @AppShortcutLimit + fun appShortcutLimit(@ApplicationOwned resources: Resources): Int { + return resources.getInteger(R.integer.config_maxShortcutTargetsPerApp) + } + + /** + * Once this value is no longer necessary it should be replaced in tests with simply replacing + * [AppShortcutLimit]: + * ``` + * @BindValue + * @AppShortcutLimit + * var shortcutLimit = Int.MAX_VALUE + * ``` + */ + @Provides + @Singleton + @EnforceShortcutLimit + fun applyShortcutLimit(): Boolean { + return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SYSTEMUI, + SystemUiDeviceConfigFlags.APPLY_SHARING_APP_LIMITS_IN_SYSUI, true) + } + + /** + * Defines the limit for the number of shortcuts presented within the direct share row. + * + * This value applies to all displayed direct share targets, including those from Shortcut + * service as well as app-provided targets. + */ + @Provides + @Singleton + @ShortcutRowLimit + fun shortcutRowLimit(@ApplicationOwned resources: Resources): Int { + return resources.getInteger(R.integer.config_chooser_max_targets_per_row) + } +} \ No newline at end of file -- cgit v1.2.3-59-g8ed1b From d4d7e961cb7a21e02a40168e3911d69279d4ce1b Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Thu, 22 Feb 2024 10:23:37 -0800 Subject: Make ImageLoader injectable Do not inject ImageLoader anywhere by v2 activity tests. Rename TestPreviewImageLoader to FakeImageLoader for naming consistency. Bug: 302691505 Test: atest IntentResolver-tests-activity Test: atest IntentResolver-tests-unit Change-Id: I5630664eab6c9546d5de19fa7410184138d15602 --- .../contentpreview/ImageLoaderModule.kt | 44 ++ .../contentpreview/ImagePreviewImageLoader.kt | 34 ++ .../UnbundledChooserActivityTest.java | 8 +- .../v2/UnbundledChooserActivityTest.java | 76 ++-- .../com/android/intentresolver/FakeImageLoader.kt | 39 ++ .../intentresolver/TestPreviewImageLoader.kt | 33 -- .../contentpreview/ChooserContentPreviewUiTest.kt | 4 +- .../contentpreview/ImagePreviewImageLoaderTest.kt | 476 ++++++++++----------- 8 files changed, 391 insertions(+), 323 deletions(-) create mode 100644 java/src/com/android/intentresolver/contentpreview/ImageLoaderModule.kt create mode 100644 tests/shared/src/com/android/intentresolver/FakeImageLoader.kt delete mode 100644 tests/shared/src/com/android/intentresolver/TestPreviewImageLoader.kt (limited to 'java') diff --git a/java/src/com/android/intentresolver/contentpreview/ImageLoaderModule.kt b/java/src/com/android/intentresolver/contentpreview/ImageLoaderModule.kt new file mode 100644 index 00000000..b861a24a --- /dev/null +++ b/java/src/com/android/intentresolver/contentpreview/ImageLoaderModule.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2024 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.content.res.Resources +import com.android.intentresolver.R +import com.android.intentresolver.inject.ApplicationOwned +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ActivityRetainedComponent +import dagger.hilt.android.scopes.ActivityRetainedScoped + +@Module +@InstallIn(ActivityRetainedComponent::class) +interface ImageLoaderModule { + @Binds + @ActivityRetainedScoped + fun imageLoader(previewImageLoader: ImagePreviewImageLoader): ImageLoader + + companion object { + @Provides + @ThumbnailSize + fun thumbnailSize(@ApplicationOwned resources: Resources): Int = + resources.getDimensionPixelSize(R.dimen.chooser_preview_image_max_dimen) + + @Provides @PreviewCacheSize fun cacheSize() = 16 + } +} diff --git a/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt b/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt index 572ccf0b..fab7203e 100644 --- a/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt +++ b/java/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoader.kt @@ -24,17 +24,31 @@ import android.util.Size import androidx.annotation.GuardedBy import androidx.annotation.VisibleForTesting import androidx.collection.LruCache +import com.android.intentresolver.inject.Background import java.util.function.Consumer +import javax.inject.Inject +import javax.inject.Qualifier import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Deferred +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Semaphore private const val TAG = "ImagePreviewImageLoader" +@Qualifier @MustBeDocumented @Retention(AnnotationRetention.BINARY) annotation class ThumbnailSize + +@Qualifier +@MustBeDocumented +@Retention(AnnotationRetention.BINARY) +annotation class PreviewCacheSize + /** * Implements preview image loading for the content preview UI. Provides requests deduplication, * image caching, and a limit on the number of parallel loadings. @@ -52,6 +66,26 @@ constructor( private val contentResolverSemaphore: Semaphore, ) : ImageLoader { + @Inject + constructor( + @Background dispatcher: CoroutineDispatcher, + @ThumbnailSize thumbnailSize: Int, + contentResolver: ContentResolver, + @PreviewCacheSize cacheSize: Int, + ) : this( + CoroutineScope( + SupervisorJob() + + dispatcher + + CoroutineExceptionHandler { _, exception -> + Log.w(TAG, "Uncaught exception in ImageLoader", exception) + } + + CoroutineName("ImageLoader") + ), + thumbnailSize, + contentResolver, + cacheSize, + ) + constructor( scope: CoroutineScope, thumbnailSize: Int, diff --git a/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityTest.java b/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityTest.java index f597d7f2..c7b41ce0 100644 --- a/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityTest.java +++ b/tests/activity/src/com/android/intentresolver/UnbundledChooserActivityTest.java @@ -800,7 +800,7 @@ public class UnbundledChooserActivityTest { Uri uri = createTestContentProviderUri("image/png", null); Intent sendIntent = createSendImageIntent(uri); ChooserActivityOverrideData.getInstance().imageLoader = - new TestPreviewImageLoader(Collections.emptyMap()); + new FakeImageLoader(Collections.emptyMap()); sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); List resolvedComponentInfos = Arrays.asList( @@ -958,7 +958,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendUriIntentWithPreview(uris); ChooserActivityOverrideData.getInstance().imageLoader = - new TestPreviewImageLoader(Collections.emptyMap()); + new FakeImageLoader(Collections.emptyMap()); List resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -1076,7 +1076,7 @@ public class UnbundledChooserActivityTest { bitmaps.put(imgTwoUri, createWideBitmap(Color.GREEN)); bitmaps.put(docUri, createWideBitmap(Color.BLUE)); ChooserActivityOverrideData.getInstance().imageLoader = - new TestPreviewImageLoader(bitmaps); + new FakeImageLoader(bitmaps); List resolvedComponentInfos = createResolvedComponentsForTest(2); setupResolverControllers(resolvedComponentInfos); @@ -3122,6 +3122,6 @@ public class UnbundledChooserActivityTest { } private static ImageLoader createImageLoader(Uri uri, Bitmap bitmap) { - return new TestPreviewImageLoader(Collections.singletonMap(uri, bitmap)); + return new FakeImageLoader(Collections.singletonMap(uri, bitmap)); } } diff --git a/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityTest.java b/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityTest.java index b8113422..a7221c10 100644 --- a/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityTest.java +++ b/tests/activity/src/com/android/intentresolver/v2/UnbundledChooserActivityTest.java @@ -119,15 +119,16 @@ import androidx.test.rule.ActivityTestRule; import com.android.intentresolver.AnnotatedUserHandles; import com.android.intentresolver.ChooserListAdapter; +import com.android.intentresolver.FakeImageLoader; import com.android.intentresolver.Flags; import com.android.intentresolver.IChooserWrapper; import com.android.intentresolver.R; import com.android.intentresolver.ResolvedComponentInfo; import com.android.intentresolver.ResolverDataProvider; import com.android.intentresolver.TestContentProvider; -import com.android.intentresolver.TestPreviewImageLoader; import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.contentpreview.ImageLoader; +import com.android.intentresolver.contentpreview.ImageLoaderModule; import com.android.intentresolver.inject.PackageManagerModule; import com.android.intentresolver.logging.EventLog; import com.android.intentresolver.logging.FakeEventLog; @@ -160,7 +161,6 @@ import org.mockito.Mockito; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -183,7 +183,8 @@ import javax.inject.Inject; @UninstallModules({ AppPredictionModule.class, ImageEditorModule.class, - PackageManagerModule.class + PackageManagerModule.class, + ImageLoaderModule.class, }) public class UnbundledChooserActivityTest { @@ -239,6 +240,11 @@ public class UnbundledChooserActivityTest { @BindValue PackageManager mPackageManager; + private final FakeImageLoader mFakeImageLoader = new FakeImageLoader(); + + @BindValue + final ImageLoader mImageLoader = mFakeImageLoader; + @Before public void setUp() { // TODO: use the other form of `adoptShellPermissionIdentity()` where we explicitly list the @@ -257,6 +263,9 @@ public class UnbundledChooserActivityTest { // values to the dependency graph at activity launch time. This allows replacing // arbitrary bindings per-test case if needed. mPackageManager = mContext.getPackageManager(); + + // TODO: inject image loader in the prod code and remove this override + ChooserActivityOverrideData.getInstance().imageLoader = mFakeImageLoader; } public UnbundledChooserActivityTest(boolean appPredictionAvailable) { @@ -434,14 +443,13 @@ public class UnbundledChooserActivityTest { } @Test - public void visiblePreviewTitleAndThumbnail() throws InterruptedException { + public void visiblePreviewTitleAndThumbnail() { String previewTitle = "My Content Preview Title"; Uri uri = Uri.parse( "android.resource://com.android.frameworks.coretests/" + com.android.intentresolver.tests.R.drawable.test320x240); Intent sendIntent = createSendTextIntentWithPreview(previewTitle, uri); - ChooserActivityOverrideData.getInstance().imageLoader = - createImageLoader(uri, createBitmap()); + mFakeImageLoader.setBitmap(uri, createBitmap()); List resolvedComponentInfos = createResolvedComponentsForTest(2); setupResolverControllers(resolvedComponentInfos); @@ -707,8 +715,7 @@ public class UnbundledChooserActivityTest { public void testFilePlusTextSharing_ExcludeText() { Uri uri = createTestContentProviderUri(null, "image/png"); Intent sendIntent = createSendImageIntent(uri); - ChooserActivityOverrideData.getInstance().imageLoader = - createImageLoader(uri, createBitmap()); + mFakeImageLoader.setBitmap(uri, createBitmap()); sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); List resolvedComponentInfos = Arrays.asList( @@ -749,8 +756,7 @@ public class UnbundledChooserActivityTest { public void testFilePlusTextSharing_RemoveAndAddBackText() { Uri uri = createTestContentProviderUri("application/pdf", "image/png"); Intent sendIntent = createSendImageIntent(uri); - ChooserActivityOverrideData.getInstance().imageLoader = - createImageLoader(uri, createBitmap()); + mFakeImageLoader.setBitmap(uri, createBitmap()); final String text = "https://google.com/search?q=google"; sendIntent.putExtra(Intent.EXTRA_TEXT, text); @@ -797,8 +803,7 @@ public class UnbundledChooserActivityTest { public void testFilePlusTextSharing_TextExclusionDoesNotAffectAlternativeIntent() { Uri uri = createTestContentProviderUri("image/png", null); Intent sendIntent = createSendImageIntent(uri); - ChooserActivityOverrideData.getInstance().imageLoader = - createImageLoader(uri, createBitmap()); + mFakeImageLoader.setBitmap(uri, createBitmap()); sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); Intent alternativeIntent = createSendTextIntent(); @@ -841,8 +846,6 @@ public class UnbundledChooserActivityTest { public void testImagePlusTextSharing_failedThumbnailAndExcludedText_textChanges() { Uri uri = createTestContentProviderUri("image/png", null); Intent sendIntent = createSendImageIntent(uri); - ChooserActivityOverrideData.getInstance().imageLoader = - new TestPreviewImageLoader(Collections.emptyMap()); sendIntent.putExtra(Intent.EXTRA_TEXT, "https://google.com/search?q=google"); List resolvedComponentInfos = Arrays.asList( @@ -937,8 +940,7 @@ public class UnbundledChooserActivityTest { Uri uri = createTestContentProviderUri("image/png", null); Intent sendIntent = createSendImageIntent(uri); - ChooserActivityOverrideData.getInstance().imageLoader = - createImageLoader(uri, createBitmap()); + mFakeImageLoader.setBitmap(uri, createBitmap()); List resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -962,8 +964,7 @@ public class UnbundledChooserActivityTest { uris.add(uri); Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().imageLoader = - createImageLoader(uri, createWideBitmap()); + mFakeImageLoader.setBitmap(uri, createWideBitmap()); List resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -1000,8 +1001,6 @@ public class UnbundledChooserActivityTest { uris.add(uri); Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().imageLoader = - new TestPreviewImageLoader(Collections.emptyMap()); List resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -1019,8 +1018,7 @@ public class UnbundledChooserActivityTest { ArrayList uris = new ArrayList<>(1); uris.add(uri); Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().imageLoader = - createImageLoader(uri, createBitmap()); + mFakeImageLoader.setBitmap(uri, createBitmap()); List resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -1046,8 +1044,7 @@ public class UnbundledChooserActivityTest { } uris.add(imageUri); Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().imageLoader = - createImageLoader(imageUri, createBitmap()); + mFakeImageLoader.setBitmap(imageUri, createBitmap()); List resolvedComponentInfos = createResolvedComponentsForTest(2); setupResolverControllers(resolvedComponentInfos); @@ -1079,8 +1076,7 @@ public class UnbundledChooserActivityTest { uris.add(uri); Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().imageLoader = - createImageLoader(uri, createBitmap()); + mFakeImageLoader.setBitmap(uri, createBitmap()); List resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -1114,12 +1110,9 @@ public class UnbundledChooserActivityTest { uris.add(docUri); Intent sendIntent = createSendUriIntentWithPreview(uris); - Map 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); + mFakeImageLoader.setBitmap(imgOneUri, createWideBitmap(Color.RED)); + mFakeImageLoader.setBitmap(imgTwoUri, createWideBitmap(Color.GREEN)); + mFakeImageLoader.setBitmap(docUri, createWideBitmap(Color.BLUE)); List resolvedComponentInfos = createResolvedComponentsForTest(2); setupResolverControllers(resolvedComponentInfos); @@ -1167,8 +1160,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendUriIntentWithPreview(uris); sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); - ChooserActivityOverrideData.getInstance().imageLoader = - createImageLoader(uri, createBitmap()); + mFakeImageLoader.setBitmap(uri, createBitmap()); List resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -1197,8 +1189,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendUriIntentWithPreview(uris); sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); - ChooserActivityOverrideData.getInstance().imageLoader = - createImageLoader(uri, createBitmap()); + mFakeImageLoader.setBitmap(uri, createBitmap()); List resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -1234,8 +1225,7 @@ public class UnbundledChooserActivityTest { Intent sendIntent = createSendUriIntentWithPreview(uris); sendIntent.putExtra(Intent.EXTRA_TEXT, sharedText); - ChooserActivityOverrideData.getInstance().imageLoader = - createImageLoader(uri, createBitmap()); + mFakeImageLoader.setBitmap(uri, createBitmap()); List resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -1331,8 +1321,7 @@ public class UnbundledChooserActivityTest { uris.add(uri); Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().imageLoader = - createImageLoader(uri, createBitmap()); + mFakeImageLoader.setBitmap(uri, createBitmap()); List resolvedComponentInfos = createResolvedComponentsForTest(2); @@ -2228,8 +2217,7 @@ public class UnbundledChooserActivityTest { uris.add(uri); Intent sendIntent = createSendUriIntentWithPreview(uris); - ChooserActivityOverrideData.getInstance().imageLoader = - createImageLoader(uri, createWideBitmap()); + mFakeImageLoader.setBitmap(uri, createWideBitmap()); mActivityRule.launchActivity(Intent.createChooser(sendIntent, "Scrollable preview test")); waitForIdle(); @@ -3134,8 +3122,4 @@ public class UnbundledChooserActivityTest { }; return shortcutLoaders; } - - private static ImageLoader createImageLoader(Uri uri, Bitmap bitmap) { - return new TestPreviewImageLoader(Collections.singletonMap(uri, bitmap)); - } } diff --git a/tests/shared/src/com/android/intentresolver/FakeImageLoader.kt b/tests/shared/src/com/android/intentresolver/FakeImageLoader.kt new file mode 100644 index 00000000..c57ea78b --- /dev/null +++ b/tests/shared/src/com/android/intentresolver/FakeImageLoader.kt @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver + +import android.graphics.Bitmap +import android.net.Uri +import com.android.intentresolver.contentpreview.ImageLoader +import java.util.function.Consumer +import kotlinx.coroutines.CoroutineScope + +class FakeImageLoader(initialBitmaps: Map = emptyMap()) : ImageLoader { + private val bitmaps = HashMap().apply { putAll(initialBitmaps) } + + override fun loadImage(callerScope: CoroutineScope, uri: Uri, callback: Consumer) { + callback.accept(bitmaps[uri]) + } + + override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = bitmaps[uri] + + override fun prePopulate(uris: List) = Unit + + fun setBitmap(uri: Uri, bitmap: Bitmap) { + bitmaps[uri] = bitmap + } +} diff --git a/tests/shared/src/com/android/intentresolver/TestPreviewImageLoader.kt b/tests/shared/src/com/android/intentresolver/TestPreviewImageLoader.kt deleted file mode 100644 index f0203bb6..00000000 --- a/tests/shared/src/com/android/intentresolver/TestPreviewImageLoader.kt +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver - -import android.graphics.Bitmap -import android.net.Uri -import com.android.intentresolver.contentpreview.ImageLoader -import java.util.function.Consumer -import kotlinx.coroutines.CoroutineScope - -class TestPreviewImageLoader(private val bitmaps: Map) : ImageLoader { - override fun loadImage(callerScope: CoroutineScope, uri: Uri, callback: Consumer) { - callback.accept(bitmaps[uri]) - } - - override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = bitmaps[uri] - - override fun prePopulate(uris: List) = Unit -} diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt index c7c3c516..68b277e7 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt @@ -21,7 +21,7 @@ import android.net.Uri import android.platform.test.flag.junit.CheckFlagsRule import android.platform.test.flag.junit.DeviceFlagsValueProvider import com.android.intentresolver.ContentTypeHint -import com.android.intentresolver.TestPreviewImageLoader +import com.android.intentresolver.FakeImageLoader import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory import com.android.intentresolver.mock import com.android.intentresolver.whenever @@ -43,7 +43,7 @@ class ChooserContentPreviewUiTest { private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher()) private val previewData = mock() private val headlineGenerator = mock() - private val imageLoader = TestPreviewImageLoader(emptyMap()) + private val imageLoader = FakeImageLoader(emptyMap()) private val testMetadataText: CharSequence = "Test metadata text" private val actionFactory = object : ActionFactory { diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt index 89978707..41989bda 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt @@ -20,9 +20,6 @@ import android.content.ContentResolver import android.graphics.Bitmap import android.net.Uri import android.util.Size -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.coroutineScope -import androidx.lifecycle.testing.TestLifecycleOwner import com.android.intentresolver.any import com.android.intentresolver.anyOrNull import com.android.intentresolver.mock @@ -38,25 +35,22 @@ import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart.UNDISPATCHED -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Runnable import kotlinx.coroutines.async +import kotlinx.coroutines.cancel import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.launch -import kotlinx.coroutines.plus import kotlinx.coroutines.sync.Semaphore import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestCoroutineScheduler +import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.UnconfinedTestDispatcher -import kotlinx.coroutines.test.resetMain import kotlinx.coroutines.test.runTest -import kotlinx.coroutines.test.setMain import kotlinx.coroutines.yield -import org.junit.After import org.junit.Assert.assertTrue -import org.junit.Before import org.junit.Test import org.mockito.Mockito.never import org.mockito.Mockito.times @@ -72,281 +66,287 @@ class ImagePreviewImageLoaderTest { mock { whenever(loadThumbnail(any(), any(), anyOrNull())).thenReturn(bitmap) } - private val lifecycleOwner = TestLifecycleOwner() - private val dispatcher = UnconfinedTestDispatcher() - private lateinit var testSubject: ImagePreviewImageLoader - - @Before - fun setup() { - Dispatchers.setMain(dispatcher) - lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_CREATE) - // create test subject after we've updated the lifecycle dispatcher - testSubject = - ImagePreviewImageLoader( - lifecycleOwner.lifecycle.coroutineScope + dispatcher, - imageSize.width, - contentResolver, - cacheSize = 1, - ) - } - - @After - fun cleanup() { - lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) - Dispatchers.resetMain() - } + private val scheduler = TestCoroutineScheduler() + private val dispatcher = UnconfinedTestDispatcher(scheduler) + private val scope = TestScope(dispatcher) + private val testSubject = + ImagePreviewImageLoader( + dispatcher, + imageSize.width, + contentResolver, + cacheSize = 1, + ) @Test - fun prePopulate_cachesImagesUpToTheCacheSize() = runTest { - testSubject.prePopulate(listOf(uriOne, uriTwo)) + fun prePopulate_cachesImagesUpToTheCacheSize() = + scope.runTest { + testSubject.prePopulate(listOf(uriOne, uriTwo)) - verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) - verify(contentResolver, never()).loadThumbnail(uriTwo, imageSize, null) + verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) + verify(contentResolver, never()).loadThumbnail(uriTwo, imageSize, null) - testSubject(uriOne) - verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) - } + testSubject(uriOne) + verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) + } @Test - fun invoke_returnCachedImageWhenCalledTwice() = runTest { - testSubject(uriOne) - testSubject(uriOne) + fun invoke_returnCachedImageWhenCalledTwice() = + scope.runTest { + testSubject(uriOne) + testSubject(uriOne) - verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull()) - } + verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull()) + } @Test - fun invoke_whenInstructed_doesNotCache() = runTest { - testSubject(uriOne, false) - testSubject(uriOne, false) + fun invoke_whenInstructed_doesNotCache() = + scope.runTest { + testSubject(uriOne, false) + testSubject(uriOne, false) - verify(contentResolver, times(2)).loadThumbnail(any(), any(), anyOrNull()) - } + verify(contentResolver, times(2)).loadThumbnail(any(), any(), anyOrNull()) + } @Test - fun invoke_overlappedRequests_Deduplicate() = runTest { - val scheduler = TestCoroutineScheduler() - val dispatcher = StandardTestDispatcher(scheduler) - val testSubject = - ImagePreviewImageLoader( - lifecycleOwner.lifecycle.coroutineScope + dispatcher, - imageSize.width, - contentResolver, - cacheSize = 1, - ) - coroutineScope { - launch(start = UNDISPATCHED) { testSubject(uriOne, false) } - launch(start = UNDISPATCHED) { testSubject(uriOne, false) } - scheduler.advanceUntilIdle() - } + fun invoke_overlappedRequests_Deduplicate() = + scope.runTest { + val dispatcher = StandardTestDispatcher(scheduler) + val testSubject = + ImagePreviewImageLoader( + dispatcher, + imageSize.width, + contentResolver, + cacheSize = 1, + ) + coroutineScope { + launch(start = UNDISPATCHED) { testSubject(uriOne, false) } + launch(start = UNDISPATCHED) { testSubject(uriOne, false) } + scheduler.advanceUntilIdle() + } - verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull()) - } + verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull()) + } @Test - fun invoke_oldRecordsEvictedFromTheCache() = runTest { - testSubject(uriOne) - testSubject(uriTwo) - testSubject(uriTwo) - testSubject(uriOne) - - verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) - verify(contentResolver, times(1)).loadThumbnail(uriTwo, imageSize, null) - } + fun invoke_oldRecordsEvictedFromTheCache() = + scope.runTest { + testSubject(uriOne) + testSubject(uriTwo) + testSubject(uriTwo) + testSubject(uriOne) + + verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) + verify(contentResolver, times(1)).loadThumbnail(uriTwo, imageSize, null) + } @Test - fun invoke_doNotCacheNulls() = runTest { - whenever(contentResolver.loadThumbnail(any(), any(), anyOrNull())).thenReturn(null) - testSubject(uriOne) - testSubject(uriOne) + fun invoke_doNotCacheNulls() = + scope.runTest { + whenever(contentResolver.loadThumbnail(any(), any(), anyOrNull())).thenReturn(null) + testSubject(uriOne) + testSubject(uriOne) - verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) - } + verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) + } @Test(expected = CancellationException::class) - fun invoke_onClosedImageLoaderScope_throwsCancellationException() = runTest { - lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) - testSubject(uriOne) - } + fun invoke_onClosedImageLoaderScope_throwsCancellationException() = + scope.runTest { + val imageLoaderScope = CoroutineScope(coroutineContext) + val testSubject = + ImagePreviewImageLoader( + imageLoaderScope, + imageSize.width, + contentResolver, + cacheSize = 1, + ) + imageLoaderScope.cancel() + testSubject(uriOne) + } @Test(expected = CancellationException::class) - fun invoke_imageLoaderScopeClosedMidflight_throwsCancellationException() = runTest { - val scheduler = TestCoroutineScheduler() - val dispatcher = StandardTestDispatcher(scheduler) - val testSubject = - ImagePreviewImageLoader( - lifecycleOwner.lifecycle.coroutineScope + dispatcher, - imageSize.width, - contentResolver, - cacheSize = 1, - ) - coroutineScope { - val deferred = async(start = UNDISPATCHED) { testSubject(uriOne, false) } - lifecycleOwner.handleLifecycleEvent(Lifecycle.Event.ON_DESTROY) - scheduler.advanceUntilIdle() - deferred.await() + fun invoke_imageLoaderScopeClosedMidflight_throwsCancellationException() = + scope.runTest { + val dispatcher = StandardTestDispatcher(scheduler) + val imageLoaderScope = CoroutineScope(coroutineContext + dispatcher) + val testSubject = + ImagePreviewImageLoader( + imageLoaderScope, + imageSize.width, + contentResolver, + cacheSize = 1, + ) + coroutineScope { + val deferred = async(start = UNDISPATCHED) { testSubject(uriOne, false) } + imageLoaderScope.cancel() + scheduler.advanceUntilIdle() + deferred.await() + } } - } @Test - fun invoke_multipleCallsWithDifferentCacheInstructions_cachingPrevails() = runTest { - val scheduler = TestCoroutineScheduler() - val dispatcher = StandardTestDispatcher(scheduler) - val testSubject = - ImagePreviewImageLoader( - lifecycleOwner.lifecycle.coroutineScope + dispatcher, - imageSize.width, - contentResolver, - cacheSize = 1, - ) - coroutineScope { - launch(start = UNDISPATCHED) { testSubject(uriOne, false) } - launch(start = UNDISPATCHED) { testSubject(uriOne, true) } - scheduler.advanceUntilIdle() - } - testSubject(uriOne, true) + fun invoke_multipleCallsWithDifferentCacheInstructions_cachingPrevails() = + scope.runTest { + val dispatcher = StandardTestDispatcher(scheduler) + val imageLoaderScope = CoroutineScope(coroutineContext + dispatcher) + val testSubject = + ImagePreviewImageLoader( + imageLoaderScope, + imageSize.width, + contentResolver, + cacheSize = 1, + ) + coroutineScope { + launch(start = UNDISPATCHED) { testSubject(uriOne, false) } + launch(start = UNDISPATCHED) { testSubject(uriOne, true) } + scheduler.advanceUntilIdle() + } + testSubject(uriOne, true) - verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) - } + verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) + } @Test - fun invoke_semaphoreGuardsContentResolverCalls() = runTest { - val contentResolver = - mock { - whenever(loadThumbnail(any(), any(), anyOrNull())) - .thenThrow(SecurityException("test")) - } - val acquireCount = AtomicInteger() - val releaseCount = AtomicInteger() - val testSemaphore = - object : Semaphore { - override val availablePermits: Int - get() = error("Unexpected invocation") - - override suspend fun acquire() { - acquireCount.getAndIncrement() + fun invoke_semaphoreGuardsContentResolverCalls() = + scope.runTest { + val contentResolver = + mock { + whenever(loadThumbnail(any(), any(), anyOrNull())) + .thenThrow(SecurityException("test")) } - - override fun tryAcquire(): Boolean { - error("Unexpected invocation") + val acquireCount = AtomicInteger() + val releaseCount = AtomicInteger() + val testSemaphore = + object : Semaphore { + override val availablePermits: Int + get() = error("Unexpected invocation") + + override suspend fun acquire() { + acquireCount.getAndIncrement() + } + + override fun tryAcquire(): Boolean { + error("Unexpected invocation") + } + + override fun release() { + releaseCount.getAndIncrement() + } } - override fun release() { - releaseCount.getAndIncrement() - } - } - - val testSubject = - ImagePreviewImageLoader( - lifecycleOwner.lifecycle.coroutineScope + dispatcher, - imageSize.width, - contentResolver, - cacheSize = 1, - testSemaphore, - ) - testSubject(uriOne, false) - - verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) - assertThat(acquireCount.get()).isEqualTo(1) - assertThat(releaseCount.get()).isEqualTo(1) - } + val testSubject = + ImagePreviewImageLoader( + CoroutineScope(coroutineContext + dispatcher), + imageSize.width, + contentResolver, + cacheSize = 1, + testSemaphore, + ) + testSubject(uriOne, false) + + verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) + assertThat(acquireCount.get()).isEqualTo(1) + assertThat(releaseCount.get()).isEqualTo(1) + } @Test - fun invoke_semaphoreIsReleasedAfterContentResolverFailure() = runTest { - val semaphoreDeferred = CompletableDeferred() - val releaseCount = AtomicInteger() - val testSemaphore = - object : Semaphore { - override val availablePermits: Int - get() = error("Unexpected invocation") - - override suspend fun acquire() { - semaphoreDeferred.await() - } - - override fun tryAcquire(): Boolean { - error("Unexpected invocation") + fun invoke_semaphoreIsReleasedAfterContentResolverFailure() = + scope.runTest { + val semaphoreDeferred = CompletableDeferred() + val releaseCount = AtomicInteger() + val testSemaphore = + object : Semaphore { + override val availablePermits: Int + get() = error("Unexpected invocation") + + override suspend fun acquire() { + semaphoreDeferred.await() + } + + override fun tryAcquire(): Boolean { + error("Unexpected invocation") + } + + override fun release() { + releaseCount.getAndIncrement() + } } - override fun release() { - releaseCount.getAndIncrement() - } - } - - val testSubject = - ImagePreviewImageLoader( - lifecycleOwner.lifecycle.coroutineScope + dispatcher, - imageSize.width, - contentResolver, - cacheSize = 1, - testSemaphore, - ) - launch(start = UNDISPATCHED) { testSubject(uriOne, false) } + val testSubject = + ImagePreviewImageLoader( + CoroutineScope(coroutineContext + dispatcher), + imageSize.width, + contentResolver, + cacheSize = 1, + testSemaphore, + ) + launch(start = UNDISPATCHED) { testSubject(uriOne, false) } - verify(contentResolver, never()).loadThumbnail(any(), any(), anyOrNull()) + verify(contentResolver, never()).loadThumbnail(any(), any(), anyOrNull()) - semaphoreDeferred.complete(Unit) + semaphoreDeferred.complete(Unit) - verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) - assertThat(releaseCount.get()).isEqualTo(1) - } + verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) + assertThat(releaseCount.get()).isEqualTo(1) + } @Test - fun invoke_multipleSimultaneousCalls_limitOnNumberOfSimultaneousOutgoingCallsIsRespected() { - val requestCount = 4 - val thumbnailCallsCdl = CountDownLatch(requestCount) - val pendingThumbnailCalls = ArrayDeque() - val contentResolver = - mock { - whenever(loadThumbnail(any(), any(), anyOrNull())).thenAnswer { - val latch = CountDownLatch(1) - synchronized(pendingThumbnailCalls) { pendingThumbnailCalls.offer(latch) } - thumbnailCallsCdl.countDown() - assertTrue("Timeout waiting thumbnail calls", latch.await(1, SECONDS)) - bitmap + fun invoke_multipleSimultaneousCalls_limitOnNumberOfSimultaneousOutgoingCallsIsRespected() = + scope.runTest { + val requestCount = 4 + val thumbnailCallsCdl = CountDownLatch(requestCount) + val pendingThumbnailCalls = ArrayDeque() + val contentResolver = + mock { + whenever(loadThumbnail(any(), any(), anyOrNull())).thenAnswer { + val latch = CountDownLatch(1) + synchronized(pendingThumbnailCalls) { pendingThumbnailCalls.offer(latch) } + thumbnailCallsCdl.countDown() + assertTrue("Timeout waiting thumbnail calls", latch.await(1, SECONDS)) + bitmap + } } - } - val name = "LoadImage" - val maxSimultaneousRequests = 2 - val threadsStartedCdl = CountDownLatch(requestCount) - val dispatcher = NewThreadDispatcher(name) { threadsStartedCdl.countDown() } - val testSubject = - ImagePreviewImageLoader( - lifecycleOwner.lifecycle.coroutineScope + dispatcher + CoroutineName(name), - imageSize.width, - contentResolver, - cacheSize = 1, - maxSimultaneousRequests, - ) - runTest { - repeat(requestCount) { - launch { testSubject(Uri.parse("content://org.pkg.app/image-$it.png")) } - } - yield() - // wait for all requests to be dispatched - assertThat(threadsStartedCdl.await(5, SECONDS)).isTrue() + val name = "LoadImage" + val maxSimultaneousRequests = 2 + val threadsStartedCdl = CountDownLatch(requestCount) + val dispatcher = NewThreadDispatcher(name) { threadsStartedCdl.countDown() } + val testSubject = + ImagePreviewImageLoader( + CoroutineScope(coroutineContext + dispatcher + CoroutineName(name)), + imageSize.width, + contentResolver, + cacheSize = 1, + maxSimultaneousRequests, + ) + coroutineScope { + repeat(requestCount) { + launch { testSubject(Uri.parse("content://org.pkg.app/image-$it.png")) } + } + yield() + // wait for all requests to be dispatched + assertThat(threadsStartedCdl.await(5, SECONDS)).isTrue() - assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isFalse() - synchronized(pendingThumbnailCalls) { - assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests) - } + assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isFalse() + synchronized(pendingThumbnailCalls) { + assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests) + } - pendingThumbnailCalls.poll()?.countDown() - assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isFalse() - synchronized(pendingThumbnailCalls) { - assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests) - } + pendingThumbnailCalls.poll()?.countDown() + assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isFalse() + synchronized(pendingThumbnailCalls) { + assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests) + } - pendingThumbnailCalls.poll()?.countDown() - assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isTrue() - synchronized(pendingThumbnailCalls) { - assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests) - } - for (cdl in pendingThumbnailCalls) { - cdl.countDown() + pendingThumbnailCalls.poll()?.countDown() + assertThat(thumbnailCallsCdl.await(100, MILLISECONDS)).isTrue() + synchronized(pendingThumbnailCalls) { + assertThat(pendingThumbnailCalls.size).isEqualTo(maxSimultaneousRequests) + } + for (cdl in pendingThumbnailCalls) { + cdl.countDown() + } } } - } } private class NewThreadDispatcher( -- cgit v1.2.3-59-g8ed1b From 347999aa706194ec47034c13a481b2857ffcd015 Mon Sep 17 00:00:00 2001 From: Steve Elliott Date: Wed, 14 Feb 2024 11:25:41 -0500 Subject: Set shareousel background color Bug: 302691505 Flag: ACONFIG android.service.chooser.chooser_payload_toggling DEVELOPMENT Test: N/A - code isn't live Change-Id: Iba603fa63fdecc5b927d80f7d4bd3bae2b65201c --- .../contentpreview/shareousel/ui/composable/ShareouselComposable.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'java') diff --git a/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ShareouselComposable.kt b/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ShareouselComposable.kt index 5cf35297..0b3cdd83 100644 --- a/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ShareouselComposable.kt +++ b/java/src/com/android/intentresolver/contentpreview/shareousel/ui/composable/ShareouselComposable.kt @@ -16,6 +16,7 @@ package com.android.intentresolver.contentpreview.shareousel.ui.composable import androidx.compose.foundation.Image +import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -50,7 +51,7 @@ fun Shareousel(viewModel: ShareouselViewModel) { val centerIdx = viewModel.centerIndex.value val carouselState = rememberLazyListState(initialFirstVisibleItemIndex = centerIdx) val previewKeys by viewModel.previewKeys.collectAsStateWithLifecycle() - Column { + Column(modifier = Modifier.background(MaterialTheme.colorScheme.surfaceContainer)) { // TODO: item needs to be centered, check out ScalingLazyColumn impl or see if // HorizontalPager works for our use-case LazyRow( -- cgit v1.2.3-59-g8ed1b From 805e11b9ea1f167fe790bda9170a6e56cbd86c56 Mon Sep 17 00:00:00 2001 From: Mark Renouf Date: Tue, 5 Mar 2024 16:42:23 -0500 Subject: Create a Resolver-specific NoCrossProfileEmptyStateprovider This is a temporary measure to split upcoming CLs between actively shipping code and future work (ResolverActivity). This allows correcting the rules to work with private profile, while deferring changes to ResolverActivity which will follow soon. After ResolverActivity is updated, this copy can be removed. Bug: 328029692 Test: atest IntentResolver-tests-activity Test: atest IntentResolver-tests-unit Change-Id: I48ba6e3d1c575feec838ac829753158134c9023b --- .../intentresolver/v2/ResolverActivity.java | 4 +- .../ResolverNoCrossProfileEmptyStateProvider.java | 138 +++++++++++++++++++++ 2 files changed, 140 insertions(+), 2 deletions(-) create mode 100644 java/src/com/android/intentresolver/v2/emptystate/ResolverNoCrossProfileEmptyStateProvider.java (limited to 'java') diff --git a/java/src/com/android/intentresolver/v2/ResolverActivity.java b/java/src/com/android/intentresolver/v2/ResolverActivity.java index a9d9f8b1..be3d7ce9 100644 --- a/java/src/com/android/intentresolver/v2/ResolverActivity.java +++ b/java/src/com/android/intentresolver/v2/ResolverActivity.java @@ -102,8 +102,8 @@ import com.android.intentresolver.icons.TargetDataLoader; import com.android.intentresolver.model.ResolverRankerServiceResolverComparator; import com.android.intentresolver.v2.data.repository.DevicePolicyResources; import com.android.intentresolver.v2.emptystate.NoAppsAvailableEmptyStateProvider; -import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider; import com.android.intentresolver.v2.emptystate.NoCrossProfileEmptyStateProvider.DevicePolicyBlockerEmptyState; +import com.android.intentresolver.v2.emptystate.ResolverNoCrossProfileEmptyStateProvider; import com.android.intentresolver.v2.emptystate.ResolverWorkProfilePausedEmptyStateProvider; import com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter; import com.android.intentresolver.v2.profiles.MultiProfilePagerAdapter.ProfileType; @@ -407,7 +407,7 @@ public class ResolverActivity extends Hilt_ResolverActivity implements /* devicePolicyEventCategory= */ ResolverActivity.METRICS_CATEGORY_RESOLVER); - return new NoCrossProfileEmptyStateProvider( + return new ResolverNoCrossProfileEmptyStateProvider( requireAnnotatedUserHandles().personalProfileUserHandle, noWorkToPersonalEmptyState, noPersonalToWorkEmptyState, diff --git a/java/src/com/android/intentresolver/v2/emptystate/ResolverNoCrossProfileEmptyStateProvider.java b/java/src/com/android/intentresolver/v2/emptystate/ResolverNoCrossProfileEmptyStateProvider.java new file mode 100644 index 00000000..f133c31d --- /dev/null +++ b/java/src/com/android/intentresolver/v2/emptystate/ResolverNoCrossProfileEmptyStateProvider.java @@ -0,0 +1,138 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.intentresolver.v2.emptystate; + +import android.app.admin.DevicePolicyEventLogger; +import android.app.admin.DevicePolicyManager; +import android.content.Context; +import android.os.UserHandle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import com.android.intentresolver.ResolverListAdapter; +import com.android.intentresolver.emptystate.CrossProfileIntentsChecker; +import com.android.intentresolver.emptystate.EmptyState; +import com.android.intentresolver.emptystate.EmptyStateProvider; + +/** + * Empty state provider that does not allow cross profile sharing, it will return a blocker + * in case if the profile of the current tab is not the same as the profile of the calling app. + */ +public class ResolverNoCrossProfileEmptyStateProvider implements EmptyStateProvider { + + private final UserHandle mPersonalProfileUserHandle; + private final EmptyState mNoWorkToPersonalEmptyState; + private final EmptyState mNoPersonalToWorkEmptyState; + private final CrossProfileIntentsChecker mCrossProfileIntentsChecker; + private final UserHandle mTabOwnerUserHandleForLaunch; + + public ResolverNoCrossProfileEmptyStateProvider(UserHandle personalUserHandle, + EmptyState noWorkToPersonalEmptyState, + EmptyState noPersonalToWorkEmptyState, + CrossProfileIntentsChecker crossProfileIntentsChecker, + UserHandle tabOwnerUserHandleForLaunch) { + mPersonalProfileUserHandle = personalUserHandle; + mNoWorkToPersonalEmptyState = noWorkToPersonalEmptyState; + mNoPersonalToWorkEmptyState = noPersonalToWorkEmptyState; + mCrossProfileIntentsChecker = crossProfileIntentsChecker; + mTabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch; + } + + @Nullable + @Override + public EmptyState getEmptyState(ResolverListAdapter resolverListAdapter) { + boolean shouldShowBlocker = + !mTabOwnerUserHandleForLaunch.equals(resolverListAdapter.getUserHandle()) + && !mCrossProfileIntentsChecker + .hasCrossProfileIntents(resolverListAdapter.getIntents(), + mTabOwnerUserHandleForLaunch.getIdentifier(), + resolverListAdapter.getUserHandle().getIdentifier()); + + if (!shouldShowBlocker) { + return null; + } + + if (resolverListAdapter.getUserHandle().equals(mPersonalProfileUserHandle)) { + return mNoWorkToPersonalEmptyState; + } else { + return mNoPersonalToWorkEmptyState; + } + } + + + /** + * Empty state that gets strings from the device policy manager and tracks events into + * event logger of the device policy events. + */ + public static class DevicePolicyBlockerEmptyState implements EmptyState { + + @NonNull + private final Context mContext; + private final String mDevicePolicyStringTitleId; + @StringRes + private final int mDefaultTitleResource; + private final String mDevicePolicyStringSubtitleId; + @StringRes + private final int mDefaultSubtitleResource; + private final int mEventId; + @NonNull + private final String mEventCategory; + + public DevicePolicyBlockerEmptyState(@NonNull Context context, + String devicePolicyStringTitleId, @StringRes int defaultTitleResource, + String devicePolicyStringSubtitleId, @StringRes int defaultSubtitleResource, + int devicePolicyEventId, @NonNull String devicePolicyEventCategory) { + mContext = context; + mDevicePolicyStringTitleId = devicePolicyStringTitleId; + mDefaultTitleResource = defaultTitleResource; + mDevicePolicyStringSubtitleId = devicePolicyStringSubtitleId; + mDefaultSubtitleResource = defaultSubtitleResource; + mEventId = devicePolicyEventId; + mEventCategory = devicePolicyEventCategory; + } + + @Nullable + @Override + public String getTitle() { + return mContext.getSystemService(DevicePolicyManager.class).getResources().getString( + mDevicePolicyStringTitleId, + () -> mContext.getString(mDefaultTitleResource)); + } + + @Nullable + @Override + public String getSubtitle() { + return mContext.getSystemService(DevicePolicyManager.class).getResources().getString( + mDevicePolicyStringSubtitleId, + () -> mContext.getString(mDefaultSubtitleResource)); + } + + @Override + public void onEmptyStateShown() { + DevicePolicyEventLogger.createEvent(mEventId) + .setStrings(mEventCategory) + .write(); + } + + @Override + public boolean shouldSkipDataRebuild() { + return true; + } + } +} -- cgit v1.2.3-59-g8ed1b From da8c0a0cdef1dd191204a0119fab176c60d673ec Mon Sep 17 00:00:00 2001 From: Andrey Epin Date: Mon, 4 Mar 2024 13:15:24 -0800 Subject: ChooserActivity: return missing ResolverActivity onDestroy logic A destroy method is added to ChooserMultiProfilePagerAdapter that calls destroy on every page's list adapter; ChooserActivity uses this method instead of the legacy code that called destroy only on the active page's list adapter. A weak reference is added between Binder callbacks to limit retained lifetime of remotely referenced objects. Fix: 328084479 Test: manual functionality testing Test: launch Chooser multiple times, collect a process heap dump forcing garbage collection, check the heap dump and verify that no ChooserActivity remains in memory. Change-Id: Ie6e51ed447c6a609a890edaf97aa19f8ad824238 --- .../ResolverRankerServiceResolverComparator.java | 43 +++++++++++++++------- .../android/intentresolver/v2/ChooserActivity.java | 6 +++ .../profiles/ChooserMultiProfilePagerAdapter.java | 10 +++++ 3 files changed, 45 insertions(+), 14 deletions(-) (limited to 'java') diff --git a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java index f3804154..963091b5 100644 --- a/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java +++ b/java/src/com/android/intentresolver/model/ResolverRankerServiceResolverComparator.java @@ -28,6 +28,7 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ResolveInfo; import android.metrics.LogMaker; +import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.RemoteException; @@ -48,6 +49,7 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.google.android.collect.Lists; +import java.lang.ref.WeakReference; import java.text.Collator; import java.util.ArrayList; import java.util.Comparator; @@ -392,20 +394,7 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom } public final IResolverRankerResult resolverRankerResult = - new IResolverRankerResult.Stub() { - @Override - public void sendResult(List targets) throws RemoteException { - if (DEBUG) { - Log.d(TAG, "Sending Result back to Resolver: " + targets); - } - synchronized (mLock) { - final Message msg = Message.obtain(); - msg.what = RANKER_SERVICE_RESULT; - msg.obj = targets; - mHandler.sendMessage(msg); - } - } - }; + new ResolverRankerResultCallback(mLock, mHandler); @Override public void onServiceConnected(ComponentName name, IBinder service) { @@ -437,6 +426,32 @@ public class ResolverRankerServiceResolverComparator extends AbstractResolverCom } } + private static class ResolverRankerResultCallback extends IResolverRankerResult.Stub { + private final Object mLock; + private final WeakReference mHandlerRef; + + private ResolverRankerResultCallback(Object lock, Handler handler) { + mLock = lock; + mHandlerRef = new WeakReference<>(handler); + } + + @Override + public void sendResult(List targets) throws RemoteException { + if (DEBUG) { + Log.d(TAG, "Sending Result back to Resolver: " + targets); + } + synchronized (mLock) { + final Message msg = Message.obtain(); + msg.what = RANKER_SERVICE_RESULT; + msg.obj = targets; + Handler handler = mHandlerRef.get(); + if (handler != null) { + handler.sendMessage(msg); + } + } + } + } + @Override void beforeCompute() { super.beforeCompute(); diff --git a/java/src/com/android/intentresolver/v2/ChooserActivity.java b/java/src/com/android/intentresolver/v2/ChooserActivity.java index 765d7c2d..4ffe942b 100644 --- a/java/src/com/android/intentresolver/v2/ChooserActivity.java +++ b/java/src/com/android/intentresolver/v2/ChooserActivity.java @@ -485,6 +485,12 @@ public class ChooserActivity extends Hilt_ChooserActivity implements @Override protected final void onDestroy() { super.onDestroy(); + if (!isChangingConfigurations() && mPickOptionRequest != null) { + mPickOptionRequest.cancel(); + } + if (mChooserMultiProfilePagerAdapter != null) { + mChooserMultiProfilePagerAdapter.destroy(); + } if (isFinishing()) { mLatencyTracker.onActionCancel(ACTION_LOAD_SHARE_SHEET); diff --git a/java/src/com/android/intentresolver/v2/profiles/ChooserMultiProfilePagerAdapter.java b/java/src/com/android/intentresolver/v2/profiles/ChooserMultiProfilePagerAdapter.java index 0ee9d141..c078c43f 100644 --- a/java/src/com/android/intentresolver/v2/profiles/ChooserMultiProfilePagerAdapter.java +++ b/java/src/com/android/intentresolver/v2/profiles/ChooserMultiProfilePagerAdapter.java @@ -151,6 +151,16 @@ public class ChooserMultiProfilePagerAdapter extends MultiProfilePagerAdapter< } } + /** Cleanup system resources */ + public void destroy() { + for (int i = 0, count = getItemCount(); i < count; i++) { + ChooserGridAdapter adapter = getPageAdapterForIndex(i); + if (adapter != null) { + adapter.getListAdapter().onDestroy(); + } + } + } + private static class BottomPaddingOverrideSupplier implements Supplier> { private final Context mContext; private int mBottomOffset; -- cgit v1.2.3-59-g8ed1b From 721a5cc970c4358ab0eccd9aeb0531fb0d2c2045 Mon Sep 17 00:00:00 2001 From: Tiger Date: Wed, 6 Mar 2024 23:01:50 +0800 Subject: Change the cutout mode of ChooserWrapperActivity This CL changes the mode from shortEdge to always. Bug: 309578419 Bug: 327607282 Test: atest UnbundledChooserActivityTest Change-Id: I5df32f9b067e5b3f6574e444f33921deb4cf3713 --- java/res/values/styles.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'java') diff --git a/java/res/values/styles.xml b/java/res/values/styles.xml index 0ccab4c0..143009d0 100644 --- a/java/res/values/styles.xml +++ b/java/res/values/styles.xml @@ -45,7 +45,7 @@