diff --git a/core/java/android/app/InstantAppResolverService.java b/core/java/android/app/InstantAppResolverService.java index a413c606b76f1..0cd030e0f6998 100644 --- a/core/java/android/app/InstantAppResolverService.java +++ b/core/java/android/app/InstantAppResolverService.java @@ -35,6 +35,7 @@ import android.os.UserHandle; import android.util.Log; import android.util.Slog; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.SomeArgs; import java.util.Arrays; @@ -297,7 +298,10 @@ public abstract class InstantAppResolverService extends Service { public static final class InstantAppResolutionCallback { private final IRemoteCallback mCallback; private final int mSequence; - InstantAppResolutionCallback(int sequence, IRemoteCallback callback) { + + /** @hide **/ + @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE) + public InstantAppResolutionCallback(int sequence, IRemoteCallback callback) { mCallback = callback; mSequence = sequence; } diff --git a/core/java/android/app/TEST_MAPPING b/core/java/android/app/TEST_MAPPING index c585b5fcdc19d..7b45b725f5b6e 100644 --- a/core/java/android/app/TEST_MAPPING +++ b/core/java/android/app/TEST_MAPPING @@ -48,6 +48,10 @@ } ], "file_patterns": ["INotificationManager\\.aidl"] + }, + { + "name": "FrameworksInstantAppResolverTests", + "file_patterns": ["(/|^)InstantAppResolve[^/]*"] } ], "postsubmit": [ diff --git a/core/java/android/content/pm/TEST_MAPPING b/core/java/android/content/pm/TEST_MAPPING index df4ae097a158a..0549c34cc034b 100644 --- a/core/java/android/content/pm/TEST_MAPPING +++ b/core/java/android/content/pm/TEST_MAPPING @@ -9,5 +9,11 @@ { "path": "system/apex/tests" } + ], + "presubmit": [ + { + "name": "FrameworksInstantAppResolverTests", + "file_patterns": ["(/|^)InstantApp[^/]*"] + } ] } diff --git a/core/tests/InstantAppResolverTests/Android.bp b/core/tests/InstantAppResolverTests/Android.bp new file mode 100644 index 0000000000000..7b01010ebb2be --- /dev/null +++ b/core/tests/InstantAppResolverTests/Android.bp @@ -0,0 +1,32 @@ +// +// Copyright (C) 2019 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. +// + +android_test { + name: "FrameworksInstantAppResolverTests", + srcs: [ "src/**/*.kt" ], + libs: [ + "android.test.runner", + "android.test.base", + ], + platform_apis: true, + static_libs: [ + "androidx.test.ext.junit", + "androidx.test.rules", + "mockito-target-minus-junit4", + "truth-prebuilt", + ], + test_suites: ["device-tests"], +} diff --git a/core/tests/InstantAppResolverTests/AndroidManifest.xml b/core/tests/InstantAppResolverTests/AndroidManifest.xml new file mode 100644 index 0000000000000..f95978b5a63e9 --- /dev/null +++ b/core/tests/InstantAppResolverTests/AndroidManifest.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + diff --git a/core/tests/InstantAppResolverTests/AndroidTest.xml b/core/tests/InstantAppResolverTests/AndroidTest.xml new file mode 100644 index 0000000000000..fcc6344efd836 --- /dev/null +++ b/core/tests/InstantAppResolverTests/AndroidTest.xml @@ -0,0 +1,29 @@ + + + + + diff --git a/core/tests/InstantAppResolverTests/src/android/app/instantapp/resolver/test/ResolverServiceMethodFallbackTest.kt b/core/tests/InstantAppResolverTests/src/android/app/instantapp/resolver/test/ResolverServiceMethodFallbackTest.kt new file mode 100644 index 0000000000000..2a17ef210d360 --- /dev/null +++ b/core/tests/InstantAppResolverTests/src/android/app/instantapp/resolver/test/ResolverServiceMethodFallbackTest.kt @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2019 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 android.app.instantapp.resolver.test + +import android.app.InstantAppResolverService +import android.app.InstantAppResolverService.InstantAppResolutionCallback +import android.content.Intent +import android.content.pm.InstantAppRequestInfo +import android.net.Uri +import android.os.Bundle +import android.os.IRemoteCallback +import android.os.UserHandle +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.mockito.Answers +import org.mockito.Mock +import org.mockito.Mockito.doNothing +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.Mockito.verifyNoMoreInteractions +import org.mockito.MockitoAnnotations +import java.util.UUID +import kotlin.random.Random + +private typealias Method = InstantAppResolverService.(InstantAppRequestInfo) -> Unit + +@Suppress("max-line-length") +@RunWith(Parameterized::class) +class ResolverServiceMethodFallbackTest @Suppress("UNUSED_PARAMETER") constructor( + private val version: Int, + private val methodList: List, + private val info: InstantAppRequestInfo, + // Remaining only used to print human-readable test name + name: String, + isWebIntent: Boolean +) { + + companion object { + // Since the resolution callback class is final, mock the IRemoteCallback and have it throw + // a unique exception to indicate it was called. + class TestRemoteCallbackException : Exception() + + private val testIntentWeb = Intent(Intent.ACTION_VIEW, + Uri.parse("https://${this::class.java.canonicalName}.com")) + private val testIntentNotWeb = Intent(Intent.ACTION_VIEW, + Uri.parse("content://${this::class.java.canonicalName}")) + + private val testRemoteCallback = object : IRemoteCallback { + override fun sendResult(data: Bundle?) = throw TestRemoteCallbackException() + override fun asBinder() = throw UnsupportedOperationException() + } + private val testResolutionCallback = InstantAppResolutionCallback(0, testRemoteCallback) + private val testArray = IntArray(10) { Random.nextInt() } + private val testToken = UUID.randomUUID().toString() + private val testUser = UserHandle(Integer.MAX_VALUE) + private val testInfoWeb = InstantAppRequestInfo(testIntentWeb, testArray, testUser, + false, testToken) + private val testInfoNotWeb = InstantAppRequestInfo(testIntentNotWeb, testArray, testUser, + false, testToken) + + // Each section defines methods versions with later definitions falling back to + // earlier definitions. Each block receives an [InstantAppResolverService] and invokes + // the appropriate version with the test data defined above. + private val infoOne: Method = { onGetInstantAppResolveInfo(testArray, testToken, + testResolutionCallback) } + private val infoTwo: Method = { onGetInstantAppResolveInfo(it.intent, testArray, testToken, + testResolutionCallback) } + private val infoThree: Method = { onGetInstantAppResolveInfo(it.intent, testArray, testUser, + testToken, testResolutionCallback) } + private val infoFour: Method = { onGetInstantAppResolveInfo(it, testResolutionCallback) } + + private val filterOne: Method = { onGetInstantAppIntentFilter(testArray, testToken, + testResolutionCallback) } + private val filterTwo: Method = { onGetInstantAppIntentFilter(it.intent, testArray, + testToken, testResolutionCallback) } + private val filterThree: Method = { onGetInstantAppIntentFilter(it.intent, testArray, + testUser, testToken, testResolutionCallback) } + private val filterFour: Method = { onGetInstantAppIntentFilter(it, testResolutionCallback) } + + private val infoList = listOf(infoOne, infoTwo, infoThree, infoFour) + private val filterList = listOf(filterOne, filterTwo, filterThree, filterFour) + + @JvmStatic + @Parameterized.Parameters(name = "{3} version {0}, isWeb = {4}") + fun parameters(): Array> { + // Sanity check that web intent logic hasn't changed + assertThat(testInfoWeb.intent.isWebIntent).isTrue() + assertThat(testInfoNotWeb.intent.isWebIntent).isFalse() + + // Declare all the possible params + val versions = Array(5) { it } + val methods = arrayOf("ResolveInfo" to infoList, "IntentFilter" to filterList) + val infos = arrayOf(testInfoWeb, testInfoNotWeb) + + // FlatMap params into every possible combination + return infos.flatMap { info -> + methods.flatMap { (name, methods) -> + versions.map { version -> + arrayOf(version, methods, info, name, info.intent.isWebIntent) + } + } + }.toTypedArray() + } + } + + @field:Mock(answer = Answers.CALLS_REAL_METHODS) + lateinit var mockService: InstantAppResolverService + + @get:Rule + val expectedException = ExpectedException.none() + + @Before + fun setUpMocks() { + MockitoAnnotations.initMocks(this) + } + + @Test + fun onGetInstantApp() { + if (version == 0) { + // No version of the API was implemented, so expect terminal case + if (info.intent.isWebIntent) { + // If web intent, terminal is total failure + expectedException.expect(IllegalStateException::class.java) + } else { + // Otherwise, terminal is a fail safe by calling [testRemoteCallback] + expectedException.expect(TestRemoteCallbackException::class.java) + } + } else if (version < 2 && !info.intent.isWebIntent) { + // Starting from v2, if resolving a non-web intent and a v2+ method isn't implemented, + // it fails safely by calling [testRemoteCallback] + expectedException.expect(TestRemoteCallbackException::class.java) + } + + // Version 1 is the first method (index 0) + val methodIndex = version - 1 + + // Implement a method if necessary + methodList.getOrNull(methodIndex)?.invoke(doNothing().`when`(mockService), info) + + // Call the latest API + methodList.last().invoke(mockService, info) + + // Check all methods before implemented method are never called + (0 until methodIndex).forEach { + methodList[it].invoke(verify(mockService, never()), info) + } + + // Check all methods from implemented method are called + (methodIndex until methodList.size).forEach { + methodList[it].invoke(verify(mockService), info) + } + + verifyNoMoreInteractions(mockService) + } +}