aboutsummaryrefslogtreecommitdiff
path: root/tests/shared
diff options
context:
space:
mode:
Diffstat (limited to 'tests/shared')
-rw-r--r--tests/shared/Android.bp37
-rw-r--r--tests/shared/src/com/android/intentresolver/MatcherUtils.java51
-rw-r--r--tests/shared/src/com/android/intentresolver/MockitoKotlinHelpers.kt177
-rw-r--r--tests/shared/src/com/android/intentresolver/ResolverDataProvider.java257
-rw-r--r--tests/shared/src/com/android/intentresolver/TestContentPreviewViewModel.kt57
-rw-r--r--tests/shared/src/com/android/intentresolver/TestPreviewImageLoader.kt33
-rw-r--r--tests/shared/src/com/android/intentresolver/logging/FakeEventLog.kt197
-rw-r--r--tests/shared/src/com/android/intentresolver/logging/FakeFrameworkStatsLogger.kt95
-rw-r--r--tests/shared/src/com/android/intentresolver/v2/platform/FakeSecureSettings.kt44
-rw-r--r--tests/shared/src/com/android/intentresolver/v2/platform/FakeUserManager.kt239
-rw-r--r--tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt22
11 files changed, 1209 insertions, 0 deletions
diff --git a/tests/shared/Android.bp b/tests/shared/Android.bp
new file mode 100644
index 0000000..55188ee
--- /dev/null
+++ b/tests/shared/Android.bp
@@ -0,0 +1,37 @@
+//
+// Copyright (C) 2023 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package {
+ default_applicable_licenses: ["Android-Apache-2.0"],
+}
+
+java_library {
+ name: "IntentResolver-tests-shared",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ libs: [
+ "android.test.mock",
+ "framework",
+ ],
+ static_libs: [
+ "hamcrest",
+ "IntentResolver-core",
+ "mockito-target-minus-junit4",
+ "truth"
+ ],
+}
diff --git a/tests/shared/src/com/android/intentresolver/MatcherUtils.java b/tests/shared/src/com/android/intentresolver/MatcherUtils.java
new file mode 100644
index 0000000..97cc698
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/MatcherUtils.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2020 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 org.hamcrest.BaseMatcher;
+import org.hamcrest.Description;
+import org.hamcrest.Matcher;
+
+/**
+ * Utils for helping with more customized matching options, for example matching the first
+ * occurrence of a set criteria.
+ */
+public class MatcherUtils {
+
+ /**
+ * Returns a {@link Matcher} which only matches the first occurrence of a set criteria.
+ */
+ public static <T> Matcher<T> first(final Matcher<T> matcher) {
+ return new BaseMatcher<T>() {
+ boolean isFirstMatch = true;
+
+ @Override
+ public boolean matches(final Object item) {
+ if (isFirstMatch && matcher.matches(item)) {
+ isFirstMatch = false;
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void describeTo(final Description description) {
+ description.appendText("Returns the first matching item");
+ }
+ };
+ }
+}
diff --git a/tests/shared/src/com/android/intentresolver/MockitoKotlinHelpers.kt b/tests/shared/src/com/android/intentresolver/MockitoKotlinHelpers.kt
new file mode 100644
index 0000000..db9fbd9
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/MockitoKotlinHelpers.kt
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+/**
+ * Kotlin versions of popular mockito methods that can return null in situations when Kotlin expects
+ * a non-null value. Kotlin will throw an IllegalStateException when this takes place ("x must not
+ * be null"). To fix this, we can use methods that modify the return type to be nullable. This
+ * causes Kotlin to skip the null checks.
+ * Cloned from frameworks/base/packages/SystemUI/tests/utils/src/com/android/systemui/util/mockito/KotlinMockitoHelpers.kt
+ */
+import org.mockito.ArgumentCaptor
+import org.mockito.ArgumentMatcher
+import org.mockito.ArgumentMatchers
+import org.mockito.MockSettings
+import org.mockito.Mockito
+import org.mockito.stubbing.Answer
+import org.mockito.stubbing.OngoingStubbing
+import org.mockito.stubbing.Stubber
+
+/**
+ * Returns Mockito.eq() as nullable type to avoid java.lang.IllegalStateException when
+ * null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> eq(obj: T): T = Mockito.eq<T>(obj)
+
+/**
+ * Returns Mockito.any() as nullable type to avoid java.lang.IllegalStateException when
+ * null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> any(type: Class<T>): T = Mockito.any<T>(type)
+inline fun <reified T> any(): T = any(T::class.java)
+
+/**
+ * Returns Mockito.argThat() as nullable type to avoid java.lang.IllegalStateException when
+ * null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> argThat(matcher: ArgumentMatcher<T>): T = Mockito.argThat(matcher)
+
+/**
+ * Kotlin type-inferred version of Mockito.nullable()
+ */
+inline fun <reified T> nullable(): T? = Mockito.nullable(T::class.java)
+
+/**
+ * Returns ArgumentCaptor.capture() as nullable type to avoid java.lang.IllegalStateException
+ * when null is returned.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+fun <T> capture(argumentCaptor: ArgumentCaptor<T>): T = argumentCaptor.capture()
+
+/**
+ * Helper function for creating an argumentCaptor in kotlin.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+inline fun <reified T : Any> argumentCaptor(): ArgumentCaptor<T> =
+ ArgumentCaptor.forClass(T::class.java)
+
+/**
+ * Helper function for creating new mocks, without the need to pass in a [Class] instance.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ *
+ * @param apply builder function to simplify stub configuration by improving type inference.
+ */
+inline fun <reified T : Any> mock(
+ mockSettings: MockSettings = Mockito.withSettings(),
+ apply: T.() -> Unit = {}
+): T = Mockito.mock(T::class.java, mockSettings).apply(apply)
+
+/**
+ * Helper function for stubbing methods without the need to use backticks.
+ *
+ * @see Mockito.when
+ */
+fun <T> whenever(methodCall: T): OngoingStubbing<T> = Mockito.`when`(methodCall)
+
+/**
+ * Helper function for stubbing methods without the need to use backticks.
+ */
+fun <T> Stubber.whenever(mock: T): T = `when`(mock)
+
+/**
+ * A kotlin implemented wrapper of [ArgumentCaptor] which prevents the following exception when
+ * kotlin tests are mocking kotlin objects and the methods take non-null parameters:
+ *
+ * java.lang.NullPointerException: capture() must not be null
+ */
+class KotlinArgumentCaptor<T> constructor(clazz: Class<T>) {
+ private val wrapped: ArgumentCaptor<T> = ArgumentCaptor.forClass(clazz)
+ fun capture(): T = wrapped.capture()
+ val value: T
+ get() = wrapped.value
+ val allValues: List<T>
+ get() = wrapped.allValues
+}
+
+/**
+ * Helper function for creating an argumentCaptor in kotlin.
+ *
+ * Generic T is nullable because implicitly bounded by Any?.
+ */
+inline fun <reified T : Any> kotlinArgumentCaptor(): KotlinArgumentCaptor<T> =
+ KotlinArgumentCaptor(T::class.java)
+
+/**
+ * Helper function for creating and using a single-use ArgumentCaptor in kotlin.
+ *
+ * val captor = argumentCaptor<Foo>()
+ * verify(...).someMethod(captor.capture())
+ * val captured = captor.value
+ *
+ * becomes:
+ *
+ * val captured = withArgCaptor<Foo> { verify(...).someMethod(capture()) }
+ *
+ * NOTE: this uses the KotlinArgumentCaptor to avoid the NullPointerException.
+ */
+inline fun <reified T : Any> withArgCaptor(block: KotlinArgumentCaptor<T>.() -> Unit): T =
+ kotlinArgumentCaptor<T>().apply { block() }.value
+
+/**
+ * Variant of [withArgCaptor] for capturing multiple arguments.
+ *
+ * val captor = argumentCaptor<Foo>()
+ * verify(...).someMethod(captor.capture())
+ * val captured: List<Foo> = captor.allValues
+ *
+ * becomes:
+ *
+ * val capturedList = captureMany<Foo> { verify(...).someMethod(capture()) }
+ */
+inline fun <reified T : Any> captureMany(block: KotlinArgumentCaptor<T>.() -> Unit): List<T> =
+ kotlinArgumentCaptor<T>().apply { block() }.allValues
+
+inline fun <reified T> anyOrNull() = ArgumentMatchers.argThat(ArgumentMatcher<T?> { true })
+
+/**
+ * Intended as a default Answer for a mock to prevent dependence on defaults.
+ *
+ * Use as:
+ * ```
+ * val context = mock<Context>(withSettings()
+ * .defaultAnswer(THROWS_EXCEPTION))
+ * ```
+ *
+ * To avoid triggering the exception during stubbing, must ONLY use one of the doXXX() methods, such
+ * as:
+ * * [doAnswer][Mockito.doAnswer]
+ * * [doCallRealMethod][Mockito.doCallRealMethod]
+ * * [doNothing][Mockito.doNothing]
+ * * [doReturn][Mockito.doReturn]
+ * * [doThrow][Mockito.doThrow]
+ */
+val THROWS_EXCEPTION = Answer { error("Unstubbed behavior was accessed.") }
diff --git a/tests/shared/src/com/android/intentresolver/ResolverDataProvider.java b/tests/shared/src/com/android/intentresolver/ResolverDataProvider.java
new file mode 100644
index 0000000..db10994
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/ResolverDataProvider.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2008 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.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.os.UserHandle;
+import android.test.mock.MockContext;
+import android.test.mock.MockPackageManager;
+import android.test.mock.MockResources;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Utility class used by resolver tests to create mock data
+ */
+public class ResolverDataProvider {
+
+ static private int USER_SOMEONE_ELSE = 10;
+
+ static ResolvedComponentInfo createResolvedComponentInfo(int i) {
+ return new ResolvedComponentInfo(
+ createComponentName(i),
+ createResolverIntent(i),
+ createResolveInfo(i, UserHandle.USER_CURRENT));
+ }
+
+ public static ResolvedComponentInfo createResolvedComponentInfo(int i,
+ UserHandle resolvedForUser) {
+ return new ResolvedComponentInfo(
+ createComponentName(i),
+ createResolverIntent(i),
+ createResolveInfo(i, UserHandle.USER_CURRENT, resolvedForUser));
+ }
+
+ static ResolvedComponentInfo createResolvedComponentInfo(
+ ComponentName componentName, Intent intent) {
+ return new ResolvedComponentInfo(
+ componentName,
+ intent,
+ createResolveInfo(componentName, UserHandle.USER_CURRENT));
+ }
+
+ public static ResolvedComponentInfo createResolvedComponentInfo(
+ ComponentName componentName, Intent intent, UserHandle resolvedForUser) {
+ return new ResolvedComponentInfo(
+ componentName,
+ intent,
+ createResolveInfo(componentName, UserHandle.USER_CURRENT, resolvedForUser));
+ }
+
+ static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i) {
+ return new ResolvedComponentInfo(
+ createComponentName(i),
+ createResolverIntent(i),
+ createResolveInfo(i, USER_SOMEONE_ELSE));
+ }
+
+ public static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i,
+ UserHandle resolvedForUser) {
+ return new ResolvedComponentInfo(
+ createComponentName(i),
+ createResolverIntent(i),
+ createResolveInfo(i, USER_SOMEONE_ELSE, resolvedForUser));
+ }
+
+ static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i, int userId) {
+ return new ResolvedComponentInfo(
+ createComponentName(i),
+ createResolverIntent(i),
+ createResolveInfo(i, userId));
+ }
+
+ public static ResolvedComponentInfo createResolvedComponentInfoWithOtherId(int i,
+ int userId, UserHandle resolvedForUser) {
+ return new ResolvedComponentInfo(
+ createComponentName(i),
+ createResolverIntent(i),
+ createResolveInfo(i, userId, resolvedForUser));
+ }
+
+ public static ComponentName createComponentName(int i) {
+ final String name = "component" + i;
+ return new ComponentName("foo.bar." + name, name);
+ }
+
+ public static ResolveInfo createResolveInfo(int i, int userId) {
+ return createResolveInfo(i, userId, UserHandle.of(userId));
+ }
+
+ public static ResolveInfo createResolveInfo(int i, int userId, UserHandle resolvedForUser) {
+ return createResolveInfo(createActivityInfo(i), userId, resolvedForUser);
+ }
+
+ public static ResolveInfo createResolveInfo(ComponentName componentName, int userId) {
+ return createResolveInfo(componentName, userId, UserHandle.of(userId));
+ }
+
+ public static ResolveInfo createResolveInfo(
+ ComponentName componentName, int userId, UserHandle resolvedForUser) {
+ return createResolveInfo(createActivityInfo(componentName), userId, resolvedForUser);
+ }
+
+ public static ResolveInfo createResolveInfo(
+ ActivityInfo activityInfo, int userId, UserHandle resolvedForUser) {
+ final ResolveInfo resolveInfo = new ResolveInfo();
+ resolveInfo.activityInfo = activityInfo;
+ resolveInfo.targetUserId = userId;
+ resolveInfo.userHandle = resolvedForUser;
+ return resolveInfo;
+ }
+
+ static ActivityInfo createActivityInfo(int i) {
+ ActivityInfo ai = new ActivityInfo();
+ ai.name = "activity_name" + i;
+ ai.packageName = "foo_bar" + i;
+ ai.enabled = true;
+ ai.exported = true;
+ ai.permission = null;
+ ai.applicationInfo = createApplicationInfo();
+ return ai;
+ }
+
+ static ActivityInfo createActivityInfo(ComponentName componentName) {
+ ActivityInfo ai = new ActivityInfo();
+ ai.name = componentName.getClassName();
+ ai.packageName = componentName.getPackageName();
+ ai.enabled = true;
+ ai.exported = true;
+ ai.permission = null;
+ ai.applicationInfo = createApplicationInfo();
+ ai.applicationInfo.packageName = componentName.getPackageName();
+ return ai;
+ }
+
+ static ApplicationInfo createApplicationInfo() {
+ ApplicationInfo ai = new ApplicationInfo();
+ ai.name = "app_name";
+ ai.packageName = "foo.bar";
+ ai.enabled = true;
+ return ai;
+ }
+
+ static class PackageManagerMockedInfo {
+ public Context ctx;
+ public ApplicationInfo appInfo;
+ public ActivityInfo activityInfo;
+ public ResolveInfo resolveInfo;
+ public String setAppLabel;
+ public String setActivityLabel;
+ public String setResolveInfoLabel;
+ }
+
+ /** Create a {@link PackageManagerMockedInfo} with all distinct labels. */
+ static PackageManagerMockedInfo createPackageManagerMockedInfo(boolean hasOverridePermission) {
+ return createPackageManagerMockedInfo(
+ hasOverridePermission, "app_label", "activity_label", "resolve_info_label");
+ }
+
+ static PackageManagerMockedInfo createPackageManagerMockedInfo(
+ boolean hasOverridePermission,
+ String appLabel,
+ String activityLabel,
+ String resolveInfoLabel) {
+ MockContext ctx = new MockContext() {
+ @Override
+ public PackageManager getPackageManager() {
+ return new MockPackageManager() {
+ @Override
+ public int checkPermission(String permName, String pkgName) {
+ if (hasOverridePermission) return PERMISSION_GRANTED;
+ return PERMISSION_DENIED;
+ }
+ };
+ }
+
+ @Override
+ public Resources getResources() {
+ return new MockResources() {
+ @NonNull
+ @Override
+ public String getString(int id) throws NotFoundException {
+ if (id == 1) return appLabel;
+ if (id == 2) return activityLabel;
+ if (id == 3) return resolveInfoLabel;
+ throw new NotFoundException();
+ }
+ };
+ }
+ };
+
+ ApplicationInfo appInfo = new ApplicationInfo() {
+ @NonNull
+ @Override
+ public CharSequence loadLabel(@NonNull PackageManager pm) {
+ return appLabel;
+ }
+ };
+ appInfo.labelRes = 1;
+
+ ActivityInfo activityInfo = new ActivityInfo() {
+ @NonNull
+ @Override
+ public CharSequence loadLabel(@NonNull PackageManager pm) {
+ return activityLabel;
+ }
+ };
+ activityInfo.labelRes = 2;
+ activityInfo.applicationInfo = appInfo;
+
+ ResolveInfo resolveInfo = new ResolveInfo() {
+ @NonNull
+ @Override
+ public CharSequence loadLabel(@NonNull PackageManager pm) {
+ return resolveInfoLabel;
+ }
+ };
+ resolveInfo.activityInfo = activityInfo;
+ resolveInfo.resolvePackageName = "super.fake.packagename";
+ resolveInfo.labelRes = 3;
+
+ PackageManagerMockedInfo mockedInfo = new PackageManagerMockedInfo();
+ mockedInfo.activityInfo = activityInfo;
+ mockedInfo.appInfo = appInfo;
+ mockedInfo.ctx = ctx;
+ mockedInfo.resolveInfo = resolveInfo;
+ mockedInfo.setAppLabel = appLabel;
+ mockedInfo.setActivityLabel = activityLabel;
+ mockedInfo.setResolveInfoLabel = resolveInfoLabel;
+
+ return mockedInfo;
+ }
+
+ static Intent createResolverIntent(int i) {
+ return new Intent("intentAction" + i);
+ }
+}
diff --git a/tests/shared/src/com/android/intentresolver/TestContentPreviewViewModel.kt b/tests/shared/src/com/android/intentresolver/TestContentPreviewViewModel.kt
new file mode 100644
index 0000000..888fc16
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/TestContentPreviewViewModel.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.content.Intent
+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
+import com.android.intentresolver.contentpreview.PreviewDataProvider
+
+/** A test content preview model that supports image loader override. */
+class TestContentPreviewViewModel(
+ private val viewModel: BasePreviewViewModel,
+ private val imageLoader: ImageLoader? = null,
+) : BasePreviewViewModel() {
+ override fun createOrReuseProvider(
+ targetIntent: Intent
+ ): PreviewDataProvider = viewModel.createOrReuseProvider(targetIntent)
+
+ override fun createOrReuseImageLoader(): ImageLoader =
+ imageLoader ?: viewModel.createOrReuseImageLoader()
+
+ 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 {
+ return TestContentPreviewViewModel(
+ factory.create(modelClass, extras) as BasePreviewViewModel,
+ imageLoader,
+ ) as T
+ }
+ }
+ }
+}
diff --git a/tests/shared/src/com/android/intentresolver/TestPreviewImageLoader.kt b/tests/shared/src/com/android/intentresolver/TestPreviewImageLoader.kt
new file mode 100644
index 0000000..f0203bb
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/TestPreviewImageLoader.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.intentresolver
+
+import android.graphics.Bitmap
+import android.net.Uri
+import com.android.intentresolver.contentpreview.ImageLoader
+import java.util.function.Consumer
+import kotlinx.coroutines.CoroutineScope
+
+class TestPreviewImageLoader(private val bitmaps: Map<Uri, Bitmap>) : ImageLoader {
+ override fun loadImage(callerScope: CoroutineScope, uri: Uri, callback: Consumer<Bitmap?>) {
+ callback.accept(bitmaps[uri])
+ }
+
+ override suspend fun invoke(uri: Uri, caching: Boolean): Bitmap? = bitmaps[uri]
+
+ override fun prePopulate(uris: List<Uri>) = Unit
+}
diff --git a/tests/shared/src/com/android/intentresolver/logging/FakeEventLog.kt b/tests/shared/src/com/android/intentresolver/logging/FakeEventLog.kt
new file mode 100644
index 0000000..9ed47db
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/logging/FakeEventLog.kt
@@ -0,0 +1,197 @@
+/*
+ * 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.logging
+
+import android.net.Uri
+import android.util.HashedStringCache
+import android.util.Log
+import com.android.internal.logging.InstanceId
+import javax.inject.Inject
+
+private const val TAG = "EventLog"
+private const val LOG = true
+
+/** A fake EventLog. */
+class FakeEventLog @Inject constructor(private val instanceId: InstanceId) : EventLog {
+
+ var chooserActivityShown: ChooserActivityShown? = null
+ var actionSelected: ActionSelected? = null
+ var customActionSelected: CustomActionSelected? = null
+ var actionShareWithPreview: ActionShareWithPreview? = null
+ val shareTargetSelected: MutableList<ShareTargetSelected> = mutableListOf()
+
+ private fun log(message: () -> Any?) {
+ if (LOG) {
+ Log.d(TAG, "[%04x] ".format(instanceId.id) + message())
+ }
+ }
+
+ override fun logChooserActivityShown(
+ isWorkProfile: Boolean,
+ targetMimeType: String?,
+ systemCost: Long
+ ) {
+ chooserActivityShown = ChooserActivityShown(isWorkProfile, targetMimeType, systemCost)
+ log { chooserActivityShown }
+ }
+
+ override fun logShareStarted(
+ packageName: String?,
+ mimeType: String?,
+ appProvidedDirect: Int,
+ appProvidedApp: Int,
+ isWorkprofile: Boolean,
+ previewType: Int,
+ intent: String?,
+ customActionCount: Int,
+ modifyShareActionProvided: Boolean
+ ) {
+ log {
+ ShareStarted(
+ packageName,
+ mimeType,
+ appProvidedDirect,
+ appProvidedApp,
+ isWorkprofile,
+ previewType,
+ intent,
+ customActionCount,
+ modifyShareActionProvided
+ )
+ }
+ }
+
+ override fun logCustomActionSelected(positionPicked: Int) {
+ customActionSelected = CustomActionSelected(positionPicked)
+ log { "logCustomActionSelected(positionPicked=$positionPicked)" }
+ }
+
+ override fun logShareTargetSelected(
+ targetType: Int,
+ packageName: String?,
+ positionPicked: Int,
+ directTargetAlsoRanked: Int,
+ numCallerProvided: Int,
+ directTargetHashed: HashedStringCache.HashResult?,
+ isPinned: Boolean,
+ successfullySelected: Boolean,
+ selectionCost: Long
+ ) {
+ shareTargetSelected.add(
+ ShareTargetSelected(
+ targetType,
+ packageName,
+ positionPicked,
+ directTargetAlsoRanked,
+ numCallerProvided,
+ directTargetHashed,
+ isPinned,
+ successfullySelected,
+ selectionCost
+ )
+ )
+ log { shareTargetSelected.last() }
+ shareTargetSelected.limitSize(10)
+ }
+
+ private fun MutableList<*>.limitSize(n: Int) {
+ while (size > n) {
+ removeFirst()
+ }
+ }
+
+ override fun logDirectShareTargetReceived(category: Int, latency: Int) {
+ log { "logDirectShareTargetReceived(category=$category, latency=$latency)" }
+ }
+
+ override fun logActionShareWithPreview(previewType: Int) {
+ actionShareWithPreview = ActionShareWithPreview(previewType)
+ log { actionShareWithPreview }
+ }
+
+ override fun logActionSelected(targetType: Int) {
+ actionSelected = ActionSelected(targetType)
+ log { actionSelected }
+ }
+
+ override fun logContentPreviewWarning(uri: Uri?) {
+ log { "logContentPreviewWarning(uri=$uri)" }
+ }
+
+ override fun logSharesheetTriggered() {
+ log { "logSharesheetTriggered()" }
+ }
+
+ override fun logSharesheetAppLoadComplete() {
+ log { "logSharesheetAppLoadComplete()" }
+ }
+
+ override fun logSharesheetDirectLoadComplete() {
+ log { "logSharesheetAppLoadComplete()" }
+ }
+
+ override fun logSharesheetDirectLoadTimeout() {
+ log { "logSharesheetDirectLoadTimeout()" }
+ }
+
+ override fun logSharesheetProfileChanged() {
+ log { "logSharesheetProfileChanged()" }
+ }
+
+ override fun logSharesheetExpansionChanged(isCollapsed: Boolean) {
+ log { "logSharesheetExpansionChanged(isCollapsed=$isCollapsed)" }
+ }
+
+ override fun logSharesheetAppShareRankingTimeout() {
+ log { "logSharesheetAppShareRankingTimeout()" }
+ }
+
+ override fun logSharesheetEmptyDirectShareRow() {
+ log { "logSharesheetEmptyDirectShareRow()" }
+ }
+
+ 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?,
+ val appProvidedDirect: Int,
+ val appProvidedApp: Int,
+ val isWorkprofile: Boolean,
+ val previewType: Int,
+ val intent: String?,
+ val customActionCount: Int,
+ val modifyShareActionProvided: Boolean
+ )
+ data class ShareTargetSelected(
+ val targetType: Int,
+ val packageName: String?,
+ val positionPicked: Int,
+ val directTargetAlsoRanked: Int,
+ val numCallerProvided: Int,
+ val directTargetHashed: HashedStringCache.HashResult?,
+ val pinned: Boolean,
+ val successfullySelected: Boolean,
+ val selectionCost: Long
+ )
+}
diff --git a/tests/shared/src/com/android/intentresolver/logging/FakeFrameworkStatsLogger.kt b/tests/shared/src/com/android/intentresolver/logging/FakeFrameworkStatsLogger.kt
new file mode 100644
index 0000000..dcf8d23
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/logging/FakeFrameworkStatsLogger.kt
@@ -0,0 +1,95 @@
+package com.android.intentresolver.logging
+/*
+ * 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.
+ */
+
+import com.android.internal.util.FrameworkStatsLog
+
+internal data class ShareSheetStarted(
+ val frameworkEventId: Int = FrameworkStatsLog.SHARESHEET_STARTED,
+ val appEventId: Int,
+ val packageName: String?,
+ val instanceId: Int,
+ val mimeType: String?,
+ val numAppProvidedDirectTargets: Int,
+ val numAppProvidedAppTargets: Int,
+ val isWorkProfile: Boolean,
+ val previewType: Int,
+ val intentType: Int,
+ val numCustomActions: Int,
+ val modifyShareActionProvided: Boolean
+)
+
+internal data class RankingSelected(
+ val frameworkEventId: Int = FrameworkStatsLog.RANKING_SELECTED,
+ val appEventId: Int,
+ val packageName: String?,
+ val instanceId: Int,
+ val positionPicked: Int,
+ val isPinned: Boolean
+)
+
+internal class FakeFrameworkStatsLogger : FrameworkStatsLogger {
+ var shareSheetStarted: ShareSheetStarted? = null
+ var rankingSelected: RankingSelected? = null
+ override fun write(
+ frameworkEventId: Int,
+ appEventId: Int,
+ packageName: String?,
+ instanceId: Int,
+ mimeType: String?,
+ numAppProvidedDirectTargets: Int,
+ numAppProvidedAppTargets: Int,
+ isWorkProfile: Boolean,
+ previewType: Int,
+ intentType: Int,
+ numCustomActions: Int,
+ modifyShareActionProvided: Boolean
+ ) {
+ shareSheetStarted =
+ ShareSheetStarted(
+ frameworkEventId,
+ appEventId,
+ packageName,
+ instanceId,
+ mimeType,
+ numAppProvidedDirectTargets,
+ numAppProvidedAppTargets,
+ isWorkProfile,
+ previewType,
+ intentType,
+ numCustomActions,
+ modifyShareActionProvided
+ )
+ }
+ override fun write(
+ frameworkEventId: Int,
+ appEventId: Int,
+ packageName: String?,
+ instanceId: Int,
+ positionPicked: Int,
+ isPinned: Boolean
+ ) {
+ rankingSelected =
+ RankingSelected(
+ frameworkEventId,
+ appEventId,
+ packageName,
+ instanceId,
+ positionPicked,
+ isPinned
+ )
+ }
+}
diff --git a/tests/shared/src/com/android/intentresolver/v2/platform/FakeSecureSettings.kt b/tests/shared/src/com/android/intentresolver/v2/platform/FakeSecureSettings.kt
new file mode 100644
index 0000000..4e27962
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/v2/platform/FakeSecureSettings.kt
@@ -0,0 +1,44 @@
+package com.android.intentresolver.v2.platform
+
+/**
+ * Creates a SecureSettings instance with predefined values:
+ *
+ * val settings = fakeSecureSettings {
+ * putString("stringValue", "example")
+ * putInt("intValue", 42)
+ * }
+ */
+fun fakeSecureSettings(block: FakeSecureSettings.Builder.() -> Unit): SecureSettings {
+ return FakeSecureSettings.Builder().apply(block).build()
+}
+
+/** An in memory implementation of [SecureSettings]. */
+class FakeSecureSettings private constructor(private val map: Map<String, String>) :
+ SecureSettings {
+
+ override fun getString(name: String): String? = map[name]
+ override fun getInt(name: String): Int? = getString(name)?.toIntOrNull()
+ override fun getLong(name: String): Long? = getString(name)?.toLongOrNull()
+ override fun getFloat(name: String): Float? = getString(name)?.toFloatOrNull()
+
+ class Builder {
+ private val map = mutableMapOf<String, String>()
+
+ fun putString(name: String, value: String) {
+ map[name] = value
+ }
+ fun putInt(name: String, value: Int) {
+ map[name] = value.toString()
+ }
+ fun putLong(name: String, value: Long) {
+ map[name] = value.toString()
+ }
+ fun putFloat(name: String, value: Float) {
+ map[name] = value.toString()
+ }
+
+ fun build(): SecureSettings {
+ return FakeSecureSettings(map.toMap())
+ }
+ }
+}
diff --git a/tests/shared/src/com/android/intentresolver/v2/platform/FakeUserManager.kt b/tests/shared/src/com/android/intentresolver/v2/platform/FakeUserManager.kt
new file mode 100644
index 0000000..370e5a0
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/v2/platform/FakeUserManager.kt
@@ -0,0 +1,239 @@
+package com.android.intentresolver.v2.platform
+
+import android.content.Context
+import android.content.Intent.ACTION_MANAGED_PROFILE_AVAILABLE
+import android.content.Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE
+import android.content.Intent.ACTION_PROFILE_ADDED
+import android.content.Intent.ACTION_PROFILE_AVAILABLE
+import android.content.Intent.ACTION_PROFILE_REMOVED
+import android.content.Intent.ACTION_PROFILE_UNAVAILABLE
+import android.content.pm.UserInfo
+import android.content.pm.UserInfo.FLAG_FULL
+import android.content.pm.UserInfo.FLAG_INITIALIZED
+import android.content.pm.UserInfo.FLAG_PROFILE
+import android.content.pm.UserInfo.NO_PROFILE_GROUP_ID
+import android.os.IUserManager
+import android.os.UserHandle
+import android.os.UserManager
+import androidx.annotation.NonNull
+import com.android.intentresolver.THROWS_EXCEPTION
+import com.android.intentresolver.mock
+import com.android.intentresolver.v2.data.repository.UserRepositoryImpl.UserEvent
+import com.android.intentresolver.v2.platform.FakeUserManager.State
+import com.android.intentresolver.whenever
+import kotlin.random.Random
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.consumeAsFlow
+import org.mockito.Mockito.RETURNS_SELF
+import org.mockito.Mockito.doAnswer
+import org.mockito.Mockito.doReturn
+import org.mockito.Mockito.withSettings
+
+/**
+ * A stand-in for [UserManager] to support testing of data layer components which depend on it.
+ *
+ * This fake targets system applications which need to interact with any or all of the current
+ * user's associated profiles (as reported by [getEnabledProfiles]). Support for manipulating
+ * non-profile (full) secondary users (switching active foreground user, adding or removing users)
+ * is not included.
+ *
+ * Upon creation [FakeUserManager] contains a single primary (full) user with a randomized ID. This
+ * is available from [FakeUserManager.state] using [primaryUserHandle][State.primaryUserHandle] or
+ * [getPrimaryUser][State.getPrimaryUser].
+ *
+ * To make state changes, use functions available from [FakeUserManager.state]:
+ * * [createProfile][State.createProfile]
+ * * [removeProfile][State.removeProfile]
+ * * [setQuietMode][State.setQuietMode]
+ *
+ * Any functionality not explicitly overridden here is guaranteed to throw an exception when
+ * accessed (access to the real system service is prevented).
+ */
+class FakeUserManager(val state: State = State()) :
+ UserManager(/* context = */ mockContext(), /* service = */ mockService()) {
+
+ enum class ProfileType {
+ WORK,
+ CLONE,
+ PRIVATE
+ }
+
+ override fun getProfileParent(userHandle: UserHandle): UserHandle? {
+ return state.getUserOrNull(userHandle)?.let { user ->
+ if (user.isProfile) {
+ state.getUserOrNull(UserHandle.of(user.profileGroupId))?.userHandle
+ } else {
+ null
+ }
+ }
+ }
+
+ override fun getUserInfo(userId: Int): UserInfo? {
+ return state.getUserOrNull(UserHandle.of(userId))
+ }
+
+ @Suppress("OVERRIDE_DEPRECATION")
+ override fun getEnabledProfiles(userId: Int): List<UserInfo> {
+ val user = state.users.single { it.id == userId }
+ return state.users.filter { other ->
+ user.id == other.id || user.profileGroupId == other.profileGroupId
+ }
+ }
+
+ override fun requestQuietModeEnabled(
+ enableQuietMode: Boolean,
+ @NonNull userHandle: UserHandle
+ ): Boolean {
+ state.setQuietMode(userHandle, enableQuietMode)
+ return true
+ }
+
+ override fun isQuietModeEnabled(userHandle: UserHandle): Boolean {
+ return state.getUser(userHandle).isQuietModeEnabled
+ }
+
+ override fun toString(): String {
+ return "FakeUserManager(state=$state)"
+ }
+
+ class State {
+ private val eventChannel = Channel<UserEvent>()
+ private val userInfoMap: MutableMap<UserHandle, UserInfo> = mutableMapOf()
+
+ /** The id of the primary/full/system user, which is automatically created. */
+ val primaryUserHandle: UserHandle
+
+ /**
+ * Retrieves the primary user. The value returned changes, but the values are immutable.
+ *
+ * Do not cache this value in tests, between operations.
+ */
+ fun getPrimaryUser(): UserInfo = getUser(primaryUserHandle)
+
+ private var nextUserId: Int = 100 + Random.nextInt(0, 900)
+
+ /**
+ * A flow of [UserEvent] which emulates those normally generated from system broadcasts.
+ *
+ * Events are produced by calls to [createPrimaryUser], [createProfile], [removeProfile].
+ */
+ val userEvents: Flow<UserEvent>
+
+ val users: List<UserInfo>
+ get() = userInfoMap.values.toList()
+
+ val userHandles: List<UserHandle>
+ get() = userInfoMap.keys.toList()
+
+ init {
+ primaryUserHandle = createPrimaryUser(allocateNextId())
+ userEvents = eventChannel.consumeAsFlow()
+ }
+
+ private fun allocateNextId() = nextUserId++
+
+ private fun createPrimaryUser(id: Int): UserHandle {
+ val userInfo =
+ UserInfo(id, "", "", FLAG_INITIALIZED or FLAG_FULL, USER_TYPE_FULL_SYSTEM)
+ userInfoMap[userInfo.userHandle] = userInfo
+ return userInfo.userHandle
+ }
+
+ fun getUserOrNull(handle: UserHandle): UserInfo? = userInfoMap[handle]
+
+ fun getUser(handle: UserHandle): UserInfo =
+ requireNotNull(getUserOrNull(handle)) {
+ "Expected userInfoMap to contain an entry for $handle"
+ }
+
+ fun setQuietMode(user: UserHandle, quietMode: Boolean) {
+ userInfoMap[user]?.also {
+ it.flags =
+ if (quietMode) {
+ it.flags or UserInfo.FLAG_QUIET_MODE
+ } else {
+ it.flags and UserInfo.FLAG_QUIET_MODE.inv()
+ }
+ val actions = mutableListOf<String>()
+ if (quietMode) {
+ actions += ACTION_PROFILE_UNAVAILABLE
+ if (it.isManagedProfile) {
+ actions += ACTION_MANAGED_PROFILE_UNAVAILABLE
+ }
+ } else {
+ actions += ACTION_PROFILE_AVAILABLE
+ if (it.isManagedProfile) {
+ actions += ACTION_MANAGED_PROFILE_AVAILABLE
+ }
+ }
+ actions.forEach { action ->
+ eventChannel.trySend(UserEvent(action, user, quietMode))
+ }
+ }
+ }
+
+ fun createProfile(type: ProfileType, parent: UserHandle = primaryUserHandle): UserHandle {
+ val parentUser = getUser(parent)
+ require(!parentUser.isProfile) { "Parent user cannot be a profile" }
+
+ // Ensure the parent user has a valid profileGroupId
+ if (parentUser.profileGroupId == NO_PROFILE_GROUP_ID) {
+ parentUser.profileGroupId = parentUser.id
+ }
+ val id = allocateNextId()
+ val userInfo =
+ UserInfo(id, "", "", FLAG_INITIALIZED or FLAG_PROFILE, type.toUserType()).apply {
+ profileGroupId = parentUser.profileGroupId
+ }
+ userInfoMap[userInfo.userHandle] = userInfo
+ eventChannel.trySend(UserEvent(ACTION_PROFILE_ADDED, userInfo.userHandle))
+ return userInfo.userHandle
+ }
+
+ fun removeProfile(handle: UserHandle): Boolean {
+ return userInfoMap[handle]?.let { user ->
+ require(user.isProfile) { "Only profiles can be removed" }
+ userInfoMap.remove(user.userHandle)
+ eventChannel.trySend(UserEvent(ACTION_PROFILE_REMOVED, user.userHandle))
+ return true
+ }
+ ?: false
+ }
+
+ override fun toString() = buildString {
+ append("State(nextUserId=$nextUserId, userInfoMap=[")
+ userInfoMap.entries.forEach {
+ append("UserHandle[${it.key.identifier}] = ${it.value.debugString},")
+ }
+ append("])")
+ }
+ }
+}
+
+/** A safe mock of [Context] which throws on any unstubbed method call. */
+private fun mockContext(user: UserHandle = UserHandle.SYSTEM): Context {
+ return mock<Context>(withSettings().defaultAnswer(THROWS_EXCEPTION)) {
+ doAnswer(RETURNS_SELF).whenever(this).applicationContext
+ doReturn(user).whenever(this).user
+ doReturn(user.identifier).whenever(this).userId
+ }
+}
+
+private fun FakeUserManager.ProfileType.toUserType(): String {
+ return when (this) {
+ FakeUserManager.ProfileType.WORK -> UserManager.USER_TYPE_PROFILE_MANAGED
+ FakeUserManager.ProfileType.CLONE -> UserManager.USER_TYPE_PROFILE_CLONE
+ FakeUserManager.ProfileType.PRIVATE -> UserManager.USER_TYPE_PROFILE_PRIVATE
+ }
+}
+
+/** A safe mock of [IUserManager] which throws on any unstubbed method call. */
+fun mockService(): IUserManager {
+ return mock<IUserManager>(withSettings().defaultAnswer(THROWS_EXCEPTION))
+}
+
+val UserInfo.debugString: String
+ get() =
+ "UserInfo(id=$id, profileGroupId=$profileGroupId, name=$name, " +
+ "type=$userType, flags=${UserInfo.flagsToString(flags)})"
diff --git a/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt b/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt
new file mode 100644
index 0000000..1ff0ce8
--- /dev/null
+++ b/tests/shared/src/com/android/intentresolver/v2/validation/ValidationResultSubject.kt
@@ -0,0 +1,22 @@
+package com.android.intentresolver.v2.validation
+
+import com.google.common.truth.FailureMetadata
+import com.google.common.truth.IterableSubject
+import com.google.common.truth.Subject
+import com.google.common.truth.Truth.assertAbout
+
+class ValidationResultSubject(metadata: FailureMetadata, private val actual: ValidationResult<*>?) :
+ Subject(metadata, actual) {
+
+ fun isSuccess() = check("isSuccess()").that(actual?.isSuccess()).isTrue()
+ fun isFailure() = check("isSuccess()").that(actual?.isSuccess()).isFalse()
+
+ fun value(): Subject = check("value").that(actual?.value)
+
+ fun findings(): IterableSubject = check("findings").that(actual?.findings)
+
+ companion object {
+ fun assertThat(input: ValidationResult<*>): ValidationResultSubject =
+ assertAbout(::ValidationResultSubject).that(input)
+ }
+}