Merge "Add PackageInfoCompat signature verification APIs" into androidx-master-dev
diff --git a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
index eba1f5c..d89b288 100644
--- a/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
+++ b/buildSrc/src/main/kotlin/androidx/build/dependencies/Dependencies.kt
@@ -42,6 +42,7 @@
 const val DAGGER = "com.google.dagger:dagger:2.29.1"
 const val DAGGER_COMPILER = "com.google.dagger:dagger-compiler:2.29.1"
 const val DEXMAKER_MOCKITO = "com.linkedin.dexmaker:dexmaker-mockito:2.25.0"
+const val DEXMAKER_MOCKITO_INLINE = "com.linkedin.dexmaker:dexmaker-mockito-inline:2.25.0"
 const val ESPRESSO_CONTRIB = "androidx.test.espresso:espresso-contrib:3.3.0"
 const val ESPRESSO_CORE = "androidx.test.espresso:espresso-core:3.3.0"
 const val ESPRESSO_INTENTS = "androidx.test.espresso:espresso-intents:3.3.0"
diff --git a/core/core/api/current.txt b/core/core/api/current.txt
index e24546b..9935027 100644
--- a/core/core/api/current.txt
+++ b/core/core/api/current.txt
@@ -1037,6 +1037,8 @@
 
   public final class PackageInfoCompat {
     method public static long getLongVersionCode(android.content.pm.PackageInfo);
+    method public static java.util.List<android.content.pm.Signature!> getSignatures(android.content.pm.PackageManager, String) throws android.content.pm.PackageManager.NameNotFoundException;
+    method public static boolean hasSignatures(android.content.pm.PackageManager, String, @Size(min=1) java.util.Map<byte[]!,java.lang.Integer!>, boolean) throws android.content.pm.PackageManager.NameNotFoundException;
   }
 
   public final class PermissionInfoCompat {
diff --git a/core/core/api/public_plus_experimental_current.txt b/core/core/api/public_plus_experimental_current.txt
index d897a76..dc43509 100644
--- a/core/core/api/public_plus_experimental_current.txt
+++ b/core/core/api/public_plus_experimental_current.txt
@@ -1037,6 +1037,8 @@
 
   public final class PackageInfoCompat {
     method public static long getLongVersionCode(android.content.pm.PackageInfo);
+    method public static java.util.List<android.content.pm.Signature!> getSignatures(android.content.pm.PackageManager, String) throws android.content.pm.PackageManager.NameNotFoundException;
+    method public static boolean hasSignatures(android.content.pm.PackageManager, String, @Size(min=1) java.util.Map<byte[]!,java.lang.Integer!>, boolean) throws android.content.pm.PackageManager.NameNotFoundException;
   }
 
   public final class PermissionInfoCompat {
diff --git a/core/core/api/restricted_current.txt b/core/core/api/restricted_current.txt
index 752422a..6e89c98 100644
--- a/core/core/api/restricted_current.txt
+++ b/core/core/api/restricted_current.txt
@@ -1143,6 +1143,8 @@
 
   public final class PackageInfoCompat {
     method public static long getLongVersionCode(android.content.pm.PackageInfo);
+    method public static java.util.List<android.content.pm.Signature!> getSignatures(android.content.pm.PackageManager, String) throws android.content.pm.PackageManager.NameNotFoundException;
+    method public static boolean hasSignatures(android.content.pm.PackageManager, String, @Size(min=1) java.util.Map<byte[]!,java.lang.Integer!>, boolean) throws android.content.pm.PackageManager.NameNotFoundException;
   }
 
   public final class PermissionInfoCompat {
diff --git a/core/core/build.gradle b/core/core/build.gradle
index 7790a23..eabe679 100644
--- a/core/core/build.gradle
+++ b/core/core/build.gradle
@@ -26,13 +26,19 @@
     androidTestImplementation(TRUTH)
     androidTestImplementation(ESPRESSO_CORE, libs.exclude_for_espresso)
     androidTestImplementation(MOCKITO_CORE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+
+    // Including both dexmakers allows support for all API levels plus final mocking support on
+    // API 28+. The implementation is swapped based on the finality of the mock type. This
+    // delegation is handled manually inside androidx.core.util.mockito.CustomMockMaker.
     androidTestImplementation(DEXMAKER_MOCKITO, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
+    androidTestImplementation(DEXMAKER_MOCKITO_INLINE, libs.exclude_bytebuddy) // DexMaker has it"s own MockMaker
     androidTestImplementation("androidx.appcompat:appcompat:1.1.0") {
         exclude group: 'androidx.core', module: 'core'
     }
     androidTestImplementation project(':internal-testutils-runtime'), {
         exclude group: 'androidx.core', module: 'core'
     }
+    androidTestImplementation project(':internal-testutils-mockito')
 
     testImplementation(ANDROIDX_TEST_CORE)
     testImplementation(ANDROIDX_TEST_RUNNER)
@@ -55,6 +61,14 @@
     buildTypes.all {
         consumerProguardFiles 'proguard-rules.pro'
     }
+
+    packagingOptions {
+        // Drop the file from external dependencies, preferring the local file inside androidTest
+        pickFirsts = [
+                "mockito-extensions/org.mockito.plugins.MockMaker",
+                "mockito-extensions/org.mockito.plugins.StackTraceCleanerProvider"
+        ]
+    }
 }
 
 androidx {
diff --git a/core/core/src/androidTest/java/androidx/core/content/pm/PackageInfoCompatHasSignaturesTest.kt b/core/core/src/androidTest/java/androidx/core/content/pm/PackageInfoCompatHasSignaturesTest.kt
new file mode 100644
index 0000000..53f5500
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/content/pm/PackageInfoCompatHasSignaturesTest.kt
@@ -0,0 +1,421 @@
+/*
+ * Copyright 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 androidx.core.content.pm
+
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.content.pm.Signature
+import android.content.pm.SigningInfo
+import android.os.Build
+import androidx.core.content.pm.PackageInfoCompatHasSignaturesTest.Companion.Params.QueryType
+import androidx.core.content.pm.PackageInfoCompatHasSignaturesTest.MockCerts.MockSignatures
+import androidx.core.content.pm.PackageInfoCompatHasSignaturesTest.MockCerts.MockSigningInfo
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import androidx.testutils.mockito.mockThrowOnUnmocked
+import androidx.testutils.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+import org.mockito.Mockito.any
+import org.mockito.Mockito.anyInt
+import org.mockito.Mockito.eq
+import org.mockito.internal.util.reflection.FieldSetter
+import java.security.MessageDigest
+
+/**
+ * Verifies [PackageInfoCompat.hasSignatures].
+ *
+ * Due to testability restrictions with the [SigningInfo] and [Signature] classes and
+ * infrastructure for install test packages in a device test, this test uses mocked classes to
+ * verify the correct method calls. Mocking in general is preferable to signing several test
+ * packages as this isolates the test parameters to inside the test class.
+ *
+ * As final class mocking is only available starting from [Build.VERSION_CODES.P], this test
+ * manually runs itself for both [Build.VERSION_CODES.O] and the current device SDK version
+ * by swapping [Build.VERSION.SDK_INT].
+ */
+@SdkSuppress(minSdkVersion = Build.VERSION_CODES.P)
+@LargeTest
+@RunWith(Parameterized::class)
+class PackageInfoCompatHasSignaturesTest {
+
+    companion object {
+        // Following are random public certs (effectively random strings) as this test does not
+        // validate the actual signature integrity. Only the fact that the hashes and comparisons
+        // work and return the correct values.
+
+        private const val CERT_1 = "2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494" +
+            "9422b44434341574767417749424167495548384f42374c355a53594f7852577056774e454f4c336" +
+            "5726c5077774451594a4b6f5a496876634e4151454c0a425141774454454c4d416b4741315545426" +
+            "84d4356564d774942634e4d6a41774f5449794d6a4d774d6a557a576867504d7a41794d4441784d6" +
+            "a51794d7a41790a4e544e614d413078437a414a42674e5642415954416c56544d4947664d4130474" +
+            "35371475349623344514542415155414134474e4144434269514b42675144450a6f3650386341636" +
+            "c77734a646e773457415a755a685244795031556473334d5766703738434448344548614d682f393" +
+            "54a7941316e5a776e2f644174747375640a6e464356713065592b32736d373663334d454a542b456" +
+            "86b443170792f6148324f366c3639314d2b334e7a6a616272752f4c457451364d736232494553454" +
+            "2690a7a63415350756a4a635458586b346a6d44535a4d6d6359653259466d506b633151534f31387" +
+            "875446a514944415141426f314d775554416442674e56485134450a4667515534746446716839634" +
+            "16d4d35707665674d514265476c442b4b774d77487759445652306a42426777466f4155347464467" +
+            "1683963416d4d35707665670a4d514265476c442b4b774d7744775944565230544151482f4241557" +
+            "7417745422f7a414e42676b71686b6947397730424151734641414f426751436a70535a760a4d546" +
+            "76f584c3042304b393577486b61353476685a6c2f5a4c6231427243752f686431746761736766434" +
+            "9566d4d34754335614774697a422b4a3335462f4f2b0a5344572b62585854314c634b4951795a625" +
+            "66772335537736c39584f5773322f55474a33653739555948473144656f497235367534475074312" +
+            "b5338746347500a464b36496e4e42534a56584a325231446b7a754e5843476d63766a4d7a4e426b7" +
+            "47034504d773d3d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a"
+
+        private const val CERT_2 = "2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494" +
+            "9422b4443434157476741774942416749554739426d31332f566c61747370564461486d46574f6c7" +
+            "65a696b45774451594a4b6f5a496876634e4151454c0a425141774454454c4d416b4741315545426" +
+            "84d4356564d774942634e4d6a41774f5449794d6a4d774d7a4130576867504d7a41794d4441784d6" +
+            "a51794d7a417a0a4d4452614d413078437a414a42674e5642415954416c56544d4947664d4130474" +
+            "35371475349623344514542415155414134474e4144434269514b42675144510a595875516f67783" +
+            "4324c77572b3568656b6f694c50507178655964494250555668743442584d6e494f7835434449665" +
+            "96d6461424650645865685546395036340a7974576a2b316963677452776e4c2f62487a525953413" +
+            "637514c39492b7a45456e2b7342777779566f51325858644c51546f49394f537a54444375744f4c4" +
+            "2430a6f65754f46727373566642676f4d6838685a4f5a31775448442f706c6b38543541384463313" +
+            "7505159774944415141426f314d775554416442674e56485134450a466751554c7a5754614673507" +
+            "23161526d304166556569704b346d6d75785977487759445652306a42426777466f41554c7a57546" +
+            "1467350723161526d3041660a556569704b346d6d7578597744775944565230544151482f4241557" +
+            "7417745422f7a414e42676b71686b6947397730424151734641414f42675141496147524d0a4d423" +
+            "74c74464957714847542f69766f56572b4f6a58664f477332554f75416455776d7a6b374b7a57727" +
+            "874744639616a355250307756637755625654444e740a464c326b4c3171574450513471613333643" +
+            "34744325555416b49474b724d514668523839756a303438514c7871386a72466f663447324572755" +
+            "85353354d79790a5669573735357038354f50704c635a753939796c2b536d7675633938685170796" +
+            "a6f564f6c773d3d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a"
+
+        private const val CERT_3 = "2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494" +
+            "9422b444343415747674177494241674955484f6f6d736b2b642f79336c4854434e3371675166413" +
+            "335646e51774451594a4b6f5a496876634e4151454c0a425141774454454c4d416b4741315545426" +
+            "84d4356564d774942634e4d6a41774f5449794d6a4d774d7a4578576867504d7a41794d4441784d6" +
+            "a51794d7a417a0a4d5446614d413078437a414a42674e5642415954416c56544d4947664d4130474" +
+            "35371475349623344514542415155414134474e4144434269514b42675144520a6355494b3450724" +
+            "d396930685834546168485055334c575665677630546668307273785153637042496a73306b6a6a6" +
+            "34e78342f31363948674c70476f5a334d0a63424350612f61574a4778794c7145514537774b77644" +
+            "a6148596b4b56706e55706a4d313030634b6b6b4a356565336b56414958746f2f6c436b626b554a6" +
+            "1730a47334f71307677774936656130707336684350313863693066727844766d6630536e2b54615" +
+            "2396a31774944415141426f314d775554416442674e56485134450a466751554464553443534c393" +
+            "746516d774954555332444e4472356b464c5177487759445652306a42426777466f4155446455344" +
+            "3534c393746516d774954550a5332444e4472356b464c517744775944565230544151482f4241557" +
+            "7417745422f7a414e42676b71686b6947397730424151734641414f426751437a567054470a59796" +
+            "1444a6c456279447775443457616b38306d5a4153613534646a69446e6335324d30776e614145776" +
+            "84e496978623547465a50357878337859302f494c520a6a72544a6e6744377643586c556f5256384" +
+            "379794653534169306f3977544b475554434d762b303446324a6c474a4b7665486a346d473544746" +
+            "6335331574b520a6644454a792b456376563658314b716a73466d524a4a6d7a30347464525363304" +
+            "c74622f2f673d3d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a"
+
+        private const val CERT_4 = "2d2d2d2d2d424547494e2043455254494649434154452d2d2d2d2d0a4d494" +
+            "9422b444343415747674177494241674955526f49427173485858413246636938324d412b73706d5" +
+            "6684d7634774451594a4b6f5a496876634e4151454c0a425141774454454c4d416b4741315545426" +
+            "84d4356564d774942634e4d6a41774f5449304d546b784d7a4d77576867504d7a41794d4441784d6" +
+            "a59784f54457a0a4d7a42614d413078437a414a42674e5642415954416c56544d4947664d4130474" +
+            "35371475349623344514542415155414134474e4144434269514b426751432b0a306549525344554" +
+            "e4972666663486f4d61697431705a6b39534769616c41694e56484b6e4950466876754233497a475" +
+            "05a4d476f6d6a3956534667766e7047360a4f7166453033734e575949503944776772485546692f6" +
+            "e356f45504f742f617643746b4b71623957737531777643746b37795163354d626276644e6b78344" +
+            "c740a3679724a7151545946424479356c49624c67454b4d744a5344584246356a38747173326e705" +
+            "145514f774944415141426f314d775554416442674e56485134450a4667515557354c6e5751344f3" +
+            "2523576515731355452564955726f744e476b77487759445652306a42426777466f415557354c6e5" +
+            "751344f32523576515731350a5452564955726f744e476b7744775944565230544151482f4241557" +
+            "7417745422f7a414e42676b71686b6947397730424151734641414f42675141685768654f0a77525" +
+            "85339365536444a705459597374754741634a77414e434d3244503938325653613136766e7769653" +
+            "842477a6a724a51794f354e4a4846637a4f566e54330a626834496a65337751787551334138566e4" +
+            "54d334230683553373030524c524337373936555a787465683874304f6c7a515031703358452b776" +
+            "571797a6c4e330a4f494f435a486f6b494b6f4957527964734e58547a55353448625850597275556" +
+            "e71574451673d3d0a2d2d2d2d2d454e442043455254494649434154452d2d2d2d2d0a"
+
+        private const val TEST_PKG_NAME = "com.example.app"
+
+        private val nullCerts: List<Certificate>? = null
+        private val emptyCerts = emptyList<Certificate>()
+        private val multiSignerCerts = listOf(CERT_1, CERT_3).map(::Certificate)
+        private val pastHistoryCerts = listOf(CERT_1, CERT_2, CERT_3).map(::Certificate)
+        private val noHistoryCerts = listOf(CERT_1).map(::Certificate)
+        private val extraCert = Certificate(CERT_4)
+
+        data class Params(
+            val sdkVersion: Int,
+            val mockCerts: MockCerts,
+            val queryType: QueryType,
+            val certType: CertType,
+            val matchExact: Boolean
+        ) {
+            enum class CertType(val flag: Int) {
+                X509(PackageManager.CERT_INPUT_RAW_X509),
+                SHA256(PackageManager.CERT_INPUT_SHA256)
+            }
+
+            enum class QueryType { NONE, EXACT_COUNT, FEWER, MORE }
+
+            val queryCerts = when (queryType) {
+                QueryType.NONE -> emptyList()
+                QueryType.EXACT_COUNT -> mockCerts.certificates.orEmpty()
+                QueryType.FEWER -> mockCerts.certificates!!.drop(1)
+                QueryType.MORE -> mockCerts.certificates.orEmpty() + extraCert
+            }
+
+            val success = when (mockCerts.certificates) {
+                // If the certs returned in the packgae are null/empty, the query can never succeed
+                nullCerts, emptyCerts -> false
+                // Otherwise success depends on what the query set is
+                else -> when (queryType) {
+                    // None always fails, to ensure verify cannot accidentally succeed
+                    QueryType.NONE -> false
+                    // If querying the exact same certs, then always succeed
+                    QueryType.EXACT_COUNT -> true
+                    // Otherwise if querying fewer, only succeed if not matching exactly all
+                    QueryType.FEWER -> !matchExact
+                    // Otherwise matching more than available, which should always fail
+                    QueryType.MORE -> false
+                }
+            }
+
+            // For naming the test method variant
+            override fun toString(): String {
+                val certsVariant = when (mockCerts.certificates) {
+                    nullCerts -> "null"
+                    emptyCerts -> "empty"
+                    multiSignerCerts -> "multiSign"
+                    pastHistoryCerts -> "pastHistory"
+                    noHistoryCerts -> "noHistory"
+                    else -> throw IllegalArgumentException("Invalid mockCerts $mockCerts")
+                }
+
+                @Suppress("DEPRECATION")
+                val queryFlag = when (val flag = mockCerts.flag) {
+                    PackageManager.GET_SIGNATURES -> "GET_SIGNATURES"
+                    PackageManager.GET_SIGNING_CERTIFICATES -> "GET_SIGNING_CERTIFICATES"
+                    else -> throw IllegalArgumentException("Invalid certs type $flag")
+                }
+
+                val sdkVersionName = sdkVersion.takeUnless { it == Build.VERSION.SDK_INT }
+                    ?: "current"
+
+                return "$queryFlag," +
+                    "$certsVariant${certType.name}," +
+                    "sdkVersion=$sdkVersionName," +
+                    "query=$queryType," +
+                    "matchExact=$matchExact"
+            }
+        }
+
+        @Suppress("DEPRECATION")
+        @JvmStatic
+        @Parameterized.Parameters(name = "{0}")
+        fun parameters(): Array<Params> {
+            return listOf(
+                listOf(
+                    MockSignatures(nullCerts),
+                    MockSignatures(emptyCerts),
+                    MockSignatures(multiSignerCerts),
+                    // Legacy GET_SIGNATURES cannot include certificate history
+                    MockSignatures(noHistoryCerts)
+                ).associateWith { Build.VERSION_CODES.O_MR1 },
+                listOf(
+                    MockSigningInfo(
+                        multiSigners = false,
+                        hasHistory = null,
+                        contentsSigners = null,
+                        certHistory = null
+                    ),
+                    MockSigningInfo(
+                        multiSigners = false,
+                        hasHistory = null,
+                        contentsSigners = null,
+                        certHistory = emptyCerts
+                    ),
+                    MockSigningInfo(
+                        multiSigners = true,
+                        hasHistory = null,
+                        contentsSigners = multiSignerCerts,
+                        certHistory = null
+                    ),
+                    MockSigningInfo(
+                        multiSigners = false,
+                        hasHistory = true,
+                        contentsSigners = null,
+                        certHistory = pastHistoryCerts
+                    ),
+                    MockSigningInfo(
+                        multiSigners = false,
+                        hasHistory = false,
+                        contentsSigners = null,
+                        certHistory = noHistoryCerts
+                    )
+                ).associateWith { Build.VERSION.SDK_INT }
+            )
+                .flatMap {
+                    // Multiply all base params by QueryType, CertType and matchExact values to
+                    // get the complete set of possibilities
+                    it.entries.flatMap { (mockCerts, sdkVersion) ->
+                        listOfNotNull(
+                            QueryType.NONE,
+                            QueryType.EXACT_COUNT,
+                            QueryType.MORE,
+                            QueryType.FEWER.takeIf {
+                                val certificates = mockCerts.certificates
+                                !certificates.isNullOrEmpty() && certificates.size > 1
+                            }
+                        ).flatMap { queryType ->
+                            listOf(
+                                Params.CertType.X509,
+                                Params.CertType.SHA256
+                            ).flatMap { certType ->
+                                listOf(true, false).map { matchExact ->
+                                    Params(sdkVersion, mockCerts, queryType, certType, matchExact)
+                                }
+                            }
+                        }
+                    }
+                }
+                .toTypedArray()
+        }
+
+        private val sdkIntField = Build.VERSION::class.java.getDeclaredField("SDK_INT")
+
+        private fun setDeviceSdkVersion(sdkVersion: Int) {
+            FieldSetter.setField(Build.VERSION::class.java, sdkIntField, sdkVersion)
+            assertThat(Build.VERSION.SDK_INT).isEqualTo(sdkVersion)
+        }
+    }
+
+    @Parameterized.Parameter(0)
+    lateinit var params: Params
+
+    private var savedSdkVersion: Int = Build.VERSION.SDK_INT
+
+    @Before
+    fun saveSdkVersion() {
+        savedSdkVersion = Build.VERSION.SDK_INT
+    }
+
+    @After
+    fun resetSdkVersion() {
+        if (Build.VERSION.SDK_INT != savedSdkVersion) {
+            setDeviceSdkVersion(savedSdkVersion)
+        }
+    }
+
+    @Test
+    fun verify() {
+        val mock = mockPackageManager()
+        val certs = params.queryCerts.map { it.bytes(params.certType) }
+            .associateWith { params.certType.flag }
+
+        // SDK_INT must be changed after mocks are built, since MockMaker will do an SDK check
+        if (Build.VERSION.SDK_INT != params.sdkVersion) {
+            setDeviceSdkVersion(params.sdkVersion)
+        }
+
+        assertThat(PackageInfoCompat.hasSignatures(mock, TEST_PKG_NAME, certs, params.matchExact))
+            .isEqualTo(params.success)
+
+        if (Build.VERSION.SDK_INT != savedSdkVersion) {
+            setDeviceSdkVersion(savedSdkVersion)
+        }
+    }
+
+    private fun mockPackageManager() = mockThrowOnUnmocked<PackageManager> {
+        val mockCerts = params.mockCerts
+        whenever(getPackageInfo(TEST_PKG_NAME, params.mockCerts.flag)) {
+            PackageInfo().apply {
+                when (mockCerts) {
+                    is MockSignatures -> {
+                        @Suppress("DEPRECATION")
+                        signatures = mockCerts.certificates?.map { it.signature }?.toTypedArray()
+                    }
+                    is MockSigningInfo -> {
+                        signingInfo = mockThrowOnUnmocked<SigningInfo> {
+                            whenever(hasMultipleSigners()) { mockCerts.multiSigners }
+
+                            mockCerts.hasHistory?.let {
+                                // Only allow this method if params specify it. to ensure past
+                                // certificates aren't considered when multi-signing is enabled
+                                whenever(hasPastSigningCertificates()) { it }
+                            }
+
+                            mockCerts.contentsSigners
+                                ?.map { it.signature }
+                                ?.toTypedArray()
+                                ?.let { whenever(apkContentsSigners) { it } }
+
+                            // Only allow fetching history if not multi signed
+                            if (!hasMultipleSigners()) {
+                                whenever(signingCertificateHistory) {
+                                    mockCerts.certHistory
+                                        ?.map { it.signature }
+                                        ?.toTypedArray()
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+
+        if (!params.matchExact && params.sdkVersion >= Build.VERSION_CODES.P) {
+            whenever(hasSigningCertificate(eq(TEST_PKG_NAME), any(), anyInt())) {
+                val certs = params.mockCerts.certificates?.asSequence() ?: return@whenever false
+                val query = getArgument(1) as ByteArray
+                val certType = when (val type = getArgument(2) as Int) {
+                    PackageManager.CERT_INPUT_RAW_X509 -> Params.CertType.X509
+                    PackageManager.CERT_INPUT_SHA256 -> Params.CertType.SHA256
+                    else -> throw IllegalArgumentException("Invalid type $type")
+                }
+                certs.map { it.bytes(certType) }.contains(query)
+            }
+        }
+    }
+
+    sealed class MockCerts {
+        abstract val certificates: List<Certificate>?
+        abstract val flag: Int
+
+        data class MockSignatures(override val certificates: List<Certificate>?) : MockCerts() {
+            @Suppress("DEPRECATION")
+            override val flag = PackageManager.GET_SIGNATURES
+        }
+
+        data class MockSigningInfo(
+            val multiSigners: Boolean,
+            val hasHistory: Boolean?,
+            val contentsSigners: List<Certificate>?,
+            val certHistory: List<Certificate>?
+        ) : MockCerts() {
+            override val certificates = contentsSigners ?: certHistory
+            override val flag = PackageManager.GET_SIGNING_CERTIFICATES
+        }
+    }
+
+    /**
+     * [Signature] wrapper to cache arrays and digests.
+     */
+    data class Certificate(val publicCertX509: String) {
+        val signature = Signature(publicCertX509)
+        private val x509Bytes = signature.toByteArray()!!
+        private val sha256Bytes = MessageDigest.getInstance("SHA256").digest(x509Bytes)
+
+        fun bytes(certType: Params.CertType): ByteArray = when (certType) {
+            Params.CertType.X509 -> x509Bytes
+            Params.CertType.SHA256 -> sha256Bytes
+        }
+    }
+}
\ No newline at end of file
diff --git a/core/core/src/androidTest/java/androidx/core/content/pm/PackageInfoCompatTest.java b/core/core/src/androidTest/java/androidx/core/content/pm/PackageInfoCompatTest.java
index 3ea56a4..b0f7a5b 100644
--- a/core/core/src/androidTest/java/androidx/core/content/pm/PackageInfoCompatTest.java
+++ b/core/core/src/androidTest/java/androidx/core/content/pm/PackageInfoCompatTest.java
@@ -18,17 +18,34 @@
 
 import static android.os.Build.VERSION_CODES.P;
 
+import static com.google.common.truth.Truth.assertThat;
+
 import static org.junit.Assert.assertEquals;
 
+import android.content.Context;
 import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature;
 
+import androidx.collection.ArrayMap;
 import androidx.test.filters.SdkSuppress;
 import androidx.test.filters.SmallTest;
+import androidx.test.platform.app.InstrumentationRegistry;
 
 import org.junit.Test;
 
+import java.util.List;
+import java.util.Map;
+
 @SmallTest
 public final class PackageInfoCompatTest {
+
+    private static final String NON_EXISTENT_PACKAGE = "com.example.app.non_existent_package_name";
+
+    private final Context mContext =
+            InstrumentationRegistry.getInstrumentation().getTargetContext();
+    private PackageManager mPackageManager = mContext.getPackageManager();
+
     @Test
     public void getLongVersionCodeLowerBitsOnly() {
         PackageInfo info = new PackageInfo();
@@ -45,4 +62,49 @@
 
         assertEquals(Long.MAX_VALUE, PackageInfoCompat.getLongVersionCode(info));
     }
+
+    /**
+     * Only verifies non-null return, to avoid hard coding certs. Actual equality and proper
+     * return value is verified as part of {@link PackageInfoCompatHasSignaturesTest}.
+     */
+    @Test
+    public void getSignaturesNonNull() throws PackageManager.NameNotFoundException {
+        List<Signature> signatures = PackageInfoCompat.getSignatures(mPackageManager,
+                mContext.getPackageName());
+
+        assertThat(signatures).isNotEmpty();
+    }
+
+    @Test(expected = PackageManager.NameNotFoundException.class)
+    public void getSignaturesThrowOnNotFound() throws PackageManager.NameNotFoundException {
+        PackageInfoCompat.getSignatures(mPackageManager, NON_EXISTENT_PACKAGE);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void hasSignaturesThrowOnInvalidType() throws PackageManager.NameNotFoundException {
+        Map<byte[], Integer> map = new ArrayMap<>(1);
+        map.put(new byte[100], PackageManager.CERT_INPUT_SHA256 + 1);
+        PackageInfoCompat.hasSignatures(mPackageManager, mContext.getPackageName(), map, false);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void hasSignaturesThrowOnNullBytes() throws PackageManager.NameNotFoundException {
+        Map<byte[], Integer> map = new ArrayMap<>(1);
+        map.put(null, PackageManager.CERT_INPUT_SHA256);
+        PackageInfoCompat.hasSignatures(mPackageManager, mContext.getPackageName(), map, false);
+    }
+
+    @Test(expected = IllegalArgumentException.class)
+    public void hasSignaturesThrowOnNullType() throws PackageManager.NameNotFoundException {
+        Map<byte[], Integer> map = new ArrayMap<>(1);
+        map.put(new byte[100], null);
+        PackageInfoCompat.hasSignatures(mPackageManager, mContext.getPackageName(), map, false);
+    }
+
+    @Test(expected = PackageManager.NameNotFoundException.class)
+    public void hasSignaturesThrowOnNotFound() throws PackageManager.NameNotFoundException {
+        Map<byte[], Integer> map = new ArrayMap<>(1);
+        map.put(new byte[100], PackageManager.CERT_INPUT_SHA256);
+        PackageInfoCompat.hasSignatures(mPackageManager, NON_EXISTENT_PACKAGE, map, false);
+    }
 }
diff --git a/core/core/src/androidTest/java/androidx/core/util/mockito/CustomMockMaker.kt b/core/core/src/androidTest/java/androidx/core/util/mockito/CustomMockMaker.kt
new file mode 100644
index 0000000..bfa1be2
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/util/mockito/CustomMockMaker.kt
@@ -0,0 +1,116 @@
+/*
+ * Copyright 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 androidx.core.util.mockito
+
+import android.os.Build
+import com.android.dx.mockito.DexmakerMockMaker
+import com.android.dx.mockito.inline.InlineDexmakerMockMaker
+import org.mockito.invocation.MockHandler
+import org.mockito.mock.MockCreationSettings
+import org.mockito.plugins.InlineMockMaker
+import org.mockito.plugins.MockMaker
+
+/**
+ * There is no official supported method for mixing dexmaker-mockito with dexmaker-mockito-inline,
+ * so this has to be done manually.
+ *
+ * Inside the build.gradle, dexmaker-mockito is taken first and preferred, and this custom
+ * implementation is responsible for delegating to the inline variant should the regular variant
+ * fall to instantiate a mock.
+ *
+ * This allows Mockito to mock final classes on test run on API 28+ devices, while still
+ * functioning for normal non-final mocks API <28.
+ *
+ * This class is placed in the core sources since the use case is rather unique to
+ * [androidx.core.content.pm.PackageInfoCompatHasSignaturesTest], and other testing solutions should
+ * be considered before using this in other modules.
+ */
+class CustomMockMaker : InlineMockMaker {
+
+    companion object {
+        private val MOCK_MAKERS = mutableListOf<MockMaker>(DexmakerMockMaker()).apply {
+            // Inline only works on API 28+
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                this += InlineDexmakerMockMaker()
+            }
+        }
+    }
+
+    override fun <T> createMock(settings: MockCreationSettings<T>, handler: MockHandler<*>): T? {
+        var lastException: Exception? = null
+        MOCK_MAKERS
+            .filter { it.isTypeMockable(settings.typeToMock).mockable() }
+            .forEach {
+                val mock = try {
+                    it.createMock(settings, handler)
+                } catch (e: Exception) {
+                    lastException = e
+                    null
+                }
+
+                if (mock != null) {
+                    return mock
+                }
+            }
+
+        lastException?.let { throw it }
+        return null
+    }
+
+    override fun getHandler(mock: Any?): MockHandler<*>? {
+        MOCK_MAKERS.forEach {
+            val handler = it.getHandler(mock)
+            if (handler != null) {
+                return handler
+            }
+        }
+        return null
+    }
+
+    override fun resetMock(
+        mock: Any?,
+        newHandler: MockHandler<*>?,
+        settings: MockCreationSettings<*>?
+    ) {
+        MOCK_MAKERS.forEach {
+            it.resetMock(mock, newHandler, settings)
+        }
+    }
+
+    override fun isTypeMockable(type: Class<*>?): MockMaker.TypeMockability? {
+        MOCK_MAKERS.forEachIndexed { index, mockMaker ->
+            val mockability = mockMaker.isTypeMockable(type)
+            // Prefer the first mockable instance, or the last one available
+            if (mockability.mockable() || index == MOCK_MAKERS.size - 1) {
+                return mockability
+            }
+        }
+        return null
+    }
+
+    override fun clearMock(mock: Any?) {
+        MOCK_MAKERS.forEach {
+            (it as? InlineMockMaker)?.clearMock(mock)
+        }
+    }
+
+    override fun clearAllMocks() {
+        MOCK_MAKERS.forEach {
+            (it as? InlineMockMaker)?.clearAllMocks()
+        }
+    }
+}
\ No newline at end of file
diff --git a/core/core/src/androidTest/java/androidx/core/util/mockito/CustomStackTraceCleaner.kt b/core/core/src/androidTest/java/androidx/core/util/mockito/CustomStackTraceCleaner.kt
new file mode 100644
index 0000000..d927ef5
--- /dev/null
+++ b/core/core/src/androidTest/java/androidx/core/util/mockito/CustomStackTraceCleaner.kt
@@ -0,0 +1,37 @@
+/*
+ * Copyright 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 androidx.core.util.mockito
+
+import com.android.dx.mockito.DexmakerMockMaker
+import com.android.dx.mockito.inline.DexmakerStackTraceCleaner
+import org.mockito.exceptions.stacktrace.StackTraceCleaner
+import org.mockito.internal.exceptions.stacktrace.DefaultStackTraceCleaner
+
+/**
+ * Similar to [CustomMockMaker], delegates the stack cleaner logic to mix dexmaker-mockito and
+ * dexmaker-mockito-inline.
+ */
+class CustomStackTraceCleaner : StackTraceCleaner {
+
+    companion object {
+        private val CLEANER_WRAPPER = DefaultStackTraceCleaner()
+            .let { DexmakerMockMaker().getStackTraceCleaner(it) }
+            .let { DexmakerStackTraceCleaner().getStackTraceCleaner(it) }
+    }
+
+    override fun isIn(candidate: StackTraceElement?) = CLEANER_WRAPPER.isIn(candidate)
+}
\ No newline at end of file
diff --git a/core/core/src/androidTest/resources/mockito-extensions/org.mockito.plugins.MockMaker b/core/core/src/androidTest/resources/mockito-extensions/org.mockito.plugins.MockMaker
new file mode 100644
index 0000000..7f9e2f4
--- /dev/null
+++ b/core/core/src/androidTest/resources/mockito-extensions/org.mockito.plugins.MockMaker
@@ -0,0 +1 @@
+androidx.core.util.mockito.CustomMockMaker
\ No newline at end of file
diff --git a/core/core/src/androidTest/resources/mockito-extensions/org.mockito.plugins.StackTraceCleaner b/core/core/src/androidTest/resources/mockito-extensions/org.mockito.plugins.StackTraceCleaner
new file mode 100644
index 0000000..d380fb4
--- /dev/null
+++ b/core/core/src/androidTest/resources/mockito-extensions/org.mockito.plugins.StackTraceCleaner
@@ -0,0 +1 @@
+androidx.core.util.mockito.CustomStackTraceCleaner
\ No newline at end of file
diff --git a/core/core/src/main/java/androidx/core/content/pm/PackageInfoCompat.java b/core/core/src/main/java/androidx/core/content/pm/PackageInfoCompat.java
index b7e7452..20ef5e5 100644
--- a/core/core/src/main/java/androidx/core/content/pm/PackageInfoCompat.java
+++ b/core/core/src/main/java/androidx/core/content/pm/PackageInfoCompat.java
@@ -16,10 +16,25 @@
 
 package androidx.core.content.pm;
 
+import android.annotation.SuppressLint;
 import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.Signature;
+import android.content.pm.SigningInfo;
 import android.os.Build;
 
 import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.Size;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
 /** Helper for accessing features in {@link PackageInfo}. */
 public final class PackageInfoCompat {
@@ -38,6 +53,236 @@
         return info.versionCode;
     }
 
+    /**
+     * Retrieve the {@link Signature} array for the given package. This returns some of
+     * certificates, depending on whether the package in question is multi-signed or has signing
+     * history.
+     *
+     * <note>
+     * <p>
+     * Security/identity verification should <b>not</b> be done with this method. This is only
+     * intended to return some array of certificates that correspond to a package.
+     * </p>
+     * <p>
+     * If verification if required, either use
+     * {@link #hasSignatures(PackageManager, String, Map, boolean)} or manually verify the set of
+     * certificates using {@link PackageManager#GET_SIGNING_CERTIFICATES} or
+     * {@link PackageManager#GET_SIGNATURES}.
+     * </p>
+     * </note>
+     *
+     * @param packageManager The {@link PackageManager} instance to query against.
+     * @param packageName    The package to query the {@param packageManager} for. Query by app
+     *                       UID is only supported by manually choosing a package name
+     *                       returned in {@link PackageManager#getPackagesForUid(int)}.
+     * @return an array of certificates the app is signed with
+     * @throws PackageManager.NameNotFoundException if the package cannot be found through the
+     *                                              provided {@param packageManager}
+     */
+    @NonNull
+    public static List<Signature> getSignatures(@NonNull PackageManager packageManager,
+            @NonNull String packageName) throws PackageManager.NameNotFoundException {
+        Signature[] array;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            PackageInfo pkgInfo = packageManager.getPackageInfo(packageName,
+                    PackageManager.GET_SIGNING_CERTIFICATES);
+            SigningInfo signingInfo = pkgInfo.signingInfo;
+            if (Api28Impl.hasMultipleSigners(signingInfo)) {
+                array = Api28Impl.getApkContentsSigners(signingInfo);
+            } else {
+                array = Api28Impl.getSigningCertificateHistory(signingInfo);
+            }
+        } else {
+            // Lint warning's vulnerability is explicitly not handled for this method.
+            @SuppressLint("PackageManagerGetSignatures")
+            PackageInfo pkgInfo = packageManager.getPackageInfo(packageName,
+                    PackageManager.GET_SIGNATURES);
+            array = pkgInfo.signatures;
+        }
+
+        // Framework code implies nullable/empty, although it may be impossible in practice.
+        if (array == null) {
+            return Collections.emptyList();
+        } else {
+            return Arrays.asList(array);
+        }
+    }
+
+    /**
+     * Check if a package on device contains set of a certificates. Supported types are raw X509 or
+     * SHA-256 bytes.
+     *
+     * @param packageManager      The {@link PackageManager} instance to query against.
+     * @param packageName         The package to query the {@param packageManager} for. Query by
+     *                            app UID is only supported by manually choosing a package name
+     *                            returned in {@link PackageManager#getPackagesForUid(int)}.
+     * @param certificatesAndType The bytes of the certificate mapped to the type, either
+     *                            {@link PackageManager#CERT_INPUT_RAW_X509} or
+     *                            {@link PackageManager#CERT_INPUT_SHA256}. A single or multiple
+     *                            certificates may be included.
+     * @param matchExact          Whether or not to check for presence of all signatures exactly.
+     *                            If false, then the check will succeed if the query contains a
+     *                            subset of the package certificates. Matching exactly is strongly
+     *                            recommended when running on devices below
+     *                            {@link Build.VERSION_CODES#LOLLIPOP} due to the fake ID
+     *                            vulnerability that allows a package to be modified to include
+     *                            an unverified signature.
+     * @return true if the package is considered signed by the given certificate set, or false
+     * otherwise
+     * @throws PackageManager.NameNotFoundException if the package cannot be found through the
+     *                                              provided {@param packageManager}
+     */
+    public static boolean hasSignatures(@NonNull PackageManager packageManager,
+            @NonNull String packageName,
+            @Size(min = 1) @NonNull Map<byte[], Integer> certificatesAndType, boolean matchExact)
+            throws PackageManager.NameNotFoundException {
+        // If empty is passed in, return false to prevent accidentally succeeding
+        if (certificatesAndType.isEmpty()) {
+            return false;
+        }
+
+        Set<byte[]> expectedCertBytes = certificatesAndType.keySet();
+
+        // The type has to be checked before any API level branching. If a new type is ever added,
+        // this code should fail and will have to be updated manually. To do otherwise would
+        // introduce a behavioral difference between the API level that added the new type and
+        // devices on prior API levels, which may not be caught by a developer calling this
+        // method if they do not test on an old API level.
+        for (byte[] bytes : expectedCertBytes) {
+            if (bytes == null) {
+                throw new IllegalArgumentException("Cert byte array cannot be null when verifying "
+                        + packageName);
+            }
+            Integer type = certificatesAndType.get(bytes);
+            if (type == null) {
+                throw new IllegalArgumentException("Type must be specified for cert when verifying "
+                        + packageName);
+            }
+
+            switch (type) {
+                case PackageManager.CERT_INPUT_RAW_X509:
+                case PackageManager.CERT_INPUT_SHA256:
+                    break;
+                default:
+                    throw new IllegalArgumentException("Unsupported certificate type " + type
+                            + " when verifying " + packageName);
+            }
+        }
+
+        // getSignatures is called first to throw NameNotFoundException if necessary
+        final List<Signature> signers = getSignatures(packageManager, packageName);
+
+        // The vulnerability requiring matchExact is not necessary on P, but the signatures
+        // must still be checked manually in order to match the behavior described by the
+        // method. Otherwise matchExact == true will allow additional certificates if run
+        // on a device >= P.
+        if (!matchExact && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+            // If not matching exact, delegate to the API 28 PackageManager API for checking
+            // individual certificates. This is less performant, but goes through a formally
+            // supported API.
+            for (byte[] bytes : expectedCertBytes) {
+                Integer type = certificatesAndType.get(bytes);
+                //noinspection ConstantConditions type cannot be null
+                if (!Api28Impl.hasSigningCertificate(packageManager, packageName, bytes, type)) {
+                    return false;
+                }
+            }
+
+            return true;
+        }
+
+        // Fail if the query is larger than the actual set, or the size doesn't match and it should.
+        if (signers.size() == 0
+                || certificatesAndType.size() > signers.size()
+                || (matchExact && certificatesAndType.size() != signers.size())) {
+            return false;
+        }
+
+        @SuppressLint("InlinedApi")
+        boolean hasSha256 = certificatesAndType.containsValue(PackageManager.CERT_INPUT_SHA256);
+        byte[][] sha256Digests = null;
+        if (hasSha256) {
+            // Since the search does several array contains checks, cache the SHA256 digests here.
+            sha256Digests = new byte[signers.size()][];
+            for (int index = 0; index < signers.size(); index++) {
+                sha256Digests[index] = computeSHA256Digest(signers.get(index).toByteArray());
+            }
+        }
+
+        for (byte[] bytes : expectedCertBytes) {
+            Integer type = certificatesAndType.get(bytes);
+            //noinspection ConstantConditions type cannot be null
+            switch (type) {
+                case PackageManager.CERT_INPUT_RAW_X509:
+                    // RAW_X509 is the type that Signatures are and always have been stored as,
+                    // so defer to the Signature equals method for the platform.
+                    Signature expectedSignature = new Signature(bytes);
+                    if (!signers.contains(expectedSignature)) {
+                        return false;
+                    }
+                    break;
+                case PackageManager.CERT_INPUT_SHA256:
+                    // sha256Digests cannot be null due to pre-checked containsValue for its type
+                    //noinspection ConstantConditions
+                    if (!byteArrayContains(sha256Digests, bytes)) {
+                        return false;
+                    }
+                    break;
+                default:
+                    // Impossible to reach this point due to check at beginning of method.
+                    throw new IllegalArgumentException("Unsupported certificate type " + type);
+            }
+
+            // If this point is reached, all searches have succeeded
+            return true;
+        }
+
+        return false;
+    }
+
+    private static boolean byteArrayContains(@NonNull byte[][] array, @NonNull byte[] expected) {
+        for (byte[] item : array) {
+            if (Arrays.equals(expected, item)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private static byte[] computeSHA256Digest(byte[] bytes) {
+        try {
+            return MessageDigest.getInstance("SHA256").digest(bytes);
+        } catch (NoSuchAlgorithmException e) {
+            // Can't happen, SHA256 required since API level 1
+            throw new RuntimeException("Device doesn't support SHA256 cert checking", e);
+        }
+    }
+
     private PackageInfoCompat() {
     }
+
+    @RequiresApi(Build.VERSION_CODES.P)
+    private static class Api28Impl {
+        private Api28Impl() {
+        }
+
+        static boolean hasSigningCertificate(@NonNull PackageManager packageManager,
+                @NonNull String packageName, @NonNull byte[] bytes, int type) {
+            return packageManager.hasSigningCertificate(packageName, bytes, type);
+        }
+
+        static boolean hasMultipleSigners(@NonNull SigningInfo signingInfo) {
+            return signingInfo.hasMultipleSigners();
+        }
+
+        @Nullable
+        static Signature[] getApkContentsSigners(@NonNull SigningInfo signingInfo) {
+            return signingInfo.getApkContentsSigners();
+        }
+
+        @Nullable
+        static Signature[] getSigningCertificateHistory(@NonNull SigningInfo signingInfo) {
+            return signingInfo.getSigningCertificateHistory();
+        }
+    }
 }
diff --git a/settings.gradle b/settings.gradle
index 9356a05..8be6eeb 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -565,6 +565,7 @@
 includeProject(":internal-testutils-navigation", "testutils/testutils-navigation", [BuildType.MAIN, BuildType.COMPOSE, BuildType.FLAN])
 includeProject(":internal-testutils-paging", "testutils/testutils-paging", [BuildType.MAIN, BuildType.COMPOSE])
 includeProject(":internal-testutils-gradle-plugin", "testutils/testutils-gradle-plugin", [BuildType.MAIN, BuildType.FLAN])
+includeProject(":internal-testutils-mockito", "testutils/testutils-mockito", [BuildType.MAIN])
 
 /////////////////////////////
 //
diff --git a/testutils/testutils-mockito/build.gradle b/testutils/testutils-mockito/build.gradle
new file mode 100644
index 0000000..ea8a7de
--- /dev/null
+++ b/testutils/testutils-mockito/build.gradle
@@ -0,0 +1,29 @@
+/*
+ * Copyright 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.
+ */
+
+import static androidx.build.dependencies.DependenciesKt.*
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("kotlin-android")
+}
+
+dependencies {
+    api(MOCKITO_CORE, libs.exclude_bytebuddy)
+
+    implementation(KOTLIN_STDLIB)
+}
diff --git a/testutils/testutils-mockito/src/main/AndroidManifest.xml b/testutils/testutils-mockito/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..dbe1e34
--- /dev/null
+++ b/testutils/testutils-mockito/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  Copyright 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.
+  -->
+<manifest package="androidx.testutils.mockito"/>
diff --git a/testutils/testutils-mockito/src/main/java/androidx/testutils/mockito/MockitoUtils.kt b/testutils/testutils-mockito/src/main/java/androidx/testutils/mockito/MockitoUtils.kt
new file mode 100644
index 0000000..96d4b0a
--- /dev/null
+++ b/testutils/testutils-mockito/src/main/java/androidx/testutils/mockito/MockitoUtils.kt
@@ -0,0 +1,88 @@
+/*
+ * Copyright 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 androidx.testutils.mockito
+
+import org.mockito.Answers
+import org.mockito.MockSettings
+import org.mockito.Mockito
+import org.mockito.invocation.InvocationOnMock
+import org.mockito.stubbing.Answer
+
+/**
+ * [Answer] variant intended for [MockSettings.defaultAnswer] that logs the unmocked method
+ * that was called, serializing the arguments used, to try and provide a more informative
+ * error message.
+ */
+val ANSWER_THROWS = Answer {
+    when (val name = it.method.name) {
+        // Delegate to the actual toString, since that will probably not be mocked by a test
+        "toString" -> Answers.CALLS_REAL_METHODS.answer(it)
+        else -> {
+            val arguments = it.arguments
+                ?.takeUnless { it.isEmpty() }
+                ?.mapIndexed { index, arg ->
+                    try {
+                        arg?.toString()
+                    } catch (e: Exception) {
+                        "toString[$index] threw ${e.message}"
+                    }
+                }
+                ?.joinToString()
+                ?: "no arguments"
+
+            throw UnsupportedOperationException(
+                "${it.mock::class.java.simpleName}#$name with $arguments should not be called"
+            )
+        }
+    }
+}
+
+fun <Type : Any?> whenever(mock: Type, block: InvocationOnMock.() -> Type) =
+    Mockito.`when`(mock).thenAnswer { block(it) }!!
+
+/**
+ * Spy an existing object and allow mocking within [block]. Once the method returns, the spied
+ * instance is prepped to throw exceptions whenever an unmocked method is called. This can be
+ * used to enforce that only specifically mocked methods are called, avoiding unexpected
+ * results when the behavior under test adds code to call an unexpected method.
+ */
+inline fun <reified T> spyThrowOnUnmocked(value: T?, block: T.() -> Unit = {}): T {
+    val swappingAnswer = object : Answer<Any?> {
+        var delegate: Answer<*> = Answers.RETURNS_DEFAULTS
+
+        override fun answer(invocation: InvocationOnMock?): Any? {
+            return delegate.answer(invocation)
+        }
+    }
+
+    val settings = Mockito.withSettings()
+        .spiedInstance(value)
+        .defaultAnswer(swappingAnswer)
+
+    return Mockito.mock(T::class.java, settings)
+        .apply(block)
+        .also {
+            // To allow Mockito.when() usage inside block, only swap to throwing afterwards
+            swappingAnswer.delegate = ANSWER_THROWS
+        }
+}
+
+/**
+ * [Mockito.mock] equivalent of [spyThrowOnUnmocked] which doesn't spy an existing instance.
+ */
+inline fun <reified T> mockThrowOnUnmocked(block: T.() -> Unit = {}) =
+    spyThrowOnUnmocked(null, block)
\ No newline at end of file