Foundation of benchmark integration to JCA (#80)

diff --git a/.idea/androidTestResultsUserPreferences.xml b/.idea/androidTestResultsUserPreferences.xml
index 6abdd98..1f01370 100644
--- a/.idea/androidTestResultsUserPreferences.xml
+++ b/.idea/androidTestResultsUserPreferences.xml
@@ -3,6 +3,19 @@
   <component name="AndroidTestResultsUserPreferences">
     <option name="androidTestResultsTableState">
       <map>
+        <entry key="-1168588695">
+          <value>
+            <AndroidTestResultsTableState>
+              <option name="preferredColumnWidths">
+                <map>
+                  <entry key="Duration" value="90" />
+                  <entry key="Pixel_7_Pro_API_34" value="120" />
+                  <entry key="Tests" value="360" />
+                </map>
+              </option>
+            </AndroidTestResultsTableState>
+          </value>
+        </entry>
         <entry key="401594821">
           <value>
             <AndroidTestResultsTableState>
@@ -16,6 +29,19 @@
             </AndroidTestResultsTableState>
           </value>
         </entry>
+        <entry key="571770275">
+          <value>
+            <AndroidTestResultsTableState>
+              <option name="preferredColumnWidths">
+                <map>
+                  <entry key="Duration" value="90" />
+                  <entry key="Pixel_7_Pro_API_34" value="120" />
+                  <entry key="Tests" value="360" />
+                </map>
+              </option>
+            </AndroidTestResultsTableState>
+          </value>
+        </entry>
         <entry key="2043991187">
           <value>
             <AndroidTestResultsTableState>
diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index ba0df2e..37f7544 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -5,12 +5,14 @@
     <option name="linkedExternalProjectsSettings">
       <GradleProjectSettings>
         <option name="testRunner" value="GRADLE" />
+        <option name="distributionType" value="DEFAULT_WRAPPED" />
         <option name="externalProjectPath" value="$PROJECT_DIR$" />
-        <option name="gradleJvm" value="JDK" />
+        <option name="gradleJvm" value="Android Studio default JDK" />
         <option name="modules">
           <set>
             <option value="$PROJECT_DIR$" />
             <option value="$PROJECT_DIR$/app" />
+            <option value="$PROJECT_DIR$/benchmark" />
             <option value="$PROJECT_DIR$/camera-viewfinder-compose" />
             <option value="$PROJECT_DIR$/core" />
             <option value="$PROJECT_DIR$/core/common" />
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index fe988de..9b7c732 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -46,6 +46,11 @@
                 "proguard-rules.pro"
             )
         }
+        create("benchmark") {
+            initWith(buildTypes.getByName("release"))
+            signingConfig = signingConfigs.getByName("debug")
+            matchingFallbacks += listOf("release")
+        }
     }
     compileOptions {
         sourceCompatibility = JavaVersion.VERSION_17
@@ -118,6 +123,10 @@
 
     // Settings Screen
     implementation(project(":feature:settings"))
+
+    // benchmark
+    implementation("androidx.profileinstaller:profileinstaller:1.3.1")
+
 }
 
 // Allow references to generated code
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 40f0301..5e399a4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -37,6 +37,7 @@
         android:supportsRtl="true"
         android:theme="@style/Theme.JetpackCamera"
         tools:targetApi="33">
+        <profileable android:shell="true"/>
         <activity
             android:name=".MainActivity"
             android:exported="true"
diff --git a/benchmark/.gitignore b/benchmark/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/benchmark/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/benchmark/build.gradle.kts b/benchmark/build.gradle.kts
new file mode 100644
index 0000000..bd20c97
--- /dev/null
+++ b/benchmark/build.gradle.kts
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+plugins {
+    id("com.android.test")
+    id("org.jetbrains.kotlin.android")
+}
+
+android {
+    namespace = "com.example.benchmark"
+    compileSdk = 34
+
+    compileOptions {
+        sourceCompatibility = JavaVersion.VERSION_1_8
+        targetCompatibility = JavaVersion.VERSION_1_8
+    }
+
+    kotlinOptions {
+        jvmTarget = "1.8"
+    }
+
+    defaultConfig {
+        //Our app has a minSDK of 21, but in order for the benchmark tool to function, it must be 23
+        minSdk = 23
+        targetSdk = 34
+
+        // allows the benchmark to be run on an emulator
+        testInstrumentationRunnerArguments["androidx.benchmark.suppressErrors"] =
+            "EMULATOR,LOW-BATTERY,NOT-PROFILEABLE"
+
+        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+    }
+
+    buildTypes {
+        // This benchmark buildType is used for benchmarking, and should function like your
+        // release build (for example, with minification on). It"s signed with a debug key
+        // for easy local/CI testing.
+        create("benchmark") {
+            isDebuggable = true
+            signingConfig = getByName("debug").signingConfig
+            matchingFallbacks += listOf("release")
+        }
+    }
+
+    targetProjectPath = ":app"
+    experimentalProperties["android.experimental.self-instrumenting"] = true
+    // required for benchmark:
+    // self instrumentation required for the tests to be able to compile, start, or kill the app
+    // ensures test and app processes are separate
+    // see https://source.android.com/docs/core/tests/development/instr-self-e2e
+}
+
+dependencies {
+    implementation("androidx.test.ext:junit:1.1.5")
+    implementation("androidx.benchmark:benchmark-macro-junit4:1.2.1")
+}
+
+androidComponents {
+    beforeVariants(selector().all()) {
+        it.enable = it.buildType == "benchmark"
+    }
+}
\ No newline at end of file
diff --git a/benchmark/src/main/AndroidManifest.xml b/benchmark/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..0370d89
--- /dev/null
+++ b/benchmark/src/main/AndroidManifest.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+  ~ Copyright (C) 2023 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~      http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+<manifest />
\ No newline at end of file
diff --git a/benchmark/src/main/java/com/example/benchmark/Permissions.kt b/benchmark/src/main/java/com/example/benchmark/Permissions.kt
new file mode 100644
index 0000000..9a6d595
--- /dev/null
+++ b/benchmark/src/main/java/com/example/benchmark/Permissions.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.benchmark
+
+import android.Manifest.permission
+import android.os.Build
+import androidx.benchmark.macro.MacrobenchmarkScope
+import org.junit.Assert
+
+fun MacrobenchmarkScope.allowCamera() {
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+        val command = "pm grant $packageName ${permission.CAMERA}"
+        val output = device.executeShellCommand(command)
+        Assert.assertEquals("", output)
+    }
+}
diff --git a/benchmark/src/main/java/com/example/benchmark/StartupBenchmark.kt b/benchmark/src/main/java/com/example/benchmark/StartupBenchmark.kt
new file mode 100644
index 0000000..34ffd3e
--- /dev/null
+++ b/benchmark/src/main/java/com/example/benchmark/StartupBenchmark.kt
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.example.benchmark
+
+import androidx.benchmark.macro.MacrobenchmarkScope
+import androidx.benchmark.macro.StartupMode
+import androidx.benchmark.macro.StartupTimingMetric
+import androidx.benchmark.macro.junit4.MacrobenchmarkRule
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * This is an example startup benchmark.
+ *
+ * It navigates to the device's home screen, and launches the default activity.
+ *
+ * Before running this benchmark:
+ * 1) switch your app's active build variant in the Studio (affects Studio runs only)
+ * 2) add `<profileable android:shell="true" />` to your app's manifest, within the `<application>` tag
+ *
+ * Run this benchmark from Studio to see startup measurements, and captured system traces
+ * for investigating your app's performance.
+ */
+@RunWith(AndroidJUnit4::class)
+class StartupBenchmark {
+    @get:Rule
+    val benchmarkRule = MacrobenchmarkRule()
+
+    @Test
+    fun startupNoCameraPermission() {
+        benchmarkStartup()
+    }
+
+    @Test
+    fun startupWithCameraPermission() {
+        benchmarkStartup(
+            setupBlock =
+            { allowCamera() }
+        )
+    }
+
+    private fun benchmarkStartup(setupBlock: MacrobenchmarkScope.() -> Unit = {}) {
+        benchmarkRule.measureRepeated(
+            packageName = "com.google.jetpackcamera",
+            metrics = listOf(StartupTimingMetric()),
+            iterations = 5,
+            startupMode = StartupMode.COLD,
+            setupBlock = setupBlock
+
+        ) {
+            pressHome()
+            startActivityAndWait()
+        }
+    }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index d22d8a2..0b240f9 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -20,6 +20,7 @@
     id("com.android.library") version "8.1.1" apply false
     id("org.jetbrains.kotlin.android") version "1.8.0" apply false
     id("com.google.dagger.hilt.android") version "2.44" apply false
+    id("com.android.test") version "8.1.1" apply false
 }
 
 tasks.register<Copy>("installGitHooks") {
diff --git a/data/settings/consumer-rules.pro b/data/settings/consumer-rules.pro
index e69de29..2b9ca75 100644
--- a/data/settings/consumer-rules.pro
+++ b/data/settings/consumer-rules.pro
@@ -0,0 +1 @@
+-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite* {<fields>;}
\ No newline at end of file
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 5e4c892..ec036b0 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -40,3 +40,4 @@
 include(":data:settings")
 include(":core:common")
 include(":feature:quicksettings")
+include(":benchmark")