diff options
Diffstat (limited to 'tests')
34 files changed, 1432 insertions, 419 deletions
diff --git a/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java b/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java index 507ce3d7..311201cf 100644 --- a/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java +++ b/tests/activity/src/com/android/intentresolver/ChooserActivityOverrideData.java @@ -26,7 +26,6 @@ import android.database.Cursor; import android.os.UserHandle; import com.android.intentresolver.chooser.TargetInfo; -import com.android.intentresolver.contentpreview.ImageLoader; import com.android.intentresolver.emptystate.CrossProfileIntentsChecker; import com.android.intentresolver.shortcuts.ShortcutLoader; @@ -58,7 +57,6 @@ public class ChooserActivityOverrideData { public Boolean isVoiceInteraction; public Cursor resolverCursor; public boolean resolverForceException; - public ImageLoader imageLoader; public Resources resources; public boolean hasCrossProfileIntents; public boolean isQuietModeEnabled; @@ -68,7 +66,6 @@ public class ChooserActivityOverrideData { public void reset() { onSafelyStartInternalCallback = null; isVoiceInteraction = null; - imageLoader = null; resolverCursor = null; resolverForceException = false; resolverListController = mock(ChooserListController.class); diff --git a/tests/activity/src/com/android/intentresolver/ChooserActivityTest.java b/tests/activity/src/com/android/intentresolver/ChooserActivityTest.java index a8b8b2e9..e103e57b 100644 --- a/tests/activity/src/com/android/intentresolver/ChooserActivityTest.java +++ b/tests/activity/src/com/android/intentresolver/ChooserActivityTest.java @@ -124,6 +124,7 @@ import com.android.intentresolver.contentpreview.ImageLoaderModule; import com.android.intentresolver.contentpreview.PreviewCacheSize; import com.android.intentresolver.contentpreview.PreviewMaxConcurrency; import com.android.intentresolver.contentpreview.ThumbnailLoader; +import com.android.intentresolver.contentpreview.ThumbnailSize; import com.android.intentresolver.data.repository.FakeUserRepository; import com.android.intentresolver.data.repository.UserRepository; import com.android.intentresolver.data.repository.UserRepositoryModule; @@ -285,6 +286,10 @@ public class ChooserActivityTest { int mPreviewMaxConcurrency = 4; @BindValue + @ThumbnailSize + int mPreviewThumbnailSize = 500; + + @BindValue ThumbnailLoader mThumbnailLoader = new FakeThumbnailLoader(); @Before @@ -305,9 +310,6 @@ public class ChooserActivityTest { // 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 ChooserActivityTest(boolean appPredictionAvailable) { diff --git a/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java b/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java index 4b71aa29..6ff7af3f 100644 --- a/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java +++ b/tests/activity/src/com/android/intentresolver/ChooserWrapperActivity.java @@ -30,8 +30,6 @@ import android.net.Uri; import android.os.Bundle; import android.os.UserHandle; -import androidx.lifecycle.ViewModelProvider; - import com.android.intentresolver.chooser.DisplayResolveInfo; import com.android.intentresolver.chooser.TargetInfo; import com.android.intentresolver.emptystate.CrossProfileIntentsChecker; @@ -67,7 +65,7 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW initialIntents, rList, filterLastUsed, - createListController(userHandle), + resolverListController, userHandle, targetIntent, referrerFillInIntent, @@ -77,8 +75,7 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW maxTargetsPerRow, userHandle, mTargetDataLoader, - null, - mFeatureFlags); + null); } @Override @@ -152,13 +149,6 @@ public class ChooserWrapperActivity extends ChooserActivity implements IChooserW } @Override - protected ViewModelProvider.Factory createPreviewViewModelFactory() { - return TestContentPreviewViewModel.Companion.wrap( - super.createPreviewViewModelFactory(), - sOverrides.imageLoader); - } - - @Override public Cursor queryResolver(ContentResolver resolver, Uri uri) { if (sOverrides.resolverCursor != null) { return sOverrides.resolverCursor; diff --git a/tests/shared/src/com/android/intentresolver/FakeImageLoader.kt b/tests/shared/src/com/android/intentresolver/FakeImageLoader.kt index c57ea78b..76eb5e0d 100644 --- a/tests/shared/src/com/android/intentresolver/FakeImageLoader.kt +++ b/tests/shared/src/com/android/intentresolver/FakeImageLoader.kt @@ -18,6 +18,7 @@ package com.android.intentresolver import android.graphics.Bitmap import android.net.Uri +import android.util.Size import com.android.intentresolver.contentpreview.ImageLoader import java.util.function.Consumer import kotlinx.coroutines.CoroutineScope @@ -25,13 +26,18 @@ import kotlinx.coroutines.CoroutineScope class FakeImageLoader(initialBitmaps: Map<Uri, Bitmap> = emptyMap()) : ImageLoader { private val bitmaps = HashMap<Uri, Bitmap>().apply { putAll(initialBitmaps) } - override fun loadImage(callerScope: CoroutineScope, uri: Uri, callback: Consumer<Bitmap?>) { + override fun loadImage( + callerScope: CoroutineScope, + uri: Uri, + size: Size, + callback: Consumer<Bitmap?>, + ) { callback.accept(bitmaps[uri]) } - override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = bitmaps[uri] + override suspend fun invoke(uri: Uri, size: Size, caching: Boolean): Bitmap? = bitmaps[uri] - override fun prePopulate(uris: List<Uri>) = Unit + override fun prePopulate(uriSizePairs: List<Pair<Uri, Size>>) = Unit fun setBitmap(uri: Uri, bitmap: Bitmap) { bitmaps[uri] = bitmap diff --git a/tests/shared/src/com/android/intentresolver/TestContentPreviewViewModel.kt b/tests/shared/src/com/android/intentresolver/TestContentPreviewViewModel.kt deleted file mode 100644 index 8f246424..00000000 --- a/tests/shared/src/com/android/intentresolver/TestContentPreviewViewModel.kt +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright (C) 2023 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.android.intentresolver - -import android.content.Intent -import android.net.Uri -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.viewmodel.CreationExtras -import com.android.intentresolver.contentpreview.BasePreviewViewModel -import com.android.intentresolver.contentpreview.ImageLoader - -/** A test content preview model that supports image loader override. */ -class TestContentPreviewViewModel( - private val viewModel: BasePreviewViewModel, - override val imageLoader: ImageLoader, -) : BasePreviewViewModel() { - - override val previewDataProvider - get() = viewModel.previewDataProvider - - override fun init( - targetIntent: Intent, - additionalContentUri: Uri?, - isPayloadTogglingEnabled: Boolean, - ) { - viewModel.init(targetIntent, additionalContentUri, isPayloadTogglingEnabled) - } - - companion object { - fun wrap( - factory: ViewModelProvider.Factory, - imageLoader: ImageLoader?, - ): ViewModelProvider.Factory = - object : ViewModelProvider.Factory { - @Suppress("UNCHECKED_CAST") - override fun <T : ViewModel> create( - modelClass: Class<T>, - extras: CreationExtras - ): T { - val wrapped = factory.create(modelClass, extras) as BasePreviewViewModel - return TestContentPreviewViewModel( - wrapped, - imageLoader ?: wrapped.imageLoader, - ) - as T - } - } - } -} diff --git a/tests/shared/src/com/android/intentresolver/contentpreview/FakeThumbnailLoader.kt b/tests/shared/src/com/android/intentresolver/contentpreview/FakeThumbnailLoader.kt index d3fdf17d..33969eb7 100644 --- a/tests/shared/src/com/android/intentresolver/contentpreview/FakeThumbnailLoader.kt +++ b/tests/shared/src/com/android/intentresolver/contentpreview/FakeThumbnailLoader.kt @@ -18,18 +18,23 @@ package com.android.intentresolver.contentpreview import android.graphics.Bitmap import android.net.Uri +import android.util.Size /** Fake implementation of [ThumbnailLoader] for use in testing. */ -class FakeThumbnailLoader : ThumbnailLoader { +class FakeThumbnailLoader(private val defaultSize: Size = Size(100, 100)) : ThumbnailLoader { - val fakeInvoke = mutableMapOf<Uri, suspend () -> Bitmap?>() + val fakeInvoke = mutableMapOf<Uri, suspend (Size) -> Bitmap?>() val invokeCalls = mutableListOf<Uri>() var unfinishedInvokeCount = 0 - override suspend fun invoke(uri: Uri): Bitmap? { + override suspend fun loadThumbnail(uri: Uri): Bitmap? = getBitmap(uri, defaultSize) + + override suspend fun loadThumbnail(uri: Uri, size: Size): Bitmap? = getBitmap(uri, size) + + private suspend fun getBitmap(uri: Uri, size: Size): Bitmap? { invokeCalls.add(uri) unfinishedInvokeCount++ - val result = fakeInvoke[uri]?.invoke() + val result = fakeInvoke[uri]?.invoke(size) unfinishedInvokeCount-- return result } diff --git a/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/PayloadToggleInteractorKosmos.kt b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/PayloadToggleInteractorKosmos.kt index cb88cd9e..7cca414f 100644 --- a/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/PayloadToggleInteractorKosmos.kt +++ b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/PayloadToggleInteractorKosmos.kt @@ -91,6 +91,7 @@ val Kosmos.selectablePreviewsInteractor SelectablePreviewsInteractor( previewsRepo = cursorPreviewsRepository, selectionInteractor = selectionInteractor, + eventLog = eventLog, ) val Kosmos.selectionInteractor diff --git a/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallbackKosmos.kt b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallbackKosmos.kt index 548b1f37..b26b562e 100644 --- a/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallbackKosmos.kt +++ b/tests/shared/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallbackKosmos.kt @@ -19,7 +19,6 @@ package com.android.intentresolver.contentpreview.payloadtoggle.domain.update import com.android.intentresolver.contentInterface import com.android.intentresolver.inject.additionalContentUri import com.android.intentresolver.inject.chooserIntent -import com.android.intentresolver.inject.chooserServiceFlags import com.android.systemui.kosmos.Kosmos val Kosmos.selectionChangeCallbackImpl by @@ -28,7 +27,6 @@ val Kosmos.selectionChangeCallbackImpl by additionalContentUri, chooserIntent, contentInterface, - chooserServiceFlags, ) } var Kosmos.selectionChangeCallback: SelectionChangeCallback by diff --git a/tests/shared/src/com/android/intentresolver/logging/FakeEventLog.kt b/tests/shared/src/com/android/intentresolver/logging/FakeEventLog.kt index 9ed47db6..c2d13f1e 100644 --- a/tests/shared/src/com/android/intentresolver/logging/FakeEventLog.kt +++ b/tests/shared/src/com/android/intentresolver/logging/FakeEventLog.kt @@ -164,14 +164,22 @@ class FakeEventLog @Inject constructor(private val instanceId: InstanceId) : Eve log { "logSharesheetEmptyDirectShareRow()" } } + override fun logPayloadSelectionChanged() { + log { "logPayloadSelectionChanged" } + } + data class ActionSelected(val targetType: Int) + data class CustomActionSelected(val positionPicked: Int) + data class ActionShareWithPreview(val previewType: Int) + data class ChooserActivityShown( val isWorkProfile: Boolean, val targetMimeType: String?, val systemCost: Long ) + data class ShareStarted( val packageName: String?, val mimeType: String?, @@ -183,6 +191,7 @@ class FakeEventLog @Inject constructor(private val instanceId: InstanceId) : Eve val customActionCount: Int, val modifyShareActionProvided: Boolean ) + data class ShareTargetSelected( val targetType: Int, val packageName: String?, diff --git a/tests/unit/Android.bp b/tests/unit/Android.bp index 857d44aa..a3b30a3a 100644 --- a/tests/unit/Android.bp +++ b/tests/unit/Android.bp @@ -33,6 +33,7 @@ android_test { "android.test.mock.stubs.system", "framework", "framework-res", + "flag-junit", ], resource_dirs: ["res"], diff --git a/tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt b/tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt index c8e17de4..8dfbdbdd 100644 --- a/tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt +++ b/tests/unit/src/com/android/intentresolver/ChooserActionFactoryTest.kt @@ -69,8 +69,6 @@ class ChooserActionFactoryTest { latestReturn = resultCode } } - private val featureFlags = - FakeFeatureFlagsImpl().apply { setFlag(Flags.FLAG_FIX_PARTIAL_IMAGE_EDIT_TRANSITION, true) } @Before fun setup() { @@ -121,7 +119,6 @@ class ChooserActionFactoryTest { /* shareResultSender = */ null, /* finishCallback = */ {}, /* clipboardManager = */ mock(), - /* featureFlags = */ featureFlags, ) assertThat(testSubject.copyButtonRunnable).isNull() } @@ -143,7 +140,6 @@ class ChooserActionFactoryTest { /* shareResultSender = */ null, /* finishCallback = */ {}, /* clipboardManager = */ mock(), - /* featureFlags = */ featureFlags, ) assertThat(testSubject.copyButtonRunnable).isNull() } @@ -166,7 +162,6 @@ class ChooserActionFactoryTest { /* shareResultSender = */ resultSender, /* finishCallback = */ {}, /* clipboardManager = */ mock(), - /* featureFlags = */ featureFlags, ) assertThat(testSubject.copyButtonRunnable).isNotNull() @@ -199,7 +194,6 @@ class ChooserActionFactoryTest { /* shareResultSender = */ null, /* finishCallback = */ resultConsumer, /* clipboardManager = */ mock(), - /* featureFlags = */ featureFlags, ) } } diff --git a/tests/unit/src/com/android/intentresolver/ChooserListAdapterDataTest.kt b/tests/unit/src/com/android/intentresolver/ChooserListAdapterDataTest.kt index df0c5e5e..bbef6c0c 100644 --- a/tests/unit/src/com/android/intentresolver/ChooserListAdapterDataTest.kt +++ b/tests/unit/src/com/android/intentresolver/ChooserListAdapterDataTest.kt @@ -66,8 +66,6 @@ class ChooserListAdapterDataTest { private val immediateExecutor = TestExecutor(immediate = true) private val referrerFillInIntent = Intent().putExtra(Intent.EXTRA_REFERRER, "org.referrer.package") - private val featureFlags = - FakeFeatureFlagsImpl().apply { setFlag(Flags.FLAG_BESPOKE_LABEL_VIEW, false) } @Test fun test_twoTargetsWithNonOverlappingInitialIntent_threeTargetsInResolverAdapter() { @@ -86,7 +84,7 @@ class ChooserListAdapterDataTest { userHandle ) ) - .thenReturn(resolvedTargets) + .thenReturn(ArrayList(resolvedTargets)) val initialActivityInfo = createActivityInfo(3) val initialIntents = arrayOf( @@ -119,7 +117,6 @@ class ChooserListAdapterDataTest { null, backgroundExecutor, immediateExecutor, - featureFlags, ) val doPostProcessing = true @@ -152,7 +149,7 @@ class ChooserListAdapterDataTest { userHandle ) ) - .thenReturn(resolvedTargets) + .thenReturn(ArrayList(resolvedTargets)) val activityInfo = resolvedTargets[1].getResolveInfoAt(0).activityInfo val initialIntents = arrayOf(Intent(Intent.ACTION_SEND).apply { component = activityInfo.componentName }) @@ -183,7 +180,6 @@ class ChooserListAdapterDataTest { null, backgroundExecutor, immediateExecutor, - featureFlags, ) val doPostProcessing = true diff --git a/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt b/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt index bad3b18c..cdc84ba8 100644 --- a/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt +++ b/tests/unit/src/com/android/intentresolver/ChooserListAdapterTest.kt @@ -61,7 +61,6 @@ class ChooserListAdapterTest { private val mEventLog = mock<EventLogImpl>() private val mTargetDataLoader = mock<TargetDataLoader>() private val mPackageChangeCallback = mock<ChooserListAdapter.PackageChangeCallback>() - private val featureFlags = FeatureFlagsImpl() private val testSubject by lazy { ChooserListAdapter( @@ -81,7 +80,6 @@ class ChooserListAdapterTest { null, mTargetDataLoader, mPackageChangeCallback, - featureFlags, ) } @@ -222,15 +220,10 @@ class ChooserListAdapterTest { private fun createView(): View { val view = FrameLayout(context) - if (featureFlags.bespokeLabelView()) { - BadgeTextView(context) - } else { - TextView(context) - } - .apply { - id = R.id.text1 - view.addView(this) - } + BadgeTextView(context).apply { + id = R.id.text1 + view.addView(this) + } TextView(context).apply { id = R.id.text2 view.addView(this) diff --git a/tests/unit/src/com/android/intentresolver/ResolverListAdapterTest.kt b/tests/unit/src/com/android/intentresolver/ResolverListAdapterTest.kt index d8cb7adc..23ea33b2 100644 --- a/tests/unit/src/com/android/intentresolver/ResolverListAdapterTest.kt +++ b/tests/unit/src/com/android/intentresolver/ResolverListAdapterTest.kt @@ -79,7 +79,7 @@ class ResolverListAdapterTest { payloadIntents, userHandle ) - } doReturn resolvedTargets + } doReturn ArrayList(resolvedTargets) } val testSubject = ResolverListAdapter( @@ -128,7 +128,7 @@ class ResolverListAdapterTest { payloadIntents, userHandle ) - } doReturn resolvedTargets + } doReturn ArrayList(resolvedTargets) on { lastChosen } doReturn resolvedTargets[0].getResolveInfoAt(0) } val testSubject = @@ -177,7 +177,7 @@ class ResolverListAdapterTest { payloadIntents, userHandle ) - } doReturn resolvedTargets + } doReturn ArrayList(resolvedTargets) on { lastChosen } doReturn createResolveInfo(PKG_NAME_TWO, CLASS_NAME, userHandle) } val testSubject = @@ -228,7 +228,7 @@ class ResolverListAdapterTest { payloadIntents, userHandle ) - } doReturn resolvedTargets + } doReturn ArrayList(resolvedTargets) on { lastChosen } doReturn resolvedTargets[0].getResolveInfoAt(0) } val testSubject = @@ -302,7 +302,7 @@ class ResolverListAdapterTest { payloadIntents, userHandle ) - } doReturn resolvedTargets + } doReturn ArrayList(resolvedTargets) if (hasLastChosen) { on { lastChosen } doReturn resolvedTargets[0].getResolveInfoAt(0) } @@ -379,7 +379,7 @@ class ResolverListAdapterTest { payloadIntents, userHandle ) - } doReturn resolvedTargets + } doReturn ArrayList(resolvedTargets) on { lastChosen } doReturn createResolveInfo(PKG_NAME, CLASS_NAME + "2", userHandle) } val testSubject = @@ -434,7 +434,6 @@ class ResolverListAdapterTest { ComponentName(PKG_NAME_TWO, CLASS_NAME), ) resolvedTargets[1].getResolveInfoAt(0).targetUserId = 10 - // whenever(resolvedTargets[1].getResolveInfoAt(0).loadLabel(any())).thenReturn("Label") val resolverListController = mock<ResolverListController> { on { filterIneligibleActivities(any(), any()) } doReturn null @@ -447,7 +446,7 @@ class ResolverListAdapterTest { payloadIntents, userHandle ) - } doReturn resolvedTargets + } doReturn ArrayList(resolvedTargets) on { lastChosen } doReturn resolvedTargets[0].getResolveInfoAt(0) } val testSubject = @@ -477,7 +476,9 @@ class ResolverListAdapterTest { assertThat(testSubject.hasFilteredItem()).isFalse() assertThat(testSubject.filteredItem).isNull() assertThat(testSubject.filteredPosition).isLessThan(0) - assertThat(testSubject.unfilteredResolveList).containsExactlyElementsIn(resolvedTargets) + // The following must be an old bug i.e. unfilteredResolveList should be equal to + // resolvedTargets. Also see comments in the code. + assertThat(testSubject.unfilteredResolveList).containsExactly(resolvedTargets[0]) assertThat(testSubject.isTabLoaded).isTrue() assertThat(backgroundExecutor.pendingCommandCount).isEqualTo(0) } @@ -502,7 +503,7 @@ class ResolverListAdapterTest { payloadIntents, userHandle ) - } doReturn resolvedTargets + } doReturn ArrayList(resolvedTargets) on { sort(any()) } doAnswer { val components = it.arguments[0] as MutableList<ResolvedComponentInfo> @@ -532,11 +533,10 @@ class ResolverListAdapterTest { backgroundExecutor.runUntilIdle() - // we don't reset placeholder count (legacy logic, likely an oversight?) assertThat(testSubject.count).isEqualTo(resolvedTargets.size) - assertThat(resolvedTargets[0].getResolveInfoAt(0).activityInfo.packageName) + assertThat(testSubject.getDisplayResolveInfo(0).resolveInfo.activityInfo.packageName) .isEqualTo(PKG_NAME_TWO) - assertThat(resolvedTargets[1].getResolveInfoAt(0).activityInfo.packageName) + assertThat(testSubject.getDisplayResolveInfo(1).resolveInfo.activityInfo.packageName) .isEqualTo(PKG_NAME) } @@ -560,7 +560,7 @@ class ResolverListAdapterTest { payloadIntents, userHandle ) - } doReturn resolvedTargets + } doReturn ArrayList(resolvedTargets) on { filterIneligibleActivities(any(), any()) } doAnswer { val components = it.arguments[0] as MutableList<ResolvedComponentInfo> @@ -646,7 +646,6 @@ class ResolverListAdapterTest { backgroundExecutor.runUntilIdle() - // we don't reset placeholder count (legacy logic, likely an oversight?) assertThat(testSubject.count).isEqualTo(2) assertThat(testSubject.unfilteredResolveList).hasSize(2) } @@ -670,7 +669,7 @@ class ResolverListAdapterTest { payloadIntents, userHandle ) - } doReturn resolvedTargets + } doReturn ArrayList(resolvedTargets) on { filterLowPriority(any(), any()) } doAnswer { val components = it.arguments[0] as MutableList<ResolvedComponentInfo> @@ -730,7 +729,7 @@ class ResolverListAdapterTest { payloadIntents, userHandle ) - } doReturn resolvedTargets + } doReturn ArrayList(resolvedTargets) } whenever(packageManager.getActivityInfo(eq(initialComponent), eq(0))) .thenReturn(createActivityInfo(initialComponent)) @@ -801,7 +800,7 @@ class ResolverListAdapterTest { payloadIntents, userHandle ) - } doReturn resolvedTargets + } doReturn ArrayList(resolvedTargets) } val initialComponent = ComponentName(PKG_NAME_TWO, CLASS_NAME) val initialIntents = @@ -896,7 +895,7 @@ class ResolverListAdapterTest { on { filterIneligibleActivities(any(), any()) } doReturn null on { filterLowPriority(any(), any()) } doReturn null on { getResolversForIntentAsUser(any(), any(), any(), any(), any()) } doReturn - resolvedTargets + ArrayList(resolvedTargets) } val communicator = mock<ResolverListCommunicator> { @@ -944,7 +943,7 @@ class ResolverListAdapterTest { on { filterIneligibleActivities(any(), any()) } doReturn null on { filterLowPriority(any(), any()) } doReturn null on { getResolversForIntentAsUser(any(), any(), any(), any(), any()) } doReturn - resolvedTargets + ArrayList(resolvedTargets) } val communicator = mock<ResolverListCommunicator> { @@ -999,7 +998,7 @@ class ResolverListAdapterTest { on { filterIneligibleActivities(any(), any()) } doReturn null on { filterLowPriority(any(), any()) } doReturn null on { getResolversForIntentAsUser(any(), any(), any(), any(), any()) } doReturn - resolvedTargets + ArrayList(resolvedTargets) } val communicator = mock<ResolverListCommunicator> { diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoaderTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoaderTest.kt index 331f9f64..d5a569aa 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoaderTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/CachingImagePreviewImageLoaderTest.kt @@ -18,6 +18,7 @@ package com.android.intentresolver.contentpreview import android.graphics.Bitmap import android.net.Uri +import android.util.Size import com.google.common.truth.Truth.assertThat import kotlin.math.ceil import kotlin.math.roundToInt @@ -43,6 +44,7 @@ class CachingImagePreviewImageLoaderTest { testJobTime * ceil((testCacheSize).toFloat() / testMaxConcurrency.toFloat()).roundToInt() private val testUris = List(5) { Uri.fromParts("TestScheme$it", "TestSsp$it", "TestFragment$it") } + private val previewSize = Size(500, 500) private val testTimeToLoadAllUris = testJobTime * ceil((testUris.size).toFloat() / testMaxConcurrency.toFloat()).roundToInt() private val testBitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ALPHA_8) @@ -72,7 +74,7 @@ class CachingImagePreviewImageLoaderTest { var result: Bitmap? = null // Act - imageLoader.loadImage(testScope, testUris[0]) { result = it } + imageLoader.loadImage(testScope, testUris[0], previewSize) { result = it } advanceTimeBy(testJobTime) runCurrent() @@ -85,14 +87,14 @@ class CachingImagePreviewImageLoaderTest { fun loadImage_cached_usesCachedValue() = testScope.runTest { // Arrange - imageLoader.loadImage(testScope, testUris[0]) {} + imageLoader.loadImage(testScope, testUris[0], previewSize) {} advanceTimeBy(testJobTime) runCurrent() fakeThumbnailLoader.invokeCalls.clear() var result: Bitmap? = null // Act - imageLoader.loadImage(testScope, testUris[0]) { result = it } + imageLoader.loadImage(testScope, testUris[0], previewSize) { result = it } advanceTimeBy(testJobTime) runCurrent() @@ -112,7 +114,7 @@ class CachingImagePreviewImageLoaderTest { var result: Bitmap? = testBitmap // Act - imageLoader.loadImage(testScope, testUris[0]) { result = it } + imageLoader.loadImage(testScope, testUris[0], previewSize) { result = it } advanceTimeBy(testJobTime) runCurrent() @@ -130,7 +132,7 @@ class CachingImagePreviewImageLoaderTest { // Act testUris.take(testMaxConcurrency + 1).forEach { uri -> - imageLoader.loadImage(testScope, uri) { results.add(it) } + imageLoader.loadImage(testScope, uri, previewSize) { results.add(it) } } // Assert @@ -153,10 +155,10 @@ class CachingImagePreviewImageLoaderTest { assertThat(testUris.size).isGreaterThan(testCacheSize) // Act - imageLoader.loadImage(testScope, testUris[0]) { results[0] = it } + imageLoader.loadImage(testScope, testUris[0], previewSize) { results[0] = it } runCurrent() testUris.indices.drop(1).take(testCacheSize).forEach { i -> - imageLoader.loadImage(testScope, testUris[i]) { results[i] = it } + imageLoader.loadImage(testScope, testUris[i], previewSize) { results[i] = it } } advanceTimeBy(testTimeToFillCache) runCurrent() @@ -179,7 +181,7 @@ class CachingImagePreviewImageLoaderTest { assertThat(fullCacheUris).hasSize(testCacheSize) // Act - imageLoader.prePopulate(fullCacheUris) + imageLoader.prePopulate(fullCacheUris.map { it to previewSize }) advanceTimeBy(testTimeToFillCache) runCurrent() @@ -188,7 +190,7 @@ class CachingImagePreviewImageLoaderTest { // Act fakeThumbnailLoader.invokeCalls.clear() - imageLoader.prePopulate(fullCacheUris) + imageLoader.prePopulate(fullCacheUris.map { it to previewSize }) advanceTimeBy(testTimeToFillCache) runCurrent() @@ -203,7 +205,7 @@ class CachingImagePreviewImageLoaderTest { assertThat(testUris.size).isGreaterThan(testCacheSize) // Act - imageLoader.prePopulate(testUris) + imageLoader.prePopulate(testUris.map { it to previewSize }) advanceTimeBy(testTimeToLoadAllUris) runCurrent() @@ -213,7 +215,7 @@ class CachingImagePreviewImageLoaderTest { // Act fakeThumbnailLoader.invokeCalls.clear() - imageLoader.prePopulate(testUris) + imageLoader.prePopulate(testUris.map { it to previewSize }) advanceTimeBy(testTimeToLoadAllUris) runCurrent() @@ -229,7 +231,7 @@ class CachingImagePreviewImageLoaderTest { assertThat(unfilledCacheUris.size).isLessThan(testCacheSize) // Act - imageLoader.prePopulate(unfilledCacheUris) + imageLoader.prePopulate(unfilledCacheUris.map { it to previewSize }) advanceTimeBy(testJobTime) runCurrent() @@ -238,7 +240,7 @@ class CachingImagePreviewImageLoaderTest { // Act fakeThumbnailLoader.invokeCalls.clear() - imageLoader.prePopulate(unfilledCacheUris) + imageLoader.prePopulate(unfilledCacheUris.map { it to previewSize }) advanceTimeBy(testJobTime) runCurrent() @@ -252,8 +254,8 @@ class CachingImagePreviewImageLoaderTest { // Arrange // Act - imageLoader.invoke(testUris[0], caching = false) - imageLoader.invoke(testUris[0], caching = false) + imageLoader.invoke(testUris[0], previewSize, caching = false) + imageLoader.invoke(testUris[0], previewSize, caching = false) advanceTimeBy(testJobTime) runCurrent() @@ -267,8 +269,8 @@ class CachingImagePreviewImageLoaderTest { // Arrange // Act - imageLoader.invoke(testUris[0], caching = true) - imageLoader.invoke(testUris[0], caching = true) + imageLoader.invoke(testUris[0], previewSize, caching = true) + imageLoader.invoke(testUris[0], previewSize, caching = true) advanceTimeBy(testJobTime) runCurrent() diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt index 27d98ece..905c8517 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/ChooserContentPreviewUiTest.kt @@ -23,6 +23,7 @@ import android.platform.test.flag.junit.DeviceFlagsValueProvider import com.android.intentresolver.ContentTypeHint import com.android.intentresolver.FakeImageLoader import com.android.intentresolver.contentpreview.ChooserContentPreviewUi.ActionFactory +import com.android.intentresolver.data.model.ChooserRequest import com.android.intentresolver.widget.ActionRow import com.android.intentresolver.widget.ImagePreviewView import com.google.common.truth.Truth.assertThat @@ -61,13 +62,18 @@ class ChooserContentPreviewUiTest { @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() private fun createContentPreviewUi( - targetIntent: Intent, + action: String, + sharedText: CharSequence? = null, isPayloadTogglingEnabled: Boolean = false ) = ChooserContentPreviewUi( testScope, previewData, - targetIntent, + ChooserRequest( + targetIntent = Intent(action), + sharedText = sharedText, + launchedFromPackage = "org.pkg", + ), imageLoader, actionFactory, { null }, @@ -81,7 +87,7 @@ class ChooserContentPreviewUiTest { @Test fun test_textPreviewType_useTextPreviewUi() { whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_TEXT) - val testSubject = createContentPreviewUi(targetIntent = Intent(Intent.ACTION_VIEW)) + val testSubject = createContentPreviewUi(action = Intent.ACTION_VIEW) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) @@ -92,7 +98,7 @@ class ChooserContentPreviewUiTest { @Test fun test_filePreviewType_useFilePreviewUi() { whenever(previewData.previewType).thenReturn(ContentPreviewType.CONTENT_PREVIEW_FILE) - val testSubject = createContentPreviewUi(targetIntent = Intent(Intent.ACTION_SEND)) + val testSubject = createContentPreviewUi(action = Intent.ACTION_SEND) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) assertThat(testSubject.mContentPreviewUi).isInstanceOf(FileContentPreviewUi::class.java) @@ -109,8 +115,8 @@ class ChooserContentPreviewUiTest { whenever(previewData.imagePreviewFileInfoFlow).thenReturn(MutableSharedFlow()) val testSubject = createContentPreviewUi( - targetIntent = - Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_TEXT, "Shared text") } + action = Intent.ACTION_SEND, + sharedText = "Shared text", ) assertThat(testSubject.mContentPreviewUi) .isInstanceOf(FilesPlusTextContentPreviewUi::class.java) @@ -126,7 +132,7 @@ class ChooserContentPreviewUiTest { whenever(previewData.firstFileInfo) .thenReturn(FileInfo.Builder(uri).withPreviewUri(uri).withMimeType("image/png").build()) whenever(previewData.imagePreviewFileInfoFlow).thenReturn(MutableSharedFlow()) - val testSubject = createContentPreviewUi(targetIntent = Intent(Intent.ACTION_SEND)) + val testSubject = createContentPreviewUi(action = Intent.ACTION_SEND) assertThat(testSubject.preferredContentPreview) .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) assertThat(testSubject.mContentPreviewUi).isInstanceOf(UnifiedContentPreviewUi::class.java) @@ -146,10 +152,12 @@ class ChooserContentPreviewUiTest { whenever(previewData.imagePreviewFileInfoFlow).thenReturn(MutableSharedFlow()) val testSubject = createContentPreviewUi( - targetIntent = Intent(Intent.ACTION_SEND), - isPayloadTogglingEnabled = true + action = Intent.ACTION_SEND, + isPayloadTogglingEnabled = true, ) assertThat(testSubject.mContentPreviewUi) .isInstanceOf(ShareouselContentPreviewUi::class.java) + assertThat(testSubject.preferredContentPreview) + .isEqualTo(ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION) } } diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt index 3a45e2f6..d78e6665 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/ImagePreviewImageLoaderTest.kt @@ -77,24 +77,25 @@ class ImagePreviewImageLoaderTest { contentResolver, cacheSize = 1, ) + private val previewSize = Size(500, 500) @Test fun prePopulate_cachesImagesUpToTheCacheSize() = scope.runTest { - testSubject.prePopulate(listOf(uriOne, uriTwo)) + testSubject.prePopulate(listOf(uriOne to previewSize, uriTwo to previewSize)) verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) verify(contentResolver, never()).loadThumbnail(uriTwo, imageSize, null) - testSubject(uriOne) + testSubject(uriOne, previewSize) verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) } @Test fun invoke_returnCachedImageWhenCalledTwice() = scope.runTest { - testSubject(uriOne) - testSubject(uriOne) + testSubject(uriOne, previewSize) + testSubject(uriOne, previewSize) verify(contentResolver, times(1)).loadThumbnail(any(), any(), anyOrNull()) } @@ -102,8 +103,8 @@ class ImagePreviewImageLoaderTest { @Test fun invoke_whenInstructed_doesNotCache() = scope.runTest { - testSubject(uriOne, false) - testSubject(uriOne, false) + testSubject(uriOne, previewSize, false) + testSubject(uriOne, previewSize, false) verify(contentResolver, times(2)).loadThumbnail(any(), any(), anyOrNull()) } @@ -120,8 +121,8 @@ class ImagePreviewImageLoaderTest { cacheSize = 1, ) coroutineScope { - launch(start = UNDISPATCHED) { testSubject(uriOne, false) } - launch(start = UNDISPATCHED) { testSubject(uriOne, false) } + launch(start = UNDISPATCHED) { testSubject(uriOne, previewSize, false) } + launch(start = UNDISPATCHED) { testSubject(uriOne, previewSize, false) } scheduler.advanceUntilIdle() } @@ -131,10 +132,10 @@ class ImagePreviewImageLoaderTest { @Test fun invoke_oldRecordsEvictedFromTheCache() = scope.runTest { - testSubject(uriOne) - testSubject(uriTwo) - testSubject(uriTwo) - testSubject(uriOne) + testSubject(uriOne, previewSize) + testSubject(uriTwo, previewSize) + testSubject(uriTwo, previewSize) + testSubject(uriOne, previewSize) verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) verify(contentResolver, times(1)).loadThumbnail(uriTwo, imageSize, null) @@ -144,8 +145,8 @@ class ImagePreviewImageLoaderTest { fun invoke_doNotCacheNulls() = scope.runTest { whenever(contentResolver.loadThumbnail(any(), any(), anyOrNull())).thenReturn(null) - testSubject(uriOne) - testSubject(uriOne) + testSubject(uriOne, previewSize) + testSubject(uriOne, previewSize) verify(contentResolver, times(2)).loadThumbnail(uriOne, imageSize, null) } @@ -162,7 +163,7 @@ class ImagePreviewImageLoaderTest { cacheSize = 1, ) imageLoaderScope.cancel() - testSubject(uriOne) + testSubject(uriOne, previewSize) } @Test(expected = CancellationException::class) @@ -178,7 +179,8 @@ class ImagePreviewImageLoaderTest { cacheSize = 1, ) coroutineScope { - val deferred = async(start = UNDISPATCHED) { testSubject(uriOne, false) } + val deferred = + async(start = UNDISPATCHED) { testSubject(uriOne, previewSize, false) } imageLoaderScope.cancel() scheduler.advanceUntilIdle() deferred.await() @@ -198,11 +200,11 @@ class ImagePreviewImageLoaderTest { cacheSize = 1, ) coroutineScope { - launch(start = UNDISPATCHED) { testSubject(uriOne, false) } - launch(start = UNDISPATCHED) { testSubject(uriOne, true) } + launch(start = UNDISPATCHED) { testSubject(uriOne, previewSize, false) } + launch(start = UNDISPATCHED) { testSubject(uriOne, previewSize, true) } scheduler.advanceUntilIdle() } - testSubject(uriOne, true) + testSubject(uriOne, previewSize, true) verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) } @@ -243,7 +245,7 @@ class ImagePreviewImageLoaderTest { cacheSize = 1, testSemaphore, ) - testSubject(uriOne, false) + testSubject(uriOne, previewSize, false) verify(contentResolver, times(1)).loadThumbnail(uriOne, imageSize, null) assertThat(acquireCount.get()).isEqualTo(1) @@ -281,7 +283,7 @@ class ImagePreviewImageLoaderTest { cacheSize = 1, testSemaphore, ) - launch(start = UNDISPATCHED) { testSubject(uriOne, false) } + launch(start = UNDISPATCHED) { testSubject(uriOne, previewSize, false) } verify(contentResolver, never()).loadThumbnail(any(), any(), anyOrNull()) @@ -324,7 +326,9 @@ class ImagePreviewImageLoaderTest { ) coroutineScope { repeat(requestCount) { - launch { testSubject(Uri.parse("content://org.pkg.app/image-$it.png")) } + launch { + testSubject(Uri.parse("content://org.pkg.app/image-$it.png"), previewSize) + } } yield() // wait for all requests to be dispatched diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt index a2fb9693..3dae760c 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/PreviewDataProviderTest.kt @@ -21,9 +21,15 @@ import android.content.Intent import android.database.MatrixCursor import android.media.MediaMetadata import android.net.Uri -import android.platform.test.flag.junit.CheckFlagsRule -import android.platform.test.flag.junit.DeviceFlagsValueProvider +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.FlagsParameterization +import android.platform.test.flag.junit.SetFlagsRule import android.provider.DocumentsContract +import android.provider.Downloads +import android.provider.OpenableColumns +import android.service.chooser.Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING +import com.android.intentresolver.Flags.FLAG_INDIVIDUAL_METADATA_TITLE_READ import com.google.common.truth.Truth.assertThat import kotlin.coroutines.EmptyCoroutineContext import kotlinx.coroutines.CoroutineScope @@ -34,19 +40,24 @@ import kotlinx.coroutines.test.UnconfinedTestDispatcher import kotlinx.coroutines.test.runTest import org.junit.Rule import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +@RunWith(Parameterized::class) @OptIn(ExperimentalCoroutinesApi::class) -class PreviewDataProviderTest { +class PreviewDataProviderTest(flags: FlagsParameterization) { private val contentResolver = mock<ContentInterface>() private val mimeTypeClassifier = DefaultMimeTypeClassifier private val testScope = TestScope(EmptyCoroutineContext + UnconfinedTestDispatcher()) - @get:Rule val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + @get:Rule val setFlagsRule = SetFlagsRule(flags) private fun createDataProvider( targetIntent: Intent, @@ -54,16 +65,7 @@ class PreviewDataProviderTest { additionalContentUri: Uri? = null, resolver: ContentInterface = contentResolver, typeClassifier: MimeTypeClassifier = mimeTypeClassifier, - isPayloadTogglingEnabled: Boolean = false - ) = - PreviewDataProvider( - scope, - targetIntent, - additionalContentUri, - resolver, - isPayloadTogglingEnabled, - typeClassifier, - ) + ) = PreviewDataProvider(scope, targetIntent, additionalContentUri, resolver, typeClassifier) @Test fun test_nonSendIntentAction_resolvesToTextPreviewUiSynchronously() { @@ -75,21 +77,49 @@ class PreviewDataProviderTest { } @Test - fun test_sendSingleTextFileWithoutPreview_resolvesToFilePreviewUi() { - val uri = Uri.parse("content://org.pkg.app/notes.txt") - val targetIntent = - Intent(Intent.ACTION_SEND).apply { - putExtra(Intent.EXTRA_STREAM, uri) - type = "text/plain" - } - whenever(contentResolver.getType(uri)).thenReturn("text/plain") - val testSubject = createDataProvider(targetIntent) + fun test_sendSingleTextFileWithoutPreview_resolvesToFilePreviewUi() = + testScope.runTest { + val fileName = "notes.txt" + val uri = Uri.parse("content://org.pkg.app/$fileName") + val targetIntent = + Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_STREAM, uri) + type = "text/plain" + } + whenever(contentResolver.getType(uri)).thenReturn("text/plain") + val testSubject = createDataProvider(targetIntent) - assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) - assertThat(testSubject.uriCount).isEqualTo(1) - assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) - verify(contentResolver, times(1)).getType(any()) - } + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.getFirstFileName()).isEqualTo(fileName) + verify(contentResolver, times(1)).getType(any()) + } + + @Test + fun test_sendSingleTextFileWithDisplayNameAndTitle_displayNameTakesPrecedenceOverTitle() = + testScope.runTest { + val uri = Uri.parse("content://org.pkg.app/1234") + val targetIntent = + Intent(Intent.ACTION_SEND).apply { + putExtra(Intent.EXTRA_STREAM, uri) + type = "text/plain" + } + whenever(contentResolver.getType(uri)).thenReturn("text/plain") + val title = "Notes" + val displayName = "Notes.txt" + whenever(contentResolver.query(eq(uri), anyOrNull(), anyOrNull(), anyOrNull())) + .thenReturn( + MatrixCursor(arrayOf(Downloads.Impl.COLUMN_TITLE, OpenableColumns.DISPLAY_NAME)) + .apply { addRow(arrayOf(title, displayName)) } + ) + contentResolver.setTitle(uri, title) + contentResolver.setDisplayName(uri, displayName) + val testSubject = createDataProvider(targetIntent) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.getFirstFileName()).isEqualTo(displayName) + } @Test fun test_sendIntentWithoutUris_resolvesToTextPreviewUiSynchronously() { @@ -115,60 +145,145 @@ 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) } - whenever(contentResolver.getType(uri)).thenReturn("application/pdf") - val testSubject = createDataProvider(targetIntent) + fun test_sendSingleFile_resolvesToFilePreviewUi() = + testScope.runTest { + val fileName = "paper.pdf" + val uri = Uri.parse("content://org.pkg.app/$fileName") + val targetIntent = + Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } + whenever(contentResolver.getType(uri)).thenReturn("application/pdf") + val testSubject = createDataProvider(targetIntent) - assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) - assertThat(testSubject.uriCount).isEqualTo(1) - assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) - assertThat(testSubject.firstFileInfo?.previewUri).isNull() - verify(contentResolver, times(1)).getType(any()) - } + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + assertThat(testSubject.getFirstFileName()).isEqualTo(fileName) + verify(contentResolver, times(1)).getType(any()) + } @Test - 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) - } - whenever(contentResolver.getType(uri)).thenThrow(SecurityException("test failure")) - val testSubject = createDataProvider(targetIntent) + fun test_sendSingleImageWithFailingGetType_resolvesToFilePreviewUi() = + testScope.runTest { + val fileName = "image.png" + val uri = Uri.parse("content://org.pkg.app/$fileName") + val targetIntent = + Intent(Intent.ACTION_SEND).apply { + type = "image/png" + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getType(uri)).thenThrow(SecurityException("test failure")) + val testSubject = createDataProvider(targetIntent) - assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) - assertThat(testSubject.uriCount).isEqualTo(1) - assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) - assertThat(testSubject.firstFileInfo?.previewUri).isNull() - verify(contentResolver, times(1)).getType(any()) - } + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + assertThat(testSubject.getFirstFileName()).isEqualTo(fileName) + verify(contentResolver, times(1)).getType(any()) + } @Test - 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) - } - whenever(contentResolver.getStreamTypes(uri, "*/*")) - .thenThrow(SecurityException("test failure")) - whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null)) - .thenThrow(SecurityException("test failure")) - val testSubject = createDataProvider(targetIntent) + fun test_sendSingleFileWithFailingMetadata_resolvesToFilePreviewUi() = + testScope.runTest { + val fileName = "manual.pdf" + val uri = Uri.parse("content://org.pkg.app/$fileName") + val targetIntent = + Intent(Intent.ACTION_SEND).apply { + type = "application/pdf" + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getType(uri)).thenReturn("application/pdf") + whenever(contentResolver.getStreamTypes(uri, "*/*")) + .thenThrow(SecurityException("test failure")) + whenever(contentResolver.query(eq(uri), anyOrNull(), anyOrNull(), anyOrNull())) + .thenThrow(SecurityException("test failure")) + val testSubject = createDataProvider(targetIntent) - assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) - assertThat(testSubject.uriCount).isEqualTo(1) - assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) - assertThat(testSubject.firstFileInfo?.previewUri).isNull() - verify(contentResolver, times(1)).getType(any()) - } + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + assertThat(testSubject.getFirstFileName()).isEqualTo(fileName) + verify(contentResolver, times(1)).getType(any()) + } @Test - fun test_SingleNonImageUriWithImageTypeInGetStreamTypes_useImagePreviewUi() { + @EnableFlags(FLAG_INDIVIDUAL_METADATA_TITLE_READ) + fun test_sendSingleImageWithFailingGetTypeDisjointTitleRead_resolvesToFilePreviewUi() = + testScope.runTest { + 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) + } + whenever(contentResolver.getType(uri)).thenThrow(SecurityException("test failure")) + val title = "Image Title" + contentResolver.setTitle(uri, title) + val testSubject = createDataProvider(targetIntent) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + assertThat(testSubject.getFirstFileName()).isEqualTo(title) + verify(contentResolver, times(1)).getType(any()) + } + + @Test + fun test_sendSingleFileWithFailingImageMetadata_resolvesToFilePreviewUi() = + testScope.runTest { + val fileName = "notes.pdf" + val uri = Uri.parse("content://org.pkg.app/$fileName") + val targetIntent = + Intent(Intent.ACTION_SEND).apply { + type = "application/pdf" + putExtra(Intent.EXTRA_STREAM, uri) + } + whenever(contentResolver.getType(uri)).thenReturn("application/pdf") + whenever(contentResolver.getStreamTypes(uri, "*/*")) + .thenThrow(SecurityException("test failure")) + whenever(contentResolver.query(eq(uri), anyOrNull(), anyOrNull(), anyOrNull())) + .thenThrow(SecurityException("test failure")) + val testSubject = createDataProvider(targetIntent) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + assertThat(testSubject.getFirstFileName()).isEqualTo(fileName) + verify(contentResolver, times(1)).getType(any()) + } + + @Test + @EnableFlags(FLAG_INDIVIDUAL_METADATA_TITLE_READ) + fun test_sendSingleFileWithFailingImageMetadataIndividualTitleRead_resolvesToFilePreviewUi() = + testScope.runTest { + 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) + } + whenever(contentResolver.getStreamTypes(uri, "*/*")) + .thenThrow(SecurityException("test failure")) + whenever(contentResolver.query(uri, ICON_METADATA_COLUMNS, null, null)) + .thenThrow(SecurityException("test failure")) + val displayName = "display name" + contentResolver.setDisplayName(uri, displayName) + val testSubject = createDataProvider(targetIntent) + + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(1) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + assertThat(testSubject.getFirstFileName()).isEqualTo(displayName) + verify(contentResolver, times(1)).getType(any()) + } + + @Test + fun test_SingleFileUriWithImageTypeInGetStreamTypes_useImagePreviewUi() { val uri = Uri.parse("content://org.pkg.app/paper.pdf") val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } whenever(contentResolver.getStreamTypes(uri, "*/*")) @@ -190,7 +305,7 @@ class PreviewDataProviderTest { arrayOf( DocumentsContract.Document.FLAG_SUPPORTS_THUMBNAIL or DocumentsContract.Document.FLAG_SUPPORTS_METADATA - ) + ), ) } @@ -207,7 +322,8 @@ class PreviewDataProviderTest { val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } whenever(contentResolver.getType(uri)).thenReturn("application/pdf") val cursor = MatrixCursor(columns).apply { addRow(values) } - whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null)).thenReturn(cursor) + whenever(contentResolver.query(eq(uri), anyOrNull(), anyOrNull(), anyOrNull())) + .thenReturn(cursor) val testSubject = createDataProvider(targetIntent) @@ -225,12 +341,13 @@ class PreviewDataProviderTest { val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } whenever(contentResolver.getType(uri)).thenReturn("application/pdf") val cursor = MatrixCursor(emptyArray()) - whenever(contentResolver.query(uri, METADATA_COLUMNS, null, null)).thenReturn(cursor) + whenever(contentResolver.query(eq(uri), anyOrNull(), anyOrNull(), anyOrNull())) + .thenReturn(cursor) val testSubject = createDataProvider(targetIntent) assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) - verify(contentResolver, times(1)).query(uri, METADATA_COLUMNS, null, null) + verify(contentResolver, times(1)).query(eq(uri), anyOrNull(), anyOrNull(), anyOrNull()) assertThat(cursor.isClosed).isTrue() } @@ -245,7 +362,7 @@ class PreviewDataProviderTest { ArrayList<Uri>().apply { add(uri1) add(uri2) - } + }, ) } whenever(contentResolver.getType(uri1)).thenReturn("image/png") @@ -273,7 +390,7 @@ class PreviewDataProviderTest { ArrayList<Uri>().apply { add(uri1) add(uri2) - } + }, ) } val testSubject = createDataProvider(targetIntent) @@ -287,7 +404,7 @@ class PreviewDataProviderTest { } @Test - fun test_someNonImageUriWithPreview_useImagePreviewUi() { + fun test_someFileUrisWithPreview_useImagePreviewUi() { val uri1 = Uri.parse("content://org.pkg.app/test.mp4") val uri2 = Uri.parse("content://org.pkg.app/test.pdf") val targetIntent = @@ -297,7 +414,7 @@ class PreviewDataProviderTest { ArrayList<Uri>().apply { add(uri1) add(uri2) - } + }, ) } whenever(contentResolver.getType(uri1)).thenReturn("video/mpeg4") @@ -313,29 +430,32 @@ class PreviewDataProviderTest { } @Test - fun test_allNonImageUrisWithoutPreview_useFilePreviewUi() { - 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") - val testSubject = createDataProvider(targetIntent) + fun test_allFileUrisWithoutPreview_useFilePreviewUi() = + testScope.runTest { + val firstFileName = "test.html" + val uri1 = Uri.parse("content://org.pkg.app/$firstFileName") + 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") + val testSubject = createDataProvider(targetIntent) - assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) - assertThat(testSubject.uriCount).isEqualTo(2) - assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri1) - assertThat(testSubject.firstFileInfo?.previewUri).isNull() - verify(contentResolver, times(2)).getType(any()) - } + assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_FILE) + assertThat(testSubject.uriCount).isEqualTo(2) + assertThat(testSubject.firstFileInfo?.uri).isEqualTo(uri1) + assertThat(testSubject.firstFileInfo?.previewUri).isNull() + assertThat(testSubject.getFirstFileName()).isEqualTo(firstFileName) + verify(contentResolver, times(2)).getType(any()) + } @Test fun test_imagePreviewFileInfoFlow_dataLoadedOnce() = @@ -349,7 +469,7 @@ class PreviewDataProviderTest { ArrayList<Uri>().apply { add(uri1) add(uri2) - } + }, ) } whenever(contentResolver.getType(uri1)).thenReturn("text/html") @@ -373,7 +493,8 @@ class PreviewDataProviderTest { } @Test - fun sendItemsWithAdditionalContentUri_showPayloadTogglingUi() { + @EnableFlags(FLAG_CHOOSER_PAYLOAD_TOGGLING) + fun sendImageWithAdditionalContentUri_showPayloadTogglingUi() { val uri = Uri.parse("content://org.pkg.app/image.png") val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } whenever(contentResolver.getType(uri)).thenReturn("image/png") @@ -381,7 +502,6 @@ class PreviewDataProviderTest { createDataProvider( targetIntent, additionalContentUri = Uri.parse("content://org.pkg.app.extracontent"), - isPayloadTogglingEnabled = true, ) assertThat(testSubject.previewType) @@ -393,7 +513,8 @@ class PreviewDataProviderTest { } @Test - fun sendItemsWithAdditionalContentUri_showImagePreviewUi() { + @DisableFlags(FLAG_CHOOSER_PAYLOAD_TOGGLING) + fun sendImageWithAdditionalContentUriAndDisabledFlag_showImagePreviewUi() { val uri = Uri.parse("content://org.pkg.app/image.png") val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } whenever(contentResolver.getType(uri)).thenReturn("image/png") @@ -411,6 +532,7 @@ class PreviewDataProviderTest { } @Test + @EnableFlags(FLAG_CHOOSER_PAYLOAD_TOGGLING) fun sendItemsWithAdditionalContentUriWithSameAuthority_showImagePreviewUi() { val uri = Uri.parse("content://org.pkg.app/image.png") val targetIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) } @@ -419,7 +541,6 @@ class PreviewDataProviderTest { createDataProvider( targetIntent, additionalContentUri = Uri.parse("content://org.pkg.app/extracontent"), - isPayloadTogglingEnabled = true, ) assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_IMAGE) @@ -435,10 +556,28 @@ class PreviewDataProviderTest { val testSubject = createDataProvider( targetIntent, - additionalContentUri = Uri.parse("content://org.pkg.app/extracontent") + additionalContentUri = Uri.parse("content://org.pkg.app/extracontent"), ) assertThat(testSubject.previewType).isEqualTo(ContentPreviewType.CONTENT_PREVIEW_TEXT) verify(contentResolver, never()).getType(any()) } + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun parameters(): List<FlagsParameterization> = + FlagsParameterization.allCombinationsOf(FLAG_INDIVIDUAL_METADATA_TITLE_READ) + } +} + +private fun ContentInterface.setDisplayName(uri: Uri, displayName: String) = + setMetadata(uri, arrayOf(OpenableColumns.DISPLAY_NAME), arrayOf(displayName)) + +private fun ContentInterface.setTitle(uri: Uri, title: String) = + setMetadata(uri, arrayOf(Downloads.Impl.COLUMN_TITLE), arrayOf(title)) + +private fun ContentInterface.setMetadata(uri: Uri, columns: Array<String>, values: Array<String>) { + whenever(query(uri, columns, null, null)) + .thenReturn(MatrixCursor(columns).apply { addRow(values) }) } diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/PreviewImageLoaderTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/PreviewImageLoaderTest.kt new file mode 100644 index 00000000..8c810058 --- /dev/null +++ b/tests/unit/src/com/android/intentresolver/contentpreview/PreviewImageLoaderTest.kt @@ -0,0 +1,496 @@ +/* + * 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.graphics.Bitmap +import android.net.Uri +import android.util.Size +import com.google.common.truth.Truth.assertThat +import java.util.concurrent.atomic.AtomicInteger +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.CoroutineStart +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.cancel +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class PreviewImageLoaderTest { + private val scope = TestScope() + + @Test + fun test_cachingImageRequest_imageCached() = + scope.runTest { + val uri = createUri(0) + val thumbnailLoader = + FakeThumbnailLoader().apply { + fakeInvoke[uri] = { size -> createBitmap(size.width, size.height) } + } + val testSubject = + PreviewImageLoader( + backgroundScope, + 1, + 100, + thumbnailLoader, + StandardTestDispatcher(scope.testScheduler), + ) + + val b1 = testSubject.invoke(uri, Size(200, 100)) + val b2 = testSubject.invoke(uri, Size(200, 100), caching = false) + assertThat(b1).isEqualTo(b2) + assertThat(thumbnailLoader.invokeCalls).hasSize(1) + } + + @Test + fun test_nonCachingImageRequest_imageNotCached() = + scope.runTest { + val uri = createUri(0) + val thumbnailLoader = + FakeThumbnailLoader().apply { + fakeInvoke[uri] = { size -> createBitmap(size.width, size.height) } + } + val testSubject = + PreviewImageLoader( + backgroundScope, + 1, + 100, + thumbnailLoader, + StandardTestDispatcher(scope.testScheduler), + ) + + testSubject.invoke(uri, Size(200, 100), caching = false) + testSubject.invoke(uri, Size(200, 100), caching = false) + assertThat(thumbnailLoader.invokeCalls).hasSize(2) + } + + @Test + fun test_twoSimultaneousImageRequests_requestsDeduplicated() = + scope.runTest { + val uri = createUri(0) + val loadingStartedDeferred = CompletableDeferred<Unit>() + val bitmapDeferred = CompletableDeferred<Bitmap>() + val thumbnailLoader = + FakeThumbnailLoader().apply { + fakeInvoke[uri] = { + loadingStartedDeferred.complete(Unit) + bitmapDeferred.await() + } + } + val testSubject = + PreviewImageLoader( + backgroundScope, + 1, + 100, + thumbnailLoader, + StandardTestDispatcher(scope.testScheduler), + ) + + val b1Deferred = async { testSubject.invoke(uri, Size(200, 100), caching = false) } + loadingStartedDeferred.await() + val b2Deferred = + async(start = CoroutineStart.UNDISPATCHED) { + testSubject.invoke(uri, Size(200, 100), caching = true) + } + bitmapDeferred.complete(createBitmap(200, 200)) + + val b1 = b1Deferred.await() + val b2 = b2Deferred.await() + assertThat(b1).isEqualTo(b2) + assertThat(thumbnailLoader.invokeCalls).hasSize(1) + } + + @Test + fun test_cachingRequestCancelledAndEvoked_imageLoadingCancelled() = + scope.runTest { + val uriOne = createUri(1) + val uriTwo = createUri(2) + val loadingStartedDeferred = CompletableDeferred<Unit>() + val cancelledRequests = mutableSetOf<Uri>() + val thumbnailLoader = + FakeThumbnailLoader().apply { + fakeInvoke[uriOne] = { + loadingStartedDeferred.complete(Unit) + try { + awaitCancellation() + } catch (e: CancellationException) { + cancelledRequests.add(uriOne) + throw e + } + } + fakeInvoke[uriTwo] = { createBitmap(200, 200) } + } + val testSubject = + PreviewImageLoader( + backgroundScope, + cacheSize = 1, + defaultPreviewSize = 100, + thumbnailLoader, + StandardTestDispatcher(scope.testScheduler), + ) + + val jobOne = launch { testSubject.invoke(uriOne, Size(200, 100)) } + loadingStartedDeferred.await() + jobOne.cancel() + scope.runCurrent() + + assertThat(cancelledRequests).isEmpty() + + // second URI should evict the first item from the cache + testSubject.invoke(uriTwo, Size(200, 100)) + + assertThat(thumbnailLoader.invokeCalls).hasSize(2) + assertThat(cancelledRequests).containsExactly(uriOne) + } + + @Test + fun test_nonCachingRequestClientCancels_imageLoadingCancelled() = + scope.runTest { + val uri = createUri(1) + val loadingStartedDeferred = CompletableDeferred<Unit>() + val cancelledRequests = mutableSetOf<Uri>() + val thumbnailLoader = + FakeThumbnailLoader().apply { + fakeInvoke[uri] = { + loadingStartedDeferred.complete(Unit) + try { + awaitCancellation() + } catch (e: CancellationException) { + cancelledRequests.add(uri) + throw e + } + } + } + val testSubject = + PreviewImageLoader( + backgroundScope, + cacheSize = 1, + defaultPreviewSize = 100, + thumbnailLoader, + StandardTestDispatcher(scope.testScheduler), + ) + + val job = launch { testSubject.invoke(uri, Size(200, 100), caching = false) } + loadingStartedDeferred.await() + job.cancel() + scope.runCurrent() + + assertThat(cancelledRequests).containsExactly(uri) + } + + @Test + fun test_requestHigherResImage_newImageLoaded() = + scope.runTest { + val uri = createUri(0) + val thumbnailLoader = + FakeThumbnailLoader().apply { + fakeInvoke[uri] = { size -> createBitmap(size.width, size.height) } + } + val testSubject = + PreviewImageLoader( + backgroundScope, + 1, + 100, + thumbnailLoader, + StandardTestDispatcher(scope.testScheduler), + ) + + val b1 = testSubject.invoke(uri, Size(100, 100)) + val b2 = testSubject.invoke(uri, Size(200, 200)) + assertThat(b1).isNotNull() + assertThat(b1!!.width).isEqualTo(100) + assertThat(b2).isNotNull() + assertThat(b2!!.width).isEqualTo(200) + assertThat(thumbnailLoader.invokeCalls).hasSize(2) + } + + @Test + fun test_imageLoadingThrowsException_returnsNull() = + scope.runTest { + val uri = createUri(0) + val thumbnailLoader = + FakeThumbnailLoader().apply { + fakeInvoke[uri] = { throw SecurityException("test") } + } + val testSubject = + PreviewImageLoader( + backgroundScope, + 1, + 100, + thumbnailLoader, + StandardTestDispatcher(scope.testScheduler), + ) + + val bitmap = testSubject.invoke(uri, Size(100, 100)) + assertThat(bitmap).isNull() + } + + @Test + fun test_requestHigherResImage_cancelsLowerResLoading() = + scope.runTest { + val uri = createUri(0) + val cancelledRequestCount = AtomicInteger(0) + val imageLoadingStarted = CompletableDeferred<Unit>() + val bitmapDeferred = CompletableDeferred<Bitmap>() + val thumbnailLoader = + FakeThumbnailLoader().apply { + fakeInvoke[uri] = { + imageLoadingStarted.complete(Unit) + try { + bitmapDeferred.await() + } catch (e: CancellationException) { + cancelledRequestCount.getAndIncrement() + throw e + } + } + } + val testSubject = + PreviewImageLoader( + backgroundScope, + 1, + 100, + thumbnailLoader, + StandardTestDispatcher(scope.testScheduler), + ) + + val lowResSize = 100 + val highResSize = 200 + launch(start = CoroutineStart.UNDISPATCHED) { + testSubject.invoke(uri, Size(lowResSize, lowResSize)) + } + imageLoadingStarted.await() + val result = async { testSubject.invoke(uri, Size(highResSize, highResSize)) } + runCurrent() + assertThat(cancelledRequestCount.get()).isEqualTo(1) + + bitmapDeferred.complete(createBitmap(highResSize, highResSize)) + val bitmap = result.await() + assertThat(bitmap).isNotNull() + assertThat(bitmap!!.width).isEqualTo(highResSize) + assertThat(thumbnailLoader.invokeCalls).hasSize(2) + } + + @Test + fun test_requestLowerResImage_cachedHigherResImageReturned() = + scope.runTest { + val uri = createUri(0) + val thumbnailLoader = + FakeThumbnailLoader().apply { + fakeInvoke[uri] = { size -> createBitmap(size.width, size.height) } + } + val lowResSize = 100 + val highResSize = 200 + val testSubject = + PreviewImageLoader( + backgroundScope, + 1, + 100, + thumbnailLoader, + StandardTestDispatcher(scope.testScheduler), + ) + + val b1 = testSubject.invoke(uri, Size(highResSize, highResSize)) + val b2 = testSubject.invoke(uri, Size(lowResSize, lowResSize)) + assertThat(b1).isEqualTo(b2) + assertThat(b2!!.width).isEqualTo(highResSize) + assertThat(thumbnailLoader.invokeCalls).hasSize(1) + } + + @Test + fun test_incorrectSizeRequested_defaultSizeIsUsed() = + scope.runTest { + val uri = createUri(0) + val defaultPreviewSize = 100 + val thumbnailLoader = + FakeThumbnailLoader().apply { + fakeInvoke[uri] = { size -> createBitmap(size.width, size.height) } + } + val testSubject = + PreviewImageLoader( + backgroundScope, + cacheSize = 1, + defaultPreviewSize, + thumbnailLoader, + StandardTestDispatcher(scope.testScheduler), + ) + + val b1 = testSubject(uri, Size(0, 0)) + assertThat(b1!!.width).isEqualTo(defaultPreviewSize) + + val largerImageSize = 200 + val b2 = testSubject(uri, Size(largerImageSize, largerImageSize)) + assertThat(b2!!.width).isEqualTo(largerImageSize) + } + + @Test + fun test_prePopulateImages_cachesImagesUpToTheCacheSize() = + scope.runTest { + val previewSize = Size(100, 100) + val uris = List(2) { createUri(it) } + val loadingCount = AtomicInteger(0) + val thumbnailLoader = + FakeThumbnailLoader().apply { + for (uri in uris) { + fakeInvoke[uri] = { size -> + loadingCount.getAndIncrement() + createBitmap(size.width, size.height) + } + } + } + val testSubject = + PreviewImageLoader( + backgroundScope, + 1, + 100, + thumbnailLoader, + StandardTestDispatcher(scope.testScheduler), + ) + + testSubject.prePopulate(uris.map { it to previewSize }) + runCurrent() + + assertThat(loadingCount.get()).isEqualTo(1) + assertThat(thumbnailLoader.invokeCalls).containsExactly(uris[0]) + + testSubject(uris[0], previewSize) + runCurrent() + + assertThat(loadingCount.get()).isEqualTo(1) + } + + @Test + fun test_oldRecordEvictedFromTheCache() = + scope.runTest { + val previewSize = Size(100, 100) + val uriOne = createUri(1) + val uriTwo = createUri(2) + val requestsPerUri = HashMap<Uri, AtomicInteger>() + val thumbnailLoader = + FakeThumbnailLoader().apply { + for (uri in arrayOf(uriOne, uriTwo)) { + fakeInvoke[uri] = { size -> + requestsPerUri.getOrPut(uri) { AtomicInteger() }.incrementAndGet() + createBitmap(size.width, size.height) + } + } + } + val testSubject = + PreviewImageLoader( + backgroundScope, + 1, + 100, + thumbnailLoader, + StandardTestDispatcher(scope.testScheduler), + ) + + testSubject(uriOne, previewSize) + testSubject(uriTwo, previewSize) + testSubject(uriTwo, previewSize) + testSubject(uriOne, previewSize) + + assertThat(requestsPerUri[uriOne]?.get()).isEqualTo(2) + assertThat(requestsPerUri[uriTwo]?.get()).isEqualTo(1) + } + + @Test + fun test_doNotCacheNulls() = + scope.runTest { + val previewSize = Size(100, 100) + val uri = createUri(1) + val loadingCount = AtomicInteger(0) + val thumbnailLoader = + FakeThumbnailLoader().apply { + fakeInvoke[uri] = { + loadingCount.getAndIncrement() + null + } + } + val testSubject = + PreviewImageLoader( + backgroundScope, + 1, + 100, + thumbnailLoader, + StandardTestDispatcher(scope.testScheduler), + ) + + testSubject(uri, previewSize) + testSubject(uri, previewSize) + + assertThat(loadingCount.get()).isEqualTo(2) + } + + @Test(expected = CancellationException::class) + fun invoke_onClosedImageLoaderScope_throwsCancellationException() = + scope.runTest { + val uri = createUri(1) + val thumbnailLoader = FakeThumbnailLoader().apply { fakeInvoke[uri] = { null } } + val imageLoaderScope = CoroutineScope(coroutineContext) + val testSubject = + PreviewImageLoader( + imageLoaderScope, + 1, + 100, + thumbnailLoader, + StandardTestDispatcher(scope.testScheduler), + ) + imageLoaderScope.cancel() + testSubject(uri, Size(200, 200)) + } + + @Test(expected = CancellationException::class) + fun invoke_imageLoaderScopeClosedMidflight_throwsCancellationException() = + scope.runTest { + val uri = createUri(1) + val loadingStarted = CompletableDeferred<Unit>() + val bitmapDeferred = CompletableDeferred<Bitmap?>() + val thumbnailLoader = + FakeThumbnailLoader().apply { + fakeInvoke[uri] = { + loadingStarted.complete(Unit) + bitmapDeferred.await() + } + } + val imageLoaderScope = CoroutineScope(coroutineContext) + val testSubject = + PreviewImageLoader( + imageLoaderScope, + 1, + 100, + thumbnailLoader, + StandardTestDispatcher(scope.testScheduler), + ) + + launch { + loadingStarted.await() + imageLoaderScope.cancel() + } + testSubject(uri, Size(200, 200)) + } +} + +private fun createUri(id: Int) = Uri.parse("content://org.pkg.app/image-$id.png") + +private fun createBitmap(width: Int, height: Int) = + Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/PayloadToggleCursorResolverTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/PayloadToggleCursorResolverTest.kt index 5d81ec2a..f0813623 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/PayloadToggleCursorResolverTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/cursor/PayloadToggleCursorResolverTest.kt @@ -30,9 +30,12 @@ import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.test.runTest import org.junit.Test import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.capture import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.verify class PayloadToggleCursorResolverTest { private val cursorUri = Uri.parse("content://org.pkg.app.extra") @@ -101,6 +104,9 @@ class PayloadToggleCursorResolverTest { assertThat(row!!.uri).isEqualTo(uri) assertThat(row.previewSize).isEqualTo(Size(100, 50)) } + val columnsCaptor = argumentCaptor<Array<String>>() + verify(fakeContentProvider).query(eq(cursorUri), columnsCaptor.capture(), any(), any()) + assertThat(columnsCaptor.firstValue.toList()).containsExactly(URI, WIDTH, HEIGHT) } @Test diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt index 48e43190..c4ba8105 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/CursorPreviewsInteractorTest.kt @@ -18,10 +18,13 @@ package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor +import android.database.Cursor import android.database.MatrixCursor import android.net.Uri import android.provider.MediaStore.MediaColumns.HEIGHT import android.provider.MediaStore.MediaColumns.WIDTH +import android.service.chooser.AdditionalContentContract.Columns.URI +import android.service.chooser.AdditionalContentContract.CursorExtraKeys.POSITION import android.util.Size import androidx.core.os.bundleOf import com.android.intentresolver.contentpreview.FileInfo @@ -39,6 +42,7 @@ import com.android.intentresolver.util.cursor.CursorView import com.android.intentresolver.util.cursor.viewBy import com.android.intentresolver.util.runTest import com.android.systemui.kosmos.Kosmos +import com.google.common.truth.Correspondence import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch @@ -93,9 +97,9 @@ class CursorPreviewsInteractorTest { private val cursorSizes: Map<Int, Size>, ) { val cursor: CursorView<CursorRow?> = - MatrixCursor(arrayOf("uri", WIDTH, HEIGHT)) + MatrixCursor(arrayOf(URI, WIDTH, HEIGHT)) .apply { - extras = bundleOf("position" to cursorStartPosition) + extras = bundleOf(POSITION to cursorStartPosition) for (i in cursorRange) { val size = cursorSizes[i] addRow( @@ -279,22 +283,83 @@ class CursorPreviewsInteractorTest { ) { deps -> previewSelectionsRepository.selections.value = PreviewModel( - uri = uri(1), - mimeType = "image/png", - order = 0, - ).let { mapOf(it.uri to it) } + uri = uri(1), + mimeType = "image/png", + order = 0, + ) + .let { mapOf(it.uri to it) } backgroundScope.launch { cursorPreviewsInteractor.launch(deps.cursor, deps.initialPreviews) } runCurrent() - assertThat(previewSelectionsRepository.selections.value.values).containsExactly( - PreviewModel( - uri = uri(1), - mimeType = "image/bitmap", - order = 1, + assertThat(previewSelectionsRepository.selections.value.values) + .containsExactly( + PreviewModel( + uri = uri(1), + mimeType = "image/bitmap", + order = 1, + ) + ) + } + + @Test + fun testReadFailedPages() = + runTestWithDeps( + initialSelection = listOf(4), + cursor = emptyList(), + cursorStartPosition = 0, + pageSize = 2, + maxLoadedPages = 5, + ) { deps -> + val cursor = + MatrixCursor(arrayOf(URI)).apply { + extras = bundleOf(POSITION to 4) + for (i in 0 until 10) { + addRow(arrayOf(uri(i))) + } + } + val failingPositions = setOf(1, 5, 8) + val failingCursor = + object : Cursor by cursor { + override fun move(offset: Int): Boolean = moveToPosition(position + offset) + + override fun moveToPosition(position: Int): Boolean { + if (failingPositions.contains(position)) { + throw RuntimeException( + "A test exception when moving the cursor to position $position" + ) + } + return cursor.moveToPosition(position) + } + + override fun moveToFirst(): Boolean = moveToPosition(0) + + override fun moveToLast(): Boolean = moveToPosition(count - 1) + + override fun moveToNext(): Boolean = move(1) + + override fun moveToPrevious(): Boolean = move(-1) + } + .viewBy { + getString(0)?.let { uriStr -> + CursorRow(Uri.parse(uriStr), readSize(), position) + } + } + backgroundScope.launch { + cursorPreviewsInteractor.launch(failingCursor, deps.initialPreviews) + } + runCurrent() + + assertThat(cursorPreviewsRepository.previewsModel.value).isNotNull() + assertThat(cursorPreviewsRepository.previewsModel.value!!.previewModels) + .comparingElementsUsing<PreviewModel, Uri>( + Correspondence.transforming({ it.uri }, "has a Uri of") + ) + .containsExactlyElementsIn( + (0..7).filterNot { failingPositions.contains(it) }.map { uri(it) } ) - ) + .inOrder() } } diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractorTest.kt index f329b8a7..5d9ddbb6 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractorTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectablePreviewInteractorTest.kt @@ -26,13 +26,16 @@ import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.Tar import com.android.intentresolver.contentpreview.payloadtoggle.domain.intent.targetIntentModifier import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel import com.android.intentresolver.data.repository.chooserRequestRepository +import com.android.intentresolver.logging.FakeEventLog import com.android.intentresolver.util.runKosmosTest +import com.android.internal.logging.InstanceId import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.first import org.junit.Test class SelectablePreviewInteractorTest { + private val eventLog = FakeEventLog(InstanceId.fakeInstanceId(0)) @Test fun reflectPreviewRepo_initState() = runKosmosTest { @@ -46,6 +49,7 @@ class SelectablePreviewInteractorTest { order = 0, ), selectionInteractor = selectionInteractor, + eventLog = eventLog, ) runCurrent() @@ -64,6 +68,7 @@ class SelectablePreviewInteractorTest { order = 0, ), selectionInteractor = selectionInteractor, + eventLog = eventLog, ) assertThat(underTest.isSelected.first()).isFalse() @@ -93,6 +98,7 @@ class SelectablePreviewInteractorTest { order = 0, ), selectionInteractor = selectionInteractor, + eventLog = eventLog, ) underTest.setSelected(true) diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractorTest.kt index 87db243d..c8242333 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractorTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/SelectionInteractorTest.kt @@ -18,16 +18,24 @@ package com.android.intentresolver.contentpreview.payloadtoggle.domain.interacto import android.content.Intent import android.net.Uri +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import com.android.intentresolver.Flags import com.android.intentresolver.contentpreview.mimetypeClassifier import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.previewSelectionsRepository import com.android.intentresolver.contentpreview.payloadtoggle.shared.model.PreviewModel import com.android.intentresolver.util.runKosmosTest import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.first +import org.junit.Rule import org.junit.Test class SelectionInteractorTest { + @get:Rule val flagsRule = SetFlagsRule() + @Test + @DisableFlags(Flags.FLAG_UNSELECT_FINAL_ITEM) fun singleSelection_removalPrevented() = runKosmosTest { val initialPreview = PreviewModel( @@ -54,6 +62,33 @@ class SelectionInteractorTest { } @Test + @EnableFlags(Flags.FLAG_UNSELECT_FINAL_ITEM) + fun singleSelection_itemRemovedNoPendingIntentUpdates() = runKosmosTest { + val initialPreview = + PreviewModel( + uri = Uri.fromParts("scheme", "ssp", "fragment"), + mimeType = null, + order = 0 + ) + previewSelectionsRepository.selections.value = mapOf(initialPreview.uri to initialPreview) + + val underTest = + SelectionInteractor( + previewSelectionsRepository, + { Intent() }, + updateTargetIntentInteractor, + mimetypeClassifier, + ) + + assertThat(underTest.selections.first()).containsExactly(initialPreview.uri) + + underTest.unselect(initialPreview) + + assertThat(underTest.selections.first()).isEmpty() + assertThat(previewSelectionsRepository.selections.value).isEmpty() + } + + @Test fun multipleSelections_removalAllowed() = runKosmosTest { val first = PreviewModel( diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractorTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractorTest.kt index 570c346c..32d040fe 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractorTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/interactor/UpdateChooserRequestInteractorTest.kt @@ -18,7 +18,11 @@ package com.android.intentresolver.contentpreview.payloadtoggle.domain.interactor +import android.content.ComponentName import android.content.Intent +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import com.android.intentresolver.Flags.FLAG_SHAREOUSEL_UPDATE_EXCLUDE_COMPONENTS_EXTRA import com.android.intentresolver.contentpreview.payloadtoggle.data.repository.pendingSelectionCallbackRepository import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ShareouselUpdate import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate @@ -29,9 +33,12 @@ import com.android.intentresolver.util.runKosmosTest import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch +import org.junit.Rule import org.junit.Test class UpdateChooserRequestInteractorTest { + @get:Rule val setFlagsRule = SetFlagsRule() + @Test fun updateTargetIntentWithSelection() = runKosmosTest { val selectionCallbackResult = ShareouselUpdate(metadataText = ValueUpdate.Value("update")) @@ -45,4 +52,21 @@ class UpdateChooserRequestInteractorTest { assertThat(pendingSelectionCallbackRepository.pendingTargetIntent.value).isNull() assertThat(chooserRequestRepository.chooserRequest.value.metadataText).isEqualTo("update") } + + @Test + @EnableFlags(FLAG_SHAREOUSEL_UPDATE_EXCLUDE_COMPONENTS_EXTRA) + fun testSelectionResultWithExcludedComponents_chooserRequestIsUpdated() = runKosmosTest { + val excludedComponent = ComponentName("org.pkg.app", "Class") + val selectionCallbackResult = + ShareouselUpdate(excludeComponents = ValueUpdate.Value(listOf(excludedComponent))) + selectionChangeCallback = SelectionChangeCallback { selectionCallbackResult } + + backgroundScope.launch { processTargetIntentUpdatesInteractor.activate() } + + updateTargetIntentInteractor.updateTargetIntent(Intent()) + runCurrent() + + assertThat(chooserRequestRepository.chooserRequest.value.filteredComponentNames) + .containsExactly(excludedComponent) + } } diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallbackImplTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallbackImplTest.kt index 91bbd151..c1a1833a 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallbackImplTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/domain/update/SelectionChangeCallbackImplTest.kt @@ -29,32 +29,34 @@ import android.content.Intent.EXTRA_CHOOSER_MODIFY_SHARE_ACTION import android.content.Intent.EXTRA_CHOOSER_REFINEMENT_INTENT_SENDER import android.content.Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER import android.content.Intent.EXTRA_CHOOSER_TARGETS +import android.content.Intent.EXTRA_EXCLUDE_COMPONENTS import android.content.Intent.EXTRA_INTENT import android.content.Intent.EXTRA_METADATA_TEXT import android.content.Intent.EXTRA_STREAM import android.graphics.drawable.Icon import android.net.Uri import android.os.Bundle +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.service.chooser.AdditionalContentContract.MethodNames.ON_SELECTION_CHANGED import android.service.chooser.ChooserAction import android.service.chooser.ChooserTarget -import android.service.chooser.Flags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry +import com.android.intentresolver.Flags.FLAG_SHAREOUSEL_UPDATE_EXCLUDE_COMPONENTS_EXTRA import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate import com.android.intentresolver.contentpreview.payloadtoggle.domain.model.ValueUpdate.Absent -import com.android.intentresolver.inject.FakeChooserServiceFlags import com.google.common.truth.Correspondence import com.google.common.truth.Correspondence.BinaryPredicate import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import java.lang.IllegalArgumentException import kotlinx.coroutines.test.runTest +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor -import org.mockito.kotlin.capture import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -62,20 +64,16 @@ import org.mockito.kotlin.whenever @RunWith(AndroidJUnit4::class) class SelectionChangeCallbackImplTest { + @get:Rule val setFlagsRule = SetFlagsRule() + private val uri = Uri.parse("content://org.pkg/content-provider") private val chooserIntent = Intent(ACTION_CHOOSER) private val contentResolver = mock<ContentInterface>() private val context = InstrumentationRegistry.getInstrumentation().context - private val flags = - FakeChooserServiceFlags().apply { - setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, false) - setFlag(Flags.FLAG_CHOOSER_ALBUM_TEXT, false) - setFlag(Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA, false) - } @Test fun testPayloadChangeCallbackContact() = runTest { - val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags) + val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver) val u1 = createUri(1) val u2 = createUri(2) @@ -170,7 +168,7 @@ class SelectionChangeCallbackImplTest { Bundle().apply { putParcelableArray(EXTRA_CHOOSER_CUSTOM_ACTIONS, arrayOf(a1, a2)) } ) - val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags) + val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver) val targetIntent = Intent(ACTION_SEND_MULTIPLE) val result = testSubject.onSelectionChanged(targetIntent) @@ -187,6 +185,7 @@ class SelectionChangeCallbackImplTest { assertThat(result.refinementIntentSender).isEqualTo(Absent) assertThat(result.resultIntentSender).isEqualTo(Absent) assertThat(result.metadataText).isEqualTo(Absent) + assertThat(result.excludeComponents).isEqualTo(Absent) } @Test @@ -208,7 +207,7 @@ class SelectionChangeCallbackImplTest { Bundle().apply { putParcelable(EXTRA_CHOOSER_MODIFY_SHARE_ACTION, modifyShare) } ) - val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags) + val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver) val targetIntent = Intent(ACTION_SEND) val result = testSubject.onSelectionChanged(targetIntent) @@ -227,6 +226,7 @@ class SelectionChangeCallbackImplTest { assertThat(result.refinementIntentSender).isEqualTo(Absent) assertThat(result.resultIntentSender).isEqualTo(Absent) assertThat(result.metadataText).isEqualTo(Absent) + assertThat(result.excludeComponents).isEqualTo(Absent) } @Test @@ -243,7 +243,7 @@ class SelectionChangeCallbackImplTest { Bundle().apply { putParcelableArray(EXTRA_ALTERNATE_INTENTS, alternateIntents) } ) - val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags) + val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver) val targetIntent = Intent(ACTION_SEND) val result = testSubject.onSelectionChanged(targetIntent) @@ -268,6 +268,7 @@ class SelectionChangeCallbackImplTest { assertThat(result.refinementIntentSender).isEqualTo(Absent) assertThat(result.resultIntentSender).isEqualTo(Absent) assertThat(result.metadataText).isEqualTo(Absent) + assertThat(result.excludeComponents).isEqualTo(Absent) } @Test @@ -293,7 +294,7 @@ class SelectionChangeCallbackImplTest { Bundle().apply { putParcelableArray(EXTRA_CHOOSER_TARGETS, arrayOf(t1, t2)) } ) - val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags) + val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver) val targetIntent = Intent(ACTION_SEND) val result = testSubject.onSelectionChanged(targetIntent) @@ -321,6 +322,7 @@ class SelectionChangeCallbackImplTest { assertThat(result.refinementIntentSender).isEqualTo(Absent) assertThat(result.resultIntentSender).isEqualTo(Absent) assertThat(result.metadataText).isEqualTo(Absent) + assertThat(result.excludeComponents).isEqualTo(Absent) } @Test @@ -335,7 +337,7 @@ class SelectionChangeCallbackImplTest { } ) - val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags) + val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver) val targetIntent = Intent(ACTION_SEND) val result = testSubject.onSelectionChanged(targetIntent) @@ -348,6 +350,7 @@ class SelectionChangeCallbackImplTest { assertThat(result.refinementIntentSender.getOrThrow()).isNotNull() assertThat(result.resultIntentSender).isEqualTo(Absent) assertThat(result.metadataText).isEqualTo(Absent) + assertThat(result.excludeComponents).isEqualTo(Absent) } @Test @@ -362,7 +365,7 @@ class SelectionChangeCallbackImplTest { } ) - val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags) + val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver) val targetIntent = Intent(ACTION_SEND) val result = testSubject.onSelectionChanged(targetIntent) @@ -375,15 +378,16 @@ class SelectionChangeCallbackImplTest { assertThat(result.refinementIntentSender).isEqualTo(Absent) assertThat(result.resultIntentSender.getOrThrow()).isNotNull() assertThat(result.metadataText).isEqualTo(Absent) + assertThat(result.excludeComponents).isEqualTo(Absent) } @Test - fun testPayloadChangeCallbackUpdatesMetadataTextWithDisabledFlag_noUpdates() = runTest { + fun testPayloadChangeCallbackUpdatesMetadataTextWithEnabledFlag_valueUpdated() = runTest { val metadataText = "[Metadata]" whenever(contentResolver.call(any<String>(), any(), any(), any())) .thenReturn(Bundle().apply { putCharSequence(EXTRA_METADATA_TEXT, metadataText) }) - val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags) + val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver) val targetIntent = Intent(ACTION_SEND) val result = testSubject.onSelectionChanged(targetIntent) @@ -395,20 +399,26 @@ class SelectionChangeCallbackImplTest { assertThat(result.callerTargets).isEqualTo(Absent) assertThat(result.refinementIntentSender).isEqualTo(Absent) assertThat(result.resultIntentSender).isEqualTo(Absent) - assertThat(result.metadataText).isEqualTo(Absent) + assertThat(result.metadataText.getOrThrow()).isEqualTo(metadataText) + assertThat(result.excludeComponents).isEqualTo(Absent) } @Test - fun testPayloadChangeCallbackUpdatesMetadataTextWithEnabledFlag_valueUpdated() = runTest { - val metadataText = "[Metadata]" - flags.setFlag(Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA, true) + @EnableFlags(FLAG_SHAREOUSEL_UPDATE_EXCLUDE_COMPONENTS_EXTRA) + fun testPayloadChangeCallbackUpdatesExcludedComponents_valueUpdated() = runTest { + val excludedComponent = ComponentName("org.pkg.app", "org.pkg.app.TheClass") whenever(contentResolver.call(any<String>(), any(), any(), any())) - .thenReturn(Bundle().apply { putCharSequence(EXTRA_METADATA_TEXT, metadataText) }) + .thenReturn( + Bundle().apply { + putParcelableArray(EXTRA_EXCLUDE_COMPONENTS, arrayOf(excludedComponent)) + } + ) - val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags) + val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver) val targetIntent = Intent(ACTION_SEND) val result = testSubject.onSelectionChanged(targetIntent) + assertWithMessage("Callback result should not be null").that(result).isNotNull() requireNotNull(result) assertThat(result.customActions).isEqualTo(Absent) @@ -417,12 +427,12 @@ class SelectionChangeCallbackImplTest { assertThat(result.callerTargets).isEqualTo(Absent) assertThat(result.refinementIntentSender).isEqualTo(Absent) assertThat(result.resultIntentSender).isEqualTo(Absent) - assertThat(result.metadataText.getOrThrow()).isEqualTo(metadataText) + assertThat(result.metadataText).isEqualTo(Absent) + assertThat(result.excludeComponents.getOrThrow()).containsExactly(excludedComponent) } @Test fun testPayloadChangeCallbackProvidesInvalidData_invalidDataIgnored() = runTest { - flags.setFlag(Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA, true) whenever(contentResolver.call(any<String>(), any(), any(), any())) .thenReturn( Bundle().apply { @@ -436,7 +446,7 @@ class SelectionChangeCallbackImplTest { } ) - val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver, flags) + val testSubject = SelectionChangeCallbackImpl(uri, chooserIntent, contentResolver) val targetIntent = Intent(ACTION_SEND) val result = testSubject.onSelectionChanged(targetIntent) diff --git a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt index bb67e084..fc7ac751 100644 --- a/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt +++ b/tests/unit/src/com/android/intentresolver/contentpreview/payloadtoggle/ui/viewmodel/ShareouselViewModelTest.kt @@ -76,23 +76,25 @@ class ShareouselViewModelTest { scope = viewModelScope, ) } + private val previewHeight = 500 @Test fun headline_images() = runTest { assertThat(shareouselViewModel.headline.first()).isEqualTo("FILES: 1") previewSelectionsRepository.selections.value = listOf( - PreviewModel( - uri = Uri.fromParts("scheme", "ssp", "fragment"), - mimeType = "image/png", - order = 0, - ), - PreviewModel( - uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), - mimeType = "image/jpeg", - order = 1, + PreviewModel( + uri = Uri.fromParts("scheme", "ssp", "fragment"), + mimeType = "image/png", + order = 0, + ), + PreviewModel( + uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), + mimeType = "image/jpeg", + order = 1, + ) ) - ).associateBy { it.uri } + .associateBy { it.uri } runCurrent() assertThat(shareouselViewModel.headline.first()).isEqualTo("IMAGES: 2") } @@ -101,17 +103,18 @@ class ShareouselViewModelTest { fun headline_videos() = runTest { previewSelectionsRepository.selections.value = listOf( - PreviewModel( - uri = Uri.fromParts("scheme", "ssp", "fragment"), - mimeType = "video/mpeg", - order = 0, - ), - PreviewModel( - uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), - mimeType = "video/mpeg", - order = 1, + PreviewModel( + uri = Uri.fromParts("scheme", "ssp", "fragment"), + mimeType = "video/mpeg", + order = 0, + ), + PreviewModel( + uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), + mimeType = "video/mpeg", + order = 1, + ) ) - ).associateBy { it.uri } + .associateBy { it.uri } runCurrent() assertThat(shareouselViewModel.headline.first()).isEqualTo("VIDEOS: 2") } @@ -120,17 +123,18 @@ class ShareouselViewModelTest { fun headline_mixed() = runTest { previewSelectionsRepository.selections.value = listOf( - PreviewModel( - uri = Uri.fromParts("scheme", "ssp", "fragment"), - mimeType = "image/jpeg", - order = 0, - ), - PreviewModel( - uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), - mimeType = "video/mpeg", - order = 1, + PreviewModel( + uri = Uri.fromParts("scheme", "ssp", "fragment"), + mimeType = "image/jpeg", + order = 0, + ), + PreviewModel( + uri = Uri.fromParts("scheme1", "ssp1", "fragment1"), + mimeType = "video/mpeg", + order = 1, + ) ) - ).associateBy { it.uri } + .associateBy { it.uri } runCurrent() assertThat(shareouselViewModel.headline.first()).isEqualTo("FILES: 2") } @@ -194,6 +198,7 @@ class ShareouselViewModelTest { mimeType = "video/mpeg", order = 0, ), + previewHeight, /* index = */ 1, viewModelScope, ) @@ -245,6 +250,7 @@ class ShareouselViewModelTest { mimeType = "video/mpeg", order = 1, ), + previewHeight, /* index = */ 1, viewModelScope, ) @@ -308,10 +314,11 @@ class ShareouselViewModelTest { this.targetIntentModifier = targetIntentModifier previewSelectionsRepository.selections.value = PreviewModel( - uri = Uri.fromParts("scheme", "ssp", "fragment"), - mimeType = null, - order = 0, - ).let { mapOf(it.uri to it) } + uri = Uri.fromParts("scheme", "ssp", "fragment"), + mimeType = null, + order = 0, + ) + .let { mapOf(it.uri to it) } payloadToggleImageLoader = FakeImageLoader( initialBitmaps = @@ -340,6 +347,8 @@ class ShareouselViewModelTest { override fun getVideosHeadline(count: Int): String = "VIDEOS: $count" override fun getFilesHeadline(count: Int): String = "FILES: $count" + + override fun getNotItemsSelectedHeadline() = "Select items to share" } // instantiate the view model, and then runCurrent() so that it is fully hydrated before // starting the test diff --git a/tests/unit/src/com/android/intentresolver/ext/CreationExtrasExtTest.kt b/tests/unit/src/com/android/intentresolver/ext/CreationExtrasExtTest.kt index c09047a1..dbaee3d0 100644 --- a/tests/unit/src/com/android/intentresolver/ext/CreationExtrasExtTest.kt +++ b/tests/unit/src/com/android/intentresolver/ext/CreationExtrasExtTest.kt @@ -51,4 +51,19 @@ class CreationExtrasExtTest { assertThat(defaultArgs).parcelable<Point>("POINT1").marshallsEquallyTo(Point(1, 1)) assertThat(defaultArgs).parcelable<Point>("POINT2").marshallsEquallyTo(Point(2, 2)) } + + @Test + fun replaceDefaultArgs_replacesExisting() { + val creationExtras: CreationExtras = + MutableCreationExtras().apply { + set(DEFAULT_ARGS_KEY, bundleOf("POINT1" to Point(1, 1))) + } + + val updated = creationExtras.replaceDefaultArgs("POINT2" to Point(2, 2)) + + val defaultArgs = updated[DEFAULT_ARGS_KEY] + assertThat(defaultArgs).doesNotContainKey("POINT1") + assertThat(defaultArgs).containsKey("POINT2") + assertThat(defaultArgs).parcelable<Point>("POINT2").marshallsEquallyTo(Point(2, 2)) + } } diff --git a/tests/unit/src/com/android/intentresolver/logging/EventLogImplTest.java b/tests/unit/src/com/android/intentresolver/logging/EventLogImplTest.java index feb277ea..528c4613 100644 --- a/tests/unit/src/com/android/intentresolver/logging/EventLogImplTest.java +++ b/tests/unit/src/com/android/intentresolver/logging/EventLogImplTest.java @@ -152,6 +152,45 @@ public final class EventLogImplTest { } @Test + public void shareStartedWithShareouselAndEnabledReportingFlag_imagePreviewTypeReported() { + final String packageName = "com.test.foo"; + final String mimeType = "text/plain"; + final int appProvidedDirectTargets = 123; + final int appProvidedAppTargets = 456; + final boolean workProfile = true; + final int previewType = ContentPreviewType.CONTENT_PREVIEW_PAYLOAD_SELECTION; + final String intentAction = Intent.ACTION_SENDTO; + final int numCustomActions = 3; + final boolean modifyShareProvided = true; + + mChooserLogger.logShareStarted( + packageName, + mimeType, + appProvidedDirectTargets, + appProvidedAppTargets, + workProfile, + previewType, + intentAction, + numCustomActions, + modifyShareProvided); + + verify(mFrameworkLog).write( + eq(FrameworkStatsLog.SHARESHEET_STARTED), + eq(SharesheetStartedEvent.SHARE_STARTED.getId()), + eq(packageName), + /* instanceId=*/ gt(0), + eq(mimeType), + eq(appProvidedDirectTargets), + eq(appProvidedAppTargets), + eq(workProfile), + eq(FrameworkStatsLog + .SHARESHEET_STARTED__PREVIEW_TYPE__CONTENT_PREVIEW_TOGGLEABLE_MEDIA), + eq(FrameworkStatsLog.SHARESHEET_STARTED__INTENT_TYPE__INTENT_ACTION_SENDTO), + /* custom actions provided */ eq(numCustomActions), + /* reselection action provided */ eq(modifyShareProvided)); + } + + @Test public void testLogShareTargetSelected() { final int targetType = EventLogImpl.SELECTION_TYPE_SERVICE; final String packageName = "com.test.foo"; diff --git a/tests/unit/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt b/tests/unit/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt index fbdc062b..d11cb460 100644 --- a/tests/unit/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt +++ b/tests/unit/src/com/android/intentresolver/shortcuts/ShortcutLoaderTest.kt @@ -26,7 +26,12 @@ import android.content.pm.PackageManager.ApplicationInfoFlags import android.content.pm.ShortcutManager import android.os.UserHandle import android.os.UserManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import androidx.test.filters.SmallTest +import com.android.intentresolver.Flags.FLAG_FIX_SHORTCUTS_FLASHING +import com.android.intentresolver.Flags.FLAG_FIX_SHORTCUT_LOADER_JOB_LEAK import com.android.intentresolver.chooser.DisplayResolveInfo import com.android.intentresolver.createAppTarget import com.android.intentresolver.createShareShortcutInfo @@ -42,6 +47,7 @@ import org.junit.Assert.assertArrayEquals import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue +import org.junit.Rule import org.junit.Test import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor @@ -56,6 +62,8 @@ import org.mockito.kotlin.whenever @OptIn(ExperimentalCoroutinesApi::class) @SmallTest class ShortcutLoaderTest { + @get:Rule val flagRule = SetFlagsRule() + private val appInfo = ApplicationInfo().apply { enabled = true @@ -317,6 +325,143 @@ class ShortcutLoaderTest { } @Test + @DisableFlags(FLAG_FIX_SHORTCUTS_FLASHING) + fun test_appPredictorNotResponding_noCallbackFromShortcutLoader() { + scope.runTest { + val shortcutManagerResult = + listOf( + ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), + // mismatching shortcut + createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) + ) + val shortcutManager = + mock<ShortcutManager> { + on { getShareTargets(intentFilter) } doReturn shortcutManagerResult + } + whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) + val testSubject = + ShortcutLoader( + context, + backgroundScope, + appPredictor, + UserHandle.of(0), + true, + intentFilter, + dispatcher, + callback + ) + + testSubject.updateAppTargets(appTargets) + + verify(appPredictor, times(1)).requestPredictionUpdate() + + scheduler.advanceTimeBy(ShortcutLoader.APP_PREDICTOR_RESPONSE_TIMEOUT_MS * 2) + verify(callback, never()).accept(any()) + } + } + + @Test + @EnableFlags(FLAG_FIX_SHORTCUTS_FLASHING) + fun test_appPredictorNotResponding_timeoutAndFallbackToShortcutManager() { + scope.runTest { + val testSubject = + ShortcutLoader( + context, + backgroundScope, + appPredictor, + UserHandle.of(0), + true, + intentFilter, + dispatcher, + callback + ) + + testSubject.updateAppTargets(appTargets) + + val matchingAppTarget = createAppTarget(matchingShortcutInfo) + val shortcuts = + listOf( + matchingAppTarget, + // an AppTarget that does not belong to any resolved application; should be + // ignored + createAppTarget( + createShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) + ) + ) + val appPredictorCallbackCaptor = argumentCaptor<AppPredictor.Callback>() + verify(appPredictor, atLeastOnce()) + .registerPredictionUpdates(any(), appPredictorCallbackCaptor.capture()) + appPredictorCallbackCaptor.firstValue.onTargetsAvailable(shortcuts) + + scheduler.advanceTimeBy(ShortcutLoader.APP_PREDICTOR_RESPONSE_TIMEOUT_MS * 2) + verify(callback, times(1)).accept(any()) + } + } + + @Test + @EnableFlags(FLAG_FIX_SHORTCUTS_FLASHING) + fun test_appPredictorResponding_appPredictorTimeoutJobIsCancelled() { + scope.runTest { + val shortcutManagerResult = + listOf( + ShortcutManager.ShareShortcutInfo(matchingShortcutInfo, componentName), + // mismatching shortcut + createShareShortcutInfo("id-1", ComponentName("mismatching.pkg", "Class"), 1) + ) + val shortcutManager = + mock<ShortcutManager> { + on { getShareTargets(intentFilter) } doReturn shortcutManagerResult + } + whenever(context.getSystemService(Context.SHORTCUT_SERVICE)).thenReturn(shortcutManager) + val testSubject = + ShortcutLoader( + context, + backgroundScope, + appPredictor, + UserHandle.of(0), + true, + intentFilter, + dispatcher, + callback + ) + + testSubject.updateAppTargets(appTargets) + + verify(appPredictor, times(1)).requestPredictionUpdate() + + scheduler.advanceTimeBy(ShortcutLoader.APP_PREDICTOR_RESPONSE_TIMEOUT_MS / 2) + verify(callback, never()).accept(any()) + + val resultCaptor = argumentCaptor<ShortcutLoader.Result>() + scheduler.advanceTimeBy(ShortcutLoader.APP_PREDICTOR_RESPONSE_TIMEOUT_MS) + verify(callback, times(1)).accept(resultCaptor.capture()) + val result = resultCaptor.firstValue + assertWithMessage("An ShortcutManager result is expected") + .that(result.isFromAppPredictor) + .isFalse() + assertWithMessage("Wrong input app targets in the result") + .that(appTargets) + .asList() + .containsExactlyElementsIn(result.appTargets) + .inOrder() + assertWithMessage("Wrong shortcut count").that(result.shortcutsByApp).hasLength(1) + assertWithMessage("Wrong app target") + .that(appTarget) + .isEqualTo(result.shortcutsByApp[0].appTarget) + for (shortcut in result.shortcutsByApp[0].shortcuts) { + assertWithMessage( + "AppTargets are not expected the cache of a ShortcutManager result" + ) + .that(result.directShareAppTargetCache) + .isEmpty() + assertWithMessage("Wrong ShortcutInfo in the cache") + .that(matchingShortcutInfo) + .isEqualTo(result.directShareShortcutInfoCache[shortcut]) + } + } + } + + @Test fun test_ShortcutLoader_shortcutsRequestedIndependentlyFromAppTargets() = scope.runTest { ShortcutLoader( @@ -465,6 +610,30 @@ class ShortcutLoaderTest { testAlwaysCallSystemForMainProfile(isQuietModeEnabled = true) } + @Test + @EnableFlags(FLAG_FIX_SHORTCUT_LOADER_JOB_LEAK) + fun test_ShortcutLoaderDestroyed_appPredictorCallbackUnregisteredAndWatchdogCancelled() { + scope.runTest { + val testSubject = + ShortcutLoader( + context, + backgroundScope, + appPredictor, + UserHandle.of(0), + true, + intentFilter, + dispatcher, + callback + ) + + testSubject.updateAppTargets(appTargets) + testSubject.destroy() + + verify(appPredictor, times(1)).registerPredictionUpdates(any(), any()) + verify(appPredictor, times(1)).unregisterPredictionUpdates(any()) + } + } + private fun testDisabledWorkProfileDoNotCallSystem( isUserRunning: Boolean = true, isUserUnlocked: Boolean = true, diff --git a/tests/unit/src/com/android/intentresolver/ui/ShareResultSenderImplTest.kt b/tests/unit/src/com/android/intentresolver/ui/ShareResultSenderImplTest.kt index c254a856..d8b1b175 100644 --- a/tests/unit/src/com/android/intentresolver/ui/ShareResultSenderImplTest.kt +++ b/tests/unit/src/com/android/intentresolver/ui/ShareResultSenderImplTest.kt @@ -22,9 +22,7 @@ import android.content.ComponentName import android.content.Intent import android.os.Process import android.service.chooser.ChooserResult -import android.service.chooser.Flags import androidx.test.platform.app.InstrumentationRegistry -import com.android.intentresolver.inject.FakeChooserServiceFlags import com.android.intentresolver.ui.model.ShareAction import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage @@ -46,8 +44,6 @@ class ShareResultSenderImplTest { @get:Rule val compatChangeRule: TestRule = PlatformCompatChangeRule() - val flags = FakeChooserServiceFlags() - @OptIn(ExperimentalCoroutinesApi::class) @EnableCompatChanges(ChooserResult.SEND_CHOOSER_RESULT) @Test @@ -56,11 +52,8 @@ class ShareResultSenderImplTest { val deferred = CompletableDeferred<Intent>() val intentDispatcher = IntentSenderDispatcher { _, intent -> deferred.complete(intent) } - flags.setFlag(Flags.FLAG_ENABLE_CHOOSER_RESULT, true) - val resultSender = ShareResultSenderImpl( - flags = flags, scope = this, backgroundDispatcher = UnconfinedTestDispatcher(testScheduler), callerUid = Process.myUid(), @@ -68,7 +61,7 @@ class ShareResultSenderImplTest { intentDispatcher = intentDispatcher ) - resultSender.onComponentSelected(ComponentName("example.com", "Foo"), true) + resultSender.onComponentSelected(ComponentName("example.com", "Foo"), true, false) runCurrent() val intentReceived = deferred.await() @@ -83,6 +76,40 @@ class ShareResultSenderImplTest { assertThat(chooserResult?.isShortcut).isTrue() } + @OptIn(ExperimentalCoroutinesApi::class) + @EnableCompatChanges(ChooserResult.SEND_CHOOSER_RESULT) + @Test + fun onComponentSelected_crossProfile_chooserResultEnabled() = runTest { + val pi = PendingIntent.getBroadcast(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE) + val deferred = CompletableDeferred<Intent>() + val intentDispatcher = IntentSenderDispatcher { _, intent -> deferred.complete(intent) } + + val resultSender = + ShareResultSenderImpl( + scope = this, + backgroundDispatcher = UnconfinedTestDispatcher(testScheduler), + callerUid = Process.myUid(), + resultSender = pi.intentSender, + intentDispatcher = intentDispatcher + ) + + // Invoke as in the previous test, but this time say that the selection was cross-profile. + resultSender.onComponentSelected(ComponentName("example.com", "Foo"), true, true) + runCurrent() + + val intentReceived = deferred.await() + val chooserResult = + intentReceived.getParcelableExtra( + Intent.EXTRA_CHOOSER_RESULT, + ChooserResult::class.java + ) + assertThat(chooserResult).isNotNull() + assertThat(chooserResult?.type).isEqualTo(ChooserResult.CHOOSER_RESULT_UNKNOWN) + assertThat(chooserResult?.selectedComponent).isNull() + assertThat(chooserResult?.isShortcut).isTrue() + assertThat(intentReceived.hasExtra(Intent.EXTRA_CHOSEN_COMPONENT)).isFalse() + } + @DisableCompatChanges(ChooserResult.SEND_CHOOSER_RESULT) @Test fun onComponentSelected_chooserResultDisabled() = runTest { @@ -90,11 +117,8 @@ class ShareResultSenderImplTest { val deferred = CompletableDeferred<Intent>() val intentDispatcher = IntentSenderDispatcher { _, intent -> deferred.complete(intent) } - flags.setFlag(Flags.FLAG_ENABLE_CHOOSER_RESULT, true) - val resultSender = ShareResultSenderImpl( - flags = flags, scope = this, backgroundDispatcher = UnconfinedTestDispatcher(testScheduler), callerUid = Process.myUid(), @@ -102,7 +126,7 @@ class ShareResultSenderImplTest { intentDispatcher = intentDispatcher ) - resultSender.onComponentSelected(ComponentName("example.com", "Foo"), true) + resultSender.onComponentSelected(ComponentName("example.com", "Foo"), true, false) runCurrent() val intentReceived = deferred.await() @@ -121,6 +145,30 @@ class ShareResultSenderImplTest { .isFalse() } + @DisableCompatChanges(ChooserResult.SEND_CHOOSER_RESULT) + @Test + fun onComponentSelected_crossProfile_chooserResultDisabled() = runTest { + val pi = PendingIntent.getBroadcast(context, 0, Intent(), PendingIntent.FLAG_IMMUTABLE) + val deferred = CompletableDeferred<Intent>() + val intentDispatcher = IntentSenderDispatcher { _, intent -> deferred.complete(intent) } + + val resultSender = + ShareResultSenderImpl( + scope = this, + backgroundDispatcher = UnconfinedTestDispatcher(testScheduler), + callerUid = Process.myUid(), + resultSender = pi.intentSender, + intentDispatcher = intentDispatcher + ) + + // Invoke as in the previous test, but this time say that the selection was cross-profile. + resultSender.onComponentSelected(ComponentName("example.com", "Foo"), true, true) + runCurrent() + + // In the pre-ChooserResult API, no callback intent is sent for cross-profile selections. + assertWithMessage("deferred result isComplete").that(deferred.isCompleted).isFalse() + } + @EnableCompatChanges(ChooserResult.SEND_CHOOSER_RESULT) @Test fun onActionSelected_chooserResultEnabled() = runTest { @@ -128,11 +176,8 @@ class ShareResultSenderImplTest { val deferred = CompletableDeferred<Intent>() val intentDispatcher = IntentSenderDispatcher { _, intent -> deferred.complete(intent) } - flags.setFlag(Flags.FLAG_ENABLE_CHOOSER_RESULT, true) - val resultSender = ShareResultSenderImpl( - flags = flags, scope = this, backgroundDispatcher = UnconfinedTestDispatcher(testScheduler), callerUid = Process.myUid(), @@ -169,11 +214,8 @@ class ShareResultSenderImplTest { val deferred = CompletableDeferred<Intent>() val intentDispatcher = IntentSenderDispatcher { _, intent -> deferred.complete(intent) } - flags.setFlag(Flags.FLAG_ENABLE_CHOOSER_RESULT, true) - val resultSender = ShareResultSenderImpl( - flags = flags, scope = this, backgroundDispatcher = UnconfinedTestDispatcher(testScheduler), callerUid = Process.myUid(), diff --git a/tests/unit/src/com/android/intentresolver/ui/model/ActivityModelTest.kt b/tests/unit/src/com/android/intentresolver/ui/model/ActivityModelTest.kt index 737f02fe..5f86159c 100644 --- a/tests/unit/src/com/android/intentresolver/ui/model/ActivityModelTest.kt +++ b/tests/unit/src/com/android/intentresolver/ui/model/ActivityModelTest.kt @@ -21,6 +21,7 @@ import android.content.Intent.ACTION_CHOOSER import android.content.Intent.EXTRA_TEXT import android.net.Uri import com.android.intentresolver.ext.toParcelAndBack +import com.android.intentresolver.shared.model.ActivityModel import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import org.junit.Test @@ -54,7 +55,7 @@ class ActivityModelTest { intent = Intent(), launchedFromUid = 1000, launchedFromPackage = "other.example.com", - referrer = Uri.parse("android-app://app.example.com") + referrer = Uri.parse("android-app://app.example.com"), ) assertThat(launch1.referrerPackage).isEqualTo("app.example.com") @@ -67,7 +68,7 @@ class ActivityModelTest { intent = Intent(), launchedFromUid = 1000, launchedFromPackage = "example.com", - referrer = Uri.parse("http://some.other.value") + referrer = Uri.parse("http://some.other.value"), ) assertThat(launch.referrerPackage).isNull() @@ -80,7 +81,7 @@ class ActivityModelTest { intent = Intent(), launchedFromUid = 1000, launchedFromPackage = "example.com", - referrer = null + referrer = null, ) assertThat(launch.referrerPackage).isNull() diff --git a/tests/unit/src/com/android/intentresolver/ui/viewmodel/ChooserRequestTest.kt b/tests/unit/src/com/android/intentresolver/ui/viewmodel/ChooserRequestTest.kt index 56c019fd..7bd4edee 100644 --- a/tests/unit/src/com/android/intentresolver/ui/viewmodel/ChooserRequestTest.kt +++ b/tests/unit/src/com/android/intentresolver/ui/viewmodel/ChooserRequestTest.kt @@ -25,6 +25,8 @@ import android.content.Intent.EXTRA_CHOOSER_ADDITIONAL_CONTENT_URI import android.content.Intent.EXTRA_CHOOSER_FOCUSED_ITEM_POSITION import android.content.Intent.EXTRA_INTENT import android.content.Intent.EXTRA_REFERRER +import android.content.Intent.EXTRA_TEXT +import android.content.Intent.EXTRA_TITLE import android.net.Uri import android.service.chooser.Flags import androidx.core.net.toUri @@ -32,7 +34,7 @@ import androidx.core.os.bundleOf import com.android.intentresolver.ContentTypeHint import com.android.intentresolver.data.model.ChooserRequest import com.android.intentresolver.inject.FakeChooserServiceFlags -import com.android.intentresolver.ui.model.ActivityModel +import com.android.intentresolver.shared.model.ActivityModel import com.android.intentresolver.validation.Importance import com.android.intentresolver.validation.Invalid import com.android.intentresolver.validation.NoValue @@ -43,7 +45,7 @@ import org.junit.Test private fun createActivityModel( targetIntent: Intent?, referrer: Uri? = null, - additionalIntents: List<Intent>? = null + additionalIntents: List<Intent>? = null, ) = ActivityModel( Intent(ACTION_CHOOSER).apply { @@ -52,17 +54,13 @@ private fun createActivityModel( }, launchedFromUid = 10000, launchedFromPackage = "com.android.example", - referrer = referrer ?: "android-app://com.android.example".toUri() + referrer = referrer ?: "android-app://com.android.example".toUri(), ) class ChooserRequestTest { private val fakeChooserServiceFlags = - FakeChooserServiceFlags().apply { - setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, false) - setFlag(Flags.FLAG_CHOOSER_ALBUM_TEXT, false) - setFlag(Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA, false) - } + FakeChooserServiceFlags().apply { setFlag(Flags.FLAG_CHOOSER_PAYLOAD_TOGGLING, false) } @Test fun missingIntent() { @@ -244,11 +242,10 @@ class ChooserRequestTest { @Test fun testAlbumType() { - fakeChooserServiceFlags.setFlag(Flags.FLAG_CHOOSER_ALBUM_TEXT, true) val model = createActivityModel(Intent(ACTION_SEND)) model.intent.putExtra( Intent.EXTRA_CHOOSER_CONTENT_TYPE_HINT, - Intent.CHOOSER_CONTENT_TYPE_ALBUM + Intent.CHOOSER_CONTENT_TYPE_ALBUM, ) val result = readChooserRequest(model, fakeChooserServiceFlags) @@ -261,8 +258,8 @@ class ChooserRequestTest { } @Test - fun metadataText_whenFlagFalse_isNull() { - fakeChooserServiceFlags.setFlag(Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA, false) + fun metadataText_isPassedText() { + // Arrange val metadataText: CharSequence = "Test metadata text" val model = createActivityModel(targetIntent = Intent()).apply { @@ -274,24 +271,26 @@ class ChooserRequestTest { assertThat(result).isInstanceOf(Valid::class.java) result as Valid<ChooserRequest> - assertThat(result.value.metadataText).isNull() + assertThat(result.value.metadataText).isEqualTo(metadataText) } @Test - fun metadataText_whenFlagTrue_isPassedText() { - // Arrange - fakeChooserServiceFlags.setFlag(Flags.FLAG_ENABLE_SHARESHEET_METADATA_EXTRA, true) - val metadataText: CharSequence = "Test metadata text" - val model = - createActivityModel(targetIntent = Intent()).apply { - intent.putExtra(Intent.EXTRA_METADATA_TEXT, metadataText) + fun textSharedTextAndTitle() { + val text: CharSequence = "Shared text" + val title: CharSequence = "Title" + val targetIntent = + Intent().apply { + putExtra(EXTRA_TITLE, title) + putExtra(EXTRA_TEXT, text) } + val model = createActivityModel(targetIntent) val result = readChooserRequest(model, fakeChooserServiceFlags) assertThat(result).isInstanceOf(Valid::class.java) - result as Valid<ChooserRequest> - - assertThat(result.value.metadataText).isEqualTo(metadataText) + (result as Valid<ChooserRequest>).value.let { request -> + assertThat(request.sharedText).isEqualTo(text) + assertThat(request.sharedTextTitle).isEqualTo(title) + } } } diff --git a/tests/unit/src/com/android/intentresolver/ui/viewmodel/ResolverRequestTest.kt b/tests/unit/src/com/android/intentresolver/ui/viewmodel/ResolverRequestTest.kt index bd80235d..70512021 100644 --- a/tests/unit/src/com/android/intentresolver/ui/viewmodel/ResolverRequestTest.kt +++ b/tests/unit/src/com/android/intentresolver/ui/viewmodel/ResolverRequestTest.kt @@ -22,8 +22,8 @@ import android.os.UserHandle import androidx.core.net.toUri import androidx.core.os.bundleOf import com.android.intentresolver.ResolverActivity.PROFILE_WORK +import com.android.intentresolver.shared.model.ActivityModel import com.android.intentresolver.shared.model.Profile.Type.WORK -import com.android.intentresolver.ui.model.ActivityModel import com.android.intentresolver.ui.model.ResolverRequest import com.android.intentresolver.validation.Invalid import com.android.intentresolver.validation.UncaughtException @@ -34,15 +34,12 @@ import org.junit.Test private val targetUri = Uri.parse("content://example.com/123") -private fun createActivityModel( - targetIntent: Intent, - referrer: Uri? = null, -) = +private fun createActivityModel(targetIntent: Intent, referrer: Uri? = null) = ActivityModel( intent = targetIntent, launchedFromUid = 10000, launchedFromPackage = "com.android.example", - referrer = referrer ?: "android-app://com.android.example".toUri() + referrer = referrer ?: "android-app://com.android.example".toUri(), ) class ResolverRequestTest { diff --git a/tests/unit/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt b/tests/unit/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt index 4f4223c0..b1e8593d 100644 --- a/tests/unit/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt +++ b/tests/unit/src/com/android/intentresolver/widget/BatchPreviewLoaderTest.kt @@ -18,6 +18,7 @@ package com.android.intentresolver.widget import android.graphics.Bitmap import android.net.Uri +import android.util.Size import com.android.intentresolver.captureMany import com.android.intentresolver.mock import com.android.intentresolver.widget.ScrollableImagePreviewView.BatchPreviewLoader @@ -49,6 +50,7 @@ class BatchPreviewLoaderTest { private val testScope = CoroutineScope(dispatcher) private val onCompletion = mock<() -> Unit>() private val onUpdate = mock<(List<Preview>) -> Unit>() + private val previewSize = Size(500, 500) @Before fun setup() { @@ -71,6 +73,7 @@ class BatchPreviewLoaderTest { BatchPreviewLoader( imageLoader, previews(uriOne, uriTwo), + previewSize, totalItemCount = 2, onUpdate, onCompletion @@ -94,6 +97,7 @@ class BatchPreviewLoaderTest { BatchPreviewLoader( imageLoader, previews(uriOne, uriTwo, uriThree), + previewSize, totalItemCount = 3, onUpdate, onCompletion @@ -122,7 +126,14 @@ class BatchPreviewLoaderTest { } imageLoader.setUriLoadingOrder(*loadingOrder) val testSubject = - BatchPreviewLoader(imageLoader, previews(*uris), uris.size, onUpdate, onCompletion) + BatchPreviewLoader( + imageLoader, + previews(*uris), + previewSize, + uris.size, + onUpdate, + onCompletion + ) testSubject.loadAspectRatios(200) { _, _, _ -> 100 } dispatcher.scheduler.advanceUntilIdle() @@ -151,7 +162,14 @@ class BatchPreviewLoaderTest { val expectedUris = Array(uris.size / 2) { createUri(it * 2 + 1) } imageLoader.setUriLoadingOrder(*loadingOrder) val testSubject = - BatchPreviewLoader(imageLoader, previews(*uris), uris.size, onUpdate, onCompletion) + BatchPreviewLoader( + imageLoader, + previews(*uris), + previewSize, + uris.size, + onUpdate, + onCompletion + ) testSubject.loadAspectRatios(200) { _, _, _ -> 100 } dispatcher.scheduler.advanceUntilIdle() @@ -166,7 +184,9 @@ class BatchPreviewLoaderTest { private fun createUri(idx: Int): Uri = Uri.parse("content://org.pkg.app/image-$idx.png") 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 -> @@ -175,7 +195,7 @@ class BatchPreviewLoaderTest { .asFlow() } -private class TestImageLoader(scope: CoroutineScope) : suspend (Uri, Boolean) -> Bitmap? { +private class TestImageLoader(scope: CoroutineScope) : suspend (Uri, Size, Boolean) -> Bitmap? { private val loadingOrder = ArrayDeque<Pair<Uri, Boolean>>() private val pendingRequests = LinkedHashMap<Uri, CompletableDeferred<Bitmap?>>() private val flow = MutableSharedFlow<Unit>(replay = 1) @@ -203,7 +223,7 @@ private class TestImageLoader(scope: CoroutineScope) : suspend (Uri, Boolean) -> loadingOrder.addAll(uris) } - override suspend fun invoke(uri: Uri, cache: Boolean): Bitmap? { + override suspend fun invoke(uri: Uri, size: Size, cache: Boolean): Bitmap? { val deferred = pendingRequests.getOrPut(uri) { CompletableDeferred() } flow.tryEmit(Unit) return deferred.await() |