Created ProfileVerifier to check whether a compilation profile exists

The ProfileVerifier checks if a compilation profile is installed, either through play store, package manager or profile installer.
A reference profile can be the result of either a baseline profile, a cloud profile or a bg dex opt.
If a reference profile exists at the first app start or after an app update,
this means that a baseline profile or a cloud profile was installed, since the bg dex opt happen
later in background. The ProfileVerifier caches the result after the first app start to exclude bg dex opt
and rechecks for reference profile only after app update. The same is applied also to current profiles, with the exception
that the check for current profiles can be forced, in case the installation was done at a later time through ProfileInstallerReceiver.

Test: ./gradlew profileinstaller:integration-tests:profile-verification:cC
Bug: 246653809
Change-Id: I263a45582743afce0c3d9aefe629c07dfde77a72
Relnote: Added ProfileVerifier api to check whether a compilation profile exists for the app
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/build.gradle b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/build.gradle
new file mode 100644
index 0000000..386b139
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/build.gradle
@@ -0,0 +1,93 @@
+/*
+ * Copyright 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.
+ */
+
+import com.android.build.api.artifact.SingleArtifact
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+    id("kotlin-android")
+}
+
+// This project can be removed once b/239659205 has landed and use only the
+// profile-verification-sample project.
+android {
+    defaultConfig {
+        minSdkVersion 28
+    }
+    buildTypes {
+        release {
+            // Minification and shrinking are disabled to avoid r8 removing unused methods and
+            // speed up build process.
+            minifyEnabled false
+            shrinkResources false
+        }
+    }
+
+    flavorDimensions = ["version"]
+    productFlavors {
+        v1 {
+            dimension "version"
+            versionCode 1
+        }
+        v2 {
+            dimension "version"
+            versionCode 2
+        }
+        v3 {
+            dimension "version"
+            versionCode 3
+        }
+    }
+
+    namespace "androidx.profileinstaller.integration.profileverification.target.no_initializer"
+}
+
+dependencies {
+    implementation(libs.kotlinStdlib)
+    implementation(project(":profileinstaller:profileinstaller"))
+
+    // These projects are not used directly but added to baseline-prof.txt to increase number of
+    // methods, in order to have dex opt to run.
+    implementation(project(":core:core"))
+    implementation(project(":core:core-ktx"))
+}
+
+// Define a configuration that can be consumed, as this project is a provider of test apks for
+// profile verification integration test.
+configurations {
+    apkAssets {
+        canBeConsumed = true
+        canBeResolved = false
+        attributes {
+            attribute(
+                    LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
+                    objects.named(LibraryElements, 'profileverification-apkAssets')
+            )
+        }
+    }
+}
+
+// Release apk variants are added as output artifacts to the apkAssets configuration.
+// The apkAssets configuration is consumed by profile-verification integration test and
+// artifacts are placed in the assets folder.
+androidComponents {
+    onVariants(selector().all().withBuildType("release"), { variant ->
+        artifacts {
+            apkAssets(variant.artifacts.get(SingleArtifact.APK.INSTANCE))
+        }
+    })
+}
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/AndroidManifest.xml b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1c2eb27
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/AndroidManifest.xml
@@ -0,0 +1,46 @@
+<!--
+  ~ Copyright 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <application
+        android:testOnly="false"
+        android:allowBackup="false"
+        android:label="Profileinstaller verification sample no profile"
+        android:supportsRtl="true"
+        tools:ignore="MissingApplicationIcon">
+
+        <activity
+            android:name=".SampleActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+
+        <provider
+            android:name="androidx.startup.InitializationProvider"
+            android:authorities="${applicationId}.androidx-startup"
+            android:exported="false"
+            tools:node="merge">
+            <meta-data
+                android:name="androidx.profileinstaller.ProfileInstallerInitializer"
+                tools:node="remove" />
+        </provider>
+
+    </application>
+</manifest>
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/baseline-prof.txt b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/baseline-prof.txt
new file mode 100644
index 0000000..11d9e76
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/baseline-prof.txt
@@ -0,0 +1,2 @@
+Landroidx/**;
+HSPLandroidx/**;->**(**)**
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/java/androidx/profileinstaller/integration/profileverification/target/no_initializer/SampleActivity.kt b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/java/androidx/profileinstaller/integration/profileverification/target/no_initializer/SampleActivity.kt
new file mode 100644
index 0000000..b742486
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/java/androidx/profileinstaller/integration/profileverification/target/no_initializer/SampleActivity.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 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 androidx.profileinstaller.integration.profileverification.target.no_initializer
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+import androidx.profileinstaller.ProfileVerifier
+import java.util.concurrent.Executors
+
+class SampleActivity : Activity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
+    }
+
+    override fun onResume() {
+        super.onResume()
+        Executors.newSingleThreadExecutor().submit {
+            val result = ProfileVerifier.writeProfileVerification(this)
+            runOnUiThread {
+                findViewById<TextView>(R.id.txtNotice).text = """
+                    Profile installed: ${result.profileInstallResultCode}
+                    Has reference profile: ${result.isCompiledWithProfile}
+                    Has current profile: ${result.hasProfileEnqueuedForCompilation()}
+                """.trimIndent()
+            }
+        }
+    }
+}
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/layout/activity_main.xml b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..b0a97a6
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/layout/activity_main.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  Copyright 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/txtNotice"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="22sp"
+        android:layout_gravity="center" />
+
+</FrameLayout>
+
diff --git a/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/values/donottranslate-strings.xml b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/values/donottranslate-strings.xml
new file mode 100644
index 0000000..1ade21e
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample-no-initializer/src/main/res/values/donottranslate-strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 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.
+  -->
+
+<resources>
+    <string name="app_notice">profileinstaller verification app.</string>
+</resources>
diff --git a/profileinstaller/integration-tests/profile-verification-sample/build.gradle b/profileinstaller/integration-tests/profile-verification-sample/build.gradle
new file mode 100644
index 0000000..8c3fd8e
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/build.gradle
@@ -0,0 +1,93 @@
+/*
+ * Copyright 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.
+ */
+
+import com.android.build.api.artifact.SingleArtifact
+
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.application")
+    id("kotlin-android")
+}
+android {
+    defaultConfig {
+        minSdkVersion 28
+    }
+    buildTypes {
+        release {
+            // Minification and shrinking are disabled to avoid r8 removing unused methods and
+            // speed up build process.
+            minifyEnabled false
+            shrinkResources false
+        }
+    }
+
+    flavorDimensions = ["version"]
+    productFlavors {
+        v1 {
+            dimension "version"
+            versionCode 1
+        }
+        v2 {
+            dimension "version"
+            versionCode 2
+        }
+        v3 {
+            dimension "version"
+            versionCode 3
+        }
+    }
+
+    namespace "androidx.profileinstaller.integration.profileverification.target"
+}
+
+dependencies {
+    implementation(libs.kotlinStdlib)
+    implementation(project(":profileinstaller:profileinstaller"))
+
+    // These projects are not used directly but added to baseline-prof.txt to increase number of
+    // methods, in order to have dex opt to run.
+    implementation(project(":core:core"))
+    implementation(project(":core:core-ktx"))
+}
+
+// Define a configuration that can be consumed, as this project is a provider of test apks for
+// profile verification integration test.
+configurations {
+    apkAssets {
+        canBeConsumed = true
+        canBeResolved = false
+        attributes {
+            attribute(
+                    LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
+                    objects.named(LibraryElements, 'profileverification-apkAssets')
+            )
+        }
+    }
+}
+
+// Release apk variants are added as output artifacts to the apkAssets configuration.
+// The apkAssets configuration is consumed by profile-verification integration test and
+// artifacts are placed in the assets folder.
+// Release apk variants are added as output artifacts to the apkAssets configuration.
+// The apkAssets configuration is consumed by profile-verification integration test and
+// artifacts are placed in the assets folder.
+androidComponents {
+    onVariants(selector().all().withBuildType("release"), { variant ->
+        artifacts {
+            apkAssets(variant.artifacts.get(SingleArtifact.APK.INSTANCE))
+        }
+    })
+}
diff --git a/profileinstaller/integration-tests/profile-verification-sample/src/main/AndroidManifest.xml b/profileinstaller/integration-tests/profile-verification-sample/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..26fa0b3
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/src/main/AndroidManifest.xml
@@ -0,0 +1,35 @@
+<!--
+  ~ Copyright 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.
+  -->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools">
+
+    <application
+        android:allowBackup="false"
+        android:label="Profileinstaller verification sample"
+        android:supportsRtl="true"
+        android:testOnly="false"
+        tools:ignore="MissingApplicationIcon">
+
+        <activity
+            android:name=".SampleActivity"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+                <category android:name="android.intent.category.LAUNCHER" />
+            </intent-filter>
+        </activity>
+    </application>
+</manifest>
diff --git a/profileinstaller/integration-tests/profile-verification-sample/src/main/baseline-prof.txt b/profileinstaller/integration-tests/profile-verification-sample/src/main/baseline-prof.txt
new file mode 100644
index 0000000..11d9e76
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/src/main/baseline-prof.txt
@@ -0,0 +1,2 @@
+Landroidx/**;
+HSPLandroidx/**;->**(**)**
diff --git a/profileinstaller/integration-tests/profile-verification-sample/src/main/java/androidx/profileinstaller/integration/profileverification/target/SampleActivity.kt b/profileinstaller/integration-tests/profile-verification-sample/src/main/java/androidx/profileinstaller/integration/profileverification/target/SampleActivity.kt
new file mode 100644
index 0000000..fd1a1e1
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/src/main/java/androidx/profileinstaller/integration/profileverification/target/SampleActivity.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 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 androidx.profileinstaller.integration.profileverification.target
+
+import android.app.Activity
+import android.os.Bundle
+import android.widget.TextView
+import androidx.profileinstaller.ProfileVerifier
+import java.util.concurrent.Executors
+
+class SampleActivity : Activity() {
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
+    }
+
+    override fun onResume() {
+        super.onResume()
+        Executors.newSingleThreadExecutor().submit {
+            val result = ProfileVerifier.getCompilationStatusAsync().get()
+            runOnUiThread {
+                findViewById<TextView>(R.id.txtNotice).text = """
+                    Profile installed: ${result.profileInstallResultCode}
+                    Has reference profile: ${result.isCompiledWithProfile}
+                    Has current profile: ${result.hasProfileEnqueuedForCompilation()}
+                """.trimIndent()
+            }
+        }
+    }
+}
diff --git a/profileinstaller/integration-tests/profile-verification-sample/src/main/res/layout/activity_main.xml b/profileinstaller/integration-tests/profile-verification-sample/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..b0a97a6
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/src/main/res/layout/activity_main.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+  Copyright 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.
+-->
+
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <TextView
+        android:id="@+id/txtNotice"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:textSize="22sp"
+        android:layout_gravity="center" />
+
+</FrameLayout>
+
diff --git a/profileinstaller/integration-tests/profile-verification-sample/src/main/res/values/donottranslate-strings.xml b/profileinstaller/integration-tests/profile-verification-sample/src/main/res/values/donottranslate-strings.xml
new file mode 100644
index 0000000..1ade21e
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification-sample/src/main/res/values/donottranslate-strings.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  Copyright 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.
+  -->
+
+<resources>
+    <string name="app_notice">profileinstaller verification app.</string>
+</resources>
diff --git a/profileinstaller/integration-tests/profile-verification/build.gradle b/profileinstaller/integration-tests/profile-verification/build.gradle
new file mode 100644
index 0000000..a25c998
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/build.gradle
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+plugins {
+    id("AndroidXPlugin")
+    id("com.android.library")
+    id("kotlin-android")
+}
+
+// This task copies the apks provided by the `apkAssets` configuration and places them in the
+// assets folder. This allows a build time generation of the sample apps.
+def copyApkTaskProvider = tasks.register("copyApkAssets", Copy) {
+    description = "Copies the asset apks provided by profile-verification-sample projects"
+    dependsOn(configurations.getByName("apkAssets"))
+    from(configurations.getByName("apkAssets").incoming.artifactView {}.files)
+    into(layout.buildDirectory.dir("intermediates/apkAssets"))
+
+    // Note that the artifact directory included contain multiple output-metadata.json files built
+    // with the apks. Since we're not interested in those we can simply exclude duplicates.
+    duplicatesStrategy(DuplicatesStrategy.EXCLUDE)
+}
+
+android {
+    defaultConfig {
+        minSdkVersion 23
+    }
+    sourceSets.androidTest.assets.srcDir(copyApkTaskProvider)
+    namespace "androidx.profileinstaller.integration.profileverification"
+}
+
+// Define a configuration that can be resolved. This project is the consumer of test apks, i.e. it
+// contains the integration tests.
+configurations {
+    apkAssets {
+        canBeConsumed = false
+        canBeResolved = true
+        attributes {
+            attribute(
+                    LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
+                    objects.named(LibraryElements, 'profileverification-apkAssets')
+            )
+        }
+    }
+}
+
+dependencies {
+    androidTestImplementation(project(":profileinstaller:profileinstaller"))
+    androidTestImplementation(libs.testRules)
+    androidTestImplementation(libs.testExtJunit)
+    androidTestImplementation(libs.testCore)
+    androidTestImplementation(libs.testRunner)
+    androidTestImplementation(libs.testUiautomator)
+    androidTestImplementation(libs.testExtTruth)
+    apkAssets(project(":profileinstaller:integration-tests:profile-verification-sample"))
+    apkAssets(project(":profileinstaller:integration-tests:profile-verification-sample-no-initializer"))
+}
+
+// It makes sure that the apks are generated before the assets are packed.
+afterEvaluate {
+    tasks.named("generateDebugAndroidTestAssets").configure { it.dependsOn(copyApkTaskProvider) }
+}
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/AndroidManifest.xml b/profileinstaller/integration-tests/profile-verification/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..bae036b
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ 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.
+  -->
+<manifest />
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.prof b/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.prof
new file mode 100644
index 0000000..dafbbbb
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.prof
Binary files differ
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.profm b/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.profm
new file mode 100644
index 0000000..d72fd91
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/assets/baseline.profm
Binary files differ
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithProfileInstallerInitializer.kt b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithProfileInstallerInitializer.kt
new file mode 100644
index 0000000..190a008
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithProfileInstallerInitializer.kt
@@ -0,0 +1,238 @@
+/*
+ * Copyright 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 androidx.profileinstaller.integration.profileverification
+
+import androidx.profileinstaller.ProfileVersion
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * This test uses the project "profileinstaller:integration-tests:profile-verification-sample". The
+ * release version of it has been embedded in the assets in 3 different versions with increasing
+ * versionCode to allow updating the app through pm command.
+ *
+ * The SampleActivity invoked displays the status of the reference profile install on the UI after
+ * the callback from {@link ProfileVerifier} returns. This test checks the status visualized to
+ * confirm if the reference profile has been installed.
+ *
+ * This test needs min sdk version `P` because it's first version to introduce support for dm file:
+ * https://googleplex-android-review.git.corp.google.com/c/platform/frameworks/base/+/3368431/
+ */
+@SdkSuppress(
+    minSdkVersion = android.os.Build.VERSION_CODES.P,
+    maxSdkVersion = ProfileVersion.MAX_SUPPORTED_SDK
+)
+@LargeTest
+class ProfileVerificationTestWithProfileInstallerInitializer {
+
+    @Before
+    fun setUp() = withPackageName(PACKAGE_NAME) {
+        // Note that this test fails on emulator api 30 (b/251540646)
+        assumeTrue(!isApi30)
+        uninstall()
+    }
+
+    @After
+    fun tearDown() = withPackageName(PACKAGE_NAME) {
+        uninstall()
+    }
+
+    @Test
+    fun installNewApp() = withPackageName(PACKAGE_NAME) {
+        // Install without reference profile
+        install(apkName = V1_APK, withProfile = false)
+
+        // Start
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(AWAITING_COMPILATION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(true)
+        }
+    }
+
+    @Test
+    fun installNewAppAndWaitForCompilation() = withPackageName(PACKAGE_NAME) {
+
+        // Install without reference profile
+        install(apkName = V1_APK, withProfile = false)
+
+        // Start once to install profile
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(AWAITING_COMPILATION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(true)
+        }
+        stop()
+
+        // Start again, should still be awaiting compilation
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(AWAITING_COMPILATION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(true)
+        }
+        stop()
+
+        // Compile
+        compileCurrentProfile()
+
+        // Start again to check profile is compiled
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(false)
+        }
+
+        // Profile should still be compiled
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(false)
+        }
+    }
+
+    @Test
+    fun installAppWithReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+        // Install with reference profile.
+        install(apkName = V1_APK, withProfile = true)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(true)
+        }
+    }
+
+    @Test
+    fun updateFromReferenceProfileToReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+        // Install without reference profile
+        install(apkName = V1_APK, withProfile = true)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(true)
+        }
+
+        // Updates adding reference profile
+        install(apkName = V2_APK, withProfile = true)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(true)
+        }
+    }
+
+    @Test
+    fun updateFromNoReferenceProfileToReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+        // Install without reference profile
+        install(apkName = V2_APK, withProfile = false)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(AWAITING_COMPILATION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(true)
+        }
+
+        // Updates adding reference profile
+        install(apkName = V3_APK, withProfile = true)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(true)
+        }
+    }
+
+    @Test
+    fun updateFromReferenceProfileToNoReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+        // Install with reference profile
+        install(apkName = V1_APK, withProfile = true)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(true)
+        }
+
+        // Updates removing reference profile
+        install(apkName = V2_APK, withProfile = false)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(AWAITING_COMPILATION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(true)
+        }
+    }
+
+    @Test
+    fun installWithReferenceProfileThenUpdateNoProfileThenUpdateProfileAgain() =
+        withPackageName(PACKAGE_NAME) {
+
+            // Install with reference profile
+            install(apkName = V1_APK, withProfile = true)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(COMPILED)
+                hasReferenceProfile(true)
+                hasCurrentProfile(true)
+            }
+
+            // Updates removing reference profile
+            install(apkName = V2_APK, withProfile = false)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(AWAITING_COMPILATION)
+                hasReferenceProfile(false)
+                hasCurrentProfile(true)
+            }
+
+            // Reinstall with reference profile
+            install(apkName = V3_APK, withProfile = true)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(COMPILED)
+                hasReferenceProfile(true)
+                hasCurrentProfile(true)
+            }
+        }
+
+    companion object {
+        private const val PACKAGE_NAME =
+            "androidx.profileinstaller.integration.profileverification.target"
+        private const val ACTIVITY_NAME =
+            ".SampleActivity"
+
+        // Note that these version differ only for version code 1..3 to allow update
+        private const val V1_APK = "profile-verification-sample-v1-release.apk"
+        private const val V2_APK = "profile-verification-sample-v2-release.apk"
+        private const val V3_APK = "profile-verification-sample-v3-release.apk"
+    }
+}
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithoutProfileInstallerInitializer.kt b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithoutProfileInstallerInitializer.kt
new file mode 100644
index 0000000..c83ac77
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/ProfileVerificationTestWithoutProfileInstallerInitializer.kt
@@ -0,0 +1,309 @@
+/*
+ * Copyright 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 androidx.profileinstaller.integration.profileverification
+
+import androidx.profileinstaller.ProfileVersion
+import androidx.test.filters.LargeTest
+import androidx.test.filters.SdkSuppress
+import org.junit.After
+import org.junit.Assume.assumeTrue
+import org.junit.Before
+import org.junit.Test
+
+/**
+ * This test uses the project
+ * "profileinstaller:integration-tests:profile-verification-sample-no-initializer". The release
+ * version of it has been embedded in the assets in 3 different versions with increasing
+ * versionCode to allow updating the app through pm command. In this test the
+ * ProfileInstallerInitializer has been disabled from the target app manifest.
+ *
+ * The SampleActivity invoked displays the status of the reference profile install on the UI after
+ * the callback from {@link ProfileVerifier} returns. This test checks the status visualized to
+ * confirm if the reference profile has been installed.
+ *
+ * This test needs min sdk version `P` because it's first version to introduce support for dm file:
+ * https://googleplex-android-review.git.corp.google.com/c/platform/frameworks/base/+/3368431/
+ */
+@SdkSuppress(
+    minSdkVersion = android.os.Build.VERSION_CODES.P,
+    maxSdkVersion = ProfileVersion.MAX_SUPPORTED_SDK
+)
+@LargeTest
+class ProfileVerificationTestWithoutProfileInstallerInitializer {
+
+    @Before
+    fun setUp() = withPackageName(PACKAGE_NAME) {
+        // Note that this test fails on emulator api 30 (b/251540646)
+        assumeTrue(!isApi30)
+        uninstall()
+    }
+
+    @After
+    fun tearDown() = withPackageName(PACKAGE_NAME) {
+        uninstall()
+    }
+
+    @Test
+    fun installNewAppWithoutReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+        // Install without reference profile
+        install(apkName = V1_APK, withProfile = false)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(NONE)
+            hasReferenceProfile(false)
+            hasCurrentProfile(false)
+        }
+    }
+
+    @Test
+    fun installNewAppAndWaitForCompilation() = withPackageName(PACKAGE_NAME) {
+
+        // Install without reference profile
+        install(apkName = V1_APK, withProfile = false)
+
+        // Start once to check there is no profile
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(NONE)
+            hasReferenceProfile(false)
+            hasCurrentProfile(false)
+        }
+        stop()
+
+        // Start again to check there is no profile
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(NONE)
+            hasReferenceProfile(false)
+            hasCurrentProfile(false)
+        }
+        stop()
+
+        // Install profile through broadcast receiver
+        broadcastProfileInstallAction()
+
+        // Start again to check there it's now awaiting compilation
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(AWAITING_COMPILATION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(true)
+        }
+        stop()
+
+        // Start again to check there it's now awaiting compilation
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(AWAITING_COMPILATION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(true)
+        }
+        stop()
+
+        // Compile
+        compileCurrentProfile()
+
+        // Start again to check profile is compiled
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(false)
+        }
+
+        // Start again to check profile is compiled
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(false)
+        }
+    }
+
+    @Test
+    fun installAppWithReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+        // Install with reference profile
+        install(apkName = V1_APK, withProfile = true)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(false)
+        }
+    }
+
+    @Test
+    fun updateFromNoReferenceProfileToReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+        // Install without reference profile
+        install(apkName = V2_APK, withProfile = false)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(NONE)
+            hasReferenceProfile(false)
+            hasCurrentProfile(false)
+        }
+
+        // Updates adding reference profile
+        install(apkName = V3_APK, withProfile = true)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(false)
+        }
+    }
+
+    @Test
+    fun updateFromReferenceProfileToNoReferenceProfile() = withPackageName(PACKAGE_NAME) {
+
+        // Install with reference profile
+        install(apkName = V1_APK, withProfile = true)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(COMPILED)
+            hasReferenceProfile(true)
+            hasCurrentProfile(false)
+        }
+
+        // Updates removing reference profile
+        install(apkName = V2_APK, withProfile = false)
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(NONE)
+            hasReferenceProfile(false)
+            hasCurrentProfile(false)
+        }
+    }
+
+    @Test
+    fun installWithReferenceProfileThenUpdateNoProfileThenUpdateProfileAgain() =
+        withPackageName(PACKAGE_NAME) {
+
+            // Install with reference profile
+            install(apkName = V1_APK, withProfile = true)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(COMPILED)
+                hasReferenceProfile(true)
+                hasCurrentProfile(false)
+            }
+
+            // Updates removing reference profile
+            install(apkName = V2_APK, withProfile = false)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(NONE)
+                hasReferenceProfile(false)
+                hasCurrentProfile(false)
+            }
+
+            // Reinstall with reference profile
+            install(apkName = V3_APK, withProfile = true)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(COMPILED)
+                hasReferenceProfile(true)
+                hasCurrentProfile(false)
+            }
+        }
+
+    @Test
+    fun forceInstallCurrentProfileThroughBroadcastReceiver() = withPackageName(PACKAGE_NAME) {
+
+        // Install without reference profile
+        install(apkName = V1_APK, withProfile = false)
+
+        // Start and assess there is no profile
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(NONE)
+            hasReferenceProfile(false)
+            hasCurrentProfile(false)
+        }
+        stop()
+
+        // Force update through broadcast receiver
+        broadcastProfileInstallAction()
+
+        // Start and assess there is a current profile
+        start(ACTIVITY_NAME)
+        evaluateUI {
+            profileInstalled(AWAITING_COMPILATION)
+            hasReferenceProfile(false)
+            hasCurrentProfile(true)
+        }
+    }
+
+    @Test
+    fun forceInstallCurrentProfileThroughBroadcastReceiverAndUpdateWithReference() =
+        withPackageName(PACKAGE_NAME) {
+
+            // Install without reference profile, start and assess there is no profile
+            install(apkName = V1_APK, withProfile = false)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(NONE)
+                hasReferenceProfile(false)
+                hasCurrentProfile(false)
+            }
+            stop()
+
+            // Force update through ProfileInstallerReceiver
+            broadcastProfileInstallAction()
+
+            // Start again and assess there is a current profile now installed
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(AWAITING_COMPILATION)
+                hasReferenceProfile(false)
+                hasCurrentProfile(true)
+            }
+
+            // Update to v2 and assert that the current profile was uninstalled
+            install(apkName = V2_APK, withProfile = false)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(NONE)
+                hasReferenceProfile(false)
+                hasCurrentProfile(false)
+            }
+
+            // Update to v3 with reference profile and assess this is correctly recognized
+            install(apkName = V3_APK, withProfile = true)
+            start(ACTIVITY_NAME)
+            evaluateUI {
+                profileInstalled(COMPILED)
+                hasReferenceProfile(true)
+                hasCurrentProfile(false)
+            }
+        }
+
+    companion object {
+        private const val PACKAGE_NAME =
+            "androidx.profileinstaller.integration.profileverification.target.no_initializer"
+        private const val ACTIVITY_NAME =
+            ".SampleActivity"
+
+        // Note that these version differ only for version code 1..3 to allow update
+        private const val V1_APK = "profile-verification-sample-no-initializer-v1-release.apk"
+        private const val V2_APK = "profile-verification-sample-no-initializer-v2-release.apk"
+        private const val V3_APK = "profile-verification-sample-no-initializer-v3-release.apk"
+    }
+}
diff --git a/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/TestManager.kt b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/TestManager.kt
new file mode 100644
index 0000000..d289ce0
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/androidTest/java/androidx/profileinstaller/integration/profileverification/TestManager.kt
@@ -0,0 +1,309 @@
+/*
+ * Copyright 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 androidx.profileinstaller.integration.profileverification
+
+import android.os.Build
+import android.os.Environment
+import android.os.ParcelFileDescriptor
+import android.util.Log
+import androidx.concurrent.futures.DirectExecutor
+import androidx.profileinstaller.DeviceProfileWriter
+import androidx.profileinstaller.ProfileInstaller
+import androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE
+import androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE
+import androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.By
+import androidx.test.uiautomator.UiDevice
+import androidx.test.uiautomator.Until
+import com.google.common.truth.Truth.assertThat
+import com.google.common.truth.Truth.assertWithMessage
+import java.io.File
+import java.util.zip.ZipEntry
+import java.util.zip.ZipOutputStream
+
+fun withPackageName(packageName: String, block: WithPackageBlock.() -> Unit) {
+    block(WithPackageBlock(packageName))
+}
+
+class WithPackageBlock internal constructor(private val packageName: String) {
+
+    private val instrumentation = InstrumentationRegistry.getInstrumentation()
+    private val uiAutomation = instrumentation.uiAutomation
+    private val uiDevice = UiDevice.getInstance(instrumentation)
+
+    // `externalMediaDirs` is deprecated. Docs encourage to put media in MediaStore instead. Here
+    // we just need a folder that can be accessed by both app and shell user so should be fine.
+    @Suppress("deprecation")
+    private val dirUsableByAppAndShell by lazy {
+        when {
+            Build.VERSION.SDK_INT >= 29 -> {
+                // On Android Q+ we are using the media directory because that is
+                // the directory that the shell has access to. Context: b/181601156
+                // This is the same logic of Outputs#init to determine `dirUsableByAppAndShell`.
+                InstrumentationRegistry.getInstrumentation().context.externalMediaDirs.firstOrNull {
+                    Environment.getExternalStorageState(it) == Environment.MEDIA_MOUNTED
+                }
+            }
+            Build.VERSION.SDK_INT <= 22 -> {
+                // prior to API 23, shell didn't have access to externalCacheDir
+                InstrumentationRegistry.getInstrumentation().context.cacheDir
+            }
+            else -> InstrumentationRegistry.getInstrumentation().context.externalCacheDir
+        } ?: throw IllegalStateException("Unable to select a directory for writing files.")
+    }
+
+    fun uninstall() = executeCommand("pm uninstall $packageName")
+
+    fun install(apkName: String, withProfile: Boolean) {
+
+        // Contains all the clean up actions to perform at the end of the execution
+        val cleanUpBlocks = mutableListOf<() -> (Unit)>()
+
+        try {
+
+            // First writes in a temp file the apk from the assets
+            val tmpApkFile = File(dirUsableByAppAndShell, "tmp_$apkName").also { file ->
+                file.delete()
+                file.createNewFile()
+                file.deleteOnExit()
+                file.outputStream().use { instrumentation.context.assets.open(apkName).copyTo(it) }
+            }
+            cleanUpBlocks.add { tmpApkFile.delete() }
+
+            // Then moves it to a destination that can be used to install it
+            val destApkPath = "$TEMP_DIR/$apkName"
+            assertThat(executeCommand("mv ${tmpApkFile.absolutePath} $destApkPath")).isEmpty()
+            cleanUpBlocks.add { executeCommand("rm $destApkPath") }
+
+            // This mimes the behaviour of `adb install-multiple` using an install session.
+            // For reference:
+            // https://source.corp.google.com/android-internal/packages/modules/adb/client/adb_install.cpp
+
+            // Creates an install session
+            val installCreateOutput = executeCommand("pm install-create -t").first().trim()
+            val sessionId = REGEX_SESSION_ID
+                .find(installCreateOutput)
+                .throwIfNull("pm install session is invalid.")
+                .groups[1]
+                .throwIfNull("pm install session is invalid.")
+                .value
+                .toLong()
+
+            // Adds the base apk to the install session
+            val successBaseApk =
+                executeCommand("pm install-write $sessionId base.apk $TEMP_DIR/$apkName")
+                    .first()
+                    .trim()
+                    .startsWith("Success")
+            if (!successBaseApk) {
+                throw IllegalStateException("Could not add $apkName to install session $sessionId")
+            }
+
+            // Adds the base dm (profile) to the install session
+            if (withProfile) {
+
+                // Generates the profiles using device profile writer
+                val tmpProfileProfFile = File(dirUsableByAppAndShell, "tmp_profile.prof")
+                    .also {
+                        it.delete()
+                        it.createNewFile()
+                        it.deleteOnExit()
+                    }
+                cleanUpBlocks.add { tmpProfileProfFile.delete() }
+
+                val deviceProfileWriter = DeviceProfileWriter(
+                    instrumentation.context.assets,
+                    DirectExecutor.INSTANCE,
+                    EMPTY_DIAGNOSTICS,
+                    apkName,
+                    BASELINE_PROF,
+                    BASELINE_PROFM,
+                    tmpProfileProfFile
+                )
+                if (!deviceProfileWriter.deviceAllowsProfileInstallerAotWrites()) {
+                    throw IllegalStateException(
+                        "The device does not allow profile installer aot writes"
+                    )
+                }
+                val success = deviceProfileWriter.read()
+                    .transcodeIfNeeded()
+                    .write()
+                if (!success) {
+                    throw IllegalStateException(
+                        "Profile was not installed correctly."
+                    )
+                }
+
+                // Compress the profile to generate the dex metadata file
+                val tmpDmFile = File(dirUsableByAppAndShell, "tmp_base.dm")
+                    .also {
+                        it.delete()
+                        it.createNewFile()
+                        it.deleteOnExit()
+                        it.outputStream().use { os ->
+                            ZipOutputStream(os).use {
+
+                                // One single zip entry named `primary.prof`
+                                it.putNextEntry(ZipEntry("primary.prof"))
+                                it.write(tmpProfileProfFile.readBytes())
+                            }
+                        }
+                    }
+                cleanUpBlocks.add { tmpDmFile.delete() }
+
+                // Then moves it to a destination that can be used to install it
+                val dmFilePath = "$TEMP_DIR/$DM_FILE_NAME"
+                executeCommand("mv ${tmpDmFile.absolutePath} $dmFilePath")
+                cleanUpBlocks.add { executeCommand("rm $dmFilePath") }
+
+                // Tries to install using pm install write
+                val successBaseDm =
+                    executeCommand("pm install-write $sessionId base.dm $dmFilePath")
+                        .first()
+                        .trim()
+                        .startsWith("Success")
+
+                if (!successBaseDm) {
+                    throw IllegalStateException(
+                        "Could not add $dmFilePath to install session $sessionId"
+                    )
+                }
+            }
+
+            // Commit the install transaction. Note that install-commit may not print any output
+            // if it fails.
+            val commitCommandOutput = executeCommand("pm install-commit $sessionId")
+            val firstLine = commitCommandOutput.firstOrNull()?.trim()
+            if (firstLine == null || firstLine != "Success") {
+                throw IllegalStateException(
+                    "pm install-commit failed: ${commitCommandOutput.joinToString("\n")}"
+                )
+            }
+        } finally {
+
+            // Runs all the clean up blocks. This will clean up also partial operations in case
+            // there is an issue during install
+            cleanUpBlocks.forEach { it() }
+        }
+    }
+
+    fun start(activityName: String) {
+        val error = executeCommand("am start -n $packageName/$activityName")
+            .any { it.startsWith("Error") }
+        assertThat(error).isFalse()
+        uiDevice.waitForIdle()
+    }
+
+    fun stop() {
+        val error = executeCommand("am force-stop $packageName")
+            .any { it.startsWith("Error") }
+        assertThat(error).isFalse()
+        uiDevice.waitForIdle()
+    }
+
+    fun compileCurrentProfile() {
+        val stdout = executeCommand("cmd package compile -f -m speed-profile $packageName")
+        val success = stdout.first().trim() == "Success"
+        assertWithMessage("Profile compilation failed: `$stdout`").that(success).isTrue()
+    }
+
+    fun broadcastProfileInstallAction() {
+        val result = broadcast(
+            packageName = packageName,
+            action = "androidx.profileinstaller.action.INSTALL_PROFILE",
+            receiverClass = "androidx.profileinstaller.ProfileInstallReceiver"
+        )
+        assertWithMessage("Profile install action broadcast failed with code.")
+            .that(result)
+            .isEqualTo(ProfileInstaller.RESULT_INSTALL_SUCCESS)
+    }
+
+    private fun broadcast(packageName: String, action: String, receiverClass: String) =
+        executeCommand("am broadcast -a $action $packageName/$receiverClass")
+            .first { it.contains("Broadcast completed: result=") }
+            .split("=")[1]
+            .trim()
+            .toInt()
+
+    private fun executeCommand(command: String): List<String> {
+        Log.d(TAG, "Executing command: `$command`")
+        return ParcelFileDescriptor
+            .AutoCloseInputStream(uiAutomation.executeShellCommand(command))
+            .bufferedReader()
+            .lineSequence()
+            .toList()
+    }
+
+    fun evaluateUI(block: AssertUiBlock.() -> Unit) {
+        val resourceId = "id/txtNotice"
+        val lines = uiDevice
+            .wait(Until.findObject(By.res("$packageName:$resourceId")), UI_TIMEOUT)
+            .text
+            .lines()
+            .map { it.split(":")[1].trim() }
+        assertThat(lines).hasSize(3)
+        block(AssertUiBlock(lines))
+    }
+
+    companion object {
+        private const val TAG = "TestManager"
+        private const val TEMP_DIR = "/data/local/tmp/"
+        private const val UI_TIMEOUT = 20000L
+        private const val BASELINE_PROF = "baseline.prof"
+        private const val BASELINE_PROFM = "baseline.profm"
+        private const val DM_FILE_NAME = "base.dm"
+        private val REGEX_SESSION_ID = """\[(\d+)\]""".toRegex()
+    }
+
+    class AssertUiBlock(private val lines: List<String>) {
+        fun profileInstalled(resultCode: Int) =
+            assertWithMessage("Unexpected profile verification result code")
+                .that(lines[0].toInt())
+                .isEqualTo(resultCode)
+
+        fun hasReferenceProfile(value: Boolean) =
+            assertWithMessage("Unexpected hasReferenceProfile value")
+                .that(lines[1].toBoolean())
+                .isEqualTo(value)
+
+        fun hasCurrentProfile(value: Boolean) =
+            assertWithMessage("Unexpected hasCurrentProfile value")
+                .that(lines[2].toBoolean())
+                .isEqualTo(value)
+    }
+}
+
+val isApi30 by lazy { Build.VERSION.SDK_INT == Build.VERSION_CODES.R }
+
+const val COMPILED = RESULT_CODE_COMPILED_WITH_PROFILE
+const val AWAITING_COMPILATION = RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION
+const val NONE = RESULT_CODE_NO_PROFILE
+
+private fun <T> T?.throwIfNull(message: String): T = this ?: throw Exception(message)
+
+private val EMPTY_DIAGNOSTICS: ProfileInstaller.DiagnosticsCallback =
+    object : ProfileInstaller.DiagnosticsCallback {
+        private val TAG = "ProfileVerifierDiagnosticsCallback"
+        override fun onDiagnosticReceived(code: Int, data: Any?) {
+            Log.d(TAG, "onDiagnosticReceived: $code")
+        }
+
+        override fun onResultReceived(code: Int, data: Any?) {
+            Log.d(TAG, "onResultReceived: $code")
+        }
+    }
diff --git a/profileinstaller/integration-tests/profile-verification/src/main/AndroidManifest.xml b/profileinstaller/integration-tests/profile-verification/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..d4c1970
--- /dev/null
+++ b/profileinstaller/integration-tests/profile-verification/src/main/AndroidManifest.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ 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.
+  -->
+<manifest />
diff --git a/profileinstaller/profileinstaller/api/current.txt b/profileinstaller/profileinstaller/api/current.txt
index ea6199b..8eed65c 100644
--- a/profileinstaller/profileinstaller/api/current.txt
+++ b/profileinstaller/profileinstaller/api/current.txt
@@ -46,5 +46,24 @@
     ctor public ProfileInstallerInitializer.Result();
   }
 
+  public final class ProfileVerifier {
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.profileinstaller.ProfileVerifier.CompilationStatus!> getCompilationStatusAsync();
+    method @WorkerThread public static androidx.profileinstaller.ProfileVerifier.CompilationStatus writeProfileVerification(android.content.Context);
+  }
+
+  public static class ProfileVerifier.CompilationStatus {
+    method public int getProfileInstallResultCode();
+    method public boolean hasProfileEnqueuedForCompilation();
+    method public boolean isCompiledWithProfile();
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE = 1; // 0x1
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING = 3; // 0x3
+    field public static final int RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ = 131072; // 0x20000
+    field public static final int RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE = 196608; // 0x30000
+    field public static final int RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST = 65536; // 0x10000
+    field public static final int RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION = 262144; // 0x40000
+    field public static final int RESULT_CODE_NO_PROFILE = 0; // 0x0
+    field public static final int RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION = 2; // 0x2
+  }
+
 }
 
diff --git a/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt b/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
index ea6199b..8eed65c 100644
--- a/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
+++ b/profileinstaller/profileinstaller/api/public_plus_experimental_current.txt
@@ -46,5 +46,24 @@
     ctor public ProfileInstallerInitializer.Result();
   }
 
+  public final class ProfileVerifier {
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.profileinstaller.ProfileVerifier.CompilationStatus!> getCompilationStatusAsync();
+    method @WorkerThread public static androidx.profileinstaller.ProfileVerifier.CompilationStatus writeProfileVerification(android.content.Context);
+  }
+
+  public static class ProfileVerifier.CompilationStatus {
+    method public int getProfileInstallResultCode();
+    method public boolean hasProfileEnqueuedForCompilation();
+    method public boolean isCompiledWithProfile();
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE = 1; // 0x1
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING = 3; // 0x3
+    field public static final int RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ = 131072; // 0x20000
+    field public static final int RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE = 196608; // 0x30000
+    field public static final int RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST = 65536; // 0x10000
+    field public static final int RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION = 262144; // 0x40000
+    field public static final int RESULT_CODE_NO_PROFILE = 0; // 0x0
+    field public static final int RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION = 2; // 0x2
+  }
+
 }
 
diff --git a/profileinstaller/profileinstaller/api/restricted_current.txt b/profileinstaller/profileinstaller/api/restricted_current.txt
index ea6199b..8eed65c 100644
--- a/profileinstaller/profileinstaller/api/restricted_current.txt
+++ b/profileinstaller/profileinstaller/api/restricted_current.txt
@@ -46,5 +46,24 @@
     ctor public ProfileInstallerInitializer.Result();
   }
 
+  public final class ProfileVerifier {
+    method public static com.google.common.util.concurrent.ListenableFuture<androidx.profileinstaller.ProfileVerifier.CompilationStatus!> getCompilationStatusAsync();
+    method @WorkerThread public static androidx.profileinstaller.ProfileVerifier.CompilationStatus writeProfileVerification(android.content.Context);
+  }
+
+  public static class ProfileVerifier.CompilationStatus {
+    method public int getProfileInstallResultCode();
+    method public boolean hasProfileEnqueuedForCompilation();
+    method public boolean isCompiledWithProfile();
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE = 1; // 0x1
+    field public static final int RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING = 3; // 0x3
+    field public static final int RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ = 131072; // 0x20000
+    field public static final int RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE = 196608; // 0x30000
+    field public static final int RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST = 65536; // 0x10000
+    field public static final int RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION = 262144; // 0x40000
+    field public static final int RESULT_CODE_NO_PROFILE = 0; // 0x0
+    field public static final int RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION = 2; // 0x2
+  }
+
 }
 
diff --git a/profileinstaller/profileinstaller/build.gradle b/profileinstaller/profileinstaller/build.gradle
index 06356df..4a26114 100644
--- a/profileinstaller/profileinstaller/build.gradle
+++ b/profileinstaller/profileinstaller/build.gradle
@@ -24,6 +24,8 @@
 dependencies {
     annotationProcessor(libs.nullaway)
     api("androidx.startup:startup-runtime:1.1.1")
+    api(libs.guavaListenableFuture)
+    implementation("androidx.concurrent:concurrent-futures:1.1.0")
     implementation("androidx.annotation:annotation:1.2.0")
     testImplementation(libs.junit)
     testImplementation(libs.truth)
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
index 0bcb376..f1465e1 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/DeviceProfileWriter.java
@@ -286,8 +286,9 @@
     }
 
     private static @Nullable byte[] desiredVersion() {
-        // If SDK is pre-N, we don't want to do anything, so return null.
-        if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK) {
+        // If SDK is pre or post supported version, we don't want to do anything, so return null.
+        if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK
+                || Build.VERSION.SDK_INT > ProfileVersion.MAX_SUPPORTED_SDK) {
             return null;
         }
 
@@ -318,7 +319,8 @@
 
     private static boolean requiresMetadata() {
         // If SDK is pre-N, we don't want to do anything, so return null.
-        if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK) {
+        if (Build.VERSION.SDK_INT < ProfileVersion.MIN_SUPPORTED_SDK
+                || Build.VERSION.SDK_INT > ProfileVersion.MAX_SUPPORTED_SDK) {
             return false;
         }
 
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
index 6f103d4..357b96f 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileInstaller.java
@@ -388,8 +388,9 @@
      * @param filesDir for noting successful installation
      * @param apkName The apk file name the profile is targeting
      * @param diagnostics The diagnostics callback to pass diagnostics to
+     * @return True whether the operation was successful, false otherwise
      */
-    private static void transcodeAndWrite(
+    private static boolean transcodeAndWrite(
             @NonNull AssetManager assets,
             @NonNull String packageName,
             @NonNull PackageInfo packageInfo,
@@ -400,7 +401,7 @@
     ) {
         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
             result(executor, diagnostics, ProfileInstaller.RESULT_UNSUPPORTED_ART_VERSION, null);
-            return;
+            return false;
         }
         File curProfile = new File(new File(PROFILE_BASE_DIR, packageName), PROFILE_FILE);
 
@@ -408,7 +409,7 @@
                 diagnostics, apkName, PROFILE_SOURCE_LOCATION, PROFILE_META_LOCATION, curProfile);
 
         if (!deviceProfileWriter.deviceAllowsProfileInstallerAotWrites()) {
-            return; /* nothing else to do here */
+            return false; /* nothing else to do here */
         }
 
         boolean success = deviceProfileWriter.read()
@@ -418,6 +419,7 @@
         if (success) {
             noteProfileWrittenFor(packageInfo, filesDir);
         }
+        return success;
     }
 
     /**
@@ -531,16 +533,23 @@
             packageInfo = packageManager.getPackageInfo(packageName, 0);
         } catch (PackageManager.NameNotFoundException e) {
             diagnostics.onResultReceived(RESULT_IO_EXCEPTION, e);
+
+            // Calls the verification. Note that in this case since the force install failed we
+            // don't need to report it to the ProfileVerifier.
+            ProfileVerifier.writeProfileVerification(context, false);
             return;
         }
         File filesDir = context.getFilesDir();
         if (forceWriteProfile
                 || !hasAlreadyWrittenProfileForThisInstall(packageInfo, filesDir, diagnostics)) {
             Log.d(TAG, "Installing profile for " + context.getPackageName());
-            transcodeAndWrite(assetManager, packageName, packageInfo, filesDir, apkName, executor,
-                    diagnostics);
+            boolean profileWritten = transcodeAndWrite(assetManager, packageName, packageInfo,
+                    filesDir, apkName, executor, diagnostics);
+            ProfileVerifier.writeProfileVerification(
+                    context, profileWritten && forceWriteProfile);
         } else {
             Log.d(TAG, "Skipping profile installation for " + context.getPackageName());
+            ProfileVerifier.writeProfileVerification(context, false);
         }
     }
 
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVerifier.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVerifier.java
new file mode 100644
index 0000000..e194442
--- /dev/null
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVerifier.java
@@ -0,0 +1,676 @@
+/*
+ * Copyright 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 androidx.profileinstaller;
+
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_NO_PROFILE;
+import static androidx.profileinstaller.ProfileVerifier.CompilationStatus.RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION;
+
+import android.content.Context;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.DoNotInline;
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.RestrictTo;
+import androidx.annotation.WorkerThread;
+import androidx.concurrent.futures.ResolvableFuture;
+
+import com.google.common.util.concurrent.ListenableFuture;
+
+import java.io.BufferedReader;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Provides API to verify whether a compilation profile was installed with the app. This does not
+ * make a distinction between cloud or baseline profile. The output of
+ * {@link #getCompilationStatusAsync()}  allows to check if the app has been compiled with a
+ * compiled profile or whether there is a profile enqueued for compilation.
+ *
+ * If {@link ProfileInstallerInitializer} was disabled, it's necessary to manually trigger the
+ * method {@link #writeProfileVerification(Context)} or the {@link ListenableFuture} returned by
+ * {@link ProfileVerifier#getCompilationStatusAsync()} will hang or timeout.
+ *
+ * Note that {@link ProfileVerifier} requires {@link Build.VERSION_CODES#P} due to a permission
+ * issue: the reference profile folder is not accessible to pre api 28. When calling this api on
+ * unsupported api, {@link #getCompilationStatusAsync()} returns
+ * {@link CompilationStatus#RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION}.
+ */
+public final class ProfileVerifier {
+    private static final String REF_PROFILES_BASE_DIR = "/data/misc/profiles/ref/";
+    private static final String CUR_PROFILES_BASE_DIR = "/data/misc/profiles/cur/0/";
+    private static final String PROFILE_FILE_NAME = "primary.prof";
+    private static final String PROFILE_INSTALLED_CACHE_FILE_NAME = "profileInstalled";
+    private static final ResolvableFuture<CompilationStatus> sFuture = ResolvableFuture.create();
+    private static final Object SYNC_OBJ = new Object();
+    private static final String TAG = "ProfileVerifier";
+
+    @Nullable
+    private static CompilationStatus sCompilationStatus = null;
+
+    private ProfileVerifier() {
+    }
+
+    /**
+     * Caches the information on whether a reference profile exists for this app. This method
+     * performs IO operations and should not be executed on main thread. Note that this method
+     * should be called manually a few seconds after app startup if
+     * {@link  ProfileInstallerInitializer} has been disabled.
+     *
+     * @param context an instance of the {@link Context}.
+     * @return the {@link CompilationStatus} of the app profile. Note that this is the same
+     * {@link CompilationStatus} obtained through {@link #getCompilationStatusAsync()}.
+     */
+    @WorkerThread
+    @NonNull
+    public static CompilationStatus writeProfileVerification(@NonNull Context context
+    ) {
+        return writeProfileVerification(context, false);
+    }
+
+    /**
+     * Caches the information on whether a reference profile exists for this app. This method
+     * performs IO operations and should not be executed on main thread. This specific api is for
+     * internal usage of this package only. The flag {@code forceVerifyCurrentProfile} should
+     * be triggered only when installing from broadcast receiver to force a current profile
+     * verification.
+     *
+     * @param context                   an instance of the {@link Context}.
+     * @param forceVerifyCurrentProfile requests a force verification for current profile. This
+     *                                  should be used when installing profile through
+     *                                  {@link ProfileInstallReceiver}.
+     * @return the {@link CompilationStatus} of the app profile. Note that this is the same
+     * {@link CompilationStatus} obtained through {@link #getCompilationStatusAsync()}.
+     * @hide
+     */
+    @NonNull
+    @WorkerThread
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    static CompilationStatus writeProfileVerification(
+            @NonNull Context context,
+            boolean forceVerifyCurrentProfile
+    ) {
+
+        // `forceVerifyCurrentProfile` can force a verification for the current profile only.
+        // Current profile can be installed at any time through the ProfileInstallerReceiver so
+        // the cached result won't work.
+        if (!forceVerifyCurrentProfile && sCompilationStatus != null) {
+            return sCompilationStatus;
+        }
+
+        synchronized (SYNC_OBJ) {
+
+            if (!forceVerifyCurrentProfile && sCompilationStatus != null) {
+                return sCompilationStatus;
+            }
+
+            // ProfileVerifier supports only api 28 and above.
+            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
+                return setCompilationStatus(
+                        RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION,
+                        false,
+                        false
+                );
+            }
+
+            // Check reference profile file existence. Note that when updating from a version with
+            // profile to a version without profile, a new reference profile of size zero is
+            // created. This should be equivalent to no reference profile.
+            File referenceProfileFile = new File(
+                    new File(REF_PROFILES_BASE_DIR, context.getPackageName()), PROFILE_FILE_NAME);
+            long referenceProfileSize = referenceProfileFile.length();
+            boolean hasReferenceProfile =
+                    referenceProfileFile.exists() && referenceProfileSize > 0;
+
+            // If the reference profile does not exist we need to check if this is simply not
+            // accessible due to a permission issue or if it really doesn't exist. Note that this
+            // operation requires spawning a process and therefore is not done by default. This
+            // behaviour seems to happen only on api 30.
+            if (!hasReferenceProfile && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
+
+                String permissionCheckOutput = executeCommandFirstLineOrFilter(
+                        /* command = */ "ls " + referenceProfileFile.getAbsolutePath(),
+                        /* findLineStartingWith = */ null);
+
+                // If it was actually not possible to check the reference profile because of a
+                // permission issue then try to use oatdump.
+                if (permissionCheckOutput != null
+                        && permissionCheckOutput.contains("Permission denied")) {
+                    String odexBss = getBssFromOdex(context);
+                    if (odexBss != null && !odexBss.contains("empty")) {
+                        hasReferenceProfile = true;
+                        referenceProfileSize = -1L;
+                    }
+                }
+            }
+
+            // Check current profile file existence
+            File currentProfileFile = new File(
+                    new File(CUR_PROFILES_BASE_DIR, context.getPackageName()), PROFILE_FILE_NAME);
+            long currentProfileSize = currentProfileFile.length();
+            boolean hasCurrentProfile =
+                    currentProfileFile.exists() && currentProfileSize > 0;
+
+            // Checks package last update time that will be used to determine whether the app
+            // has been updated.
+            long packageLastUpdateTime;
+            try {
+                packageLastUpdateTime = getPackageLastUpdateTime(context);
+            } catch (PackageManager.NameNotFoundException e) {
+                return setCompilationStatus(
+                        RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST,
+                        hasReferenceProfile,
+                        hasCurrentProfile
+                );
+            }
+
+            // Reads the current profile verification cache file
+            File cacheFile = new File(context.getFilesDir(), PROFILE_INSTALLED_CACHE_FILE_NAME);
+            Cache currentCache = null;
+            if (cacheFile.exists()) {
+                try {
+                    currentCache = Cache.readFromFile(cacheFile);
+                } catch (IOException e) {
+                    return setCompilationStatus(
+                            RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ,
+                            hasReferenceProfile,
+                            hasCurrentProfile
+                    );
+                }
+            }
+
+            // Here it's calculated the result code, initially set to either the latest saved value
+            // or `no profile exists`
+            int resultCode;
+
+            // There are 2 profiles: reference and current. These 2 are handled differently.
+            // The reference profile can be installed only by package manager or app Store.
+            // This can be assessed only at first app start or app updates (i.e. when the package
+            // info last update has changed). After the first install a reference profile can be
+            // created as a result of bg dex opt.
+
+            // Check if this is a first start or an update or the previous profile was awaiting
+            // compilation.
+            if (currentCache == null
+                    || currentCache.mPackageLastUpdateTime != packageLastUpdateTime
+                    || currentCache.mResultCode
+                    == RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION) {
+
+                // If so, reevaluate if the app has a reference profile and whether a current
+                // profile has been installed (since this runs after profile installer).
+                if (hasReferenceProfile) {
+                    resultCode = RESULT_CODE_COMPILED_WITH_PROFILE;
+                } else if (hasCurrentProfile) {
+                    resultCode = RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION;
+                } else {
+                    resultCode = RESULT_CODE_NO_PROFILE;
+                }
+            } else {
+
+                // If not, utilize the cached result since the reference profile might be the result
+                // of a bg dex opt.
+                resultCode = currentCache.mResultCode;
+            }
+
+            // A current profile can be installed by the profile installer also through broadcast,
+            // therefore if this was a forced installation it can happen at anytime. the flag
+            // `forceVerifyCurrentProfile` can request a force verification for the current
+            // profile only.
+            if (forceVerifyCurrentProfile && hasCurrentProfile
+                    && resultCode != RESULT_CODE_COMPILED_WITH_PROFILE) {
+                resultCode = RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION;
+            }
+
+            // If a profile has just been compiled, verify if the size matches between reference
+            // and current matches.
+            if (currentCache != null
+                    && (currentCache.mResultCode == RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION)
+                    && resultCode == RESULT_CODE_COMPILED_WITH_PROFILE) {
+
+                // If there is an issue with the profile compilation, the reference profile size
+                // might be smaller than the current profile installed by profileinstaller. Note
+                // that this is not 100% accurate and it may return the wrong information if the
+                // portion of current profile added to the installed current profile, when the
+                // user uses the app, is larger than the installed current profile itself.
+
+                // The size of the reference profile should be at least the same in current if
+                // the compilation worked. Otherwise something went wrong. Note that on some api
+                // levels the reference profile file may not be visible to the app, so size
+                // cannot be read. In this case we fallback to the odex and set size -1.
+                if (referenceProfileSize != -1L
+                        && referenceProfileSize < currentCache.mInstalledCurrentProfileSize) {
+                    resultCode = RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING;
+                }
+            }
+
+            // We now have a new verification result.
+            Cache newCache = new Cache(
+                    /* schema = */ Cache.SCHEMA,
+                    /* resultCode = */ resultCode,
+                    /* packageLastUpdateTime = */ packageLastUpdateTime,
+                    /* installedCurrentProfileSize = */ currentProfileSize
+            );
+
+            // At this point we can cache the result if there was no cache file or if the result has
+            // changed (for example due to a force install).
+            if (currentCache == null || !currentCache.equals(newCache)) {
+                try {
+                    newCache.writeOnFile(cacheFile);
+                } catch (IOException e) {
+                    resultCode =
+                            RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE;
+                }
+            }
+
+            // Set and report the calculated value
+            return setCompilationStatus(resultCode, hasReferenceProfile, hasCurrentProfile);
+        }
+    }
+
+    private static CompilationStatus setCompilationStatus(
+            int resultCode,
+            boolean hasReferenceProfile,
+            boolean hasCurrentProfile
+    ) {
+        sCompilationStatus = new CompilationStatus(
+                /* resultCode = */ resultCode,
+                /* hasReferenceProfile */ hasReferenceProfile,
+                /* hasCurrentProfile */ hasCurrentProfile
+        );
+        sFuture.set(sCompilationStatus);
+        return sCompilationStatus;
+    }
+
+    @SuppressWarnings("deprecation")
+    private static long getPackageLastUpdateTime(Context context)
+            throws PackageManager.NameNotFoundException {
+
+        // PackageManager#getPackageInfo(String, int) was deprecated in API 33.
+        PackageManager packageManager = context.getApplicationContext().getPackageManager();
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            return Api33Impl.getPackageInfo(packageManager, context).lastUpdateTime;
+        } else {
+            return packageManager.getPackageInfo(context.getPackageName(), 0).lastUpdateTime;
+        }
+    }
+
+    /**
+     * Parses the output of `oatdump` on the odex file generated during compilation. Note that if
+     * the odex file does not exist, this method returns null.
+     *
+     * @param context an instance of the {@link Context}.
+     * @return the compilation filter property from the oatdump output.
+     */
+    @Nullable
+    private static String getBssFromOdex(Context context) {
+
+        // Get odex file path
+        String baseApkPath = executeCommandFirstLineOrFilter(
+                "pm path " + context.getPackageName(),
+                "package:");
+        if (baseApkPath == null) {
+            return null;
+        }
+
+        // Use oatdump to parse odex
+        String odexPath = new File(
+                new File(baseApkPath).getParentFile(), "oat/arm64/base.odex").getAbsolutePath();
+        return executeCommandFirstLineOrFilter(
+                /* command = */ "oatdump"
+                        + " --oat-file=" + odexPath
+                        + " --header-only"
+                        + " --no-disassemble",
+                /* findLineStartingWith = */ ".bss:");
+    }
+
+    @Nullable
+    private static String executeCommandFirstLineOrFilter(
+            String command,
+            @Nullable String findLineStartingWith) {
+
+        // Note that this method does not wait for the spawned process to end, rather it looks for
+        // the given line and kills the process after finding it.
+        String output = null;
+        try {
+
+            // Starts the process
+            Process p = new ProcessBuilder(command.split(" "))
+                    .redirectErrorStream(true)
+                    .start();
+            BufferedReader reader =
+                    new BufferedReader(new InputStreamReader(p.getInputStream()));
+
+            // Finds the requested line
+            String line;
+            while ((line = reader.readLine()) != null) {
+
+                // If findLineStartingWith is null, only returns the first line
+                if (findLineStartingWith == null) {
+                    output = line;
+                    break;
+                }
+
+                // Otherwise find a line starting with the given param
+                if (line.startsWith(findLineStartingWith)) {
+                    String[] parts = line.split(findLineStartingWith);
+                    output = parts.length < 2 ? null : parts[1].trim();
+                    break;
+                }
+            }
+
+            // Kills the process in case it's still alive
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                Api26Impl.destroyForcibly(p);
+            } else {
+                p.destroy();
+            }
+
+        } catch (Exception e) {
+            Log.e(TAG, "Cannot execute " + command);
+        }
+
+        return output;
+    }
+
+    /**
+     * Returns a future containing the {@link CompilationStatus} of the app profile. The
+     * {@link CompilationStatus} can be used to determine whether a baseline or cloud profile is
+     * installed either through app store or package manager (reference profile) or profile
+     * installer (current profile), in order to tag performance metrics versions. In the first
+     * case a reference profile is immediately installed, i.e. a the app has been compiled with a
+     * profile. In the second case the profile is awaiting compilation that will happen at some
+     * point later in background.
+     *
+     * @return A future containing the {@link CompilationStatus}.
+     */
+    @NonNull
+    public static ListenableFuture<CompilationStatus> getCompilationStatusAsync() {
+        return sFuture;
+    }
+
+    /** @hide */
+    @RestrictTo(RestrictTo.Scope.LIBRARY)
+    static class Cache {
+        private static final int SCHEMA = 1;
+        final int mSchema;
+        final int mResultCode;
+        final long mPackageLastUpdateTime;
+        final long mInstalledCurrentProfileSize;
+
+        Cache(
+                int schema,
+                int resultCode,
+                long packageLastUpdateTime,
+                long installedCurrentProfileSize
+        ) {
+            mSchema = schema;
+            mResultCode = resultCode;
+            mPackageLastUpdateTime = packageLastUpdateTime;
+            mInstalledCurrentProfileSize = installedCurrentProfileSize;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || !(o instanceof Cache)) return false;
+            Cache cacheFile = (Cache) o;
+            return mResultCode == cacheFile.mResultCode
+                    && mPackageLastUpdateTime == cacheFile.mPackageLastUpdateTime
+                    && mSchema == cacheFile.mSchema
+                    && mInstalledCurrentProfileSize == cacheFile.mInstalledCurrentProfileSize;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(
+                    mResultCode,
+                    mPackageLastUpdateTime,
+                    mSchema,
+                    mInstalledCurrentProfileSize
+            );
+        }
+
+        void writeOnFile(@NonNull File file) throws IOException {
+            file.delete();
+            try (DataOutputStream dos = new DataOutputStream(new FileOutputStream(file))) {
+                dos.writeInt(mSchema);
+                dos.writeInt(mResultCode);
+                dos.writeLong(mPackageLastUpdateTime);
+                dos.writeLong(mInstalledCurrentProfileSize);
+            }
+        }
+
+        static Cache readFromFile(@NonNull File file) throws IOException {
+            try (DataInputStream dis = new DataInputStream(new FileInputStream(file))) {
+                return new Cache(
+                        dis.readInt(),
+                        dis.readInt(),
+                        dis.readLong(),
+                        dis.readLong()
+                );
+            }
+        }
+    }
+
+    /**
+     * {@link CompilationStatus} contains the result of a profile verification operation. It
+     * offers API to determine whether a profile was installed
+     * {@link CompilationStatus#getProfileInstallResultCode()} and to check whether the app has
+     * been compiled with a profile or a profile is enqueued for compilation. Note that the
+     * app can be compiled with a profile also as result of background dex optimization.
+     */
+    public static class CompilationStatus {
+
+        /** @hide */
+        @RestrictTo(RestrictTo.Scope.LIBRARY)
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef({
+                RESULT_CODE_NO_PROFILE,
+                RESULT_CODE_COMPILED_WITH_PROFILE,
+                RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION,
+                RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING,
+                RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST,
+                RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ,
+                RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE,
+                RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION
+        })
+        public @interface ResultCode {
+        }
+
+        private static final int RESULT_CODE_ERROR_CODE_BIT_SHIFT = 16;
+
+        /**
+         * Indicates that no profile was installed for this app. This means that no profile was
+         * installed when installing the app through app store or package manager and profile
+         * installer either didn't run ({@link ProfileInstallerInitializer} disabled) or the app
+         * was packaged without a compilation profile.
+         */
+        public static final int RESULT_CODE_NO_PROFILE = 0;
+
+        /**
+         * Indicates that a profile is installed and the app has been compiled with it. This is the
+         * result of installation through app store or package manager, or installation through
+         * profile installer and subsequent compilation during background dex optimization.
+         */
+        public static final int RESULT_CODE_COMPILED_WITH_PROFILE = 1;
+
+        /**
+         * Indicates that a profile is installed and the app will be compiled with it later when
+         * background dex optimization runs. This is the result of installation through profile
+         * installer. When the profile is compiled, the result code will change to
+         * {@link #RESULT_CODE_COMPILED_WITH_PROFILE}.
+         */
+        public static final int RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION = 2;
+
+        /**
+         * Indicates that a profile is installed and the app has been compiled with it.
+         * This is the result of installation through app store or package manager. Note that
+         * this result differs from {@link #RESULT_CODE_COMPILED_WITH_PROFILE} as the profile
+         * is smaller than expected and may not include all the methods initially included in the
+         * baseline profile.
+         */
+        public static final int RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING = 3;
+
+        /**
+         * Indicates an error during the verification process: a
+         * {@link PackageManager.NameNotFoundException} was launched when querying the
+         * {@link PackageManager} for the app package.
+         */
+        public static final int RESULT_CODE_ERROR_PACKAGE_NAME_DOES_NOT_EXIST =
+                1 << RESULT_CODE_ERROR_CODE_BIT_SHIFT;
+
+        /**
+         * Indicates that a previous verification result cache file exists but it cannot be read.
+         */
+        public static final int RESULT_CODE_ERROR_CACHE_FILE_EXISTS_BUT_CANNOT_BE_READ =
+                2 << RESULT_CODE_ERROR_CODE_BIT_SHIFT;
+
+        /**
+         * Indicates that wasn't possible to write the verification result cache file. This can
+         * happen only because something is wrong with app folder permissions or if there is no
+         * free disk space on the device.
+         */
+        public static final int
+                RESULT_CODE_ERROR_CANT_WRITE_PROFILE_VERIFICATION_RESULT_CACHE_FILE =
+                3 << RESULT_CODE_ERROR_CODE_BIT_SHIFT;
+
+        /**
+         * Indicates that ProfileVerifier runs on an unsupported api version of Android.
+         * Note that ProfileVerifier supports only {@link Build.VERSION_CODES#P} and above.
+         * Note that when this result code is returned {@link #isCompiledWithProfile()} and
+         * {@link #hasProfileEnqueuedForCompilation()} return false.
+         */
+        public static final int RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION =
+                4 << RESULT_CODE_ERROR_CODE_BIT_SHIFT;
+
+        final int mResultCode;
+        private final boolean mHasReferenceProfile;
+        private final boolean mHasCurrentProfile;
+
+        CompilationStatus(
+                int resultCode,
+                boolean hasReferenceProfile,
+                boolean hasCurrentProfile
+        ) {
+            this.mResultCode = resultCode;
+            this.mHasCurrentProfile = hasCurrentProfile;
+            this.mHasReferenceProfile = hasReferenceProfile;
+        }
+
+        /**
+         * @return a result code that indicates whether there is a baseline profile installed and
+         * whether the app has been compiled with it. This depends on the installation method: if it
+         * was installed through app store or package manager the app gets compiled immediately
+         * with the profile and the return code is
+         * {@link CompilationStatus#RESULT_CODE_COMPILED_WITH_PROFILE},
+         * otherwise it'll be in `awaiting compilation` state and it'll be compiled at some point
+         * later in the future, so the return code will be
+         * {@link CompilationStatus#RESULT_CODE_PROFILE_ENQUEUED_FOR_COMPILATION}.
+         * In the case that no profile was installed, the result code will be
+         * {@link CompilationStatus#RESULT_CODE_NO_PROFILE}.
+         *
+         * Note that even if no profile was installed it's still possible for the app to have a
+         * profile and be compiled with it, as result of background dex optimization.
+         * The result code does a simple size check to ensure the compilation process completed
+         * without errors. If the size check fails this method will return
+         * {@link CompilationStatus#RESULT_CODE_COMPILED_WITH_PROFILE_NON_MATCHING}. The size
+         * check is
+         * not 100% accurate as the actual compiled methods are not checked.
+         *
+         * If something fails during the verification process, this method will return one of the
+         * result codes associated with an error.
+         *
+         * Note that only api 28 {@link Build.VERSION_CODES#P} and above is supported
+         * and that {@link #RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION} is returned when calling
+         * this api on pre api 28.
+         */
+        @ResultCode
+        public int getProfileInstallResultCode() {
+            return mResultCode;
+        }
+
+        /**
+         * @return True whether this app has been compiled with a profile, false otherwise. An
+         * app can be compiled with a profile because of profile installation through app store,
+         * package manager or profileinstaller and subsequent background dex optimization. There
+         * should be a performance improvement when an app has been compiled with a profile. Note
+         * that if {@link #getProfileInstallResultCode()} returns
+         * {@link #RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION} this method always returns
+         * always false.
+         */
+        public boolean isCompiledWithProfile() {
+            return mHasReferenceProfile;
+        }
+
+        /**
+         * @return True whether this app has a profile enqueued for compilation, false otherwise. An
+         * app can have a profile enqueued for compilation because of profile installation through
+         * profileinstaller or simply when the user starts interacting with the app. Note that if
+         * {@link #getProfileInstallResultCode()} returns
+         * {@link #RESULT_CODE_ERROR_UNSUPPORTED_API_VERSION} this method always returns false.
+         */
+        public boolean hasProfileEnqueuedForCompilation() {
+            return mHasCurrentProfile;
+        }
+    }
+
+    @RequiresApi(26)
+    private static class Api26Impl {
+        private Api26Impl() {
+        }
+
+        @DoNotInline
+        static Process destroyForcibly(Process process) {
+            return process.destroyForcibly();
+        }
+    }
+
+    @RequiresApi(33)
+    private static class Api33Impl {
+        private Api33Impl() {
+        }
+
+        @DoNotInline
+        static PackageInfo getPackageInfo(
+                PackageManager packageManager,
+                Context context) throws PackageManager.NameNotFoundException {
+            return packageManager.getPackageInfo(
+                    context.getPackageName(),
+                    PackageManager.PackageInfoFlags.of(0)
+            );
+        }
+    }
+}
diff --git a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVersion.java b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVersion.java
index e98c680..783346a 100644
--- a/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVersion.java
+++ b/profileinstaller/profileinstaller/src/main/java/androidx/profileinstaller/ProfileVersion.java
@@ -18,9 +18,13 @@
 
 import android.os.Build;
 
+import androidx.annotation.RestrictTo;
+
 import java.util.Arrays;
 
-class ProfileVersion {
+/** @hide */
+@RestrictTo(RestrictTo.Scope.LIBRARY)
+public class ProfileVersion {
     private ProfileVersion() {}
     static final byte[] V015_S = new byte[]{'0', '1', '5', '\0'};
     static final byte[] V010_P = new byte[]{'0', '1', '0', '\0'};
@@ -29,7 +33,8 @@
     static final byte[] V001_N = new byte[]{'0', '0', '1', '\0'};
     static final byte[] METADATA_V001_N = new byte[]{'0', '0', '1', '\0'};
     static final byte[] METADATA_V002 = new byte[]{'0', '0', '2', '\0'};
-    static final int MIN_SUPPORTED_SDK = Build.VERSION_CODES.N;
+    public static final int MIN_SUPPORTED_SDK = Build.VERSION_CODES.N;
+    public static final int MAX_SUPPORTED_SDK = Build.VERSION_CODES.TIRAMISU;
 
     static String dexKeySeparator(byte[] version) {
         if (Arrays.equals(version, V001_N)) {
diff --git a/settings.gradle b/settings.gradle
index 014208e..1aeb29e 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -571,7 +571,7 @@
 includeProject(":compose:ui:ui-viewbinding:ui-viewbinding-samples", "compose/ui/ui-viewbinding/samples", [BuildType.COMPOSE])
 includeProject(":compose:ui:ui:integration-tests:ui-demos", [BuildType.COMPOSE])
 includeProject(":compose:ui:ui:ui-samples", "compose/ui/ui/samples", [BuildType.COMPOSE])
-includeProject(":concurrent:concurrent-futures", [BuildType.MAIN, BuildType.CAMERA])
+includeProject(":concurrent:concurrent-futures", [BuildType.MAIN, BuildType.CAMERA, BuildType.COMPOSE])
 includeProject(":concurrent:concurrent-futures-ktx", [BuildType.MAIN, BuildType.CAMERA])
 includeProject(":constraintlayout:constraintlayout-compose", [BuildType.COMPOSE])
 includeProject(":constraintlayout:constraintlayout-compose:integration-tests:constraintlayout-compose-demos", [BuildType.COMPOSE])
@@ -785,6 +785,9 @@
 includeProject(":privacysandbox:tools:tools-core", [BuildType.MAIN])
 includeProject(":privacysandbox:tools:tools-testing", [BuildType.MAIN])
 includeProject(":profileinstaller:profileinstaller", [BuildType.MAIN, BuildType.COMPOSE])
+includeProject(":profileinstaller:integration-tests:profile-verification", [BuildType.MAIN])
+includeProject(":profileinstaller:integration-tests:profile-verification-sample", [BuildType.MAIN])
+includeProject(":profileinstaller:integration-tests:profile-verification-sample-no-initializer", [BuildType.MAIN])
 includeProject(":profileinstaller:integration-tests:init-macrobenchmark", [BuildType.MAIN])
 includeProject(":profileinstaller:integration-tests:init-macrobenchmark-target", [BuildType.MAIN])
 includeProject(":profileinstaller:profileinstaller-benchmark", [BuildType.MAIN])