Merge "Initial commit for AOSP merge" into androidx-master-dev
diff --git a/camera/integration-tests/timingtestapp/build.gradle b/camera/integration-tests/timingtestapp/build.gradle
index 3aa3257..e6d2fea 100644
--- a/camera/integration-tests/timingtestapp/build.gradle
+++ b/camera/integration-tests/timingtestapp/build.gradle
@@ -1,16 +1,15 @@
 /*
- * Copyright (C) 2019 The Android Open Source Project
+ * Copyright 2019 The Android Open Source Project
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
- *      http://www.apache.org/licenses/LICENSE-2.0
+ *     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.
+ * 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.
  */
@@ -20,16 +19,19 @@
 plugins {
     id("AndroidXPlugin")
     id("com.android.application")
+    id("kotlin-android")
+    id("kotlin-android-extensions")
 }
 
 android {
+    compileSdkVersion 28
     defaultConfig {
-        applicationId "androidx.camera.integration.timing"
+        applicationId "androidx.camera.integration.antelope"
         minSdkVersion 21
-        versionCode 1
-        multiDexEnabled true
+        targetSdkVersion 28
+        versionCode 34
+        versionName "1.34"
     }
-
     sourceSets {
         main.manifest.srcFile 'src/main/AndroidManifest.xml'
         main.java.srcDirs = ['src/main/java']
@@ -37,26 +39,42 @@
         main.java.includes = ['**/*.java']
         main.res.srcDirs = ['src/main/res']
     }
-
     buildTypes {
-        debug {
-            testCoverageEnabled true
-        }
-
         release {
+            minifyEnabled false
         }
     }
+    compileOptions {
+        sourceCompatibility JavaVersion.VERSION_1_8
+        targetCompatibility JavaVersion.VERSION_1_8
+    }
 }
 
 dependencies {
     // Internal library
     implementation(project(":camera:camera-camera2"))
+    implementation(project(":camera:camera-core"))
 
-    // Android Support Library
-    api(CONSTRAINT_LAYOUT, { transitive = true })
-    implementation(project(":appcompat"))
+    // Lifecycle and LiveData
+    implementation(ARCH_LIFECYCLE_EXTENSIONS)
 
-    // Guava
+    // Android support library
+    implementation(SUPPORT_APPCOMPAT)
+    implementation(ANDROIDX_COLLECTION)
+    implementation(project(":preference")) // This one is still in alpha
+    implementation("androidx.exifinterface:exifinterface:1.0.0")
+    implementation(CONSTRAINT_LAYOUT, { transitive = true })
+    implementation(KOTLIN_STDLIB)
+
+    // Testing framework
+    implementation 'androidx.test.espresso:espresso-idling-resource:3.1.0'
+    androidTestImplementation(ANDROIDX_TEST_EXT_JUNIT)
+    androidTestImplementation(ANDROIDX_TEST_CORE)
+    androidTestImplementation(ANDROIDX_TEST_RUNNER)
+    androidTestImplementation(ANDROIDX_TEST_RULES)
+    androidTestImplementation(ANDROIDX_TEST_UIAUTOMATOR)
+    androidTestImplementation(ESPRESSO_CORE)
+
+    // Statistics library
     implementation(GUAVA_ANDROID)
 }
-
diff --git a/camera/integration-tests/timingtestapp/src/main/AndroidManifest.xml b/camera/integration-tests/timingtestapp/src/main/AndroidManifest.xml
index 921b5cd..20539cc 100644
--- a/camera/integration-tests/timingtestapp/src/main/AndroidManifest.xml
+++ b/camera/integration-tests/timingtestapp/src/main/AndroidManifest.xml
@@ -1,34 +1,54 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2019 The Android Open Source Project
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
 
-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"
-    package="androidx.camera.integration.timing">
+    package="androidx.camera.integration.antelope">
+
     <uses-permission android:name="android.permission.CAMERA" />
+
+    <uses-feature
+        android:name="android.hardware.camera.any"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.camera.external"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.camera.front"
+        android:required="false" />
+    <uses-feature
+        android:name="android.hardware.camera.raw"
+        android:required="false" />
+
     <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
-    <uses-permission android:name="android.permission.RECORD_AUDIO" />
+    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
 
-    <uses-feature android:name="android.hardware.camera" />
-
-    <application android:theme="@style/AppTheme">
-        <activity
-            android:name=".TakePhotoActivity"
-            android:label="Taking Photo">
+    <application
+        android:allowBackup="true"
+        android:icon="@mipmap/ic_launcher"
+        android:label="@string/app_name"
+        android:roundIcon="@mipmap/ic_launcher_round"
+        android:supportsRtl="true"
+        android:theme="@style/AppTheme">
+        <activity android:name="androidx.camera.integration.antelope.MainActivity">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
+
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
     </application>
-</manifest>
+
+</manifest>
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/ic_launcher-web.png b/camera/integration-tests/timingtestapp/src/main/ic_launcher-web.png
new file mode 100644
index 0000000..ae4c109
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/ic_launcher-web.png
Binary files differ
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/AutoFitSurfaceView.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/AutoFitSurfaceView.kt
new file mode 100644
index 0000000..e996710
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/AutoFitSurfaceView.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.SurfaceView
+import android.view.View
+
+/**
+ * A [SurfaceView] that can be adjusted to a specified aspect ratio.
+ */
+class AutoFitSurfaceView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyle: Int = 0
+) : SurfaceView(context, attrs, defStyle) {
+
+    private var ratioWidth = 0
+    private var ratioHeight = 0
+
+    /**
+     * Sets the aspect ratio for this view. The size of the view will be measured based on the ratio
+     * calculated from the parameters. Note that the actual sizes of parameters don't matter, that
+     * is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) have the same result.
+     *
+     * @param width Relative horizontal size
+     * @param height Relative vertical size
+     */
+    fun setAspectRatio(width: Int, height: Int) {
+        if (width < 0 || height < 0) {
+            throw IllegalArgumentException("Size cannot be negative.")
+        }
+        ratioWidth = width
+        ratioHeight = height
+        requestLayout()
+    }
+
+    /**
+     * When the surface is given a new width/height, ensure that it maintains the correct aspect
+     * ratio.
+     */
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+        val width = View.MeasureSpec.getSize(widthMeasureSpec)
+        val height = View.MeasureSpec.getSize(heightMeasureSpec)
+        if (ratioWidth == 0 || ratioHeight == 0) {
+            setMeasuredDimension(width, height)
+        } else {
+            if (width < height * ratioWidth / ratioHeight) {
+                setMeasuredDimension(width, width * ratioHeight / ratioWidth)
+            } else {
+                setMeasuredDimension(height * ratioWidth / ratioHeight, height)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/AutoFitTextureView.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/AutoFitTextureView.kt
new file mode 100644
index 0000000..7f814b5
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/AutoFitTextureView.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import android.content.Context
+import android.util.AttributeSet
+import android.view.TextureView
+import android.view.View
+
+/**
+ * A [TextureView] that can be adjusted to a specified aspect ratio.
+ */
+class AutoFitTextureView @JvmOverloads constructor(
+    context: Context,
+    attrs: AttributeSet? = null,
+    defStyle: Int = 0
+) : TextureView(context, attrs, defStyle) {
+
+    private var ratioWidth = 0
+    private var ratioHeight = 0
+
+    /**
+     * Sets the aspect ratio for this view. The size of the view will be measured based on the ratio
+     * calculated from the parameters. Note that the actual sizes of parameters don't matter, that
+     * is, calling setAspectRatio(2, 3) and setAspectRatio(4, 6) make the same result.
+     *
+     * @param width Relative horizontal size
+     * @param height Relative vertical size
+     */
+    fun setAspectRatio(width: Int, height: Int) {
+        if (width < 0 || height < 0) {
+            throw IllegalArgumentException("Size cannot be negative.")
+        }
+        ratioWidth = width
+        ratioHeight = height
+        requestLayout()
+    }
+
+    /**
+     * When the surface is given a new width/height, ensure that it maintains the correct aspect
+     * ratio.
+     */
+    override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+        val width = View.MeasureSpec.getSize(widthMeasureSpec)
+        val height = View.MeasureSpec.getSize(heightMeasureSpec)
+        if (ratioWidth == 0 || ratioHeight == 0) {
+            setMeasuredDimension(width, height)
+        } else {
+            if (width < height * ratioWidth / ratioHeight) {
+                setMeasuredDimension(width, width * ratioHeight / ratioWidth)
+            } else {
+                setMeasuredDimension(height * ratioWidth / ratioHeight, height)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CamViewModel.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CamViewModel.kt
new file mode 100644
index 0000000..3c031e7
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CamViewModel.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+/**
+ * ViewModel for keeping track of application state across resizes and configuration changes.
+ * This includes:
+ */
+class CamViewModel : ViewModel() {
+    private var cameraParams: HashMap<String, CameraParams> = HashMap<String, CameraParams>()
+
+    private val currentAPI: MutableLiveData<CameraAPI> = MutableLiveData()
+    private val currentCamera: MutableLiveData<Int> = MutableLiveData()
+    private val currentFocusMode: MutableLiveData<FocusMode> = MutableLiveData()
+    private val currentImageCaptureSize: MutableLiveData<ImageCaptureSize> = MutableLiveData()
+    private val shouldOutputLog: MutableLiveData<Boolean> = MutableLiveData()
+    private val humanReadableReport: MutableLiveData<String> = MutableLiveData()
+
+    /** Camera API of the current test */
+    fun getCurrentAPI(): MutableLiveData<CameraAPI> {
+        if (currentAPI.value == null)
+            currentAPI.value = CameraAPI.CAMERA2
+        return currentAPI
+    }
+
+    /** Camera ID of the current test */
+    fun getCurrentCamera(): MutableLiveData<Int> {
+        if (currentCamera.value == null)
+            currentCamera.value = 0
+        return currentCamera
+    }
+
+    /** Focus mode of the current test */
+    fun getCurrentFocusMode(): MutableLiveData<FocusMode> {
+        if (currentFocusMode.value == null)
+            currentFocusMode.value = FocusMode.AUTO
+        return currentFocusMode
+    }
+
+    /** Requested image capture size of the current test */
+    fun getCurrentImageCaptureSize(): MutableLiveData<ImageCaptureSize> {
+        if (currentImageCaptureSize.value == null)
+            currentImageCaptureSize.value = ImageCaptureSize.MAX
+        return currentImageCaptureSize
+    }
+
+    /** Hashmap of the CameraParams associated with all the cameras on the device */
+    fun getCameraParams(): HashMap<String, CameraParams> {
+        return cameraParams
+    }
+
+    /** If the user has asked to output the debugging log */
+    fun getShouldOutputLog(): MutableLiveData<Boolean> {
+        if (shouldOutputLog.value == null)
+            shouldOutputLog.value = false
+        return shouldOutputLog
+    }
+
+    /** Current value of the main output window on screen */
+    fun getHumanReadableReport(): MutableLiveData<String> {
+        if (humanReadableReport.value == null)
+            humanReadableReport.value = "Android Camera Performance Tool"
+        return humanReadableReport
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CameraParams.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CameraParams.kt
new file mode 100644
index 0000000..4c84e6c
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CameraParams.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CaptureRequest
+import android.media.ImageReader
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Size
+import androidx.camera.integration.antelope.MainActivity.Companion.FIXED_FOCUS_DISTANCE
+import androidx.camera.integration.antelope.MainActivity.Companion.INVALID_FOCAL_LENGTH
+import androidx.camera.integration.antelope.MainActivity.Companion.NO_APERTURE
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCaptureConfig
+import androidx.camera.core.PreviewConfig
+import androidx.camera.integration.antelope.cameracontrollers.Camera2CaptureSessionCallback
+import androidx.camera.integration.antelope.cameracontrollers.Camera2DeviceStateCallback
+import androidx.camera.integration.antelope.cameracontrollers.CameraState
+import androidx.camera.integration.antelope.cameracontrollers.CameraXDeviceStateCallback
+import androidx.camera.integration.antelope.cameracontrollers.CameraXPreviewSessionStateCallback
+import androidx.camera.integration.antelope.cameracontrollers.CameraXCaptureSessionCallback
+
+/**
+ * CameraParams contains a list of device characteristics for a given camera device.
+ *
+ * These are populated on app startup using initializeCameras in CameraUtils to prevent multiple
+ * calls to the CameraManager.
+ *
+ * In addition, some convenience variables for camera API callbacks, UI surfaces, and ImageReaders
+ * are included to facilitate testing. The calling Activity is responsible to make sure these
+ * convenience variables are coordinated with the active camera device.
+ */
+class CameraParams {
+    // Intrinsic device characteristics
+    internal var id: String = ""
+    internal var device: CameraDevice? = null
+    internal var isFront: Boolean = false
+    internal var isExternal: Boolean = false
+    internal var hasFlash: Boolean = false
+    internal var hasMulti: Boolean = false
+    internal var hasManualControl: Boolean = false
+    internal var physicalCameras: Set<String> = HashSet<String>()
+    internal var focalLengths: FloatArray = FloatArray(0)
+    internal var apertures: FloatArray = FloatArray(0)
+    internal var smallestFocalLength: Float = INVALID_FOCAL_LENGTH
+    internal var minDeltaFromNormal: Float = INVALID_FOCAL_LENGTH
+    internal var minFocusDistance: Float = FIXED_FOCUS_DISTANCE
+    internal var largestAperture: Float = NO_APERTURE
+    internal var effects: IntArray = IntArray(0)
+    internal var hasSepia: Boolean = false
+    internal var hasMono: Boolean = false
+    internal var hasAF: Boolean = false
+    internal var megapixels: Int = 0
+    internal var cam1AFSupported: Boolean = false
+    internal var characteristics: CameraCharacteristics? = null
+
+    // Camera1/Camera2 min/max size (sometimes different for some devices)
+    internal var cam1MinSize: Size = Size(0, 0)
+    internal var cam1MaxSize: Size = Size(0, 0)
+    internal var cam2MinSize: Size = Size(0, 0)
+    internal var cam2MaxSize: Size = Size(0, 0)
+
+    // Current state
+    internal var state = CameraState.UNINITIALIZED
+    internal var isOpen: Boolean = false
+    internal var isPreviewing: Boolean = false
+
+    // Thread to use for this device
+    internal var backgroundThread: HandlerThread? = null
+    internal var backgroundHandler: Handler? = null
+
+    // Convenience UI references
+    internal var imageReader: ImageReader? = null
+    internal var previewSurfaceView: AutoFitSurfaceView? = null
+    internal var cameraXPreviewTexture: AutoFitTextureView? = null
+
+    // Camera 2 API callback references
+    internal var captureRequestBuilder: CaptureRequest.Builder? = null
+
+    internal var camera2CaptureSession: CameraCaptureSession? = null
+    internal var camera2CaptureSessionCallback: Camera2CaptureSessionCallback? = null
+    internal var camera2DeviceStateCallback: Camera2DeviceStateCallback? = null
+    internal var imageAvailableListener: ImageAvailableListener? = null
+
+    // Camera X
+    internal var cameraXDeviceStateCallback: CameraXDeviceStateCallback? = null
+    internal var cameraXPreviewSessionStateCallback: CameraXPreviewSessionStateCallback? = null
+    internal var cameraXCaptureSessionCallback: CameraXCaptureSessionCallback? = null
+    internal var cameraXPreviewConfig: PreviewConfig =
+        PreviewConfig.Builder().build()
+    internal var cameraXCaptureConfig: ImageCaptureConfig =
+        ImageCaptureConfig.Builder().build()
+
+    // Custom lifecycle for CameraX API
+    internal var cameraXLifecycle: CustomLifecycle = CustomLifecycle()
+    internal var cameraXImageCaptureUseCase: ImageCapture =
+        ImageCapture(ImageCaptureConfig.Builder().build())
+
+    // Testing variables
+    internal var timer: CameraTimer = CameraTimer()
+    internal var autoFocusStuckCounter = 0
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CameraTimer.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CameraTimer.kt
new file mode 100644
index 0000000..17ab0fd
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CameraTimer.kt
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+/** Contains all the different timing values for a test run */
+class CameraTimer {
+    internal var testStart: Long = 0
+    internal var testEnd: Long = 0
+
+    internal var openStart: Long = 0
+    internal var openEnd: Long = 0
+
+    internal var cameraCloseStart: Long = 0
+    internal var cameraCloseEnd: Long = 0
+
+    internal var previewFillStart: Long = 0
+    internal var previewFillEnd: Long = 0
+
+    internal var captureStart: Long = 0
+    internal var captureEnd: Long = 0
+
+    internal var autofocusStart: Long = 0
+    internal var autofocusEnd: Long = 0
+
+    internal var imageReaderStart: Long = 0
+    internal var imageReaderEnd: Long = 0
+
+    internal var imageSaveStart: Long = 0
+    internal var imageSaveEnd: Long = 0
+
+    internal var previewStart: Long = 0
+    internal var previewEnd: Long = 0
+
+    internal var previewCloseStart: Long = 0
+    internal var previewCloseEnd: Long = 0
+
+    internal var switchToSecondStart: Long = 0
+    internal var switchToSecondEnd: Long = 0
+    internal var switchToFirstStart: Long = 0
+    internal var switchToFirstEnd: Long = 0
+
+    internal var isFirstPhoto: Boolean = true
+    internal var isHDRPlus: Boolean = false
+
+    /** Reset timers related to an individual capture */
+    fun clearImageTimers() {
+        captureStart = 0L
+        captureEnd = 0L
+        autofocusStart = 0L
+        autofocusEnd = 0L
+        imageReaderStart = 0L
+        imageReaderEnd = 0L
+        imageSaveStart = 0L
+        imageSaveEnd = 0L
+        isHDRPlus = false
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CameraUtils.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CameraUtils.kt
new file mode 100644
index 0000000..19d3b13
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CameraUtils.kt
@@ -0,0 +1,281 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import android.graphics.ImageFormat
+import android.graphics.Rect
+import android.hardware.camera2.CameraAccessException
+import android.hardware.camera2.CameraCharacteristics
+import android.hardware.camera2.CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.CaptureRequest
+import android.media.ImageReader
+import android.os.Build
+import android.util.SparseIntArray
+import android.view.Surface
+import androidx.appcompat.app.AppCompatActivity
+import androidx.camera.core.CameraX
+import androidx.camera.core.ImageCaptureConfig
+import androidx.camera.core.PreviewConfig
+import androidx.camera.integration.antelope.MainActivity.Companion.FIXED_FOCUS_DISTANCE
+import androidx.camera.integration.antelope.MainActivity.Companion.cameraParams
+import androidx.camera.integration.antelope.MainActivity.Companion.logd
+import androidx.camera.integration.antelope.cameracontrollers.Camera2CaptureSessionCallback
+import androidx.camera.integration.antelope.cameracontrollers.Camera2DeviceStateCallback
+import kotlinx.android.synthetic.main.activity_main.surface_preview
+import kotlinx.android.synthetic.main.activity_main.texture_preview
+import java.util.Arrays
+import java.util.Collections
+
+/** The camera API to use */
+enum class CameraAPI(private val api: String) {
+    /** Camera 1 API */
+    CAMERA1("Camera1"),
+    /** Camera 2 API */
+    CAMERA2("Camera2"),
+    /** Camera X API */
+    CAMERAX("CameraX")
+}
+
+/** The output capture size to request for captures */
+enum class ImageCaptureSize(private val size: String) {
+    /** Request captures to be the maximum supported size for this camera sensor */
+    MAX("Max"),
+    /** Request captures to be the minimum supported size for this camera sensor */
+    MIN("Min"),
+}
+
+/** The focus mechanism to request for captures */
+enum class FocusMode(private val mode: String) {
+    /** Auto-focus */
+    AUTO("Auto"),
+    /** Continuous auto-focus */
+    CONTINUOUS("Continuous"),
+    /** For fixed-focus lenses */
+    FIXED("Fixed")
+}
+
+/**
+ * For all the cameras associated with the device, populate the CameraParams values and add them to
+ * the companion object for the activity.
+ */
+fun initializeCameras(activity: MainActivity) {
+    val manager = activity.getSystemService(AppCompatActivity.CAMERA_SERVICE) as CameraManager
+    try {
+        val numCameras = manager.cameraIdList.size
+
+        for (cameraId in manager.cameraIdList) {
+            val tempCameraParams = CameraParams().apply {
+
+                val cameraChars = manager.getCameraCharacteristics(cameraId)
+                val cameraCapabilities =
+                    cameraChars.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES)
+
+                // Multi-camera
+                for (capability in cameraCapabilities) {
+                    if (capability ==
+                        CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA) {
+                        hasMulti = true
+                    } else if (capability ==
+                        CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_MANUAL_SENSOR) {
+                        hasManualControl = true
+                    }
+                }
+
+                logd("Camera " + cameraId + " of " + numCameras)
+
+                id = cameraId
+                isOpen = false
+                hasFlash = cameraChars.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) ?: false
+                isFront = CameraCharacteristics.LENS_FACING_FRONT ==
+                    cameraChars.get(CameraCharacteristics.LENS_FACING)
+
+                isExternal = (Build.VERSION.SDK_INT >= 23 &&
+                    CameraCharacteristics.LENS_FACING_EXTERNAL ==
+                    cameraChars.get(CameraCharacteristics.LENS_FACING))
+
+                characteristics = cameraChars
+                focalLengths =
+                    cameraChars.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS)
+                        ?: FloatArray(0)
+                smallestFocalLength = smallestFocalLength(focalLengths)
+                minDeltaFromNormal = focalLengthMinDeltaFromNormal(focalLengths)
+
+                apertures = cameraChars.get(CameraCharacteristics.LENS_INFO_AVAILABLE_APERTURES)
+                    ?: FloatArray(0)
+                largestAperture = largestAperture(apertures)
+                minFocusDistance =
+                    cameraChars.get(CameraCharacteristics.LENS_INFO_MINIMUM_FOCUS_DISTANCE)
+                        ?: MainActivity.FIXED_FOCUS_DISTANCE
+
+                for (focalLength in focalLengths) {
+                    logd("In " + id + " found focalLength: " + focalLength)
+                }
+                logd("Smallest smallestFocalLength: " + smallestFocalLength)
+                logd("minFocusDistance: " + minFocusDistance)
+
+                for (aperture in apertures) {
+                    logd("In " + id + " found aperture: " + aperture)
+                }
+                logd("Largest aperture: " + largestAperture)
+
+                if (hasManualControl) {
+                    logd("Has Manual, minFocusDistance: " + minFocusDistance)
+                }
+
+                // Autofocus
+                hasAF = minFocusDistance != FIXED_FOCUS_DISTANCE // If camera is fixed focus, no AF
+
+                effects = cameraChars.get(CameraCharacteristics.CONTROL_AVAILABLE_EFFECTS)
+                    ?: IntArray(0)
+                hasSepia = effects.contains(CameraMetadata.CONTROL_EFFECT_MODE_SEPIA)
+                hasMono = effects.contains(CameraMetadata.CONTROL_EFFECT_MODE_MONO)
+
+                if (hasSepia)
+                    logd("WE HAVE Sepia!")
+                if (hasMono)
+                    logd("WE HAVE Mono!")
+
+                val activeSensorRect: Rect = cameraChars.get(SENSOR_INFO_ACTIVE_ARRAY_SIZE)
+                megapixels = (activeSensorRect.width() * activeSensorRect.height()) / 1000000
+
+                camera2DeviceStateCallback =
+                    Camera2DeviceStateCallback(this, activity, TestConfig())
+                camera2CaptureSessionCallback =
+                    Camera2CaptureSessionCallback(activity, this, TestConfig())
+
+                previewSurfaceView = activity.surface_preview
+                cameraXPreviewTexture = activity.texture_preview
+
+                // TODO: As of 0.3.0 CameraX only has front and back cameras. Update in the future
+                val cameraXcameraID = if (id.equals("1"))
+                    CameraX.LensFacing.BACK
+                else CameraX.LensFacing.FRONT
+
+                cameraXPreviewConfig = PreviewConfig.Builder()
+                    .setLensFacing(cameraXcameraID)
+                    .build()
+
+                cameraXCaptureConfig = ImageCaptureConfig.Builder()
+                    .setLensFacing(cameraXcameraID)
+                    .build()
+
+                imageAvailableListener =
+                    ImageAvailableListener(activity, this, TestConfig())
+
+                if (Build.VERSION.SDK_INT >= 28) {
+                    physicalCameras = cameraChars.physicalCameraIds
+                }
+
+                // Get Camera2 and CameraX image capture sizes
+                val map =
+                    characteristics?.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
+                if (map != null) {
+                    cam2MaxSize = Collections.max(
+                        Arrays.asList(*map.getOutputSizes(ImageFormat.JPEG)),
+                        CompareSizesByArea())
+                    cam2MinSize = Collections.min(
+                        Arrays.asList(*map.getOutputSizes(ImageFormat.JPEG)),
+                        CompareSizesByArea())
+
+                    // Use minimum image size for preview
+                    previewSurfaceView?.holder?.setFixedSize(cam2MinSize.width, cam2MinSize.height)
+                }
+
+                setupImageReader(activity, this, TestConfig())
+            }
+
+            cameraParams.put(cameraId, tempCameraParams)
+        } // For all camera devices
+    } catch (accessError: CameraAccessException) {
+        accessError.printStackTrace()
+    }
+}
+
+/**
+ * Convenience method to configure the ImageReaders required for Camera1 and Camera2 APIs.
+ *
+ * Uses JPEG image format, checks the current test configuration to determine the needed size.
+ */
+fun setupImageReader(activity: MainActivity, params: CameraParams, testConfig: TestConfig) {
+
+    // Only use ImageReader for Camera1 and Camera2
+    if (CameraAPI.CAMERAX != testConfig.api) {
+        params.imageAvailableListener = ImageAvailableListener(activity, params, testConfig)
+
+        val useLargest = testConfig.imageCaptureSize == ImageCaptureSize.MAX
+
+        val size = if (useLargest)
+            params.cam2MaxSize
+        else
+            params.cam2MinSize
+
+        params.imageReader?.close()
+        params.imageReader = ImageReader.newInstance(size.width, size.height,
+            ImageFormat.JPEG, 5)
+        params.imageReader?.setOnImageAvailableListener(
+            params.imageAvailableListener, params.backgroundHandler)
+    }
+}
+
+/** Finds the smallest focal length in the given array, useful for finding the widest angle lens */
+fun smallestFocalLength(focalLengths: FloatArray): Float = focalLengths.min()
+    ?: MainActivity.INVALID_FOCAL_LENGTH
+
+/** Finds the largest aperture in the array of focal lengths */
+fun largestAperture(apertures: FloatArray): Float = apertures.max()
+    ?: MainActivity.NO_APERTURE
+
+/** Finds the most "normal" focal length in the array of focal lengths */
+fun focalLengthMinDeltaFromNormal(focalLengths: FloatArray): Float =
+    focalLengths.minBy { Math.abs(it - MainActivity.NORMAL_FOCAL_LENGTH) }
+        ?: Float.MAX_VALUE
+
+/** Adds automatic flash to the given CaptureRequest.Builder */
+fun setAutoFlash(params: CameraParams, requestBuilder: CaptureRequest.Builder?) {
+    try {
+        if (params.hasFlash) {
+            requestBuilder?.set(CaptureRequest.CONTROL_AE_MODE,
+                CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH)
+
+            // Force flash always on
+//            requestBuilder?.set(CaptureRequest.CONTROL_AE_MODE,
+//                    CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH)
+        }
+    } catch (e: Exception) {
+        // Do nothing
+    }
+}
+
+/**
+ * We have to take sensor orientation into account and rotate JPEG properly.
+ */
+fun getOrientation(params: CameraParams, rotation: Int): Int {
+    val orientations = SparseIntArray()
+    orientations.append(Surface.ROTATION_0, 90)
+    orientations.append(Surface.ROTATION_90, 0)
+    orientations.append(Surface.ROTATION_180, 270)
+    orientations.append(Surface.ROTATION_270, 180)
+
+    logd("Orientation: sensor: " +
+        params.characteristics?.get(CameraCharacteristics.SENSOR_ORIENTATION) +
+        " and current rotation: " + orientations.get(rotation))
+    val sensorRotation: Int =
+        params.characteristics?.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 0
+    return (orientations.get(rotation) + sensorRotation + 270) % 360
+}
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CustomLifecycle.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CustomLifecycle.kt
new file mode 100644
index 0000000..4f3b0d4
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/CustomLifecycle.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import android.os.Handler
+import android.os.Looper
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleOwner
+import androidx.lifecycle.LifecycleRegistry
+import androidx.camera.integration.antelope.MainActivity.Companion.logd
+
+/**
+ * Camera X normally handles lifecycle events itself. Optimizations in the API make it difficult
+ * to perform a series of clean tests like Antelope does, so it requires its own custom lifecycle.
+ */
+class CustomLifecycle : LifecycleOwner {
+    private var lifecycleRegistry = LifecycleRegistry(this)
+    internal val mainHandler: Handler = Handler(Looper.getMainLooper())
+
+    init {
+        lifecycleRegistry.markState(Lifecycle.State.INITIALIZED)
+        lifecycleRegistry.markState(Lifecycle.State.CREATED)
+    }
+
+    override fun getLifecycle(): Lifecycle {
+        return lifecycleRegistry
+    }
+
+    fun start() {
+        if (Looper.myLooper() != mainHandler.looper) {
+            mainHandler.post { start() }
+            return
+        }
+
+        if (lifecycleRegistry.currentState != Lifecycle.State.CREATED) {
+            logd("CustomLifecycle start error: Prior state should be CREATED. Instead it is: " +
+                lifecycleRegistry.currentState)
+        } else {
+            try {
+                lifecycleRegistry.markState(Lifecycle.State.STARTED)
+                lifecycleRegistry.markState(Lifecycle.State.RESUMED)
+            } catch (e: IllegalArgumentException) {
+                logd("CustomLifecycle start error: unable to start " + e.message)
+            }
+        }
+    }
+
+    fun pauseAndStop() {
+        if (Looper.myLooper() != mainHandler.looper) {
+            mainHandler.post { pauseAndStop() }
+            return
+        }
+
+        if (lifecycleRegistry.currentState != Lifecycle.State.RESUMED) {
+            logd("CustomLifecycle pause error: Prior state should be RESUMED. Instead it is: " +
+                lifecycleRegistry.currentState)
+        } else {
+            try {
+                lifecycleRegistry.markState(Lifecycle.State.STARTED)
+                lifecycleRegistry.markState(Lifecycle.State.CREATED)
+            } catch (e: IllegalArgumentException) {
+                logd("CustomLifecycle pause error: unable to pause " + e.message)
+            }
+        }
+    }
+
+    fun finish() {
+        if (Looper.myLooper() != mainHandler.looper) {
+            mainHandler.post { finish() }
+            return
+        }
+
+        if (lifecycleRegistry.currentState != Lifecycle.State.CREATED) {
+            logd("CustomLifecycle finish error: Prior state should be CREATED. Instead it is: " +
+                lifecycleRegistry.currentState)
+        } else {
+            try {
+                lifecycleRegistry.markState(Lifecycle.State.DESTROYED)
+            } catch (e: IllegalArgumentException) {
+                logd("CustomLifecycle finish error: unable to finish " + e.message)
+            }
+        }
+    }
+
+    fun isFinished(): Boolean {
+        return (Lifecycle.State.DESTROYED == lifecycleRegistry.currentState)
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/DeviceInfo.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/DeviceInfo.kt
new file mode 100644
index 0000000..e34dddc
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/DeviceInfo.kt
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import androidx.appcompat.app.AppCompatActivity
+
+/**
+ * Convenience class to access device information
+ */
+class DeviceInfo(activity: AppCompatActivity) {
+    /** Detailed string with device and OS information */
+    val device: String = android.os.Build.MANUFACTURER + " " +
+        android.os.Build.BRAND + " " +
+        android.os.Build.DEVICE + " " +
+        android.os.Build.MODEL + " " +
+        android.os.Build.PRODUCT + " " +
+        "(" + android.os.Build.VERSION.RELEASE + android.os.Build.VERSION.INCREMENTAL + ") " +
+        "\nSDK: " + android.os.Build.VERSION.SDK_INT
+
+    /** Short string with device information */
+    val deviceShort: String = android.os.Build.DEVICE
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/ImageUtils.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/ImageUtils.kt
new file mode 100644
index 0000000..69b8b89
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/ImageUtils.kt
@@ -0,0 +1,346 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import android.annotation.TargetApi
+import android.app.Activity
+import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.ImageFormat
+import android.graphics.Matrix
+import android.media.ImageReader
+import android.net.Uri
+import android.os.Build
+import android.os.Environment
+import android.widget.Toast
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageProxy
+import androidx.exifinterface.media.ExifInterface
+import androidx.camera.integration.antelope.MainActivity.Companion.PHOTOS_DIR
+import androidx.camera.integration.antelope.MainActivity.Companion.logd
+import androidx.camera.integration.antelope.cameracontrollers.CameraState
+import androidx.camera.integration.antelope.cameracontrollers.closeCameraX
+import androidx.camera.integration.antelope.cameracontrollers.closePreviewAndCamera
+import java.io.ByteArrayInputStream
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.text.SimpleDateFormat
+import java.util.Date
+import java.util.Locale
+
+/**
+ * ImageReader listener for use with Camera 2 API.
+ *
+ * Extract image and write to disk
+ */
+class ImageAvailableListener(
+    internal val activity: MainActivity,
+    internal var params: CameraParams,
+    internal val testConfig: TestConfig
+) : ImageReader.OnImageAvailableListener {
+
+    override fun onImageAvailable(reader: ImageReader) {
+        logd("onImageAvailable enter. Current test: " + testConfig.currentRunningTest +
+            " state: " + params.state)
+
+        // Only save 1 photo each time
+        if (CameraState.IMAGE_REQUESTED != params.state)
+            return
+        else
+            params.state = CameraState.UNINITIALIZED
+
+        val image = reader.acquireLatestImage()
+
+        when (image.format) {
+            ImageFormat.JPEG -> {
+                // Orientation
+                val rotation = activity.windowManager.defaultDisplay.rotation
+                val capturedImageRotation = getOrientation(params, rotation)
+
+                params.timer.imageReaderEnd = System.currentTimeMillis()
+                params.timer.imageSaveStart = System.currentTimeMillis()
+
+                val bytes = ByteArray(image.planes[0].buffer.remaining())
+                image.planes[0].buffer.get(bytes)
+
+                params.backgroundHandler?.post(ImageSaver(activity, bytes, capturedImageRotation,
+                    params.isFront, params, testConfig))
+            }
+
+            // TODO: add RAW support
+            ImageFormat.RAW_SENSOR -> {
+            }
+
+            else -> {
+            }
+        }
+
+        image.close()
+    }
+}
+
+/**
+ * Asynchronously save ByteArray to disk
+ */
+class ImageSaver internal constructor(
+    private val activity: MainActivity,
+    private val bytes: ByteArray,
+    private val rotation: Int,
+    private val flip: Boolean,
+    private val params: CameraParams,
+    private val testConfig: TestConfig
+) : Runnable {
+
+    override fun run() {
+        logd("ImageSaver. ImageSaver is running, saving image to disk.")
+
+        // TODO: Once Android supports HDR+ detection add this in
+//        if (isHDRPlus(bytes))
+//            params.timer.isHDRPlus = true;
+
+        writeFile(activity, bytes)
+
+        params.timer.imageSaveEnd = System.currentTimeMillis()
+
+        // The test is over only if the capture call back has already been hit
+        // It is possible to be here before the callback is hit
+        if (0L != params.timer.captureEnd) {
+            if (TestType.MULTI_PHOTO_CHAIN == testConfig.currentRunningTest) {
+                testEnded(activity, params, testConfig)
+            } else {
+                logd("ImageSaver: photo saved, test is finished, closing the camera.")
+                testConfig.testFinished = true
+                closePreviewAndCamera(activity, params, testConfig)
+            }
+        }
+    }
+}
+
+/**
+ * Rotate a given Bitmap by degrees
+ */
+fun rotateBitmap(original: Bitmap, degrees: Float): Bitmap {
+    val matrix = Matrix()
+    matrix.postRotate(degrees)
+    return Bitmap.createBitmap(original, 0, 0, original.width, original.height,
+        matrix, true)
+}
+
+/**
+ * Scale a given Bitmap by scaleFactor
+ */
+fun scaleBitmap(activity: Activity, bitmap: Bitmap, scaleFactor: Float): Bitmap {
+    val scaledWidth = Math.round(bitmap.width * scaleFactor)
+    val scaledHeight = Math.round(bitmap.height * scaleFactor)
+
+    return Bitmap.createScaledBitmap(bitmap, scaledWidth, scaledHeight, true)
+}
+
+/**
+ * Flip a Bitmap horizontal
+ */
+fun horizontalFlip(activity: Activity, bitmap: Bitmap): Bitmap {
+    val matrix = Matrix()
+    matrix.preScale(-1.0f, 1.0f)
+    return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
+}
+
+/**
+ * Generate a timestamp to append to saved filenames.
+ */
+fun generateTimestamp(): String {
+    val sdf = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_SSS", Locale.US)
+    return sdf.format(Date())
+}
+
+/**
+ * Actually write a byteArray file to disk. Assume the file is a jpg and use that extension
+ */
+fun writeFile(activity: MainActivity, bytes: ByteArray) {
+    val rawFile = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),
+        "Antelope" + generateTimestamp() + ".dng")
+
+    val jpgFile = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),
+        File.separatorChar + PHOTOS_DIR + File.separatorChar +
+            "Antelope" + generateTimestamp() + ".jpg")
+
+    val photosDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),
+        PHOTOS_DIR)
+
+    if (!photosDir.exists()) {
+        val createSuccess = photosDir.mkdir()
+        if (!createSuccess) {
+            Toast.makeText(activity, "DCIM/" + PHOTOS_DIR + " creation failed.",
+                Toast.LENGTH_SHORT).show()
+            logd("Photo storage directory DCIM/" + PHOTOS_DIR + " creation failed!!")
+        } else {
+            logd("Photo storage directory DCIM/" + PHOTOS_DIR + " did not exist. Created.")
+        }
+    }
+
+    var output: FileOutputStream? = null
+    try {
+        output = FileOutputStream(jpgFile)
+        output.write(bytes)
+    } catch (e: IOException) {
+        e.printStackTrace()
+    } finally {
+        if (null != output) {
+            try {
+                output.close()
+
+                if (!PrefHelper.getAutoDelete(activity)) {
+                    // File is written, let media scanner know
+                    val scannerIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
+                    scannerIntent.data = Uri.fromFile(jpgFile)
+                    activity.sendBroadcast(scannerIntent)
+
+                    // File is written, now delete it.
+                    // TODO: make sure this does not add extra latency
+                } else {
+                    jpgFile.delete()
+                }
+            } catch (e: IOException) {
+                e.printStackTrace()
+            }
+        }
+    }
+    logd("writeFile: Completed.")
+}
+
+/**
+ * Delete all the photos generated by testing from the default Antelope PHOTOS_DIR
+ */
+fun deleteTestPhotos(activity: MainActivity) {
+    val photosDir = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM),
+        PHOTOS_DIR)
+
+    if (photosDir.exists()) {
+
+        for (photo in photosDir.listFiles())
+            photo.delete()
+
+        // Files are deleted, let media scanner know
+        val scannerIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
+        scannerIntent.data = Uri.fromFile(photosDir)
+        activity.sendBroadcast(scannerIntent)
+
+        Toast.makeText(activity, "All test photos deleted", Toast.LENGTH_SHORT).show()
+        logd("All photos in storage directory DCIM/" + PHOTOS_DIR + " deleted.")
+    }
+}
+
+/**
+ * Try to detect if a saved image file has had HDR effects applied to it by examining the EXIF tag.
+ *
+ * Note: this does not currently work.
+ */
+@TargetApi(24)
+fun isHDRPlus(bytes: ByteArray?): Boolean {
+    if (24 <= Build.VERSION.SDK_INT) {
+        val bytestream = ByteArrayInputStream(bytes)
+        val exif = ExifInterface(bytestream)
+        val software: String = exif.getAttribute(ExifInterface.TAG_SOFTWARE) ?: ""
+        val makernote: String = exif.getAttribute(ExifInterface.TAG_MAKER_NOTE) ?: ""
+        logd("In isHDRPlus, software: " + software + ", makernote: " + makernote)
+        if (software.contains("HDR+") || makernote.contains("HDRP")) {
+            logd("Photo is HDR+: " + software + ", " + makernote)
+            return true
+        }
+    }
+    return false
+}
+
+/**
+ * ImageReader listener for use with Camera X API.
+ *
+ * Extract image and write to disk
+ */
+class CameraXImageAvailableListener(
+    internal val activity: MainActivity,
+    internal var params: CameraParams,
+    internal val testConfig: TestConfig
+) : ImageCapture.OnImageCapturedListener() {
+
+    /** Image was captured successfully */
+    override fun onCaptureSuccess(image: ImageProxy?, rotationDegrees: Int) {
+        logd("CameraXImageAvailableListener onCaptureSuccess. Current test: " +
+            testConfig.currentRunningTest)
+
+        when (image?.format) {
+            ImageFormat.JPEG -> {
+                params.timer.imageReaderEnd = System.currentTimeMillis()
+
+                // TODO As of CameraX 0.3.0 has a bug so capture session callbacks are never called.
+                // As a workaround for now we use this onCaptureSuccess callback as the only measure
+                // of capture timing (as opposed to capture callback + image ready for camera2
+                // Remove these lines when bug is fixed
+                // /////////////////////////////////////////////////////////////////////////////////
+                params.timer.captureEnd = System.currentTimeMillis()
+
+                params.timer.imageReaderStart = System.currentTimeMillis()
+                params.timer.imageReaderEnd = System.currentTimeMillis()
+
+                // End Remove lines ////////////////////////////////////////////////////////////////
+
+                // Orientation
+                val rotation = activity.windowManager.defaultDisplay.rotation
+                val capturedImageRotation = getOrientation(params, rotation)
+
+                params.timer.imageSaveStart = System.currentTimeMillis()
+
+                val bytes = ByteArray(image.planes[0].buffer.remaining())
+                image.planes[0].buffer.get(bytes)
+
+                params.backgroundHandler?.post(ImageSaver(activity, bytes, capturedImageRotation,
+                    params.isFront, params, testConfig))
+            }
+
+            ImageFormat.RAW_SENSOR -> {
+            }
+
+            else -> {
+            }
+        }
+
+        image?.close()
+    }
+
+    /** Camera X was unable to capture a still image and threw an error */
+    override fun onError(
+        useCaseError: ImageCapture.UseCaseError?,
+        message: String?,
+        cause: Throwable?
+    ) {
+        logd("CameraX ImageCallback onError. Error: " + message)
+        params.timer.imageReaderEnd = System.currentTimeMillis()
+        params.timer.imageSaveStart = System.currentTimeMillis()
+        params.timer.imageSaveEnd = System.currentTimeMillis()
+
+        // The test is over only if the capture call back has already been hit
+        // It is possible to be here before the callback is hit
+        if (0L != params.timer.captureEnd) {
+            if (TestType.MULTI_PHOTO_CHAIN == testConfig.currentRunningTest) {
+                testEnded(activity, params, testConfig)
+            } else {
+                testConfig.testFinished = true
+                closeCameraX(activity, params, testConfig)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt
new file mode 100644
index 0000000..3d3d351
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MainActivity.kt
@@ -0,0 +1,465 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import android.Manifest
+import android.content.ClipData
+import android.content.Context
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.res.Configuration
+import android.os.Bundle
+import android.os.Handler
+import android.os.HandlerThread
+import android.util.Log
+import android.view.Menu
+import android.view.MenuInflater
+import android.view.MenuItem
+import android.view.View
+import android.view.WindowManager
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProviders
+import androidx.camera.integration.antelope.cameracontrollers.camera2Abort
+import androidx.camera.integration.antelope.cameracontrollers.cameraXAbort
+import androidx.camera.integration.antelope.cameracontrollers.closeAllCameras
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import kotlinx.android.synthetic.main.activity_main.button_abort
+import kotlinx.android.synthetic.main.activity_main.button_multi
+import kotlinx.android.synthetic.main.activity_main.button_single
+import kotlinx.android.synthetic.main.activity_main.progress_test
+import kotlinx.android.synthetic.main.activity_main.scroll_log
+import kotlinx.android.synthetic.main.activity_main.surface_preview
+import kotlinx.android.synthetic.main.activity_main.text_log
+import kotlinx.android.synthetic.main.activity_main.texture_preview
+
+private const val REQUEST_CAMERA_PERMISSION = 1
+private const val REQUEST_FILE_WRITE_PERMISSION = 2
+
+/**
+ * Main Antelope Activity
+ */
+class MainActivity : AppCompatActivity() {
+
+    companion object {
+        /** Directory to save image files under sdcard/DCIM */
+        const val PHOTOS_DIR: String = "Antelope"
+        /** Directory to save .csv log files to under sdcard/Documents */
+        const val LOG_DIR: String = "Antelope"
+        /** Tag to include when using the logd function */
+        val LOG_TAG = "Antelope"
+
+        /** Define "normal" focal length as 50.0mm */
+        const val NORMAL_FOCAL_LENGTH: Float = 50f
+        /** No aperture reference */
+        const val NO_APERTURE: Float = 0f
+        /** Fixed-focus lenses have a value of 0 */
+        const val FIXED_FOCUS_DISTANCE: Float = 0f
+        /** Constant for invalid focal length */
+        val INVALID_FOCAL_LENGTH: Float = Float.MAX_VALUE
+        /** For single tests, percentage completion to show in progress bar when test is running  */
+        const val PROGRESS_SINGLE_PERCENTAGE = 25
+
+        /** List of test results for current test run */
+        internal val testRun: ArrayList<TestResults> = ArrayList<TestResults>()
+        /** List of test configurations for a multiple test run */
+        internal val autoTestConfigs: ArrayList<TestConfig> = ArrayList()
+
+        /** Flag if a single test is running */
+        var isSingleTestRunning = false
+        /** Number of test remaining in a multiple test run */
+        var testsRemaining = 0
+
+        /** View model that contains state data for the application */
+        lateinit var camViewModel: CamViewModel
+
+        /** Hashmap of CameraParams for all cameras on the device */
+        lateinit var cameraParams: HashMap<String, CameraParams>
+        /** Convenience access to device information, OS build, etc. */
+        lateinit var deviceInfo: DeviceInfo
+
+        /** Array of human-readable information for each camera on this device */
+        val cameras: ArrayList<String> = ArrayList<String>()
+        /** Array of camera ids for this device */
+        val cameraIds: ArrayList<String> = ArrayList<String>()
+
+        /** Convenience wrapper for Log.d that can be toggled on/off */
+        fun logd(message: String) {
+            if (camViewModel.getShouldOutputLog().value ?: false)
+                Log.d(LOG_TAG, message)
+        }
+    }
+
+    /**
+     * Check camera permissions and set up UI
+     */
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        setContentView(R.layout.activity_main)
+
+        camViewModel = ViewModelProviders.of(this).get(CamViewModel::class.java)
+        cameraParams = camViewModel.getCameraParams()
+        deviceInfo = DeviceInfo(this)
+
+        if (checkCameraPermissions()) {
+            initializeCameras(this)
+            setupCameraNames()
+        }
+
+        button_single.setOnClickListener {
+            val testDiag = SettingsDialog.newInstance(SettingsDialog.DIALOG_TYPE_SINGLE,
+                getString(R.string.settings_single_test_dialog_title),
+                cameras.toTypedArray(), cameraIds.toTypedArray())
+            testDiag.show(supportFragmentManager, SettingsDialog.DIALOG_TYPE_SINGLE)
+        }
+
+        button_multi.setOnClickListener {
+            val testDiag = SettingsDialog.newInstance(SettingsDialog.DIALOG_TYPE_MULTI,
+                getString(R.string.settings_multi_test_dialog_title),
+                cameras.toTypedArray(), cameraIds.toTypedArray())
+            testDiag.show(supportFragmentManager, SettingsDialog.DIALOG_TYPE_MULTI)
+        }
+
+        button_abort.setOnClickListener {
+            abortTests()
+            testsRemaining = 0
+            multiCounter = 0
+            toggleControls(true)
+            toggleRotationLock(false)
+            window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+            setProgress(0)
+            showProgressBar(false)
+            updateLog("\nABORTED", true)
+        }
+
+        // Human readable report
+        val humanReadableReportObserver = object : Observer<String> {
+            override fun onChanged(newReport: String?) {
+                text_log.text = newReport ?: ""
+            }
+        }
+        camViewModel.getHumanReadableReport().observe(this, humanReadableReportObserver)
+    }
+
+    /**
+     * Set up options menu to allow debug logging and clearing cache'd data
+     */
+    override fun onCreateOptionsMenu(menu: Menu): Boolean {
+        val inflater: MenuInflater = menuInflater
+        inflater.inflate(R.menu.main_menu, menu)
+        if (camViewModel.getShouldOutputLog().value != null)
+            menu.getItem(0).isChecked = camViewModel.getShouldOutputLog().value!!
+        return true
+    }
+
+    /**
+     * Handle menu presses
+     */
+    override fun onOptionsItemSelected(item: MenuItem): Boolean {
+        return when (item.itemId) {
+            R.id.menu_logcat -> {
+                item.isChecked = !item.isChecked
+                camViewModel.getShouldOutputLog().value = item.isChecked
+                true
+            }
+            R.id.menu_delete_photos -> {
+                deleteTestPhotos(this)
+                true
+            }
+            R.id.menu_delete_logs -> {
+                deleteCSVFiles(this)
+                true
+            }
+            else -> super.onOptionsItemSelected(item)
+        }
+    }
+
+    /** Update the main scrollview text
+     *
+     * @param log The new text
+     * @param append Whether to append the new text or to replace the old
+     * @param copyToClipboard Whether or not to copy the text to the system clipboard
+     */
+    fun updateLog(log: String, append: Boolean = false, copyToClipboard: Boolean = true) {
+        runOnUiThread {
+            if (append)
+                camViewModel.getHumanReadableReport().value =
+                    camViewModel.getHumanReadableReport().value + log
+            else
+                camViewModel.getHumanReadableReport().value = log
+        }
+
+        if (copyToClipboard) {
+            runOnUiThread {
+                // Copy to clipboard
+                val clipboard = getSystemService(Context.CLIPBOARD_SERVICE)
+                    as android.content.ClipboardManager
+                val clip = ClipData.newPlainText("Log", log)
+                clipboard.primaryClip = clip
+                Toast.makeText(this, getString(R.string.log_copied),
+                    Toast.LENGTH_SHORT).show()
+            }
+        }
+    }
+
+    /**
+     * Create human readable names for the camera devices
+     */
+    private fun setupCameraNames() {
+        cameras.clear()
+        cameraIds.clear()
+        for (param in cameraParams) {
+            var camera = ""
+
+            camera += param.value.id
+            cameraIds += param.value.id
+
+            if (param.value.isFront)
+                camera += " (Front)"
+            else if (param.value.isExternal)
+                camera += " (External)"
+            else
+                camera += " (Back)"
+
+            camera += " " + param.value.megapixels + "MP"
+
+            if (!param.value.hasAF)
+                camera += " fixed-focus"
+
+            camera += " (min FL: " + param.value.smallestFocalLength + "mm)"
+            cameras.add(camera)
+        }
+    }
+
+    /**
+     * Act on the result of a permissions request. If permission granted simply restart the activity
+     */
+    override fun onRequestPermissionsResult(
+        requestCode: Int,
+        permissions: Array<String>,
+        grantResults: IntArray
+    ) {
+
+        when (requestCode) {
+            REQUEST_CAMERA_PERMISSION -> {
+                if (grantResults.size > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                    // We now have permission, restart the app
+                    val intent = this.intent
+                    finish()
+                    startActivity(intent)
+                } else {
+                }
+                return
+            }
+            REQUEST_FILE_WRITE_PERMISSION -> {
+                // If request is cancelled, the result arrays are empty.
+                if (grantResults.size > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
+                    // We now have permission, restart the app
+                    val intent = this.intent
+                    finish()
+                    startActivity(intent)
+                } else {
+                }
+                return
+            }
+        }
+    }
+
+    /**
+     * Check if we have been granted the need camera and file-system permissions
+     */
+    fun checkCameraPermissions(): Boolean {
+        if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
+            !== PackageManager.PERMISSION_GRANTED) {
+
+            // No explanation needed; request the permission
+            ActivityCompat.requestPermissions(this,
+                arrayOf(Manifest.permission.CAMERA),
+                REQUEST_CAMERA_PERMISSION)
+            return false
+        } else if (ContextCompat.checkSelfPermission(this,
+                Manifest.permission.WRITE_EXTERNAL_STORAGE)
+            !== PackageManager.PERMISSION_GRANTED) {
+            // No explanation needed; request the permission
+            ActivityCompat.requestPermissions(this,
+                arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
+                REQUEST_FILE_WRITE_PERMISSION)
+            return false
+        }
+
+        return true
+    }
+
+    /** Start the background threads associated with the given camera device/params */
+    fun startBackgroundThread(params: CameraParams) {
+        if (params.backgroundThread == null) {
+            params.backgroundThread = HandlerThread(LOG_TAG).apply {
+                this.start()
+                params.backgroundHandler = Handler(this.looper)
+            }
+        }
+    }
+
+    /** Stop the background threads associated with the given camera device/params */
+    fun stopBackgroundThread(params: CameraParams) {
+        params.backgroundThread?.quitSafely()
+        try {
+            params.backgroundThread?.join()
+            params.backgroundThread = null
+            params.backgroundHandler = null
+        } catch (e: InterruptedException) {
+            logd("Interrupted while shutting background thread down: " + e)
+        }
+    }
+
+    /** Resume all background threads associated with any given camera devices/params */
+    override fun onResume() {
+        super.onResume()
+        for (tempCameraParams in cameraParams) {
+            startBackgroundThread(tempCameraParams.value)
+        }
+    }
+
+    /** Pause all background threads associated with any camera devices/params */
+    override fun onPause() {
+        for (tempCameraParams in cameraParams) {
+            stopBackgroundThread(tempCameraParams.value)
+        }
+        super.onPause()
+    }
+
+    /** Show/hide the progress bar during a test */
+    fun showProgressBar(visible: Boolean = true, percentage: Int = PROGRESS_SINGLE_PERCENTAGE) {
+        runOnUiThread {
+            if (visible) {
+                progress_test.progress = percentage
+                progress_test.visibility = View.VISIBLE
+            } else {
+                progress_test.progress = 0
+                progress_test.visibility = View.INVISIBLE
+            }
+        }
+    }
+
+    /** Enable/disable controls during a test run */
+    fun toggleControls(enabled: Boolean = true) {
+        runOnUiThread {
+            button_multi.isEnabled = enabled
+            button_single.isEnabled = enabled
+            button_single.isEnabled = enabled
+            button_abort.isEnabled = !enabled // note: inverse of others
+        }
+    }
+
+    /** Lock orientation during a test so the camera doesn't get re-initialized mid-capture */
+    fun toggleRotationLock(lockRotation: Boolean = true) {
+        if (lockRotation) {
+            val currentOrientation = resources.configuration.orientation
+            if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
+                requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
+            } else {
+                requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
+            }
+        } else {
+            requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
+        }
+    }
+
+    /** Launch a single test based on the current configuration */
+    fun startSingleTest() {
+        testRun.clear()
+        testsRemaining = 1
+        isSingleTestRunning = true
+
+        val config = createSingleTestConfig(this)
+        setupUIForTest(config, false)
+
+        initializeTest(this, cameraParams.get(config.camera), config)
+    }
+
+    /** Launch a series of tests based on the current configuration */
+    fun startMultiTest() {
+        isSingleTestRunning = false
+        setupAutoTestRunner(this)
+        autoTestRunner(this)
+    }
+
+    /** After tests are completed, reset the UI to the initial state */
+    fun resetUIAfterTest() {
+        runOnUiThread {
+            toggleControls(true)
+            toggleRotationLock(false)
+            showProgressBar(false)
+            window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+        }
+    }
+
+    /**
+     * Prepare the main UI for a test run. This includes showing/hiding the appropriate preview
+     * surface depending on if the test is Camera 1/2/X
+     */
+    internal fun setupUIForTest(testConfig: TestConfig, append: Boolean = true) {
+        with(testConfig) {
+            MainActivity.camViewModel.getCurrentAPI().postValue(this.api)
+            MainActivity.camViewModel.getCurrentImageCaptureSize().postValue(imageCaptureSize)
+            MainActivity.camViewModel.getCurrentCamera().postValue(camera.toInt())
+
+            if (FocusMode.FIXED == focusMode)
+                MainActivity.camViewModel.getCurrentFocusMode().postValue(FocusMode.AUTO)
+            else
+                MainActivity.camViewModel.getCurrentFocusMode().postValue(focusMode)
+
+            if (CameraAPI.CAMERAX == api) {
+                surface_preview.visibility = View.INVISIBLE
+                texture_preview.visibility = View.VISIBLE
+            } else {
+                surface_preview.visibility = View.VISIBLE
+                texture_preview.visibility = View.INVISIBLE
+            }
+
+            toggleControls(false)
+            toggleRotationLock(true)
+            updateLog("Running: " + testName + "\n", append, false)
+            scroll_log.fullScroll(View.FOCUS_DOWN)
+            window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+        }
+    }
+
+    /**
+     * User has requested to abort the test. Close cameras and reset the UI.
+     */
+    fun abortTests() {
+        val currentConfig: TestConfig = createTestConfig("ABORT")
+
+        val currentCamera = camViewModel.getCurrentCamera().value ?: 0
+        val currentParams = cameraParams.get(currentCamera.toString())
+
+        when (currentConfig.api) {
+            CameraAPI.CAMERA1 -> closeAllCameras(this, currentConfig)
+            CameraAPI.CAMERAX -> {
+                if (null != currentParams)
+                    cameraXAbort(this, currentParams, currentConfig)
+            }
+            CameraAPI.CAMERA2 -> {
+                if (null != currentParams)
+                    camera2Abort(this, currentParams, currentConfig)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MeasureUtils.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MeasureUtils.kt
new file mode 100644
index 0000000..a85901e
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MeasureUtils.kt
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import android.util.Log
+import android.util.Size
+import java.util.Collections
+import kotlin.collections.ArrayList
+
+/**
+ * Compares two `Size`s based on their areas.
+ */
+internal class CompareSizesByArea : Comparator<Size> {
+    override fun compare(lhs: Size, rhs: Size): Int {
+        // We cast here to ensure the multiplications won't overflow
+        return java.lang.Long.signum(lhs.width.toLong() * lhs.height -
+            rhs.width.toLong() * rhs.height)
+    }
+}
+
+/**
+ * Given `choices` of `Size`s supported by a camera, chooses the smallest one whose
+ * width and height are at least as large as the respective requested values.
+ * @param choices The list of sizes that the camera supports for the intended output class
+ * @param width The minimum desired width
+ * @param height The minimum desired height
+ * @return The optimal `Size`, or an arbitrary one if none were big enough
+ */
+internal fun chooseBigEnoughSize(choices: Array<Size>, width: Int, height: Int): Size {
+    // Collect the supported resolutions that are at least as big as the preview Surface
+    val bigEnough = ArrayList<Size>()
+    for (option in choices) {
+        if (option.width >= width && option.height >= height) {
+            bigEnough.add(option)
+        }
+    }
+    // Pick the smallest of those, assuming we found any
+    if (bigEnough.size > 0) {
+        return Collections.min(bigEnough, CompareSizesByArea())
+    } else {
+        Log.e(MainActivity.LOG_TAG, "Couldn't find any suitable preview size")
+        return choices[0]
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MultiTestSettingsFragment.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MultiTestSettingsFragment.kt
new file mode 100644
index 0000000..b9b4166
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/MultiTestSettingsFragment.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import android.content.SharedPreferences
+import android.os.Bundle
+import androidx.preference.PreferenceFragmentCompat
+
+/**
+ * Fragment that shows the settings for the "Multiple tests" option
+ */
+class MultiTestSettingsFragment
+    : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener {
+
+    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+        setPreferencesFromResource(R.xml.multi_test_settings, rootKey)
+    }
+
+    override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
+    }
+
+    override fun onResume() {
+        super.onResume()
+        preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
+    }
+
+    override fun onPause() {
+        super.onPause()
+        preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
+    }
+}
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/PrefHelper.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/PrefHelper.kt
new file mode 100644
index 0000000..533b870
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/PrefHelper.kt
@@ -0,0 +1,286 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import android.content.SharedPreferences
+import android.os.Build
+import android.preference.PreferenceManager
+import java.util.Collections
+import kotlin.collections.ArrayList
+import kotlin.collections.HashMap
+import kotlin.collections.HashSet
+import kotlin.collections.asList
+import kotlin.collections.iterator
+
+/**
+ * Convenience class to simplify getting values for Shared Preferences
+ */
+class PrefHelper {
+    companion object {
+        internal fun getAutoDelete(activity: MainActivity): Boolean {
+            var sharedPref: SharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(activity)
+            return sharedPref.getBoolean(activity.getString(R.string.settings_autodelete_key), true)
+        }
+
+        internal fun getNumTests(activity: MainActivity): Int {
+            var sharedPref: SharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(activity)
+            return sharedPref.getString(activity
+                .getString(R.string.settings_numtests_key), "30").toInt()
+        }
+
+        internal fun getPreviewBuffer(activity: MainActivity): Long {
+            var sharedPref: SharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(activity)
+            return sharedPref.getString(activity.getString(R.string.settings_previewbuffer_key),
+                "1500").toLong()
+        }
+
+        internal fun getAPIs(activity: MainActivity): ArrayList<CameraAPI> {
+            var sharedPref: SharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(activity)
+            val defApis: HashSet<String> =
+                HashSet(activity.resources.getStringArray(R.array.array_settings_api).asList())
+
+            val apiStrings: HashSet<String> =
+                sharedPref.getStringSet(activity.getString(R.string.settings_autotest_api_key),
+                    defApis) as HashSet<String>
+
+            val apis: ArrayList<CameraAPI> = ArrayList()
+
+            for (apiString in apiStrings) {
+                when (apiString) {
+                    "Camera1" -> apis.add(CameraAPI.CAMERA1)
+                    "Camera2" -> apis.add(CameraAPI.CAMERA2)
+                    "CameraX" -> apis.add(CameraAPI.CAMERAX)
+                }
+            }
+
+            Collections.sort(apis, ApiComparator())
+            return apis
+        }
+
+        internal fun getImageSizes(activity: MainActivity): ArrayList<ImageCaptureSize> {
+            var sharedPref: SharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(activity)
+            val defSizes: HashSet<String> = HashSet(activity.resources
+                .getStringArray(R.array.array_settings_imagesize).asList())
+
+            val sizeStrings: HashSet<String> =
+                sharedPref.getStringSet(activity
+                    .getString(R.string.settings_autotest_imagesize_key), defSizes)
+                    as HashSet<String>
+
+            val sizes: ArrayList<ImageCaptureSize> = ArrayList()
+
+            for (sizeString in sizeStrings) {
+                when (sizeString) {
+                    "Min" -> sizes.add(ImageCaptureSize.MIN)
+                    "Max" -> sizes.add(ImageCaptureSize.MAX)
+                }
+            }
+
+            Collections.sort(sizes, ImageSizeComparator())
+            return sizes
+        }
+
+        internal fun getFocusModes(activity: MainActivity): ArrayList<FocusMode> {
+            var sharedPref: SharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(activity)
+            val defModes: HashSet<String> =
+                HashSet(activity.resources.getStringArray(R.array.array_settings_focus).asList())
+
+            val modeStrings: HashSet<String> =
+                sharedPref.getStringSet(activity.getString(R.string.settings_autotest_focus_key),
+                defModes) as HashSet<String>
+
+            val modes: ArrayList<FocusMode> = ArrayList()
+
+            for (modeString in modeStrings) {
+                when (modeString) {
+                    "Auto" -> modes.add(FocusMode.AUTO)
+                    "Continuous" -> modes.add(FocusMode.CONTINUOUS)
+                    "Fixed" -> modes.add(FocusMode.FIXED)
+                }
+            }
+
+            Collections.sort(modes, FocusModeComparator())
+            return modes
+        }
+
+        internal fun getOnlyLogical(activity: MainActivity): Boolean {
+            var sharedPref: SharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(activity)
+            return sharedPref.getBoolean(activity
+                .getString(R.string.settings_autotest_cameras_key), true)
+        }
+
+        internal fun getSwitchTest(activity: MainActivity): Boolean {
+            var sharedPref: SharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(activity)
+            return sharedPref
+                .getBoolean(activity.getString(R.string.settings_autotest_switchtest_key), true)
+        }
+
+        internal fun getSingleTestType(activity: MainActivity): String {
+            val sharedPref: SharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(activity)
+            return sharedPref
+                .getString(activity.getString(R.string.settings_single_test_type_key), "PHOTO")
+        }
+
+        internal fun getSingleTestFocus(activity: MainActivity): String {
+            val sharedPref: SharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(activity)
+            return sharedPref
+                .getString(activity.getString(R.string.settings_single_test_focus_key), "Auto")
+        }
+
+        internal fun getSingleTestImageSize(activity: MainActivity): String {
+            val sharedPref: SharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(activity)
+            return sharedPref
+                .getString(activity.getString(R.string.settings_single_test_imagesize_key), "Max")
+        }
+
+        internal fun getSingleTestCamera(activity: MainActivity): String {
+            val sharedPref: SharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(activity)
+            return sharedPref
+                .getString(activity.getString(R.string.settings_single_test_camera_key), "0")
+        }
+
+        internal fun getSingleTestApi(activity: MainActivity): String {
+            val sharedPref: SharedPreferences =
+                PreferenceManager.getDefaultSharedPreferences(activity)
+            return sharedPref
+                .getString(activity.getString(R.string.settings_single_test_api_key), "Camera2")
+        }
+
+        internal fun getCameraIds(
+            activity: MainActivity,
+            cameraParams: HashMap<String, CameraParams>
+        ): ArrayList<String> {
+
+            val cameraIds: ArrayList<String> = ArrayList()
+            val onlyLogical: Boolean = getOnlyLogical(activity)
+
+            // First we add all the cameras, then we either remove the logical ones or physical ones
+            for (params in cameraParams)
+                cameraIds.add(params.value.id)
+
+            // Before 28, no physical camera access
+            if (Build.VERSION.SDK_INT < 28)
+                return cameraIds
+
+            // For logical cameras, we remove all physical camera ids
+            if (onlyLogical) {
+                for (params in cameraParams) {
+                    for (physicalId in params.value.physicalCameras)
+                        cameraIds.remove(physicalId)
+                }
+
+                // For only physical, we check if it is backed by physical cameras, if so, remove it
+                // as the physical cameras should already be in the list
+            } else {
+                for (params in cameraParams) {
+                    if (params.value.hasMulti)
+                        cameraIds.remove(params.value.id)
+                }
+            }
+
+            Collections.sort(cameraIds)
+            return cameraIds
+        }
+
+        internal fun getLogicalCameraIds(
+            activity: MainActivity,
+            cameraParams: HashMap<String, CameraParams>
+        ): ArrayList<String> {
+
+            val cameraIds: ArrayList<String> = ArrayList()
+
+            // First we add all the cameras, then we either remove the logical ones or physical ones
+            for (params in cameraParams) {
+                cameraIds.add(params.value.id)
+            }
+            // Before 28, no physical camera access
+            if (Build.VERSION.SDK_INT < 28)
+                return cameraIds
+
+            // For logical cameras, we remove all physical camera ids
+            for (params in cameraParams) {
+                for (physicalId in params.value.physicalCameras) {
+                    cameraIds.remove(physicalId)
+                }
+            }
+
+            Collections.sort(cameraIds)
+            return cameraIds
+        }
+    }
+
+    // Order: Camera2, CameraX, Camera1
+    internal class ApiComparator : Comparator<CameraAPI> {
+        override fun compare(api1: CameraAPI, api2: CameraAPI): Int {
+            if (api1 == api2)
+                return 0
+            if (api1 == CameraAPI.CAMERA2)
+                return -1
+            if (api2 == CameraAPI.CAMERA2)
+                return 1
+            if (api1 == CameraAPI.CAMERAX)
+                return -1
+            if (api2 == CameraAPI.CAMERAX)
+                return 1
+
+            // This should never happen
+            return -1
+        }
+    }
+
+    // Order: Max, Min
+    internal class ImageSizeComparator : Comparator<ImageCaptureSize> {
+        override fun compare(size1: ImageCaptureSize, size2: ImageCaptureSize): Int {
+            if (size1 == size2)
+                return 0
+            if (size1 == ImageCaptureSize.MAX)
+                return -1
+            if (size2 == ImageCaptureSize.MAX)
+                return 1
+
+            // This should never happen
+            return -1
+        }
+    }
+
+    // Order: Auto, Continuous
+    internal class FocusModeComparator : Comparator<FocusMode> {
+        override fun compare(mode1: FocusMode, mode2: FocusMode): Int {
+            if (mode1 == mode2)
+                return 0
+            if (mode1 == FocusMode.AUTO)
+                return -1
+            if (mode2 == FocusMode.AUTO)
+                return 1
+
+            // This should never happen
+            return -1
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/SettingsDialog.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/SettingsDialog.kt
new file mode 100644
index 0000000..b7da3395
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/SettingsDialog.kt
@@ -0,0 +1,151 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.DialogFragment
+import kotlinx.android.synthetic.main.settings_dialog.button_cancel
+import kotlinx.android.synthetic.main.settings_dialog.button_start
+
+/**
+ * DialogFragment that backs the configuration for both single tests and multiple tests
+ */
+internal class SettingsDialog : DialogFragment() {
+
+    override fun onStart() {
+        // If we show a dialog with a title, it doesn't take up the whole screen
+        // Adjust the window to take up the full screen
+        dialog?.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT,
+            ViewGroup.LayoutParams.MATCH_PARENT)
+        super.onStart()
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        // Set the dialog style so we get a title bar
+        setStyle(DialogFragment.STYLE_NORMAL, R.style.SettingsDialogTheme)
+    }
+
+    /** Set up the dialog depending on the dialog type */
+    override fun onCreateView(
+        inflater: LayoutInflater,
+        container: ViewGroup?,
+        savedInstanceState: Bundle?
+    ): View? {
+        val args = arguments
+        val type = args?.getString(DIALOG_TYPE)
+        val title = args?.getString(DIALOG_TITLE)
+        val cameraNames = args?.getStringArray(CAMERA_NAMES)
+        val cameraIds = args?.getStringArray(CAMERA_IDS)
+
+        dialog?.setTitle(title)
+
+        val dialogView = inflater.inflate(R.layout.settings_dialog, container, false)
+
+        if (null != cameraIds && null != cameraNames) {
+            when (type) {
+                DIALOG_TYPE_MULTI -> {
+                    val settingsFragment = MultiTestSettingsFragment()
+                    val childFragmentManager = childFragmentManager
+                    val fragmentTransaction = childFragmentManager.beginTransaction()
+                    fragmentTransaction.replace(R.id.scroll_settings_dialog, settingsFragment)
+                    fragmentTransaction.commit()
+                }
+                else -> {
+                    val settingsFragment = SingleTestSettingsFragment(cameraNames, cameraIds)
+                    val childFragmentManager = childFragmentManager
+                    val fragmentTransaction = childFragmentManager.beginTransaction()
+                    fragmentTransaction.replace(R.id.scroll_settings_dialog, settingsFragment)
+                    fragmentTransaction.commit()
+                }
+            }
+        }
+
+        return dialogView
+    }
+
+    /** When view is created, set up action buttons */
+    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+        super.onViewCreated(view, savedInstanceState)
+
+        val args = arguments
+        val type = args?.getString(DIALOG_TYPE)
+
+        when (type) {
+            DIALOG_TYPE_MULTI -> {
+                button_start.text = getString(R.string.settings_multi_go)
+                button_cancel.text = getString(R.string.settings_multi_cancel)
+
+                button_start.setOnClickListener {
+                    (activity as MainActivity).startMultiTest()
+                    this.dismiss()
+                }
+                button_cancel.setOnClickListener { this.dismiss() }
+            }
+            else -> {
+                button_start.text = getString(R.string.settings_single_go)
+                button_cancel.text = getString(R.string.settings_single_cancel)
+
+                button_start.setOnClickListener {
+                    (activity as MainActivity).startSingleTest()
+                    this.dismiss()
+                }
+                button_cancel.setOnClickListener { this.dismiss() }
+            }
+        }
+    }
+
+    companion object {
+        private const val DIALOG_TYPE = "DIALOG_TYPE"
+        private const val DIALOG_TITLE = "DIALOG_TITLE"
+        private const val CAMERA_NAMES = "CAMERA_NAMES"
+        private const val CAMERA_IDS = "CAMERA_IDS"
+
+        internal const val DIALOG_TYPE_SINGLE = "DIALOG_TYPE_SINGLE"
+        internal const val DIALOG_TYPE_MULTI = "DIALOG_TYPE_MULTI"
+
+        /**
+         * Create a new Settings dialog to configure a test run
+         *
+         * @param type Dialog type (DIALOG_TYPE_MULTI or DIALOG_TYPE_SINGLE)
+         * @param title Dialog title
+         * @param cameraNames Human readable array of camera names
+         * @param cameraIds Array of camera ids
+         */
+        fun newInstance(
+            type: String,
+            title: String,
+            cameraNames: Array<String>,
+            cameraIds: Array<String>
+        ): SettingsDialog {
+
+            val args = Bundle()
+            args.putString(DIALOG_TYPE, type)
+            args.putString(DIALOG_TITLE, title)
+            args.putStringArray(CAMERA_NAMES, cameraNames)
+            args.putStringArray(CAMERA_IDS, cameraIds)
+
+            val settingsDialog = SettingsDialog()
+            settingsDialog.arguments = args
+            return settingsDialog
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/SingleTestSettingsFragment.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/SingleTestSettingsFragment.kt
new file mode 100644
index 0000000..6288ffb
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/SingleTestSettingsFragment.kt
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import android.content.SharedPreferences
+import android.os.Bundle
+import androidx.preference.ListPreference
+import androidx.preference.PreferenceFragmentCompat
+
+/**
+ * Fragment that shows the settings for the "Single test" option
+ */
+class SingleTestSettingsFragment(
+    internal val cameraNames: Array<String>,
+    internal val cameraIds: Array<String>
+) : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener {
+
+    override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+        setPreferencesFromResource(R.xml.single_test_settings, rootKey)
+
+        val cameraPref =
+            preferenceManager.findPreference<ListPreference>(
+                getString(R.string.settings_single_test_camera_key))
+        cameraPref?.entries = cameraNames
+        cameraPref?.entryValues = cameraIds
+        if (cameraIds.isNotEmpty())
+            cameraPref?.setDefaultValue(cameraIds[0])
+
+        if (null == cameraPref?.value)
+            cameraPref?.value = cameraIds[0]
+
+        // En/disable needed controls
+        toggleNumTests()
+        togglePreviewBuffer()
+    }
+
+    override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
+        if (key.equals(getString(R.string.settings_single_test_type_key))) {
+            toggleNumTests()
+            togglePreviewBuffer()
+        }
+    }
+
+    override fun onResume() {
+        super.onResume()
+        preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this)
+    }
+
+    override fun onPause() {
+        super.onPause()
+        preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this)
+    }
+
+    /** Some tests do not allow for multiple repetitions, If one of these selected,
+     * disable the number of tests control.
+     */
+    fun toggleNumTests() {
+        val typePref =
+            preferenceManager.findPreference<ListPreference>(
+                getString(R.string.settings_single_test_type_key))
+        val numberPref = preferenceManager
+            .findPreference<ListPreference>(getString(R.string.settings_numtests_key))
+        when (typePref?.value) {
+            "INIT", "PREVIEW", "SWITCH_CAMERA", "PHOTO" -> {
+                numberPref?.isEnabled = false
+            }
+            else -> {
+                numberPref?.isEnabled = true
+            }
+        }
+    }
+
+    /** Some tests do not require the preview stream to run, If one of these selected,
+     * disable the preview buffer control.
+     */
+    fun togglePreviewBuffer() {
+        val typePref =
+            preferenceManager
+                .findPreference<ListPreference>(getString(R.string.settings_single_test_type_key))
+        val previewPref =
+            preferenceManager
+                .findPreference<ListPreference>(getString(R.string.settings_previewbuffer_key))
+        when (typePref?.value) {
+            "INIT" -> {
+                previewPref?.isEnabled = false
+            }
+            else -> {
+                previewPref?.isEnabled = true
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestConfig.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestConfig.kt
new file mode 100644
index 0000000..1e25989
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestConfig.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+/** The different types of tests Antelop can perform */
+enum class TestType {
+    /** No test  */
+    NONE,
+    /** Open and close camera only*/
+    INIT,
+    /** Start preview stream */
+    PREVIEW,
+    /** Capture a single image */
+    PHOTO,
+    /** Capture multiple photos, closing/opening camera between each capture */
+    MULTI_PHOTO,
+    /** Capture multiple photos, leave camera open between captures */
+    MULTI_PHOTO_CHAIN,
+    /** Switch between two cameras one time. Preview stream only, no capture */
+    SWITCH_CAMERA,
+    /** Switch between two cameras multiple times, Preview only, no capture */
+    MULTI_SWITCH
+}
+
+/**
+ * Configuration and state variables for the current running test.
+ *
+ * Also contains the TestResults object to record the results.
+ */
+class TestConfig(
+    /** Name of test */
+    var testName: String = "",
+    /** Enum type of test */
+    var currentRunningTest: TestType = TestType.NONE,
+    /** API to use for test (Camera 1, 2, or X) */
+    var api: CameraAPI = CameraAPI.CAMERA2,
+    /** Size of capture to request */
+    var imageCaptureSize: ImageCaptureSize = ImageCaptureSize.MAX,
+    /** Auto-focus, Continuous focus, or Fixe-focus */
+    var focusMode: FocusMode = FocusMode.AUTO,
+    /** Camera ID */
+    var camera: String = "0",
+    /** Camera array to use for the switch camera test */
+    var switchTestCameras: Array<String> = arrayOf("0", "1"),
+    /** Convenience variable, currently active camera during switch test */
+    var switchTestCurrentCamera: String = "0",
+    /** Save the original camera for a switch test */
+    var switchTestRealCameraId: String = "0",
+    /** Semaphor for first onActive preview state */
+    var isFirstOnActive: Boolean = true,
+    /** Semaphor for first completed capture */
+    var isFirstOnCaptureComplete: Boolean = true,
+    /** Semaphor for when the test is completed */
+    var testFinished: Boolean = false,
+    /** Accumulate test results in this object */
+    var testResults: TestResults = TestResults()
+) {
+
+    /**
+     * Set up the TestResults object to reflect the test configuration
+     */
+    fun setupTestResults() {
+        testResults.testName = testName
+        testResults.testType = currentRunningTest
+        testResults.camera = camera
+        testResults.cameraId = camera
+        testResults.cameraAPI = api
+        testResults.imageCaptureSize = imageCaptureSize
+        testResults.focusMode = focusMode
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestResults.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestResults.kt
new file mode 100644
index 0000000..70453d5
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestResults.kt
@@ -0,0 +1,413 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import android.content.Intent
+import android.net.Uri
+import android.os.Environment
+import android.widget.Toast
+import com.google.common.math.Quantiles
+import com.google.common.math.Stats
+import androidx.camera.integration.antelope.MainActivity.Companion.logd
+import java.io.BufferedWriter
+import java.io.File
+import java.io.FileOutputStream
+import java.io.IOException
+import java.io.OutputStreamWriter
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import java.util.Date
+import java.util.Locale
+
+/**
+ * Contains the results for a specific test. Most of the variables are arrays to accommodate tests
+ * with multiple repetitions (MULTI_PHOTO, MULTI_PHOTO_CHAINED, MULTI_SWITCH, etc.)
+ */
+class TestResults {
+    /** Name of test */
+    var testName: String = ""
+    /** Human readable camera name */
+    var camera: String = ""
+    /** Camera ID */
+    var cameraId: String = ""
+    /** Which API was used (1, 2, or X) */
+    var cameraAPI: CameraAPI = CameraAPI.CAMERA2
+    /** Image size that was requested */
+    var imageCaptureSize: ImageCaptureSize = ImageCaptureSize.MAX
+    /** Auto-focus, continuous focus, or fixed-foxus */
+    var focusMode: FocusMode = FocusMode.AUTO
+    /** Enum type of the test requested */
+    var testType: TestType = TestType.NONE
+    /** Time take to open camera */
+    var initialization: ArrayList<Long> = ArrayList<Long>()
+    /** Time taken for the preview to start */
+    var previewStart: ArrayList<Long> = ArrayList<Long>()
+    /** Time taken for the preview to run before next step in the test */
+    var previewFill: ArrayList<Long> = ArrayList<Long>()
+    /** Time taken to switch from first to second cameras */
+    var switchToSecond: ArrayList<Long> = ArrayList<Long>()
+    /** Time taken to switch from second to first cameras */
+    var switchToFirst: ArrayList<Long> = ArrayList<Long>()
+    /** Time taken for the auto-focus routine to complete */
+    var autofocus: ArrayList<Long> = ArrayList<Long>()
+    /** Time taken for image capture, not including auto-focus delay */
+    var captureNoAF: ArrayList<Long> = ArrayList<Long>()
+    /** Time taken for image capture, including auto-focus delay (if applicable) */
+    var capture: ArrayList<Long> = ArrayList<Long>()
+    /** Time taken after image is captured for it to be ready in the ImageReader */
+    var imageready: ArrayList<Long> = ArrayList<Long>()
+    /** Time taken for capture and the image to appear in the ImageReader */
+    var capturePlusImageReady: ArrayList<Long> = ArrayList<Long>()
+    /** Time taken to save image to disk */
+    var imagesave: ArrayList<Long> = ArrayList<Long>()
+    /** Time taken to close the preview stream */
+    var previewClose: ArrayList<Long> = ArrayList<Long>()
+    /** Time taken to close the camera */
+    var cameraClose: ArrayList<Long> = ArrayList<Long>()
+    /** Time taken for the entire test */
+    var total: ArrayList<Long> = ArrayList<Long>()
+    /** Time taken for the entire test not including filling the preview stream */
+    var totalNoPreview: ArrayList<Long> = ArrayList<Long>()
+    /** Was the image captured an HDR+ image? */
+    var isHDRPlus: ArrayList<Boolean> = ArrayList<Boolean>()
+
+    /**
+     * Format results into a human readable string
+     */
+    fun toString(activity: MainActivity, header: Boolean): String {
+        var output = ""
+
+        if (header) {
+            val dateFormatter = SimpleDateFormat("d MMM yyyy - kk'h'mm")
+            val cal: Calendar = Calendar.getInstance()
+            output += "DATE: " + dateFormatter.format(cal.time) +
+                " (Antelope " + getVersionName(activity) + ")\n"
+
+            output += "DEVICE: " + MainActivity.deviceInfo.device + "\n\n"
+            output += "CAMERAS:\n"
+            for (camera in MainActivity.cameras)
+                output += camera + "\n"
+            output += "\n"
+        }
+
+        output += testName + "\n"
+        output += "Camera: " + camera + "\n"
+        output += "API: " + cameraAPI + "\n"
+        output += "Focus Mode: " + focusMode + "\n"
+        output += "Image Capture Size: " + imageCaptureSize + "\n\n"
+
+        output += outputResultLine("Camera open", initialization)
+        output += outputResultLine("Preview start", previewStart)
+        output += outputResultLine("Preview buffer", previewFill)
+
+        when (focusMode) {
+            FocusMode.CONTINUOUS -> {
+                output += outputResultLine("Capture (continuous focus)", capture)
+            }
+            FocusMode.FIXED -> {
+                output += outputResultLine("Capture (fixed-focus)", capture)
+            }
+            else -> {
+                // CameraX doesn't allow us insight into autofocus
+                if (CameraAPI.CAMERAX == cameraAPI) {
+                    output += outputResultLine("Capture incl. autofocus", capture)
+                } else {
+                    output += outputResultLine("Autofocus", autofocus)
+                    output += outputResultLine("Capture", captureNoAF)
+                    output += outputResultLine("Capture incl. autofocus", capture)
+                }
+            }
+        }
+
+        output += outputResultLine("Image ready", imageready)
+        output += outputResultLine("Cap + img ready", capturePlusImageReady)
+        output += outputResultLine("Image save", imagesave)
+        output += outputResultLine("Switch to 2nd", switchToSecond)
+        output += outputResultLine("Switch to 1st", switchToFirst)
+        output += outputResultLine("Preview close", previewClose)
+        output += outputResultLine("Camera close", cameraClose)
+        output += outputBooleanResultLine("HDR+", isHDRPlus)
+        output += outputResultLine("Total", total)
+        output += outputResultLine("Total w/o preview buffer", totalNoPreview)
+
+        if (1 < capturePlusImageReady.size) {
+            val captureStats = Stats.of(capturePlusImageReady)
+            output += "Capture range: " + captureStats.min() + " - " + captureStats.max() + "\n"
+            output += "Capture mean: " + captureStats.mean() +
+                " (" + captureStats.count() + " captures)\n"
+            output += "Capture median: " + Quantiles.median().compute(capturePlusImageReady) + "\n"
+            output += "Capture standard deviation: " + captureStats.sampleStandardDeviation() + "\n"
+        }
+        output += "Total batch time: " + Stats.of(total).sum() + "\n\n\n"
+        return output
+    }
+
+    /**
+     * Format results to a comma-based .csv string
+     */
+    fun toCSV(activity: MainActivity, header: Boolean = true): String {
+        val numCommas = PrefHelper.getNumTests(activity)
+
+        var output = ""
+
+        if (header) {
+            val dateFormatter = SimpleDateFormat("d MMM yyyy - kk'h'mm")
+            val cal: Calendar = Calendar.getInstance()
+            output += "DATE: " + dateFormatter.format(cal.time) + " (Antelope " +
+                getVersionName(activity) + ")" + outputCommas(numCommas) + "\n"
+
+            output += "DEVICE: " + MainActivity.deviceInfo.device + outputCommas(numCommas) +
+                "\n" + outputCommas(numCommas) + "\n"
+            output += "CAMERAS: " + outputCommas(numCommas) + "\n"
+            for (camera in MainActivity.cameras)
+                output += camera + outputCommas(numCommas) + "\n"
+            output += outputCommas(numCommas) + "\n"
+        }
+
+        output += testName + outputCommas(numCommas) + outputCommas(numCommas) + "\n"
+        output += "Camera: " + camera + outputCommas(numCommas) + "\n"
+        output += "API: " + cameraAPI + outputCommas(numCommas) + "\n"
+        output += "Focus Mode: " + focusMode + outputCommas(numCommas) + "\n"
+        output += "Image Capture Size: " + imageCaptureSize + outputCommas(numCommas) + "\n" +
+            outputCommas(numCommas) + "\n"
+
+        output += outputResultLine("Camera open", initialization, numCommas, true)
+        output += outputResultLine("Preview start", previewStart, numCommas, true)
+        output += outputResultLine("Preview buffer", previewFill, numCommas, true)
+
+        when (focusMode) {
+            FocusMode.CONTINUOUS -> {
+                output += outputResultLine("Capture (continuous focus)", capture,
+                    numCommas, true)
+            }
+            FocusMode.FIXED -> {
+                output += outputResultLine("Capture (fixed-focus)", capture,
+                    numCommas, true)
+            }
+            else -> {
+                // CameraX doesn't allow us insight into autofocus
+                if (CameraAPI.CAMERAX == cameraAPI) {
+                    output += outputResultLine("Capture incl. autofocus", capture,
+                        numCommas, true)
+                } else {
+                    output += outputResultLine("Autofocus", autofocus,
+                        numCommas, true)
+                    output += outputResultLine("Capture", captureNoAF,
+                        numCommas, true)
+                    output += outputResultLine("Capture incl. autofocus", capture,
+                        numCommas, true)
+                }
+            }
+        }
+
+        output += outputResultLine("Image ready", imageready, numCommas, true)
+        output += outputResultLine("Cap + img ready", capturePlusImageReady,
+            numCommas, true)
+        output += outputResultLine("Image save", imagesave, numCommas, true)
+        output += outputResultLine("Switch to 2nd", switchToSecond, numCommas, true)
+        output += outputResultLine("Switch to 1st", switchToFirst, numCommas, true)
+        output += outputResultLine("Preview close", previewClose, numCommas, true)
+        output += outputResultLine("Camera close", cameraClose, numCommas, true)
+        output += outputBooleanResultLine("HDR+", isHDRPlus, numCommas, true)
+        output += outputResultLine("Total", total, numCommas, true)
+        output += outputResultLine("Total w/o preview buffer", totalNoPreview,
+            numCommas, true)
+
+        if (1 < capturePlusImageReady.size) {
+            val captureStats = Stats.of(capturePlusImageReady)
+            output += "Capture range:," + captureStats.min() + " - " + captureStats.max() +
+                outputCommas(numCommas) + "\n"
+            output += "Capture mean " + " (" + captureStats.count() + " captures):," +
+                Stats.of(capturePlusImageReady).mean() + outputCommas(numCommas) + "\n"
+            output += "Capture median:," + Quantiles.median().compute(capturePlusImageReady) +
+                outputCommas(numCommas) + "\n"
+            output += "Capture standard deviation:," +
+                captureStats.sampleStandardDeviation() + outputCommas(numCommas) + "\n"
+        }
+
+        output += "Total batch time:," + Stats.of(total).sum() + outputCommas(numCommas) + "\n"
+
+        output += outputCommas(numCommas) + "\n"
+        output += outputCommas(numCommas) + "\n"
+        output += outputCommas(numCommas) + "\n"
+
+        return output
+    }
+}
+
+/**
+ * Write all results to disk in a .csv file
+ *
+ * @param activity The main activity
+ * @param filePrefix The prefix for the .csv file
+ * @param csv The comma-based csv string
+ */
+fun writeCSV(activity: MainActivity, filePrefix: String, csv: String) {
+
+    val csvFile = File(Environment.getExternalStoragePublicDirectory(
+        Environment.DIRECTORY_DOCUMENTS),
+        File.separatorChar + MainActivity.LOG_DIR + File.separatorChar +
+            filePrefix + "_" + generateCSVTimestamp() + ".csv")
+
+    val csvDir = File(Environment.getExternalStoragePublicDirectory(
+        Environment.DIRECTORY_DOCUMENTS), MainActivity.LOG_DIR)
+    val docsDir = File(Environment.getExternalStoragePublicDirectory(
+        Environment.DIRECTORY_DOCUMENTS), "")
+
+    if (!docsDir.exists()) {
+        val createSuccess = docsDir.mkdir()
+        if (!createSuccess) {
+            Toast.makeText(activity, "Documents" + " creation failed.",
+                Toast.LENGTH_SHORT).show()
+            MainActivity.logd("Log storage directory Documents" + " creation failed!!")
+        } else {
+            MainActivity.logd("Log storage directory Documents" + " did not exist. Created.")
+        }
+    }
+
+    if (!csvDir.exists()) {
+        val createSuccess = csvDir.mkdir()
+        if (!createSuccess) {
+            Toast.makeText(activity, "Documents/" + MainActivity.LOG_DIR +
+                " creation failed.", Toast.LENGTH_SHORT).show()
+            MainActivity.logd("Log storage directory Documents/" +
+                MainActivity.LOG_DIR + " creation failed!!")
+        } else {
+            MainActivity.logd("Log storage directory Documents/" +
+                MainActivity.LOG_DIR + " did not exist. Created.")
+        }
+    }
+
+    val output = BufferedWriter(OutputStreamWriter(FileOutputStream(csvFile)))
+    try {
+        output.write(csv)
+        logd("CSV write completed successfully.")
+
+        // File is written, let media scanner know
+        val scannerIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
+        scannerIntent.data = Uri.fromFile(csvFile)
+        activity.sendBroadcast(scannerIntent)
+    } catch (e: IOException) {
+        logd("IOException vail on CSV write: " + e.printStackTrace())
+    } finally {
+        try {
+            output.close()
+        } catch (e: IOException) {
+            logd("IOException vail on CSV close: " + e.printStackTrace())
+            e.printStackTrace()
+        }
+    }
+}
+
+/**
+ * Delete all Antelope .csv files in the documents directory
+ */
+fun deleteCSVFiles(activity: MainActivity) {
+    val csvDir = File(Environment.getExternalStoragePublicDirectory(
+        Environment.DIRECTORY_DOCUMENTS), MainActivity.LOG_DIR)
+
+    if (csvDir.exists()) {
+
+        for (csv in csvDir.listFiles())
+            csv.delete()
+
+        // Files are deleted, let media scanner know
+        val scannerIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
+        scannerIntent.data = Uri.fromFile(csvDir)
+        activity.sendBroadcast(scannerIntent)
+
+        Toast.makeText(activity, "CSV logs deleted", Toast.LENGTH_SHORT).show()
+        logd("All csv logs in directory DOCUMENTS/" + MainActivity.LOG_DIR + " deleted.")
+    }
+}
+
+/**
+ * Generate a timestamp for csv filenames
+ */
+fun generateCSVTimestamp(): String {
+    val sdf = SimpleDateFormat("yyyy-MM-dd-HH'h'mm", Locale.US)
+    return sdf.format(Date())
+}
+
+/**
+ * Create a string consisting solely of the number of commas indicated
+ *
+ * Handy for properly formatting comma-based .csv files as the number of columns will depend on
+ * the user configurable number of test repetitions.
+ */
+fun outputCommas(numCommas: Int): String {
+    var output = ""
+    for (i in 1..numCommas)
+        output += ","
+    return output
+}
+
+/**
+ * For a list of Longs, output a comma separated .csv line
+ */
+fun outputResultLine(
+    name: String,
+    results: ArrayList<Long>,
+    numCommas: Int = 30,
+    isCSV: Boolean = false
+): String {
+    var output = ""
+
+    if (!results.isEmpty()) {
+        output += name + ": "
+        for ((index, result) in results.withIndex()) {
+            if (isCSV || (0 != index))
+                output += ","
+            output += result
+        }
+        if (isCSV)
+            output += outputCommas(numCommas - results.size)
+        output += "\n"
+    }
+
+    return output
+}
+
+/**
+ * For a list of Booleans, output a comma separated .csv line
+ */
+fun outputBooleanResultLine(
+    name: String,
+    results: ArrayList<Boolean>,
+    numCommas: Int = 30,
+    isCSV: Boolean = false
+): String {
+    var output = ""
+
+    // If every result is false, don't output this line at all
+    if (!results.isEmpty() && results.contains(true)) {
+        output += name + ": "
+        for ((index, result) in results.withIndex()) {
+            if (isCSV || (0 != index))
+                output += ","
+            if (result)
+                output += "HDR+"
+            else
+                output += " - "
+        }
+        if (isCSV)
+            output += outputCommas(numCommas - results.size)
+        output += "\n"
+    }
+
+    return output
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestUtils.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestUtils.kt
new file mode 100644
index 0000000..fae19ae
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TestUtils.kt
@@ -0,0 +1,554 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import com.google.common.math.Stats
+import androidx.camera.integration.antelope.MainActivity.Companion.cameraParams
+import androidx.camera.integration.antelope.MainActivity.Companion.isSingleTestRunning
+import androidx.camera.integration.antelope.MainActivity.Companion.logd
+import androidx.camera.integration.antelope.MainActivity.Companion.testsRemaining
+import java.text.SimpleDateFormat
+import java.util.Calendar
+import kotlin.collections.ArrayList
+import kotlin.math.roundToInt
+
+/**
+ * During a multiple-test run, this should be called after each test is completed. Record the result
+ * and call the automatic test runner to start the next test.
+ *
+ * If all test are completed, post result to screen and save the log.
+ */
+fun postTestResults(activity: MainActivity, testConfig: TestConfig) {
+    MainActivity.testRun.add(testConfig.testResults)
+    testsRemaining--
+
+    var log = ""
+    var csv = ""
+
+    if (0 >= testsRemaining) {
+        if (!isSingleTestRunning) {
+            log += testSummaryString(activity, MainActivity.testRun)
+            csv += testSummaryCSV(activity, MainActivity.testRun)
+        }
+
+        for ((index, result) in MainActivity.testRun.withIndex()) {
+            // TODO with test summary we can combine these two cases
+            if (0 == index) {
+                log += result.toString(activity, false)
+                csv += result.toCSV(activity, false)
+            } else {
+                log += result.toString(activity, false)
+                csv += result.toCSV(activity, false)
+            }
+        }
+
+        activity.resetUIAfterTest()
+        activity.updateLog(log)
+        writeCSV(activity, DeviceInfo(activity).deviceShort, csv)
+    } else {
+        autoTestRunner(activity)
+    }
+}
+
+/**
+ * Set up the TestConfig object for a single test
+ */
+fun createSingleTestConfig(activity: MainActivity): TestConfig {
+    val config = TestConfig()
+
+    config.apply {
+        when (PrefHelper.getSingleTestType(activity)) {
+            "INIT" -> {
+                testName = "Camera Open/Close"
+                currentRunningTest = TestType.INIT
+            }
+            "PREVIEW" -> {
+                testName = "Preview Start"
+                currentRunningTest = TestType.PREVIEW
+            }
+            "SWITCH_CAMERA" -> {
+                testName = "Switch Cameras"
+                currentRunningTest = TestType.SWITCH_CAMERA
+            }
+            "MULTI_SWITCH" -> {
+                testName = "Switch Cameras (Multiple)"
+                currentRunningTest = TestType.MULTI_SWITCH
+            }
+            "MULTI_PHOTO" -> {
+                testName = "Multiple Captures"
+                currentRunningTest = TestType.MULTI_PHOTO
+            }
+            "MULTI_PHOTO_CHAIN" -> {
+                testName = "Multiple Captures (Chained)"
+                currentRunningTest = TestType.MULTI_PHOTO_CHAIN
+            }
+            else -> {
+                testName = "Single Capture"
+                currentRunningTest = TestType.PHOTO
+            }
+        }
+
+        api = CameraAPI.valueOf(PrefHelper.getSingleTestApi(activity).toUpperCase())
+        focusMode = FocusMode.valueOf(PrefHelper.getSingleTestFocus(activity).toUpperCase())
+        imageCaptureSize =
+            ImageCaptureSize.valueOf(PrefHelper.getSingleTestImageSize(activity).toUpperCase())
+        camera = PrefHelper.getSingleTestCamera(activity)
+        config.setupTestResults()
+    }
+
+    return config
+}
+
+/**
+ * Create a test configuration given the test's name and the currently selected test values
+ */
+fun createTestConfig(testName: String): TestConfig {
+    val config = TestConfig(testName)
+    config.camera = MainActivity.camViewModel.getCurrentCamera().value.toString()
+    config.api = MainActivity.camViewModel.getCurrentAPI().value ?: CameraAPI.CAMERA2
+    config.imageCaptureSize =
+        MainActivity.camViewModel.getCurrentImageCaptureSize().value ?: ImageCaptureSize.MAX
+
+    // If we don't have auto-focus, we set the focus mode to FIXED
+    if (MainActivity.cameraParams.get(config.camera)?.hasAF ?: true)
+        config.focusMode = MainActivity.camViewModel.getCurrentFocusMode().value ?: FocusMode.AUTO
+    else
+        config.focusMode = FocusMode.FIXED
+
+    config.setupTestResults()
+
+    return config
+}
+
+/**
+ * For multiple tests, configure the list of TestConfigs to run
+ */
+fun setupAutoTestRunner(activity: MainActivity) {
+    MainActivity.autoTestConfigs.clear()
+    val cameras: ArrayList<String> = PrefHelper.getCameraIds(activity, MainActivity.cameraParams)
+    val logicalCameras: ArrayList<String> =
+        PrefHelper.getLogicalCameraIds(activity, MainActivity.cameraParams)
+    val apis: ArrayList<CameraAPI> = PrefHelper.getAPIs(activity)
+    val imageSizes: ArrayList<ImageCaptureSize> = PrefHelper.getImageSizes(activity)
+    val focusModes: ArrayList<FocusMode> = PrefHelper.getFocusModes(activity)
+    val testTypes: ArrayList<TestType> = ArrayList()
+    val doSwitchTest: Boolean = PrefHelper.getSwitchTest(activity)
+
+    testTypes.add(TestType.MULTI_PHOTO)
+    testTypes.add(TestType.MULTI_PHOTO_CHAIN)
+
+    if (doSwitchTest)
+        testTypes.add(TestType.MULTI_SWITCH)
+
+    MainActivity.testRun.clear()
+
+    for (camera in cameras) {
+        for (api in apis) {
+            // Camera1 does not have access to physical cameras, only logical 0 and 1
+            // Some devices have no camera or only 1 front-facing camera (like Chromebooks)
+            // so we need to make sure they exist
+            if ((CameraAPI.CAMERA1 == api) && !logicalCameras.contains(camera))
+                continue
+
+            // Currently CameraX only supports FRONT and BACK
+            if ((CameraAPI.CAMERAX == api) && !(camera.equals("0") || camera.equals("1")))
+                continue
+
+            for (imageSize in imageSizes) {
+                for (focusMode in focusModes) {
+                    if (FocusMode.CONTINUOUS == focusMode) {
+                        // If camera is fixed-focus, only run the AUTO test
+                        if (!(MainActivity.cameraParams.get(camera)?.hasAF ?: true))
+                            continue
+                    }
+
+                    for (testType in testTypes) {
+                        // Camera1 does not have chaining capabilities
+                        if ((CameraAPI.CAMERA1 == api) && (TestType.MULTI_PHOTO_CHAIN == testType))
+                            continue
+
+                        // For now we only test 0->1->0, just add this test for the first "camera"
+                        // TODO: figure out a way to test different permutations
+                        if ((TestType.MULTI_SWITCH == testType) && !camera.equals(cameras.first()))
+                            continue
+
+                        // Switch test doesn't do a capture don't repeat for all capture sizes
+                        if (imageSize == ImageCaptureSize.MIN && testType == TestType.MULTI_SWITCH)
+                            continue
+
+                        // Switch test doesn't do a capture so don't repeat for all focus modes
+                        if (focusMode != FocusMode.AUTO && testType == TestType.MULTI_SWITCH)
+                            continue
+
+                        // If this is a fixed focus lens, focusMode here has been set to auto,
+                        // set it to fixed in the TestConfig
+                        var realFocusMode: FocusMode = focusMode
+                        if (!(MainActivity.cameraParams.get(camera)?.hasAF ?: true))
+                            realFocusMode = FocusMode.FIXED
+
+                        var testName = when (api) {
+                            CameraAPI.CAMERA1 -> "Camera1"
+                            CameraAPI.CAMERA2 -> "Camera2"
+                            CameraAPI.CAMERAX -> "CameraX"
+                        }
+
+                        testName += " - "
+                        testName += when (imageSize) {
+                            ImageCaptureSize.MIN -> "Min"
+                            ImageCaptureSize.MAX -> "Max"
+                        }
+                        testName += " image size - Camera device "
+
+                        if (testType != TestType.MULTI_SWITCH) {
+                            testName += camera
+                            testName += " "
+                        }
+
+                        testName += when (realFocusMode) {
+                            FocusMode.AUTO -> "(auto-focus)"
+                            FocusMode.CONTINUOUS -> "(continuous focus)"
+                            else -> "(fixed-focus)"
+                        }
+
+                        testName += " - "
+                        testName += when (testType) {
+                            TestType.MULTI_PHOTO -> "Multiple Captures"
+                            TestType.MULTI_PHOTO_CHAIN -> "Multiple Captures (chained)"
+                            TestType.MULTI_SWITCH -> "Switch Camera"
+                            else -> "unknown test"
+                        }
+
+                        val testConfig =
+                            TestConfig(testName, testType, api, imageSize, realFocusMode, camera)
+                        testConfig.setupTestResults()
+                        MainActivity.autoTestConfigs.add(testConfig)
+                    }
+                }
+            }
+        }
+    }
+
+    // Add Test X of Y string to test names
+    for ((index, testConfig) in MainActivity.autoTestConfigs.withIndex()) {
+        testConfig.testName = "" + (index + 1) + " of " +
+            MainActivity.autoTestConfigs.size + ": " + testConfig.testName
+    }
+
+    testsRemaining = MainActivity.autoTestConfigs.size
+}
+
+/**
+ * Run the list of tests in autoTestConfigs
+ */
+fun autoTestRunner(activity: MainActivity) {
+    if (MainActivity.cameras.isEmpty()) {
+        testsRemaining = 0
+        return
+    }
+
+    // If something goes wrong or we are aborted, stop testing
+    if (0 == testsRemaining)
+        return
+
+    val currentTest: Int = MainActivity.autoTestConfigs.size - testsRemaining + 1
+    val currentConfig: TestConfig = MainActivity.autoTestConfigs.get(currentTest - 1)
+    MainActivity.logd("autoTestRun about to run: " + currentConfig.testName)
+
+    activity.runOnUiThread {
+        if (testsRemaining == MainActivity.autoTestConfigs.size)
+            activity.setupUIForTest(currentConfig, false)
+        else
+            activity.setupUIForTest(currentConfig, true)
+    }
+
+    multiCounter = 0
+    initializeTest(activity, cameraParams.get(currentConfig.camera), currentConfig)
+}
+
+/**
+ * For an array of TestResults, generate a high-level summary of the most important values.
+ *
+ * This mostly consists of values for default rear camera.
+ */
+fun testSummaryString(activity: MainActivity, allTestResults: ArrayList<TestResults>): String {
+    var output = ""
+
+    if (allTestResults.isEmpty())
+        return output
+
+    val mainCamera = getMainCamera(activity, allTestResults)
+
+    val c2Auto = findTest(allTestResults, mainCamera, CameraAPI.CAMERA2,
+        ImageCaptureSize.MAX, FocusMode.AUTO, TestType.MULTI_PHOTO)
+    val c2AutoChain = findTest(allTestResults, mainCamera, CameraAPI.CAMERA2,
+        ImageCaptureSize.MAX, FocusMode.AUTO, TestType.MULTI_PHOTO_CHAIN)
+    val c2Caf = findTest(allTestResults, mainCamera, CameraAPI.CAMERA2,
+        ImageCaptureSize.MAX, FocusMode.CONTINUOUS, TestType.MULTI_PHOTO)
+    val c2CafChain = findTest(allTestResults, mainCamera, CameraAPI.CAMERA2,
+        ImageCaptureSize.MAX, FocusMode.CONTINUOUS, TestType.MULTI_PHOTO_CHAIN)
+    val c2AutoMin = findTest(allTestResults, mainCamera, CameraAPI.CAMERA2,
+        ImageCaptureSize.MIN, FocusMode.AUTO, TestType.MULTI_PHOTO)
+    val c2Switch = findTest(allTestResults, mainCamera, CameraAPI.CAMERA2,
+        ImageCaptureSize.MAX, FocusMode.AUTO, TestType.MULTI_SWITCH)
+    val c1Auto = findTest(allTestResults, mainCamera, CameraAPI.CAMERA1,
+        ImageCaptureSize.MAX, FocusMode.AUTO, TestType.MULTI_PHOTO)
+    val c1Caf = findTest(allTestResults, mainCamera, CameraAPI.CAMERA1,
+        ImageCaptureSize.MAX, FocusMode.CONTINUOUS, TestType.MULTI_PHOTO)
+    val c1Switch = findTest(allTestResults, mainCamera, CameraAPI.CAMERA1,
+        ImageCaptureSize.MAX, FocusMode.AUTO, TestType.MULTI_SWITCH)
+    val cXAuto = findTest(allTestResults, mainCamera, CameraAPI.CAMERAX,
+        ImageCaptureSize.MAX, FocusMode.AUTO, TestType.MULTI_PHOTO)
+    val cXCaf = findTest(allTestResults, mainCamera, CameraAPI.CAMERAX,
+        ImageCaptureSize.MAX, FocusMode.CONTINUOUS, TestType.MULTI_PHOTO)
+    val cXSwitch = findTest(allTestResults, mainCamera, CameraAPI.CAMERAX,
+        ImageCaptureSize.MAX, FocusMode.AUTO, TestType.MULTI_SWITCH)
+
+    // Header
+    val dateFormatter = SimpleDateFormat("d MMM yyyy - kk'h'mm")
+    val cal: Calendar = Calendar.getInstance()
+    output += "DATE: " + dateFormatter.format(cal.time) + " (Antelope " +
+        getVersionName(activity) + ")\n"
+
+    output += "DEVICE: " + MainActivity.deviceInfo.device + "\n\n"
+    output += "CAMERAS:\n"
+    for (camera in MainActivity.cameras)
+        output += camera + "\n"
+    output += "\n"
+
+    // Test summary
+    output += "HIGH-LEVEL OVERVIEW:\n"
+
+    output += "Capture (Cam2): " + meanOfSumOfTwoArrays(c2Auto.capture, c2Auto.imageready) +
+        ", Cap chained (Cam2): " +
+        meanOfSumOfTwoArrays(c2AutoChain.capture, c2AutoChain.imageready) +
+        "\nCapture CAF (Cam2): " + meanOfSumOfTwoArrays(c2Caf.capture, c2Caf.imageready) +
+        ", Chained CAF (Cam2): " +
+        meanOfSumOfTwoArrays(c2CafChain.capture, c2CafChain.imageready) +
+        "\nCapture (Cam1): " + meanOfSumOfTwoArrays(c1Auto.capture, c1Auto.imageready) +
+        ", Cap CAF (Cam1): " + meanOfSumOfTwoArrays(c1Caf.capture, c1Caf.imageready) +
+        "\nCapture (CamX): " + meanOfSumOfTwoArrays(cXAuto.capture, cXAuto.imageready) +
+        ", Cap CAF (CamX): " + meanOfSumOfTwoArrays(cXCaf.capture, cXCaf.imageready) +
+        "\nSwitch 1->2 (Cam2): " +
+        mean(c2Switch.switchToSecond) + ", Switch 1->2 (Cam1): " + mean(c1Switch.switchToSecond) +
+        ", Switch 1->2 (CamX): " + mean(cXSwitch.switchToSecond) +
+        ", Switch 2->1 (Cam2): " +
+        mean(c2Switch.switchToFirst) + ", Switch 2->1 (Cam1): " + mean(c1Switch.switchToFirst) +
+        ", Switch 2->1 (CamX): " + mean(cXSwitch.switchToFirst) +
+        "\nCam2 Open: " + meanOfSumOfTwoArrays(c2Auto.initialization, c2Auto.previewStart) +
+        ", Cam1 Open: " + meanOfSumOfTwoArrays(c1Auto.initialization, c1Auto.previewStart) +
+        "\nCam2 Close: " + meanOfSumOfTwoArrays(c2Auto.previewClose, c2Auto.cameraClose) +
+        ", Cam1 Close: " + meanOfSumOfTwoArrays(c1Auto.previewClose, c1Auto.cameraClose) +
+        "\n∆ Min to Max Size: " + (numericalMean(c2Auto.capture) +
+        numericalMean(c2Auto.imageready) - numericalMean(c2AutoMin.capture) -
+        numericalMean(c2AutoMin.imageready)) +
+        ", Init->Image saved (Cam2): " + mean(c2Auto.totalNoPreview) +
+        "\n"
+
+    output += "\n"
+
+    return output
+}
+
+/**
+ * For an array of TestResults, generate a high-level summary of the most important values in a
+ * comma-separated .csv string.
+ *
+ * This mostly consists of values for default rear camera.
+ */
+fun testSummaryCSV(activity: MainActivity, allTestResults: ArrayList<TestResults>): String {
+    var output = ""
+
+    if (allTestResults.isEmpty())
+        return output
+
+    val mainCamera = getMainCamera(activity, allTestResults)
+
+    val c2Auto = findTest(allTestResults, mainCamera, CameraAPI.CAMERA2,
+        ImageCaptureSize.MAX, FocusMode.AUTO, TestType.MULTI_PHOTO)
+    val c2AutoChain = findTest(allTestResults, mainCamera, CameraAPI.CAMERA2,
+        ImageCaptureSize.MAX, FocusMode.AUTO, TestType.MULTI_PHOTO_CHAIN)
+    val c2Caf = findTest(allTestResults, mainCamera, CameraAPI.CAMERA2,
+        ImageCaptureSize.MAX, FocusMode.CONTINUOUS, TestType.MULTI_PHOTO)
+    val c2CafChain = findTest(allTestResults, mainCamera, CameraAPI.CAMERA2,
+        ImageCaptureSize.MAX, FocusMode.CONTINUOUS, TestType.MULTI_PHOTO_CHAIN)
+    val c2AutoMin = findTest(allTestResults, mainCamera, CameraAPI.CAMERA2,
+        ImageCaptureSize.MIN, FocusMode.AUTO, TestType.MULTI_PHOTO)
+    val c2Switch = findTest(allTestResults, mainCamera, CameraAPI.CAMERA2,
+        ImageCaptureSize.MAX, FocusMode.AUTO, TestType.MULTI_SWITCH)
+    val c1Auto = findTest(allTestResults, mainCamera, CameraAPI.CAMERA1,
+        ImageCaptureSize.MAX, FocusMode.AUTO, TestType.MULTI_PHOTO)
+    val c1Caf = findTest(allTestResults, mainCamera, CameraAPI.CAMERA1,
+        ImageCaptureSize.MAX, FocusMode.CONTINUOUS, TestType.MULTI_PHOTO)
+    val c1Switch = findTest(allTestResults, mainCamera, CameraAPI.CAMERA1,
+        ImageCaptureSize.MAX, FocusMode.AUTO, TestType.MULTI_SWITCH)
+    val cXAuto = findTest(allTestResults, mainCamera, CameraAPI.CAMERAX,
+        ImageCaptureSize.MAX, FocusMode.AUTO, TestType.MULTI_PHOTO)
+    val cXCaf = findTest(allTestResults, mainCamera, CameraAPI.CAMERAX,
+        ImageCaptureSize.MAX, FocusMode.CONTINUOUS, TestType.MULTI_PHOTO)
+    val cXSwitch = findTest(allTestResults, mainCamera, CameraAPI.CAMERAX,
+        ImageCaptureSize.MAX, FocusMode.AUTO, TestType.MULTI_SWITCH)
+
+    // Header
+    val dateFormatter = SimpleDateFormat("d MMM yyyy - kk'h'mm")
+    val cal: Calendar = Calendar.getInstance()
+    output += "DATE: " + dateFormatter.format(cal.time) + " (Antelope " +
+        getVersionName(activity) + ")" + "\n"
+
+    output += "DEVICE: " + MainActivity.deviceInfo.device + "\n" + "\n"
+    output += "CAMERAS: " + "\n"
+    for (camera in MainActivity.cameras)
+        output += camera + "\n"
+    output += "\n"
+
+    // Test summary
+    output += "HIGH-LEVEL OVERVIEW:\n"
+
+    output += ","
+    output += "Capture (Cam2)" + "," + "Cap chained (Cam2)" + "," +
+        "Capture CAF (Cam2)" + "," + "Chained CAF (Cam2)" + "," +
+        "Capture (Cam1)" + "," + "Cap CAF (Cam1)" + "," +
+        "Capture (CamX)" + "," + "Cap CAF (CamX)" + "," +
+        "Switch 1->2 (Cam2)" + "," + "Switch 1->2 (Cam1)" + "," + "Switch 1->2 (CamX)" + "," +
+        "Switch 2->1 (Cam2)" + "," + "Switch 2->1 (Cam1)" + "," + "Switch 2->1 (CamX)" + "," +
+        "Cam2 Open" + "," + "Cam1 Open" + "," +
+        "Cam2 Close" + "," + "Cam1 Close" + "," +
+        "∆ Min to Max Size" + "," +
+        "Init->Image saved (Cam2)" +
+        "\n"
+
+    output += ","
+    output += "" + meanOfSumOfTwoArrays(c2Auto.capture, c2Auto.imageready) + "," +
+        meanOfSumOfTwoArrays(c2AutoChain.capture, c2AutoChain.imageready)
+    output += "," + meanOfSumOfTwoArrays(c2Caf.capture, c2Caf.imageready) + "," +
+        meanOfSumOfTwoArrays(c2CafChain.capture, c2CafChain.imageready)
+    output += "," + meanOfSumOfTwoArrays(c1Auto.capture, c1Auto.imageready) + "," +
+        meanOfSumOfTwoArrays(c1Caf.capture, c1Caf.imageready)
+    output += "," + meanOfSumOfTwoArrays(cXAuto.capture, cXAuto.imageready) + "," +
+        meanOfSumOfTwoArrays(cXCaf.capture, cXCaf.imageready)
+    output += "," + mean(c2Switch.switchToSecond) + "," + mean(c1Switch.switchToSecond)
+    output += "," + mean(cXSwitch.switchToSecond)
+    output += "," + mean(c2Switch.switchToFirst) + "," + mean(c1Switch.switchToFirst)
+    output += "," + mean(cXSwitch.switchToFirst)
+    output += "," + meanOfSumOfTwoArrays(c2Auto.initialization, c2Auto.previewStart) + "," +
+        meanOfSumOfTwoArrays(c1Auto.initialization, c1Auto.previewStart)
+    output += "," + meanOfSumOfTwoArrays(c2Auto.previewClose, c2Auto.cameraClose) + "," +
+        meanOfSumOfTwoArrays(c1Auto.previewClose, c1Auto.cameraClose)
+    output += "," + (numericalMean(c2Auto.capture) + numericalMean(c2Auto.imageready) -
+        numericalMean(c2AutoMin.capture) - numericalMean(c2AutoMin.imageready))
+    output += "," + mean(c2Auto.totalNoPreview) + "\n"
+
+    output += "\n"
+    return output
+}
+
+/**
+ * Search an array of TestResults for the first test result that matches the given parameters
+ */
+fun findTest(
+    allTestResults: ArrayList<TestResults>,
+    camera: String,
+    api: CameraAPI,
+    imageCaptureSize: ImageCaptureSize,
+    focusMode: FocusMode,
+    testType: TestType
+): TestResults {
+
+    for (testResult in allTestResults) {
+        // Look for the matching test result
+        if (testResult.camera.equals(camera) &&
+            testResult.cameraAPI == api &&
+            testResult.imageCaptureSize == imageCaptureSize &&
+            (testResult.focusMode == focusMode || testResult.focusMode == FocusMode.FIXED) &&
+            testResult.testType == testType) {
+            return testResult
+        }
+    }
+
+    // Return empty test result
+    return TestResults()
+}
+
+/**
+ * The mean of a given array of longs, as a string
+ */
+fun mean(array: ArrayList<Long>): String {
+    if (array.isEmpty()) {
+        return "n/a"
+    } else
+        return Stats.meanOf(array).roundToInt().toString()
+}
+
+/**
+ * The mean of a given array of longs, as a double
+ */
+fun numericalMean(array: ArrayList<Long>): Double {
+    if (array.isEmpty())
+        return 0.0
+    else
+        return Stats.meanOf(array)
+}
+
+/**
+ * The mean of two arrays of longs added together, as a string
+ */
+fun meanOfSumOfTwoArrays(array1: ArrayList<Long>, array2: ArrayList<Long>): String {
+    if (array1.isEmpty() && array2.isEmpty()) {
+        return "n/a"
+    }
+    if (array1.isEmpty())
+        return mean(array2)
+    if (array2.isEmpty())
+        return mean(array1)
+    else
+        return (Stats.meanOf(array1) + Stats.meanOf(array2)).roundToInt().toString()
+}
+
+/**
+ * Find the "main" camera id, priority is: first rear facing physical, first rear-facing logical,
+ * first camera in the system.
+ */
+fun getMainCamera(activity: MainActivity, allTestResults: ArrayList<TestResults>): String {
+    val mainCamera = allTestResults.first().cameraId
+
+    // Return the first rear-facing camera
+    for (param in MainActivity.cameraParams) {
+        if (!param.value.isFront && !param.value.isExternal)
+
+        // If only logical cameras, first rear-facing is fine
+            if (PrefHelper.getOnlyLogical(activity)) {
+                logd("The MAIN camera id is:" + param.value.id)
+                return param.value.id
+
+                // Otherwise, make sure this is a physical camera
+            } else {
+                if (param.value.physicalCameras.contains(param.value.id)) {
+                    logd("The MAIN camera id is:" + param.value.id)
+                    return param.value.id
+                }
+            }
+    }
+
+    return mainCamera
+}
+
+/**
+ * Return the version name of the Activity
+ */
+fun getVersionName(activity: MainActivity): String {
+    val packageInfo = activity.packageManager.getPackageInfo(activity.packageName, 0)
+    return packageInfo.versionName
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TimingTests.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TimingTests.kt
new file mode 100644
index 0000000..b16d784
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/TimingTests.kt
@@ -0,0 +1,593 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope
+
+import androidx.camera.integration.antelope.MainActivity.Companion.cameraParams
+import androidx.camera.integration.antelope.MainActivity.Companion.logd
+import androidx.camera.integration.antelope.cameracontrollers.camera1OpenCamera
+import androidx.camera.integration.antelope.cameracontrollers.camera2OpenCamera
+import androidx.camera.integration.antelope.cameracontrollers.cameraXOpenCamera
+import androidx.camera.integration.antelope.cameracontrollers.cameraXTakePicture
+import androidx.camera.integration.antelope.cameracontrollers.closeAllCameras
+import androidx.camera.integration.antelope.cameracontrollers.closePreviewAndCamera
+import androidx.camera.integration.antelope.cameracontrollers.initializeStillCapture
+
+// Keeps track of what iteration of a repeated test is occurring
+internal var multiCounter: Int = 0
+
+internal fun initializeTest(
+    activity: MainActivity,
+    params: CameraParams?,
+    config: TestConfig
+) {
+
+    if (null == params)
+        return
+
+    // Camera1 cannot directly access physical cameras. If we try, abort.
+    if ((CameraAPI.CAMERA1 == config.api) &&
+        !(PrefHelper.getLogicalCameraIds(activity, cameraParams).contains(config.camera))) {
+        activity.resetUIAfterTest()
+        activity.updateLog("ABORTED: Camera1 API cannot access camera with id:" + config.camera,
+            false, false)
+        return
+    }
+
+    when (config.currentRunningTest) {
+        TestType.INIT -> runInitTest(activity, params, config)
+        TestType.PREVIEW ->
+            runPreviewTest(activity, params, config)
+        TestType.SWITCH_CAMERA ->
+            runSwitchTest(activity, params, config)
+        TestType.MULTI_SWITCH ->
+            runMultiSwitchTest(activity, params, config)
+        TestType.PHOTO ->
+            runPhotoTest(activity, params, config)
+        TestType.MULTI_PHOTO ->
+            runMultiPhotoTest(activity, params, config)
+        TestType.MULTI_PHOTO_CHAIN ->
+            runMultiPhotoChainTest(activity, params, config)
+        TestType.NONE -> Unit
+    }
+}
+
+/**
+ * Run the INIT test
+ */
+internal fun runInitTest(
+    activity: MainActivity,
+    params: CameraParams,
+    config: TestConfig
+) {
+
+    logd("Running init test")
+    activity.startBackgroundThread(params)
+    activity.showProgressBar(true)
+
+    closeAllCameras(activity, config)
+
+    setupImageReader(activity, params, config)
+    params.timer = CameraTimer()
+    config.currentRunningTest = TestType.INIT
+    params.timer.testStart = System.currentTimeMillis()
+    beginTest(activity, params, config)
+}
+
+/**
+ * Run the SWITCH test
+ */
+internal fun runSwitchTest(activity: MainActivity, params: CameraParams, config: TestConfig) {
+    // For switch test, always go from default back camera to default front camera and back 0->1->0
+    // TODO: Can we handle different permutations of physical cameras?
+    if (!PrefHelper.getLogicalCameraIds(activity, cameraParams).contains("0") ||
+        !PrefHelper.getLogicalCameraIds(activity, cameraParams).contains("1")) {
+        activity.resetUIAfterTest()
+        activity.updateLog("ABORTED: Camera 0 and 1 needed for Switch test.",
+            false, false)
+        return
+    }
+
+    config.switchTestCameras = arrayOf("0", "1")
+    config.switchTestCurrentCamera = "0"
+
+    logd("Running switch test")
+    logd("Starting with camera: " + config.switchTestCurrentCamera)
+    activity.startBackgroundThread(params)
+    activity.showProgressBar(true)
+
+    closeAllCameras(activity, config)
+
+    setupImageReader(activity, params, config)
+    params.timer = CameraTimer()
+    config.currentRunningTest = TestType.SWITCH_CAMERA
+    params.timer.testStart = System.currentTimeMillis()
+    beginTest(activity, params, config)
+}
+
+/**
+ * Run the MULTI_SWITCH test
+ */
+internal fun runMultiSwitchTest(activity: MainActivity, params: CameraParams, config: TestConfig) {
+    // For switch test, always go from default back camera to default front camera and back 0->1->0
+    // TODO: Can we handle different permutations of physical cameras?
+    if (!PrefHelper.getLogicalCameraIds(activity, cameraParams).contains("0") ||
+        !PrefHelper.getLogicalCameraIds(activity, cameraParams).contains("1")) {
+        activity.resetUIAfterTest()
+        activity.updateLog("ABORTED: Camera 0 and 1 needed for Switch test.",
+            false, false)
+        return
+    }
+
+    config.switchTestCameras = arrayOf("0", "1")
+
+    if (0 == multiCounter) {
+        // New test
+        logd("Running multi switch test")
+        config.switchTestCurrentCamera = "0"
+        activity.startBackgroundThread(params)
+        activity.showProgressBar(true, 0)
+        multiCounter = PrefHelper.getNumTests(activity)
+        config.currentRunningTest = TestType.MULTI_SWITCH
+        config.testFinished = false
+    } else {
+        // Add previous result
+        params.timer.testEnd = System.currentTimeMillis()
+        activity.showProgressBar(true, precentageCompleted(activity, multiCounter))
+        logd("In Multi Switch Test. Counter: " + multiCounter)
+
+        config.testResults.initialization.add(params.timer.openEnd - params.timer.openStart)
+        config.testResults.previewStart.add(params.timer.previewEnd - params.timer.previewStart)
+        config.testResults.switchToSecond
+            .add(params.timer.switchToSecondEnd - params.timer.switchToSecondStart)
+        config.testResults.switchToFirst
+            .add(params.timer.switchToFirstEnd - params.timer.switchToFirstStart)
+        config.testResults.previewClose
+            .add(params.timer.previewCloseEnd - params.timer.previewCloseStart)
+        config.testResults.cameraClose
+            .add(params.timer.cameraCloseEnd - params.timer.cameraCloseStart)
+        config.testResults.total.add(params.timer.testEnd - params.timer.testStart)
+
+        config.testFinished = false
+        config.isFirstOnActive = true
+    }
+
+    setupImageReader(activity, params, config)
+    params.timer = CameraTimer()
+    params.timer.testStart = System.currentTimeMillis()
+    beginTest(activity, params, config)
+}
+
+/**
+ * Run the PREVIEW test
+ */
+internal fun runPreviewTest(activity: MainActivity, params: CameraParams, config: TestConfig) {
+    logd("Running preview test")
+    activity.startBackgroundThread(params)
+    activity.showProgressBar(true)
+
+    closeAllCameras(activity, config)
+
+    setupImageReader(activity, params, config)
+    params.timer = CameraTimer()
+    config.currentRunningTest = TestType.PREVIEW
+    params.timer.testStart = System.currentTimeMillis()
+    beginTest(activity, params, config)
+}
+
+/**
+ * Run the PHOTO (single capture) test
+ */
+internal fun runPhotoTest(activity: MainActivity, params: CameraParams, config: TestConfig) {
+    logd("Running photo test")
+    activity.startBackgroundThread(params)
+    activity.showProgressBar(true)
+
+    closeAllCameras(activity, config)
+
+    setupImageReader(activity, params, config)
+    params.timer = CameraTimer()
+    config.currentRunningTest = TestType.PHOTO
+    params.timer.testStart = System.currentTimeMillis()
+
+    logd("About to start photo test. " + config.currentRunningTest.toString())
+    beginTest(activity, params, config)
+}
+
+/**
+ * Run the MULTI_PHOTO (repeated capture) test
+ */
+internal fun runMultiPhotoTest(activity: MainActivity, params: CameraParams, config: TestConfig) {
+    if (0 == multiCounter) {
+        // New test
+        logd("Running multi photo test")
+        activity.startBackgroundThread(params)
+        activity.showProgressBar(true, 0)
+        multiCounter = PrefHelper.getNumTests(activity)
+        config.currentRunningTest = TestType.MULTI_PHOTO
+        logd("About to start multi photo test. multi_counter: " + multiCounter + " and test: " +
+            config.currentRunningTest.toString())
+    } else {
+        // Add previous result
+        params.timer.testEnd = System.currentTimeMillis()
+        activity.showProgressBar(true, precentageCompleted(activity, multiCounter))
+        logd("In Multi Photo Test. Counter: " + multiCounter)
+
+        config.testResults.initialization.add(params.timer.openEnd - params.timer.openStart)
+        config.testResults.previewStart.add(params.timer.previewEnd - params.timer.previewStart)
+        config.testResults.previewFill
+            .add(params.timer.previewFillEnd - params.timer.previewFillStart)
+        config.testResults.autofocus.add(params.timer.autofocusEnd - params.timer.autofocusStart)
+        config.testResults.captureNoAF.add((params.timer.captureEnd - params.timer.captureStart) -
+            (params.timer.autofocusEnd - params.timer.autofocusStart))
+        config.testResults.capture.add(params.timer.captureEnd - params.timer.captureStart)
+        config.testResults.imageready
+            .add(params.timer.imageReaderEnd - params.timer.imageReaderStart)
+        config.testResults.capturePlusImageReady
+            .add((params.timer.captureEnd - params.timer.captureStart) +
+            (params.timer.imageReaderEnd - params.timer.imageReaderStart))
+        config.testResults.imagesave
+            .add(params.timer.imageSaveEnd - params.timer.imageSaveStart)
+        config.testResults.isHDRPlus.add(params.timer.isHDRPlus)
+        config.testResults.previewClose
+            .add(params.timer.previewCloseEnd - params.timer.previewCloseStart)
+        config.testResults.cameraClose
+            .add(params.timer.cameraCloseEnd - params.timer.cameraCloseStart)
+        config.testResults.total.add(params.timer.testEnd - params.timer.testStart)
+        config.testResults.totalNoPreview.add((params.timer.testEnd - params.timer.testStart) -
+            (params.timer.previewFillEnd - params.timer.previewFillStart))
+        config.testFinished = false
+        config.isFirstOnActive = true
+        config.isFirstOnCaptureComplete = true
+    }
+
+    closeAllCameras(activity, config)
+
+    setupImageReader(activity, params, config)
+    params.timer = CameraTimer()
+    params.timer.testStart = System.currentTimeMillis()
+    beginTest(activity, params, config)
+}
+
+/**
+ * Run the MULTI_PHOTO_CHAIN (multiple captures, do not close camera) test
+ */
+internal fun runMultiPhotoChainTest(
+    activity: MainActivity,
+    params: CameraParams,
+    config: TestConfig
+) {
+
+    // Cannot chain with Camera 1, run default test
+    if (CameraAPI.CAMERA1 == config.api) {
+        logd("Cannot run Chain test with Camera 1, running regular multi-test instead")
+        config.currentRunningTest = TestType.MULTI_PHOTO
+        runMultiPhotoTest(activity, params, config)
+        return
+    }
+
+    if (0 == multiCounter) {
+        // New test
+        logd("Running multi photo (chain) test")
+        activity.startBackgroundThread(params)
+        activity.showProgressBar(true, 0)
+        multiCounter = PrefHelper.getNumTests(activity)
+        params.timer = CameraTimer()
+        params.timer.testStart = System.currentTimeMillis()
+        config.currentRunningTest = TestType.MULTI_PHOTO_CHAIN
+        logd("About to start multi chain test. multi_counter: " + multiCounter + " and test: " +
+            config.currentRunningTest.toString())
+
+        closeAllCameras(activity, config)
+
+        setupImageReader(activity, params, config)
+        beginTest(activity, params, config)
+    } else {
+        // Add previous result
+        activity.showProgressBar(true, precentageCompleted(activity, multiCounter))
+
+        if (config.api == CameraAPI.CAMERA1) {
+            beginTest(activity, params, config)
+        } else {
+
+            // Camera2 and CameraX
+            config.testResults.autofocus
+                .add(params.timer.autofocusEnd - params.timer.autofocusStart)
+            config.testResults.captureNoAF
+                .add((params.timer.captureEnd - params.timer.captureStart) -
+                (params.timer.autofocusEnd - params.timer.autofocusStart))
+            config.testResults.capture.add(params.timer.captureEnd - params.timer.captureStart)
+            config.testResults.imageready
+                .add(params.timer.imageReaderEnd - params.timer.imageReaderStart)
+            config.testResults.capturePlusImageReady
+                .add((params.timer.captureEnd - params.timer.captureStart) +
+                (params.timer.imageReaderEnd - params.timer.imageReaderStart))
+            config.testResults.imagesave
+                .add(params.timer.imageSaveEnd - params.timer.imageSaveStart)
+            config.testResults.isHDRPlus.add(params.timer.isHDRPlus)
+            config.testFinished = false
+
+            params.timer.clearImageTimers()
+            config.isFirstOnCaptureComplete = true
+
+            when (config.api) {
+                CameraAPI.CAMERA2 -> initializeStillCapture(activity, params, config)
+                CameraAPI.CAMERAX -> cameraXTakePicture(activity, params, config)
+            }
+        }
+    }
+}
+
+/**
+ * A test has ended. Depending on which test and if we are at the beginning, middle or end of a
+ * repeated test, record the results and repeat/return,
+ */
+internal fun testEnded(activity: MainActivity, params: CameraParams?, config: TestConfig) {
+    if (null == params)
+        return
+    logd("In testEnded. multi_counter: " + multiCounter + " and test: " +
+        config.currentRunningTest.toString())
+
+    when (config.currentRunningTest) {
+
+        TestType.INIT -> {
+            params.timer.testEnd = System.currentTimeMillis()
+            logd("Test ended")
+            config.testResults.initialization.add(params.timer.openEnd - params.timer.openStart)
+            config.testResults.cameraClose
+                .add(params.timer.cameraCloseEnd - params.timer.cameraCloseStart)
+            config.testResults.total.add(params.timer.testEnd - params.timer.testStart)
+        }
+
+        TestType.PREVIEW -> {
+            params.timer.testEnd = System.currentTimeMillis()
+            logd("Test ended")
+            config.testResults.initialization.add(params.timer.openEnd - params.timer.openStart)
+            config.testResults.previewStart.add(params.timer.previewEnd - params.timer.previewStart)
+            config.testResults.previewClose
+                .add(params.timer.previewCloseEnd - params.timer.previewCloseStart)
+            config.testResults.cameraClose
+                .add(params.timer.cameraCloseEnd - params.timer.cameraCloseStart)
+            config.testResults.total.add(params.timer.testEnd - params.timer.testStart)
+        }
+
+        TestType.SWITCH_CAMERA -> {
+            params.timer.testEnd = System.currentTimeMillis()
+            logd("Test ended")
+            config.testResults.initialization.add(params.timer.openEnd - params.timer.openStart)
+            config.testResults.previewStart.add(params.timer.previewEnd - params.timer.previewStart)
+            config.testResults.switchToSecond
+                .add(params.timer.switchToSecondEnd - params.timer.switchToSecondStart)
+            config.testResults.switchToFirst
+                .add(params.timer.switchToFirstEnd - params.timer.switchToFirstStart)
+            config.testResults.previewClose
+                .add(params.timer.previewCloseEnd - params.timer.previewCloseStart)
+            config.testResults.cameraClose
+                .add(params.timer.cameraCloseEnd - params.timer.cameraCloseStart)
+            config.testResults.total.add(params.timer.testEnd - params.timer.testStart)
+        }
+
+        TestType.MULTI_SWITCH -> {
+            val lastResult = params.timer.captureEnd - params.timer.captureStart
+
+            if (1 == multiCounter) {
+                params.timer.testEnd = System.currentTimeMillis()
+                config.testFinished = false // Reset flag
+
+                logd("Test ended")
+                activity.showProgressBar(true, precentageCompleted(activity, multiCounter))
+
+                config.testResults.initialization.add(params.timer.openEnd - params.timer.openStart)
+                config.testResults.previewStart
+                    .add(params.timer.previewEnd - params.timer.previewStart)
+                config.testResults.switchToSecond
+                    .add(params.timer.switchToSecondEnd - params.timer.switchToSecondStart)
+                config.testResults.switchToFirst
+                    .add(params.timer.switchToFirstEnd - params.timer.switchToFirstStart)
+                config.testResults.previewClose
+                    .add(params.timer.previewCloseEnd - params.timer.previewCloseStart)
+                config.testResults.cameraClose
+                    .add(params.timer.cameraCloseEnd - params.timer.cameraCloseStart)
+                config.testResults.total.add(params.timer.testEnd - params.timer.testStart)
+
+                multiCounter = 0
+            } else {
+                logd("Switch " + (Math.abs(multiCounter - PrefHelper.getNumTests(activity)) + 1) +
+                    " completed.")
+                multiCounter--
+                runMultiSwitchTest(activity, params, config)
+                return
+            }
+        }
+
+        TestType.PHOTO -> {
+            params.timer.testEnd = System.currentTimeMillis()
+            logd("Test ended")
+            config.testResults.initialization.add(params.timer.openEnd - params.timer.openStart)
+            config.testResults.previewStart.add(params.timer.previewEnd - params.timer.previewStart)
+            config.testResults.previewFill
+                .add(params.timer.previewFillEnd - params.timer.previewFillStart)
+            config.testResults.autofocus
+                .add(params.timer.autofocusEnd - params.timer.autofocusStart)
+            config.testResults.captureNoAF
+                .add((params.timer.captureEnd - params.timer.captureStart) -
+                (params.timer.autofocusEnd - params.timer.autofocusStart))
+            config.testResults.capture.add(params.timer.captureEnd - params.timer.captureStart)
+            config.testResults.imageready
+                .add(params.timer.imageReaderEnd - params.timer.imageReaderStart)
+            config.testResults.capturePlusImageReady
+                .add((params.timer.captureEnd - params.timer.captureStart) +
+                (params.timer.imageReaderEnd - params.timer.imageReaderStart))
+            config.testResults.imagesave
+                .add(params.timer.imageSaveEnd - params.timer.imageSaveStart)
+            config.testResults.isHDRPlus
+                .add(params.timer.isHDRPlus)
+            config.testResults.previewClose
+                .add(params.timer.previewCloseEnd - params.timer.previewCloseStart)
+            config.testResults.cameraClose
+                .add(params.timer.cameraCloseEnd - params.timer.cameraCloseStart)
+            config.testResults.total.add(params.timer.testEnd - params.timer.testStart)
+            config.testResults.totalNoPreview.add((params.timer.testEnd - params.timer.testStart) -
+                (params.timer.previewFillEnd - params.timer.previewFillStart))
+        }
+
+        TestType.MULTI_PHOTO -> {
+            val lastResult = params.timer.captureEnd - params.timer.captureStart
+
+            if (1 == multiCounter) {
+                params.timer.testEnd = System.currentTimeMillis()
+                config.testFinished = false // Reset flag
+
+                logd("Test ended")
+                activity.showProgressBar(true, precentageCompleted(activity, multiCounter))
+
+                config.testResults.initialization.add(params.timer.openEnd - params.timer.openStart)
+                config.testResults.previewStart
+                    .add(params.timer.previewEnd - params.timer.previewStart)
+                config.testResults.previewFill
+                    .add(params.timer.previewFillEnd - params.timer.previewFillStart)
+                config.testResults.autofocus
+                    .add(params.timer.autofocusEnd - params.timer.autofocusStart)
+                config.testResults.captureNoAF
+                    .add((params.timer.captureEnd - params.timer.captureStart) -
+                    (params.timer.autofocusEnd - params.timer.autofocusStart))
+                config.testResults.capture.add(params.timer.captureEnd - params.timer.captureStart)
+                config.testResults.imageready
+                    .add(params.timer.imageReaderEnd - params.timer.imageReaderStart)
+                config.testResults.capturePlusImageReady
+                    .add((params.timer.captureEnd - params.timer.captureStart) +
+                    (params.timer.imageReaderEnd - params.timer.imageReaderStart))
+                config.testResults.imagesave
+                    .add(params.timer.imageSaveEnd - params.timer.imageSaveStart)
+                config.testResults.isHDRPlus.add(params.timer.isHDRPlus)
+                config.testResults.previewClose
+                    .add(params.timer.previewCloseEnd - params.timer.previewCloseStart)
+                config.testResults.cameraClose
+                    .add(params.timer.cameraCloseEnd - params.timer.cameraCloseStart)
+                config.testResults.total.add(params.timer.testEnd - params.timer.testStart)
+                config.testResults.totalNoPreview
+                    .add((params.timer.testEnd - params.timer.testStart) -
+                    (params.timer.previewFillEnd - params.timer.previewFillStart))
+
+                closeAllCameras(activity, config)
+
+                multiCounter = 0
+            } else {
+                logd("Capture " + (Math.abs(multiCounter - PrefHelper.getNumTests(activity)) + 1) +
+                    " completed: " + lastResult + "ms")
+                multiCounter--
+                runMultiPhotoTest(activity, params, config)
+                return
+            }
+        }
+
+        TestType.MULTI_PHOTO_CHAIN -> {
+            val lastResult = params.timer.captureEnd - params.timer.captureStart
+
+            if (1 == multiCounter) {
+
+                // If this is a chain test, the camera may still be open
+                if (params.isOpen) {
+                    config.testFinished = true
+                    closePreviewAndCamera(activity, params, config)
+                    return
+                }
+
+                params.timer.testEnd = System.currentTimeMillis()
+                logd("Test ended")
+                config.testFinished = false // Reset flag
+
+                activity.showProgressBar(true, precentageCompleted(activity, multiCounter))
+
+                config.testResults.initialization.add(params.timer.openEnd -
+                    params.timer.openStart)
+                config.testResults.previewStart.add(params.timer.previewEnd -
+                    params.timer.previewStart)
+                config.testResults.previewFill.add(params.timer.previewFillEnd -
+                    params.timer.previewFillStart)
+                config.testResults.autofocus.add(params.timer.autofocusEnd -
+                    params.timer.autofocusStart)
+                config.testResults.captureNoAF
+                    .add((params.timer.captureEnd - params.timer.captureStart) -
+                    (params.timer.autofocusEnd - params.timer.autofocusStart))
+                config.testResults.capture.add(params.timer.captureEnd - params.timer.captureStart)
+                config.testResults.imageready.add(params.timer.imageReaderEnd -
+                    params.timer.imageReaderStart)
+                config.testResults.capturePlusImageReady.add((params.timer.captureEnd -
+                    params.timer.captureStart) +
+                    (params.timer.imageReaderEnd - params.timer.imageReaderStart))
+                config.testResults.imagesave.add(params.timer.imageSaveEnd -
+                    params.timer.imageSaveStart)
+                config.testResults.isHDRPlus.add(params.timer.isHDRPlus)
+                config.testResults.previewClose.add(params.timer.previewCloseEnd -
+                    params.timer.previewCloseStart)
+                config.testResults.cameraClose.add(params.timer.cameraCloseEnd -
+                    params.timer.cameraCloseStart)
+                config.testResults.total.add(params.timer.testEnd - params.timer.testStart)
+                config.testResults.totalNoPreview
+                    .add((params.timer.testEnd - params.timer.testStart) -
+                    (params.timer.previewFillEnd - params.timer.previewFillStart))
+
+                closeAllCameras(activity, config)
+
+                multiCounter = 0
+            } else {
+                logd("Capture " + (Math.abs(multiCounter - PrefHelper.getNumTests(activity)) + 1) +
+                    " completed: " + lastResult + "ms")
+                multiCounter--
+                runMultiPhotoChainTest(activity, params, config)
+                return
+            }
+        }
+    }
+
+    multiCounter = 0
+    postTestResults(activity, config)
+}
+
+/**
+ * Calculate the percentage of repeated tests that are complete
+ */
+fun precentageCompleted(activity: MainActivity, testCounter: Int): Int {
+    return (100 * (PrefHelper.getNumTests(activity) - testCounter)) /
+        PrefHelper.getNumTests(activity)
+}
+
+/**
+ * Test is configured, begin it based on the API in the test config
+ */
+internal fun beginTest(activity: MainActivity, params: CameraParams?, testConfig: TestConfig) {
+    if (null == params)
+        return
+
+    when (testConfig.api) {
+        CameraAPI.CAMERA1 -> {
+            // Camera 1 doesn't have its own threading built-in
+            val runnable = Runnable {
+                camera1OpenCamera(activity, params, testConfig)
+            }
+            params.backgroundHandler?.post(runnable)
+        }
+
+        CameraAPI.CAMERA2 -> {
+            camera2OpenCamera(activity, params, testConfig)
+        }
+
+        CameraAPI.CAMERAX -> {
+            cameraXOpenCamera(activity, params, testConfig)
+        }
+    }
+}
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera1Controller.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera1Controller.kt
new file mode 100644
index 0000000..ca8bbfa
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera1Controller.kt
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope.cameracontrollers
+
+import android.hardware.Camera
+import android.util.Size
+import androidx.camera.integration.antelope.CameraParams
+import androidx.camera.integration.antelope.CompareSizesByArea
+import androidx.camera.integration.antelope.FocusMode
+import androidx.camera.integration.antelope.ImageCaptureSize
+import androidx.camera.integration.antelope.MainActivity
+import androidx.camera.integration.antelope.MainActivity.Companion.logd
+import androidx.camera.integration.antelope.PrefHelper
+import androidx.camera.integration.antelope.TestConfig
+import androidx.camera.integration.antelope.TestType
+import androidx.camera.integration.antelope.testEnded
+import androidx.camera.integration.antelope.writeFile
+import java.util.Collections
+
+internal var camera1: Camera? = null
+
+/**
+ * Opens the camera using the Camera1 API and measures the open time synchronously. For init tests,
+ * this is the only measurement needed so end the test. Otherwise, move on to open the camera
+ * preview.
+ */
+fun camera1OpenCamera(activity: MainActivity, params: CameraParams, testConfig: TestConfig) {
+    try {
+        logd("openCamera: " + params.id)
+        params.isOpen = true
+        params.timer.openStart = System.currentTimeMillis()
+
+        logd("Camera1Switch Open camera: " + testConfig.switchTestCurrentCamera.toInt())
+
+        if ((testConfig.currentRunningTest == TestType.SWITCH_CAMERA) ||
+            (testConfig.currentRunningTest == TestType.MULTI_SWITCH))
+            camera1 = Camera.open(testConfig.switchTestCurrentCamera.toInt())
+        else
+            camera1 = Camera.open(testConfig.camera.toInt())
+
+        params.timer.openEnd = System.currentTimeMillis()
+
+        // Due to the synchronous nature of Camera1, set up Camera1 specific parameters here
+        val camera1Params: Camera.Parameters? = camera1?.parameters
+        params.cam1AFSupported =
+            camera1Params?.supportedFocusModes?.contains(Camera.Parameters.FOCUS_MODE_AUTO)
+                ?: false
+
+        when (testConfig.currentRunningTest) {
+            TestType.INIT -> {
+                // Camera opened, we're done
+                testEnded(activity, params, testConfig)
+            }
+
+            else -> {
+                startCamera1Preview(activity, params, testConfig)
+            }
+        }
+    } catch (e: Exception) {
+        logd("camera1OpenCamera exception: " + params.id + ". Error: " + e.printStackTrace())
+        camera1 = null
+    }
+}
+
+/**
+ * Begin the preview using the Camera 1 API and synchronously measure the time to begin the stream.
+ */
+fun startCamera1Preview(activity: MainActivity, params: CameraParams, testConfig: TestConfig) {
+    val camera1Params: Camera.Parameters? = camera1?.parameters
+
+    params.cam1AFSupported =
+        camera1Params?.supportedFocusModes?.contains(Camera.Parameters.FOCUS_MODE_AUTO) ?: false
+
+    // Get Camera1 image capture sizes
+    // Cannot be done this before the camera is device opened
+    val cam1Sizes = camera1Params?.getSupportedPictureSizes()
+    if (null != cam1Sizes) {
+        val saneSizes: ArrayList<Size> = ArrayList()
+
+        for (size in cam1Sizes) {
+            saneSizes.add(Size(size.width, size.height))
+        }
+
+        params.cam1MaxSize = Collections.max(saneSizes, CompareSizesByArea())
+        params.cam1MinSize = Collections.min(saneSizes, CompareSizesByArea())
+    }
+
+    if (ImageCaptureSize.MIN == testConfig.imageCaptureSize)
+        camera1Params?.setPictureSize(params.cam1MinSize.width, params.cam1MinSize.height)
+    else
+        camera1Params?.setPictureSize(params.cam1MaxSize.width, params.cam1MaxSize.height)
+
+    if (params.cam1AFSupported) {
+        if (FocusMode.CONTINUOUS == testConfig.focusMode)
+            camera1Params?.focusMode = Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE
+        else
+            camera1Params?.focusMode = Camera.Parameters.FOCUS_MODE_AUTO
+    } else {
+        camera1Params?.focusMode = Camera.Parameters.FOCUS_MODE_FIXED
+    }
+
+    // After changing camera parameters, set them
+    camera1?.parameters = camera1Params
+
+    logd("startCamera1Preview: starting Camera1 preview.")
+
+    params.isPreviewing = true
+    params.timer.previewStart = System.currentTimeMillis()
+    camera1?.startPreview()
+    camera1?.setPreviewDisplay(params.previewSurfaceView?.holder)
+    params.timer.previewEnd = System.currentTimeMillis()
+
+    when (testConfig.currentRunningTest) {
+        TestType.PREVIEW -> {
+            testConfig.testFinished = true
+            closePreviewAndCamera(activity, params, testConfig)
+        }
+
+        TestType.SWITCH_CAMERA, TestType.MULTI_SWITCH -> {
+            logd("Camera1Switch preview running:")
+            if (testConfig.switchTestCurrentCamera == testConfig.switchTestCameras.get(0)) {
+                if (testConfig.testFinished) {
+                    logd("Camera1Switch preview. On 1st camera, test finished. Closing 1st camera")
+                    params.timer.switchToFirstEnd = System.currentTimeMillis()
+                    Thread.sleep(PrefHelper.getPreviewBuffer(activity)) // Let preview run
+                    closePreviewAndCamera(activity, params, testConfig)
+                } else {
+                    logd("Camera1Switch preview. On 1st camera, Closing 1st camera, then open 2nd")
+                    Thread.sleep(PrefHelper.getPreviewBuffer(activity)) // Let preview run
+                    params.timer.switchToSecondStart = System.currentTimeMillis()
+                    closePreviewAndCamera(activity, params, testConfig)
+                }
+            } else {
+                logd("Camera1Switch preview. On 2nd camera. Closing, ready to open first 1st " +
+                    "camera")
+                params.timer.switchToSecondEnd = System.currentTimeMillis()
+                Thread.sleep(PrefHelper.getPreviewBuffer(activity)) // Let preview run
+                params.timer.switchToFirstStart = System.currentTimeMillis()
+                closePreviewAndCamera(activity, params, testConfig)
+            }
+        }
+
+        TestType.NONE -> {
+            closeAllCameras(activity, testConfig)
+        }
+
+        else -> {
+            camera1TakePicturePrep(activity, params, testConfig)
+        }
+    }
+}
+
+/**
+ * Set up timers and focus mode for taking a picture with the Camera 1 API. If auto-focus is
+ * requested, begin the auto-focus timer and asynchronously begin the auto-focus routine.
+ */
+fun camera1TakePicturePrep(activity: MainActivity, params: CameraParams, testConfig: TestConfig) {
+    if (params.timer.isFirstPhoto) {
+        logd("camera1TakePicturePrep: 1st photo in multi-chain test. Pausing for " +
+            PrefHelper.getPreviewBuffer(activity) + "ms to let preview run.")
+        params.timer.previewFillStart = System.currentTimeMillis()
+        Thread.sleep(PrefHelper.getPreviewBuffer(activity))
+        params.timer.previewFillEnd = System.currentTimeMillis()
+        params.timer.isFirstPhoto = false
+    }
+
+    params.timer.captureStart = System.currentTimeMillis()
+
+    if (params.cam1AFSupported &&
+        FocusMode.AUTO == testConfig.focusMode) {
+        MainActivity.logd("camera1TakePicturePrep: starting autofocus.")
+        params.timer.autofocusStart = System.currentTimeMillis()
+        camera1?.autoFocus(Camera1AutofocusCallback(activity, params, testConfig))
+    } else {
+        camera1TakePicture(activity, params, testConfig)
+    }
+}
+
+/**
+ * Initiate the capture request
+ */
+fun camera1TakePicture(activity: MainActivity, params: CameraParams, testConfig: TestConfig) {
+    val camera1JpegCallback = Camera1PictureCallback(activity, params, testConfig)
+
+    try {
+        MainActivity.logd("camera1TakePicture: capture start. ")
+        camera1?.takePicture(null, null, camera1JpegCallback)
+    } catch (e: RuntimeException) {
+        MainActivity.logd("camera1TakePicture: runtime exception: " + e.printStackTrace())
+    }
+}
+
+/**
+ * Close preview stream and camera device. If this is a switch test, begin the next step
+ */
+fun camera1CloseCamera(activity: MainActivity, params: CameraParams?, testConfig: TestConfig) {
+    if (params == null)
+        return
+
+    if (params.isPreviewing) {
+        params.timer.previewCloseStart = System.currentTimeMillis()
+        camera1?.stopPreview()
+        params.timer.previewCloseEnd = System.currentTimeMillis()
+        params.isPreviewing = false
+    }
+
+    params.timer.cameraCloseStart = System.currentTimeMillis()
+    camera1?.release()
+    params.timer.cameraCloseEnd = System.currentTimeMillis()
+    params.isOpen = false
+
+    logd("Camera 1 Close camera: camera released.")
+
+    if (testConfig.testFinished) {
+        logd("Camera 1 Close camera: Test finished, returning")
+        testEnded(activity, params, testConfig)
+        return
+    }
+
+    when (testConfig.currentRunningTest) {
+        TestType.SWITCH_CAMERA, TestType.MULTI_SWITCH -> {
+            logd("Camera1Switch Close camera")
+            // First camera closed, now start the second
+            if (testConfig.switchTestCurrentCamera == testConfig.switchTestCameras.get(0)) {
+                testConfig.switchTestCurrentCamera = testConfig.switchTestCameras.get(1)
+                logd("Camera1Switch Close camera 1st camera is closed, opening the second")
+                camera1OpenCamera(activity, params, testConfig)
+            }
+
+            // Second camera closed, now start the first
+            else if (testConfig.switchTestCurrentCamera == testConfig.switchTestCameras.get(1)) {
+                logd("Camera1Switch Close camera 2nd camera is closed, opening the first")
+                testConfig.switchTestCurrentCamera = testConfig.switchTestCameras.get(0)
+                testConfig.testFinished = true
+                camera1OpenCamera(activity, params, testConfig)
+            }
+        }
+        else -> {
+            Unit // no-op
+        }
+    }
+}
+
+/**
+ * Auto-focus is complete, record the elapsed time and request the capture
+ */
+class Camera1AutofocusCallback internal constructor(
+    internal val activity: MainActivity,
+    internal val params: CameraParams,
+    internal val testConfig: TestConfig
+) : Camera.AutoFocusCallback {
+
+    override fun onAutoFocus(p0: Boolean, p1: Camera?) {
+        MainActivity.logd("camera1AutofocusCallback: autofocus complete.")
+        params.timer.autofocusEnd = System.currentTimeMillis()
+        camera1TakePicture(activity, params, testConfig)
+    }
+}
+
+/**
+ * Image capture has completed. Record the time taken, synchronously write file to disk and measure
+ * the time required. This test run is finished, call to finalize test or continue the test run.
+ */
+class Camera1PictureCallback internal constructor(
+    internal val activity: MainActivity,
+    internal val params: CameraParams,
+    internal val testConfig: TestConfig
+) : Camera.PictureCallback {
+
+    override fun onPictureTaken(bytes: ByteArray?, p1: Camera?) {
+
+        params.timer.captureEnd = System.currentTimeMillis()
+
+        // With the Camera1 API, calling takePicture() stops the preview. In order to make sure the
+        // close timings are comparable across APIs, restart it here.
+        camera1?.startPreview()
+
+        logd("in Camera1PictureCallback onPictureTaken")
+
+        params.timer.imageReaderStart = System.currentTimeMillis()
+        params.timer.imageReaderEnd = System.currentTimeMillis()
+        params.timer.imageSaveStart = System.currentTimeMillis()
+
+        if (null != bytes)
+            writeFile(activity, bytes)
+
+        params.timer.imageSaveEnd = System.currentTimeMillis()
+
+        testConfig.testFinished = true
+        closePreviewAndCamera(activity, params, testConfig)
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera2CaptureCallback.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera2CaptureCallback.kt
new file mode 100644
index 0000000..84c94ac
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera2CaptureCallback.kt
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope.cameracontrollers
+
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CaptureFailure
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
+import android.view.Surface
+import androidx.annotation.NonNull
+import androidx.camera.integration.antelope.CameraParams
+import androidx.camera.integration.antelope.MainActivity
+import androidx.camera.integration.antelope.TestConfig
+import androidx.camera.integration.antelope.TestType
+import androidx.camera.integration.antelope.testEnded
+
+/**
+ * Image capture callback for Camera 2 API. Tracks state of an image capture request.
+ */
+class Camera2CaptureCallback(
+    internal val activity: MainActivity,
+    internal val params: CameraParams,
+    internal val testConfig: TestConfig
+) : CameraCaptureSession.CaptureCallback() {
+
+    override fun onCaptureSequenceAborted(session: CameraCaptureSession?, sequenceId: Int) {
+        MainActivity.logd("captureStillPicture captureCallback: Sequence aborted. Current test: " +
+            testConfig.currentRunningTest.toString())
+        super.onCaptureSequenceAborted(session, sequenceId)
+    }
+
+    override fun onCaptureFailed(
+        session: CameraCaptureSession?,
+        request: CaptureRequest?,
+        failure: CaptureFailure?
+    ) {
+
+        if (!params.isOpen) {
+            return
+        }
+
+        MainActivity.logd("captureStillPicture captureCallback: Capture Failed. Failure: " +
+            failure?.reason + " Current test: " + testConfig.currentRunningTest.toString())
+
+        // The session failed. Let's just try again (yay infinite loops)
+        closePreviewAndCamera(activity, params, testConfig)
+        camera2OpenCamera(activity, params, testConfig)
+        super.onCaptureFailed(session, request, failure)
+    }
+
+    override fun onCaptureStarted(
+        session: CameraCaptureSession?,
+        request: CaptureRequest?,
+        timestamp: Long,
+        frameNumber: Long
+    ) {
+        MainActivity.logd("captureStillPicture captureCallback: Capture Started. Current test: " +
+            testConfig.currentRunningTest.toString() + ", frame number: " + frameNumber)
+        super.onCaptureStarted(session, request, timestamp, frameNumber)
+    }
+
+    override fun onCaptureProgressed(
+        session: CameraCaptureSession?,
+        request: CaptureRequest?,
+        partialResult: CaptureResult?
+    ) {
+        MainActivity.logd("captureStillPicture captureCallback: Capture progressed. " +
+            "Current test: " + testConfig.currentRunningTest.toString())
+        super.onCaptureProgressed(session, request, partialResult)
+    }
+
+    override fun onCaptureBufferLost(
+        session: CameraCaptureSession?,
+        request: CaptureRequest?,
+        target: Surface?,
+        frameNumber: Long
+    ) {
+        MainActivity.logd("captureStillPicture captureCallback: Buffer lost. Current test: " +
+            testConfig.currentRunningTest.toString())
+        super.onCaptureBufferLost(session, request, target, frameNumber)
+    }
+
+    override fun onCaptureCompleted(
+        @NonNull session: CameraCaptureSession,
+        @NonNull request: CaptureRequest,
+        @NonNull result: TotalCaptureResult
+    ) {
+
+        if (!params.isOpen) {
+            return
+        }
+
+        MainActivity.logd("Camera2 onCaptureCompleted. CaptureEnd. Current test: " +
+            testConfig.currentRunningTest.toString())
+
+        params.timer.captureEnd = System.currentTimeMillis()
+
+        params.captureRequestBuilder?.removeTarget(params.imageReader?.surface)
+
+        // ImageReader might get the image before this callback is called, if so, the test is done
+        if (0L != params.timer.imageSaveEnd) {
+            params.timer.imageReaderStart = params.timer.imageReaderEnd // No ImageReader delay
+            MainActivity.logd("Camera2 onCaptureCompleted. Image already saved. " +
+                "Ending Test and closing camera.")
+
+            if (TestType.MULTI_PHOTO_CHAIN == testConfig.currentRunningTest) {
+                testEnded(activity, params, testConfig)
+            } else {
+                testConfig.testFinished = true
+                closePreviewAndCamera(activity, params, testConfig)
+            }
+
+            // Otherwise the test isn't done until the image appears in the reader
+        } else {
+            MainActivity.logd("Camera2 onCaptureCompleted. Still waiting on imageReader.")
+            params.timer.imageReaderStart = System.currentTimeMillis()
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera2CaptureSessionCallback.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera2CaptureSessionCallback.kt
new file mode 100644
index 0000000..458eaa2
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera2CaptureSessionCallback.kt
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope.cameracontrollers
+
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CaptureFailure
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
+import androidx.annotation.NonNull
+import androidx.camera.integration.antelope.CameraParams
+import androidx.camera.integration.antelope.MainActivity
+import androidx.camera.integration.antelope.TestConfig
+
+/**
+ * Callbacks that track an image capture session, including progress of auto-focus and
+ * auto-exposure routines.
+ *
+ * In general these callbacks encompass the intermediate states of the camera after a preview stream
+ * is running but before an actual image capture is performed.
+ */
+class Camera2CaptureSessionCallback(
+    internal val activity: MainActivity,
+    internal var params: CameraParams,
+    internal var testConfig: TestConfig
+) : CameraCaptureSession.CaptureCallback() {
+
+    override fun onCaptureSequenceCompleted(
+        session: CameraCaptureSession?,
+        sequenceId: Int,
+        frameNumber: Long
+    ) {
+        MainActivity.logd("Camera2CaptureSessionCallback : Capture sequence COMPLETED")
+        super.onCaptureSequenceCompleted(session, sequenceId, frameNumber)
+    }
+
+    override fun onCaptureSequenceAborted(session: CameraCaptureSession?, sequenceId: Int) {
+        MainActivity.logd("Camera2CaptureSessionCallback : Capture sequence ABORTED")
+        super.onCaptureSequenceAborted(session, sequenceId)
+    }
+
+    override fun onCaptureFailed(
+        session: CameraCaptureSession?,
+        request: CaptureRequest?,
+        failure: CaptureFailure?
+    ) {
+        MainActivity.logd("Camera2CaptureSessionCallback : Capture sequence FAILED - " +
+            failure?.reason)
+
+        if (!params.isOpen) {
+            return
+        }
+
+        // There has been a device failure, a restart might help
+        closePreviewAndCamera(activity, params, testConfig)
+        camera2OpenCamera(activity, params, testConfig)
+    }
+
+    private fun process(result: CaptureResult) {
+        // MainActivity.logd("Camera2CaptureSessionCallback in process")
+        if (!params.isOpen) {
+            return
+        }
+
+        when (params.state) {
+            // Preview is running normally. Nothing to do
+            CameraState.PREVIEW_RUNNING -> {
+                // MainActivity.logd("Camera2CaptureSessionCallback : PREVIEW_RUNNING
+            }
+
+            // We are waiting for AF and AE to converge, check if this has happened
+            CameraState.WAITING_FOCUS_LOCK -> {
+                val afState = result.get(CaptureResult.CONTROL_AF_STATE)
+                MainActivity.logd("Camera2CaptureSessionCallback: STATE_WAITING_LOCK, afstate == " +
+                    afState + ", frame number: " + result.frameNumber)
+
+                when (afState) {
+                    null -> {
+                        MainActivity.logd("Camera2CaptureSessionCallback: STATE_WAITING_LOCK, " +
+                            "afState == null, Calling captureStillPicture!")
+                        params.state = CameraState.IMAGE_REQUESTED
+                        captureStillPicture(activity, params, testConfig)
+                    }
+
+                    // Waiting for a focus lock but the AF mechanism is inactive. Hopefully after
+                    // waiting a few frames it will start up
+                    CaptureResult.CONTROL_AF_STATE_INACTIVE -> {
+                        // CONTROL_AF_STATE_INACTIVE should be a short-lived state (2-3 frames). On
+                        // some devices this can be longer or get stuck indefinitely. If AF has not
+                        // started after 50 frames, just run the capture.
+                        if (params.autoFocusStuckCounter++ > 50) {
+                            MainActivity.logd("Camera2CaptureSessionCallback : " +
+                                "STATE_WAITING_LOCK, AF is stuck! Calling captureStillPicture!")
+                            params.state = CameraState.IMAGE_REQUESTED
+                            captureStillPicture(activity, params, testConfig)
+                        }
+                    }
+
+                    CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED,
+                    CaptureResult.CONTROL_AF_STATE_PASSIVE_FOCUSED,
+                    CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED -> {
+                        // AF is locked, check AE. Note CONTROL_AE_STATE can be null on some devices
+                        val aeState = result.get(CaptureResult.CONTROL_AE_STATE)
+                        if (aeState == null ||
+                            aeState == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
+                            MainActivity.logd("Camera2CaptureSessionCallback : " +
+                                "STATE_WAITING_LOCK, AF and AE converged! " +
+                                "Calling captureStillPicture!")
+                            params.state = CameraState.IMAGE_REQUESTED
+                            captureStillPicture(activity, params, testConfig)
+                        } else {
+                            // AF is locked but not AE
+                            runPrecaptureSequence(activity, params, testConfig)
+                        }
+                    }
+
+                    CaptureResult.CONTROL_AF_STATE_PASSIVE_SCAN,
+                    CaptureResult.CONTROL_AF_STATE_PASSIVE_UNFOCUSED,
+                    CaptureResult.CONTROL_AF_STATE_ACTIVE_SCAN -> {
+                        Unit // no-op, keep waiting for lock
+                    }
+                }
+            }
+
+            // Waiting on AE metering
+            CameraState.WAITING_EXPOSURE_LOCK -> {
+                val aeState = result.get(CaptureResult.CONTROL_AE_STATE)
+
+                // No aeState on this device, just do the capture
+                if (aeState == null) {
+                    MainActivity.logd("Camera2CaptureSessionCallback : STATE_WAITING_PRECAPTURE, " +
+                        "aeState == null, Calling captureStillPicture!")
+                    params.state = CameraState.IMAGE_REQUESTED
+                    captureStillPicture(activity, params, testConfig)
+                } else when (aeState) {
+                    // Still metering, keep waiting
+                    CaptureResult.CONTROL_AE_STATE_PRECAPTURE,
+                    CaptureResult.CONTROL_AE_STATE_SEARCHING
+                    -> Unit // no-op
+
+                    // AE converged, double check AF is good
+                    CaptureResult.CONTROL_AE_STATE_CONVERGED,
+                    CaptureResult.CONTROL_AE_STATE_LOCKED
+                    -> params.state = CameraState.WAITING_FOCUS_LOCK
+
+                    // If we need a flash, begin the capture
+                    // If AE is INACTIVE, it's in an unusual state (AF locked but AE starting up),
+                    // just do the capture to avoid getting stuck.
+                    CaptureResult.CONTROL_AE_STATE_FLASH_REQUIRED,
+                    CaptureResult.CONTROL_AE_STATE_INACTIVE -> {
+                        MainActivity.logd("Camera2CaptureSessionCallback : " +
+                            "STATE_WAITING_PRECAPTURE, aeState: " + aeState +
+                            ", AE stuck or needs flash, calling captureStillPicture!")
+                        params.state = CameraState.IMAGE_REQUESTED
+                        captureStillPicture(activity, params, testConfig)
+                    }
+                }
+            }
+        }
+    }
+
+    /** Unused but retained here as it can be useful for debugging  */
+    override fun onCaptureStarted(
+        session: CameraCaptureSession,
+        request: CaptureRequest,
+        timestamp: Long,
+        frameNumber: Long
+    ) {
+        // MainActivity.logd("Camera2CaptureSessionCallback captureCallback: Capture Started.")
+        super.onCaptureStarted(session, request, timestamp, frameNumber)
+    }
+
+    /** Both onCaptureProgressed and onCaptureComplete call through to the processing function */
+    override fun onCaptureProgressed(
+        @NonNull session: CameraCaptureSession,
+        @NonNull request: CaptureRequest,
+        @NonNull partialResult: CaptureResult
+    ) {
+        // MainActivity.logd("Camera2CaptureSessionCallback captureCallback: onCaptureProgressed, " +
+        // "partial result frame number: " + partialResult.frameNumber)
+        process(partialResult)
+    }
+
+    /** Both onCaptureProgressed and onCaptureComplete call through to the processing function */
+    override fun onCaptureCompleted(
+        @NonNull session: CameraCaptureSession,
+        @NonNull request: CaptureRequest,
+        @NonNull result: TotalCaptureResult
+    ) {
+        // MainActivity.logd("Camera2CaptureSessionCallback captureCallback: onCaptureCompleted." +
+        // " Total result frame number: " + result.frameNumber)
+        process(result)
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera2Controller.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera2Controller.kt
new file mode 100644
index 0000000..9721216
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera2Controller.kt
@@ -0,0 +1,306 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope.cameracontrollers
+
+import android.content.Context
+import android.hardware.camera2.CameraAccessException
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CameraManager
+import android.hardware.camera2.CameraMetadata
+import android.hardware.camera2.CaptureRequest
+import androidx.camera.integration.antelope.CameraParams
+import androidx.camera.integration.antelope.FocusMode
+import androidx.camera.integration.antelope.MainActivity
+import androidx.camera.integration.antelope.MainActivity.Companion.logd
+import androidx.camera.integration.antelope.PrefHelper
+import androidx.camera.integration.antelope.TestConfig
+import androidx.camera.integration.antelope.TestType
+import androidx.camera.integration.antelope.getOrientation
+import androidx.camera.integration.antelope.setAutoFlash
+import java.lang.Thread.sleep
+import java.util.Arrays
+
+/** State of the camera during an image capture - */
+internal enum class CameraState {
+    UNINITIALIZED,
+    PREVIEW_RUNNING,
+    WAITING_FOCUS_LOCK,
+    WAITING_EXPOSURE_LOCK,
+    IMAGE_REQUESTED
+}
+
+/**
+ * Opens the camera using the Camera 2 API and starts the open counter. The open call will complete
+ * in the DeviceStateCallback asynchronously. For switch tests, the camera id will be swizzling so
+ * the original camera id is saved.
+ */
+fun camera2OpenCamera(activity: MainActivity, params: CameraParams?, testConfig: TestConfig) {
+    if (null == params)
+        return
+
+    val manager = activity.getSystemService(Context.CAMERA_SERVICE) as CameraManager
+    try {
+        // TODO make the switch test methodology more robust and handle physical cameras
+        if ((testConfig.currentRunningTest == TestType.SWITCH_CAMERA) ||
+            (testConfig.currentRunningTest == TestType.MULTI_SWITCH)) {
+            testConfig.switchTestRealCameraId = params.id // Save the original camera ID
+            params.id = testConfig.switchTestCurrentCamera
+        }
+
+        // Might be a new test, update callbacks to match the test config
+        params.camera2DeviceStateCallback = Camera2DeviceStateCallback(params, activity, testConfig)
+        params.camera2CaptureSessionCallback =
+            Camera2CaptureSessionCallback(activity, params, testConfig)
+
+        params.timer.openStart = System.currentTimeMillis()
+        logd("openCamera: " + params.id + " running test: " +
+            testConfig.currentRunningTest.toString())
+
+        manager.openCamera(params.id, params.camera2DeviceStateCallback, params.backgroundHandler)
+    } catch (e: CameraAccessException) {
+        logd("openCamera CameraAccessException: " + params.id)
+        e.printStackTrace()
+    } catch (e: SecurityException) {
+        logd("openCamera SecurityException: " + params.id)
+        e.printStackTrace()
+    }
+}
+
+/**
+ * Setup the camera preview session and output surface.
+ */
+fun createCameraPreviewSession(
+    activity: MainActivity,
+    params: CameraParams,
+    testConfig: TestConfig
+) {
+
+    logd("In createCameraPreviewSession.")
+    if (!params.isOpen) {
+        return
+    }
+
+    try {
+        val surface = params.previewSurfaceView?.holder?.surface
+        if (null == surface)
+            return
+
+        val imageSurface = params.imageReader?.surface
+        if (null == imageSurface)
+            return
+
+        params.captureRequestBuilder =
+            params.device?.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
+
+        params.captureRequestBuilder?.removeTarget(params.previewSurfaceView?.holder?.surface)
+        params.captureRequestBuilder?.addTarget(surface)
+
+        params.device?.createCaptureSession(Arrays.asList(surface, imageSurface),
+            Camera2PreviewSessionStateCallback(activity, params, testConfig), null)
+    } catch (e: CameraAccessException) {
+        MainActivity.logd("createCameraPreviewSession CameraAccessException: " + e.message)
+        e.printStackTrace()
+    } catch (e: IllegalStateException) {
+        MainActivity.logd("createCameraPreviewSession IllegalStateException: " + e.message)
+        e.printStackTrace()
+    }
+}
+
+/**
+ * Set up timers for a still capture. The preview stream is allowed to run here in order to fill the
+ * pipeline with images to simulate more realistic camera conditions.
+ */
+fun initializeStillCapture(activity: MainActivity, params: CameraParams, testConfig: TestConfig) {
+    logd("TakePicture: capture start.")
+
+    if (!params.isOpen) {
+        return
+    }
+
+    if (params.timer.isFirstPhoto) {
+        params.timer.isFirstPhoto = false
+    }
+
+    logd("Camera2 initializeStillCapture: 1st photo in a multi-photo test. " +
+        "Pausing for " + PrefHelper.getPreviewBuffer(activity) + "ms to let preview run.")
+    params.timer.previewFillStart = System.currentTimeMillis()
+    sleep(PrefHelper.getPreviewBuffer(activity))
+    params.timer.previewFillEnd = System.currentTimeMillis()
+
+    params.timer.captureStart = System.currentTimeMillis()
+    params.timer.autofocusStart = System.currentTimeMillis()
+    lockFocus(activity, params, testConfig)
+}
+
+/**
+ * Initiate the auto-focus routine if required.
+ */
+fun lockFocus(activity: MainActivity, params: CameraParams, testConfig: TestConfig) {
+    logd("In lockFocus.")
+    if (!params.isOpen) {
+        return
+    }
+
+    try {
+        if (null != params.device) {
+            setAutoFlash(params, params.captureRequestBuilder)
+            params.captureRequestBuilder?.addTarget(params.imageReader?.surface)
+
+            // If this lens can focus, we need to start a focus search and wait for focus lock
+            if (params.hasAF &&
+                FocusMode.AUTO == testConfig.focusMode) {
+                logd("In lockFocus. About to request focus lock and call capture.")
+
+                params.captureRequestBuilder?.set(CaptureRequest.CONTROL_AF_MODE,
+                    CaptureRequest.CONTROL_AF_MODE_AUTO)
+                params.captureRequestBuilder?.set(CaptureRequest.CONTROL_AF_TRIGGER,
+                    CameraMetadata.CONTROL_AF_TRIGGER_CANCEL)
+                params.camera2CaptureSession?.capture(params.captureRequestBuilder?.build(),
+                    params.camera2CaptureSessionCallback, params.backgroundHandler)
+
+                params.captureRequestBuilder?.set(CaptureRequest.CONTROL_AF_MODE,
+                    CaptureRequest.CONTROL_AF_MODE_AUTO)
+                params.captureRequestBuilder?.set(CaptureRequest.CONTROL_AF_TRIGGER,
+                    CameraMetadata.CONTROL_AF_TRIGGER_START)
+
+                params.state = CameraState.WAITING_FOCUS_LOCK
+
+                params.autoFocusStuckCounter = 0
+                params.camera2CaptureSession?.capture(params.captureRequestBuilder?.build(),
+                    params.camera2CaptureSessionCallback, params.backgroundHandler)
+            } else {
+                // If no auto-focus requested, go ahead to the still capture routine
+                logd("In lockFocus. Fixed focus or continuous focus, calling captureStillPicture.")
+                params.state = CameraState.IMAGE_REQUESTED
+                captureStillPicture(activity, params, testConfig)
+            }
+        }
+    } catch (e: CameraAccessException) {
+        e.printStackTrace()
+    }
+}
+
+/**
+ * Request pre-capture auto-exposure (AE) metering
+ */
+fun runPrecaptureSequence(activity: MainActivity, params: CameraParams, testConfig: TestConfig) {
+    if (!params.isOpen) {
+        return
+    }
+
+    try {
+        if (null != params.device) {
+            setAutoFlash(params, params.captureRequestBuilder)
+            params.captureRequestBuilder?.set(CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
+                CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START)
+
+            params.state = CameraState.WAITING_EXPOSURE_LOCK
+            params.camera2CaptureSession?.capture(params.captureRequestBuilder?.build(),
+                params.camera2CaptureSessionCallback, params.backgroundHandler)
+        }
+    } catch (e: CameraAccessException) {
+        e.printStackTrace()
+    }
+}
+
+/**
+ * Make a still capture request. At this point, AF and AE should be converged or unnecessary.
+ */
+fun captureStillPicture(activity: MainActivity, params: CameraParams, testConfig: TestConfig) {
+    if (!params.isOpen) {
+        return
+    }
+
+    try {
+        logd("In captureStillPicture. Current test: " + testConfig.currentRunningTest.toString())
+
+        if (null != params.device) {
+            params.timer.autofocusEnd = System.currentTimeMillis()
+
+            params.captureRequestBuilder =
+                params.device?.createCaptureRequest(CameraDevice.TEMPLATE_STILL_CAPTURE)
+            params.captureRequestBuilder?.addTarget(params.imageReader?.surface)
+
+            when (testConfig.focusMode) {
+                FocusMode.CONTINUOUS -> {
+                    params.captureRequestBuilder?.set(CaptureRequest.CONTROL_AF_MODE,
+                        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
+                }
+                FocusMode.AUTO -> {
+                    params.captureRequestBuilder?.set(CaptureRequest.CONTROL_AF_TRIGGER,
+                        CameraMetadata.CONTROL_AF_TRIGGER_IDLE)
+                }
+                FocusMode.FIXED -> {
+                }
+            }
+
+            // Disable HDR+ for Pixel devices
+            // This is a hack, Pixel devices do not have Sepia mode, but this forces HDR+ off
+            if (android.os.Build.MANUFACTURER.equals("Google")) {
+                //    params.captureRequestBuilder?.set(CaptureRequest.CONTROL_EFFECT_MODE,
+                //        CaptureRequest.CONTROL_EFFECT_MODE_SEPIA)
+            }
+
+            // Orientation
+            val rotation = activity.windowManager.defaultDisplay.rotation
+            val capturedImageRotation = getOrientation(params, rotation)
+            params.captureRequestBuilder
+                ?.set(CaptureRequest.JPEG_ORIENTATION, capturedImageRotation)
+
+            // Flash
+            setAutoFlash(params, params.captureRequestBuilder)
+
+            val captureCallback = Camera2CaptureCallback(activity, params, testConfig)
+            params.camera2CaptureSession?.capture(params.captureRequestBuilder?.build(),
+                captureCallback, params.backgroundHandler)
+        }
+    } catch (e: CameraAccessException) {
+        e.printStackTrace()
+    } catch (e: IllegalStateException) {
+        logd("captureStillPicture IllegalStateException, aborting: " + e.message)
+    }
+}
+
+/**
+ * Close preview stream and camera device. If this was a switch test, restore the camera id
+ */
+fun camera2CloseCamera(activity: MainActivity, params: CameraParams?, testConfig: TestConfig) {
+    if (params == null)
+        return
+
+    MainActivity.logd("closePreviewAndCamera: " + params.id)
+    if (params.isPreviewing) {
+        params.timer.previewCloseStart = System.currentTimeMillis()
+        params.camera2CaptureSession?.close()
+    } else {
+        params.timer.cameraCloseStart = System.currentTimeMillis()
+        params.device?.close()
+    }
+
+    if ((testConfig.currentRunningTest == TestType.SWITCH_CAMERA) ||
+        (testConfig.currentRunningTest == TestType.MULTI_SWITCH)) {
+        params.id = testConfig.switchTestRealCameraId // Restore the actual camera ID
+    }
+}
+
+/**
+ * An abort request has been received. Abandon everything
+ */
+fun camera2Abort(activity: MainActivity, params: CameraParams, testConfig: TestConfig) {
+    params.camera2CaptureSession?.abortCaptures()
+    activity.stopBackgroundThread(params)
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera2DeviceStateCallback.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera2DeviceStateCallback.kt
new file mode 100644
index 0000000..340b424
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera2DeviceStateCallback.kt
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope.cameracontrollers
+
+import android.hardware.camera2.CameraDevice
+import androidx.annotation.NonNull
+import androidx.camera.integration.antelope.CameraParams
+import androidx.camera.integration.antelope.MainActivity
+import androidx.camera.integration.antelope.TestConfig
+import androidx.camera.integration.antelope.TestType
+import androidx.camera.integration.antelope.testEnded
+
+/**
+ * Callbacks that track the state of the camera device using the Camera 2 API.
+ */
+class Camera2DeviceStateCallback(
+    internal var params: CameraParams,
+    internal var activity: MainActivity,
+    internal var testConfig: TestConfig
+) : CameraDevice.StateCallback() {
+
+    /**
+     * Camera device has opened successfully, record timing and initiate the preview stream.
+     */
+    override fun onOpened(@NonNull cameraDevice: CameraDevice) {
+        params.timer.openEnd = System.currentTimeMillis()
+        MainActivity.logd("In CameraStateCallback onOpened: " + cameraDevice.id +
+            " current test: " + testConfig.currentRunningTest.toString())
+        params.isOpen = true
+        params.device = cameraDevice
+
+        when (testConfig.currentRunningTest) {
+            TestType.INIT -> {
+                // Camera opened, we're done
+                testConfig.testFinished = true
+                closePreviewAndCamera(activity, params, testConfig)
+            }
+
+            else -> {
+                params.timer.previewStart = System.currentTimeMillis()
+                createCameraPreviewSession(activity, params, testConfig)
+            }
+        }
+    }
+
+    /**
+     * Camera device has been closed, recording close timing.
+     *
+     * If this is a switch test, swizzle camera ids and move to the next step of the test.
+     */
+    override fun onClosed(camera: CameraDevice?) {
+        MainActivity.logd("In CameraStateCallback onClosed. Camera: " + params.id +
+            " is closed. testFinished: " + testConfig.testFinished)
+        params.isOpen = false
+
+        if (testConfig.testFinished) {
+            params.timer.cameraCloseEnd = System.currentTimeMillis()
+            testConfig.testFinished = false
+            testEnded(activity, params, testConfig)
+            return
+        }
+
+        if ((testConfig.currentRunningTest == TestType.SWITCH_CAMERA) ||
+            (testConfig.currentRunningTest == TestType.MULTI_SWITCH)) {
+
+            // First camera closed, now start the second
+            if (testConfig.switchTestCurrentCamera == testConfig.switchTestCameras.get(0)) {
+                testConfig.switchTestCurrentCamera = testConfig.switchTestCameras.get(1)
+                camera2OpenCamera(activity, params, testConfig)
+            }
+
+            // Second camera closed, now start the first
+            else if (testConfig.switchTestCurrentCamera == testConfig.switchTestCameras.get(1)) {
+                testConfig.switchTestCurrentCamera = testConfig.switchTestCameras.get(0)
+                testConfig.testFinished = true
+                camera2OpenCamera(activity, params, testConfig)
+            }
+        }
+
+        super.onClosed(camera)
+    }
+
+    /**
+     * Camera has been disconnected. Whatever was happening, it won't work now.
+     */
+    override fun onDisconnected(@NonNull cameraDevice: CameraDevice) {
+        MainActivity.logd("In CameraStateCallback onDisconnected: " + params.id)
+        if (!params.isOpen) {
+            return
+        }
+
+        testConfig.testFinished = false // Whatever we are doing will fail now, try to exit
+        closePreviewAndCamera(activity, params, testConfig)
+    }
+
+    /**
+     * Camera device has thrown an error. Try to recover or fail gracefully.
+     */
+    override fun onError(@NonNull cameraDevice: CameraDevice, error: Int) {
+        MainActivity.logd("In CameraStateCallback onError: " + cameraDevice.id + " error: " + error)
+        if (!params.isOpen) {
+            return
+        }
+
+        when (error) {
+            CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE -> {
+                // Let's try to close an open camera and re-open this one
+                MainActivity.logd("In CameraStateCallback too many cameras open, closing one...")
+                closeACamera(activity, testConfig)
+                camera2OpenCamera(activity, params, testConfig)
+            }
+
+            CameraDevice.StateCallback.ERROR_CAMERA_DEVICE -> {
+                MainActivity.logd("Fatal camera error, close and try to re-initialize...")
+                closePreviewAndCamera(activity, params, testConfig)
+                camera2OpenCamera(activity, params, testConfig)
+            }
+
+            CameraDevice.StateCallback.ERROR_CAMERA_IN_USE -> {
+                MainActivity.logd("This camera is already open... doing nothing")
+            }
+
+            else -> {
+                testConfig.testFinished = false // Whatever we are doing will fail, just try to exit
+                closePreviewAndCamera(activity, params, testConfig)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera2PreviewSessionStateCallback.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera2PreviewSessionStateCallback.kt
new file mode 100644
index 0000000..5c3037b
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Camera2PreviewSessionStateCallback.kt
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope.cameracontrollers
+
+import android.hardware.camera2.CameraAccessException
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CaptureRequest
+import androidx.annotation.NonNull
+import androidx.camera.integration.antelope.CameraParams
+import androidx.camera.integration.antelope.FocusMode
+import androidx.camera.integration.antelope.MainActivity
+import androidx.camera.integration.antelope.PrefHelper
+import androidx.camera.integration.antelope.TestConfig
+import androidx.camera.integration.antelope.TestType
+import androidx.camera.integration.antelope.setAutoFlash
+
+/**
+ * Callbacks that track the state of a preview capture session.
+ */
+class Camera2PreviewSessionStateCallback(
+    internal val activity: MainActivity,
+    internal val params: CameraParams,
+    internal val testConfig: TestConfig
+) : CameraCaptureSession.StateCallback() {
+
+    /**
+     * Preview session is open and frames are coming through. If the test is preview only, record
+     * results and close the camera, if a switch or image capture test, proceed to the next step.
+     *
+     */
+    override fun onActive(session: CameraCaptureSession?) {
+        if (!params.isOpen) {
+            return
+        }
+
+        params.timer.previewEnd = System.currentTimeMillis()
+        params.isPreviewing = true
+
+        when (testConfig.currentRunningTest) {
+            TestType.PREVIEW -> {
+                testConfig.testFinished = true
+                closePreviewAndCamera(activity, params, testConfig)
+            }
+
+            TestType.SWITCH_CAMERA, TestType.MULTI_SWITCH -> {
+                if (testConfig.switchTestCurrentCamera == testConfig.switchTestCameras.get(0)) {
+                    if (testConfig.testFinished) {
+                        params.timer.switchToFirstEnd = System.currentTimeMillis()
+                        Thread.sleep(PrefHelper.getPreviewBuffer(activity)) // Let preview run
+                        closePreviewAndCamera(activity, params, testConfig)
+                    } else {
+                        Thread.sleep(PrefHelper.getPreviewBuffer(activity)) // Let preview run
+                        params.timer.switchToSecondStart = System.currentTimeMillis()
+                        closePreviewAndCamera(activity, params, testConfig)
+                    }
+                } else {
+                    params.timer.switchToSecondEnd = System.currentTimeMillis()
+                    Thread.sleep(PrefHelper.getPreviewBuffer(activity)) // Let preview run
+                    params.timer.switchToFirstStart = System.currentTimeMillis()
+                    closePreviewAndCamera(activity, params, testConfig)
+                }
+            }
+
+            else -> {
+                initializeStillCapture(activity, params, testConfig)
+            }
+        }
+        super.onActive(session)
+    }
+
+    /**
+     * Preview session has been configured, set up preview parameters and request that the preview
+     * capture begin.
+     */
+    override fun onConfigured(@NonNull cameraCaptureSession: CameraCaptureSession) {
+        if (!params.isOpen) {
+            return
+        }
+
+        MainActivity.logd("In onConfigured: CaptureSession configured!")
+
+        try {
+            when (testConfig.focusMode) {
+                FocusMode.AUTO -> {
+                    params.captureRequestBuilder?.set(CaptureRequest.CONTROL_AF_MODE,
+                        CaptureRequest.CONTROL_AF_MODE_AUTO)
+                }
+                FocusMode.CONTINUOUS -> {
+                    params.captureRequestBuilder?.set(CaptureRequest.CONTROL_AF_MODE,
+                        CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE)
+                }
+                FocusMode.FIXED -> {
+                    params.captureRequestBuilder?.set(CaptureRequest.CONTROL_AF_MODE,
+                        CaptureRequest.CONTROL_AF_MODE_AUTO)
+                }
+            }
+
+            // Enable flash automatically when necessary.
+            setAutoFlash(params, params.captureRequestBuilder)
+
+            params.camera2CaptureSession = cameraCaptureSession
+            params.state = CameraState.PREVIEW_RUNNING
+
+            // Request that the camera preview begins
+            cameraCaptureSession.setRepeatingRequest(params.captureRequestBuilder?.build(),
+                params.camera2CaptureSessionCallback, params.backgroundHandler)
+        } catch (e: CameraAccessException) {
+            MainActivity.logd("Create Capture Session error: " + params.id)
+            e.printStackTrace()
+        } catch (e: IllegalStateException) {
+            MainActivity.logd("createCameraPreviewSession onConfigured, IllegalStateException," +
+                " aborting: " + e)
+        }
+    }
+
+    /**
+     * Configuration of the preview stream failed, try again.
+     */
+    override fun onConfigureFailed(@NonNull cameraCaptureSession: CameraCaptureSession) {
+        if (!params.isOpen) {
+            return
+        }
+
+        MainActivity.logd("Camera preview initialization failed. Trying again")
+        createCameraPreviewSession(activity, params, testConfig)
+    }
+
+    /**
+     * Preview session has been closed. Record close timing and proceed to close camera.
+     */
+    override fun onClosed(session: CameraCaptureSession) {
+        params.timer.previewCloseEnd = System.currentTimeMillis()
+        params.isPreviewing = false
+        params.timer.cameraCloseStart = System.currentTimeMillis()
+        params.device?.close()
+        super.onClosed(session)
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXCaptureSessionCallback.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXCaptureSessionCallback.kt
new file mode 100644
index 0000000..76af9de
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXCaptureSessionCallback.kt
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope.cameracontrollers
+
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CaptureFailure
+import android.hardware.camera2.CaptureRequest
+import android.hardware.camera2.CaptureResult
+import android.hardware.camera2.TotalCaptureResult
+import android.view.Surface
+import androidx.annotation.NonNull
+import androidx.camera.integration.antelope.CameraParams
+import androidx.camera.integration.antelope.MainActivity
+import androidx.camera.integration.antelope.MainActivity.Companion.logd
+import androidx.camera.integration.antelope.TestConfig
+import androidx.camera.integration.antelope.TestType
+import androidx.camera.integration.antelope.testEnded
+
+/**
+ * Callbacks that track an image capture session
+ */
+class CameraXCaptureSessionCallback(
+    internal val activity: MainActivity,
+    internal val params: CameraParams,
+    internal val testConfig: TestConfig
+) : CameraCaptureSession.CaptureCallback() {
+
+    /** Capture has been aborted. */
+    override fun onCaptureSequenceAborted(session: CameraCaptureSession?, sequenceId: Int) {
+        MainActivity.logd("CameraX captureCallback: Sequence aborted. Current test: " +
+            testConfig.currentRunningTest.toString())
+        super.onCaptureSequenceAborted(session, sequenceId)
+    }
+
+    /** Capture has failed, try to restart */
+    override fun onCaptureFailed(
+        session: CameraCaptureSession?,
+        request: CaptureRequest?,
+        failure: CaptureFailure?
+    ) {
+        MainActivity.logd("CameraX captureStillPicture captureCallback: Capture Failed. Failure: " +
+            failure?.reason + " Current test: " + testConfig.currentRunningTest.toString())
+        closeCameraX(activity, params, testConfig)
+        cameraXOpenCamera(activity, params, testConfig)
+    }
+
+    /** Unused but retained here as it can be useful for debugging  */
+    override fun onCaptureStarted(
+        session: CameraCaptureSession?,
+        request: CaptureRequest?,
+        timestamp: Long,
+        frameNumber: Long
+    ) {
+        // MainActivity.logd("CameraX captureStillPicture captureCallback: Capture Started.")
+        super.onCaptureStarted(session, request, timestamp, frameNumber)
+    }
+
+    /** Unused but retained here as it can be useful for debugging  */
+    override fun onCaptureProgressed(
+        session: CameraCaptureSession?,
+        request: CaptureRequest?,
+        partialResult: CaptureResult?
+    ) {
+        // MainActivity.logd("CameraX captureStillPicture captureCallback: Capture progressed.")
+        super.onCaptureProgressed(session, request, partialResult)
+    }
+
+    /** Unused but retained here as it can be useful for debugging  */
+    override fun onCaptureBufferLost(
+        session: CameraCaptureSession?,
+        request: CaptureRequest?,
+        target: Surface?,
+        frameNumber: Long
+    ) {
+        // MainActivity.logd("CameraX captureStillPicture captureCallback: Buffer lost.")
+        super.onCaptureBufferLost(session, request, target, frameNumber)
+    }
+
+    /**
+     * Still capture has completed. Record timing and proceed to next test or finish.
+     */
+    override fun onCaptureCompleted(
+        @NonNull session: CameraCaptureSession,
+        @NonNull request: CaptureRequest,
+        @NonNull result: TotalCaptureResult
+    ) {
+
+        if (params.cameraXLifecycle.isFinished()) {
+            cameraXAbort(activity, params, testConfig)
+            return
+        }
+
+        logd("CameraX onCaptureCompleted!! " + request.tag)
+        // Prevent duplicate captures from being triggered
+        if (testConfig.isFirstOnCaptureComplete) {
+            testConfig.isFirstOnCaptureComplete = false
+        } else {
+            return
+        }
+
+        params.timer.captureEnd = System.currentTimeMillis()
+        MainActivity.logd("CameraX StillCapture completed. CaptureEnd. Current test: " +
+            testConfig.currentRunningTest.toString())
+
+        // ImageReader might get the image before this callback is called, if so, the test is done
+        if (0L != params.timer.imageSaveEnd) {
+            params.timer.imageReaderStart = params.timer.imageReaderEnd // No ImageReader delay
+            MainActivity.logd("StillCapture completed. Ending Test. Current test: " +
+                testConfig.currentRunningTest.toString())
+
+            if (TestType.MULTI_PHOTO_CHAIN == testConfig.currentRunningTest) {
+                testEnded(activity, params, testConfig)
+            } else {
+                testConfig.testFinished = true
+                closeCameraX(activity, params, testConfig)
+            }
+        } else {
+            MainActivity.logd("StillCapture completed. Waiting on imageReader. Current test: " +
+                testConfig.currentRunningTest.toString())
+            params.timer.imageReaderStart = System.currentTimeMillis()
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXController.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXController.kt
new file mode 100644
index 0000000..87df30d
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXController.kt
@@ -0,0 +1,305 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope.cameracontrollers
+
+import androidx.lifecycle.LifecycleOwner
+import android.graphics.SurfaceTexture
+import android.hardware.camera2.CameraCaptureSession
+import android.hardware.camera2.CameraDevice
+import android.hardware.camera2.CaptureRequest
+import android.view.ViewGroup
+import androidx.camera.integration.antelope.CameraParams
+import androidx.camera.camera2.Camera2Config
+import androidx.camera.core.CameraX
+import androidx.camera.core.ImageCapture
+import androidx.camera.core.ImageCaptureConfig
+import androidx.camera.core.Preview
+import androidx.camera.core.PreviewConfig
+import androidx.camera.integration.antelope.CameraXImageAvailableListener
+import androidx.camera.integration.antelope.CustomLifecycle
+import androidx.camera.integration.antelope.FocusMode
+import androidx.camera.integration.antelope.MainActivity
+import androidx.camera.integration.antelope.MainActivity.Companion.logd
+import androidx.camera.integration.antelope.PrefHelper
+import androidx.camera.integration.antelope.TestConfig
+import androidx.camera.integration.antelope.TestType
+
+/**
+ * Opens the camera using the Camera X API and starts the open counter. The open call will complete
+ * in the DeviceStateCallback asynchronously. For switch tests, the camera id will be swizzling so
+ * the original camera id is saved.
+ *
+ * CameraX manages its lifecycle internally, for the purpose of repeated testing, Antelope uses a
+ * custom lifecycle to allow for starting new tests cleanly which is started here.
+ *
+ * All the needed Cmaera X use cases should be bound before starting the lifecycle. Depending on
+ * the test, bind either the preview case, or both the preview and image capture case.
+ */
+internal fun cameraXOpenCamera(
+    activity: MainActivity,
+    params: CameraParams,
+    testConfig: TestConfig
+) {
+
+    try {
+        // TODO make the switch test methodology more robust and handle physical cameras
+        // Currently we swap out the ids behind the scenes
+        // This requires to save the actual camera id for after the test
+        if ((testConfig.currentRunningTest == TestType.SWITCH_CAMERA) ||
+            (testConfig.currentRunningTest == TestType.MULTI_SWITCH)) {
+            testConfig.switchTestRealCameraId = params.id // Save the actual camera ID
+            params.id = testConfig.switchTestCurrentCamera
+        }
+
+        params.cameraXDeviceStateCallback = CameraXDeviceStateCallback(params, activity, testConfig)
+        params.cameraXPreviewSessionStateCallback =
+            CameraXPreviewSessionStateCallback(activity, params, testConfig)
+
+        if (params.cameraXDeviceStateCallback != null &&
+            params.cameraXPreviewSessionStateCallback != null) {
+            params.cameraXPreviewConfig =
+                cameraXPreviewUseCaseBuilder(params.id, testConfig.focusMode,
+                    params.cameraXDeviceStateCallback!!,
+                    params.cameraXPreviewSessionStateCallback!!)
+        }
+
+        if (!params.cameraXLifecycle.isFinished()) {
+            logd("Lifecycle not finished, finishing it.")
+            params.cameraXLifecycle.pauseAndStop()
+            params.cameraXLifecycle.finish()
+        }
+        params.cameraXLifecycle = CustomLifecycle()
+
+        val lifecycleOwner: LifecycleOwner = params.cameraXLifecycle
+        val previewUseCase = Preview(params.cameraXPreviewConfig)
+
+        // Set preview to observe the surface texture
+        activity.runOnUiThread {
+            previewUseCase.setOnPreviewOutputUpdateListener {
+                viewFinderOutput: Preview.PreviewOutput? ->
+                if (viewFinderOutput?.surfaceTexture != null) {
+                    if (!isCameraSurfaceTextureReleased(viewFinderOutput.surfaceTexture)) {
+                        // View swizzling required to for the view hierarchy to update correctly
+                        val viewGroup = params.cameraXPreviewTexture?.parent as ViewGroup
+                        viewGroup.removeView(params.cameraXPreviewTexture)
+                        viewGroup.addView(params.cameraXPreviewTexture)
+                        params.cameraXPreviewTexture?.surfaceTexture =
+                            viewFinderOutput.surfaceTexture
+                    }
+                }
+            }
+        }
+
+        when (testConfig.currentRunningTest) {
+            //  Only the preview is required
+            TestType.PREVIEW,
+            TestType.SWITCH_CAMERA,
+            TestType.MULTI_SWITCH -> {
+                params.timer.openStart = System.currentTimeMillis()
+                activity.runOnUiThread {
+                    CameraX.bindToLifecycle(lifecycleOwner, previewUseCase)
+                    params.cameraXLifecycle.start()
+                }
+            }
+            else -> {
+                // Both preview and image capture are needed
+                params.cameraXCaptureSessionCallback =
+                    CameraXCaptureSessionCallback(activity, params, testConfig)
+
+                if (params.cameraXDeviceStateCallback != null &&
+                    params.cameraXCaptureSessionCallback != null) {
+                    params.cameraXCaptureConfig =
+                        cameraXImageCaptureUseCaseBuilder(params.id, testConfig.focusMode,
+                            params.cameraXDeviceStateCallback!!,
+                            params.cameraXCaptureSessionCallback!!)
+                }
+
+                params.cameraXImageCaptureUseCase = ImageCapture(params.cameraXCaptureConfig)
+
+                params.timer.openStart = System.currentTimeMillis()
+                activity.runOnUiThread {
+                    CameraX.bindToLifecycle(lifecycleOwner, previewUseCase,
+                        params.cameraXImageCaptureUseCase)
+                    params.cameraXLifecycle.start()
+                }
+            }
+        }
+    } catch (e: Exception) {
+        MainActivity.logd("cameraXOpenCamera exception: " + params.id)
+        e.printStackTrace()
+    }
+}
+
+/**
+ * End Camera X custom lifecycle, unbind use cases, and start timing the camera close.
+ */
+internal fun closeCameraX(activity: MainActivity, params: CameraParams, testConfig: TestConfig) {
+    logd("In closecameraX, camera: " + params.id + ",  test: " + testConfig.currentRunningTest)
+
+    params.timer.cameraCloseStart = System.currentTimeMillis()
+
+    if (!params.cameraXLifecycle.isFinished()) {
+        params.cameraXLifecycle.pauseAndStop()
+        params.cameraXLifecycle.finish()
+
+        // CameraX calls need to be on the main thread
+        activity.run {
+            CameraX.unbindAll()
+        }
+    }
+    if ((testConfig.currentRunningTest == TestType.SWITCH_CAMERA) ||
+        (testConfig.currentRunningTest == TestType.MULTI_SWITCH)) {
+        params.id = testConfig.switchTestRealCameraId // Restore the actual camera ID
+    }
+
+    params.isOpen = false
+}
+
+/**
+ * Proceed to take and measure a still image capture.
+ */
+internal fun cameraXTakePicture(
+    activity: MainActivity,
+    params: CameraParams,
+    testConfig: TestConfig
+) {
+    if (params.cameraXLifecycle.isFinished()) {
+        cameraXAbort(activity, params, testConfig)
+        return
+    }
+
+    logd("CameraX TakePicture: capture start.")
+
+    // Pause in multi-captures to make sure HDR routines don't get overloaded
+    logd("CameraX TakePicture. Pausing for " +
+        PrefHelper.getPreviewBuffer(activity) + "ms to let preview run.")
+    params.timer.previewFillStart = System.currentTimeMillis()
+    Thread.sleep(PrefHelper.getPreviewBuffer(activity))
+    params.timer.previewFillEnd = System.currentTimeMillis()
+
+    params.timer.captureStart = System.currentTimeMillis()
+    params.timer.autofocusStart = System.currentTimeMillis()
+    params.timer.autofocusEnd = System.currentTimeMillis()
+
+    logd("Capture timer started: " + params.timer.captureStart)
+    activity.runOnUiThread {
+        params.cameraXImageCaptureUseCase
+            .takePicture(CameraXImageAvailableListener(activity, params, testConfig))
+    }
+}
+
+/**
+ * An abort request has been received. Abandon everything
+ */
+internal fun cameraXAbort(activity: MainActivity, params: CameraParams, testConfig: TestConfig) {
+    closeCameraX(activity, params, testConfig)
+    return
+}
+
+/**
+ * Try to determine if a SurfaceTexture is released.
+ *
+ * Prior to SDK 26 there was not built in mechanism for this. This method relies on expected
+ * exceptions being thrown if a released SurfaceTexture is updated.
+ */
+private fun isCameraSurfaceTextureReleased(texture: SurfaceTexture): Boolean {
+    var released = false
+
+    if (26 <= android.os.Build.VERSION.SDK_INT) {
+        released = texture.isReleased
+    } else {
+        // WARNING: This relies on some implementation details of the SurfaceTexture native code.
+        // If the SurfaceTexture is released, we should get a RuntimeException. If not, we should
+        // get an IllegalStateException since we are not in the same EGL context as the camera.
+        var exception: Exception? = null
+        try {
+            texture.updateTexImage()
+        } catch (e: IllegalStateException) {
+            logd("in isCameraSurfaceTextureReleased: IllegalStateException: " + e.message)
+            exception = e
+            released = false
+        } catch (e: RuntimeException) {
+            logd("in isCameraSurfaceTextureReleased: RuntimeException: " + e.message)
+            exception = e
+            released = true
+        }
+
+        if (!released && exception == null) {
+            throw RuntimeException("Unable to determine if SurfaceTexture is released")
+        }
+    }
+
+    logd("The camera texture is: " + if (released) "RELEASED" else "NOT RELEASED")
+
+    return released
+}
+
+/**
+ * Setup the Camera X preview use case
+ */
+private fun cameraXPreviewUseCaseBuilder(
+    id: String,
+    focusMode: FocusMode,
+    deviceStateCallback: CameraDevice.StateCallback,
+    sessionCaptureStateCallback: CameraCaptureSession.StateCallback
+): PreviewConfig {
+
+    // TODO: As of 0.3.0 CameraX can only use front and back cameras. Update in future versions
+    val cameraXcameraID = if (id.equals("0")) CameraX.LensFacing.BACK else CameraX.LensFacing.FRONT
+    val configBuilder = PreviewConfig.Builder()
+        .setLensFacing(cameraXcameraID)
+    Camera2Config.Extender(configBuilder)
+        .setDeviceStateCallback(deviceStateCallback)
+        .setSessionStateCallback(sessionCaptureStateCallback)
+        .setCaptureRequestOption(CaptureRequest.CONTROL_AF_MODE,
+            when (focusMode) {
+                FocusMode.AUTO -> CaptureRequest.CONTROL_AF_MODE_AUTO
+                FocusMode.CONTINUOUS -> CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
+                FocusMode.FIXED -> CaptureRequest.CONTROL_AF_MODE_AUTO
+            })
+    return configBuilder.build()
+}
+
+/**
+ * Setup the Camera X image capture use case
+ */
+private fun cameraXImageCaptureUseCaseBuilder(
+    id: String,
+    focusMode: FocusMode,
+    deviceStateCallback:
+    CameraDevice.StateCallback,
+    sessionCaptureCallback: CameraCaptureSession.CaptureCallback
+): ImageCaptureConfig {
+
+    // TODO: As of 0.3.0 CameraX can only use front and back cameras. Update in future versions
+    val cameraXcameraID = if (id.equals("0")) CameraX.LensFacing.BACK else CameraX.LensFacing.FRONT
+
+    val configBuilder = ImageCaptureConfig.Builder()
+        .setLensFacing(cameraXcameraID)
+        .setCaptureMode(ImageCapture.CaptureMode.MAX_QUALITY)
+    Camera2Config.Extender(configBuilder)
+        .setDeviceStateCallback(deviceStateCallback)
+        .setSessionCaptureCallback(sessionCaptureCallback)
+        .setCaptureRequestOption(CaptureRequest.CONTROL_AF_MODE,
+            when (focusMode) {
+                FocusMode.AUTO -> CaptureRequest.CONTROL_AF_MODE_AUTO
+                FocusMode.CONTINUOUS -> CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
+                FocusMode.FIXED -> CaptureRequest.CONTROL_AF_MODE_AUTO
+            })
+
+    return configBuilder.build()
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXDeviceStateCallback.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXDeviceStateCallback.kt
new file mode 100644
index 0000000..13be3e7
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXDeviceStateCallback.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope.cameracontrollers
+
+import android.hardware.camera2.CameraDevice
+import androidx.annotation.NonNull
+import androidx.camera.integration.antelope.CameraParams
+import androidx.camera.integration.antelope.MainActivity
+import androidx.camera.integration.antelope.TestConfig
+import androidx.camera.integration.antelope.TestType
+import androidx.camera.integration.antelope.testEnded
+
+/**
+ * Callbacks that track the state of the camera device using the Camera X API.
+ */
+class CameraXDeviceStateCallback(
+    internal var params: CameraParams,
+    internal var activity: MainActivity,
+    internal var testConfig: TestConfig
+) : CameraDevice.StateCallback() {
+
+    /**
+     * Camera device has opened successfully, record timing and initiate the preview stream.
+     */
+    override fun onOpened(@NonNull cameraDevice: CameraDevice) {
+        MainActivity.logd("In CameraXStateCallback onOpened: " + cameraDevice.id +
+            " current test: " + testConfig.currentRunningTest.toString())
+
+        params.timer.openEnd = System.currentTimeMillis()
+        params.isOpen = true
+        params.device = cameraDevice
+
+        when (testConfig.currentRunningTest) {
+            TestType.INIT -> {
+                // Camera opened, we're done
+                testConfig.testFinished = true
+                closeCameraX(activity, params, testConfig)
+            }
+
+            else -> {
+                params.timer.previewStart = System.currentTimeMillis()
+            }
+        }
+    }
+
+    /**
+     * Camera device has been closed, recording close timing.
+     *
+     * If this is a switch test, swizzle camera ids and move to the next step of the test.
+     */
+    override fun onClosed(camera: CameraDevice?) {
+        MainActivity.logd("In CameraXStateCallback onClosed.")
+
+        if (testConfig.testFinished) {
+            params.timer.cameraCloseEnd = System.currentTimeMillis()
+            testConfig.testFinished = false
+            testEnded(activity, params, testConfig)
+            return
+        }
+
+        if ((testConfig.currentRunningTest == TestType.SWITCH_CAMERA) ||
+            (testConfig.currentRunningTest == TestType.MULTI_SWITCH)) {
+
+            // First camera closed, now start the second
+            if (testConfig.switchTestCurrentCamera == testConfig.switchTestCameras.get(0)) {
+                testConfig.switchTestCurrentCamera = testConfig.switchTestCameras.get(1)
+                cameraXOpenCamera(activity, params, testConfig)
+            }
+
+            // Second camera closed, now start the first
+            else if (testConfig.switchTestCurrentCamera == testConfig.switchTestCameras.get(1)) {
+                testConfig.switchTestCurrentCamera = testConfig.switchTestCameras.get(0)
+                testConfig.testFinished = true
+                cameraXOpenCamera(activity, params, testConfig)
+            }
+        }
+    }
+
+    /**
+     * Camera has been disconnected. Whatever was happening, it won't work now.
+     */
+    override fun onDisconnected(@NonNull cameraDevice: CameraDevice) {
+        MainActivity.logd("In CameraXStateCallback onDisconnected: " + params.id)
+        testConfig.testFinished = false // Whatever we are doing will fail now, try to exit
+        closeCameraX(activity, params, testConfig)
+    }
+
+    /**
+     * Camera device has thrown an error. Try to recover or fail gracefully.
+     */
+    override fun onError(@NonNull cameraDevice: CameraDevice, error: Int) {
+        MainActivity.logd("In CameraXStateCallback onError: " +
+            cameraDevice.id + " and error: " + error)
+
+        when (error) {
+            CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE -> {
+                // Let's try to close an open camera and re-open this one
+                MainActivity.logd("In CameraXStateCallback too many cameras open, closing one...")
+                closeACamera(activity, testConfig)
+                cameraXOpenCamera(activity, params, testConfig)
+            }
+
+            CameraDevice.StateCallback.ERROR_CAMERA_DEVICE -> {
+                MainActivity.logd("Fatal camerax error, close and try to re-initialize...")
+                closeCameraX(activity, params, testConfig)
+                cameraXOpenCamera(activity, params, testConfig)
+            }
+
+            CameraDevice.StateCallback.ERROR_CAMERA_IN_USE -> {
+                MainActivity.logd("This camera is already open... doing nothing")
+            }
+
+            else -> {
+                testConfig.testFinished = false // Whatever we are doing will fail now, try to exit
+                closeCameraX(activity, params, testConfig)
+            }
+        }
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXPreviewSessionStateCallback.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXPreviewSessionStateCallback.kt
new file mode 100644
index 0000000..e011f80
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/CameraXPreviewSessionStateCallback.kt
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope.cameracontrollers
+
+import android.hardware.camera2.CameraCaptureSession
+import androidx.annotation.NonNull
+import androidx.camera.integration.antelope.CameraParams
+import androidx.camera.integration.antelope.MainActivity
+import androidx.camera.integration.antelope.PrefHelper
+import androidx.camera.integration.antelope.TestConfig
+import androidx.camera.integration.antelope.TestType
+
+/**
+ * Callbacks that track the state of a preview capture session.
+ */
+class CameraXPreviewSessionStateCallback(
+    internal var activity: MainActivity,
+    internal var params: CameraParams,
+    internal var testConfig: TestConfig
+) : CameraCaptureSession.StateCallback() {
+
+    /**
+     * Preview session is open and frames are coming through. If the test is preview only, record
+     * results and close the camera, if a switch or image capture test, proceed to the next step.
+     *
+     */
+    override fun onActive(session: CameraCaptureSession?) {
+        if (params.cameraXLifecycle.isFinished()) {
+            cameraXAbort(activity, params, testConfig)
+            return
+        }
+
+        // Prevent duplicate captures from being triggered if running a capture test
+        if (testConfig.currentRunningTest != TestType.MULTI_SWITCH &&
+            testConfig.currentRunningTest != TestType.SWITCH_CAMERA) {
+            if (testConfig.isFirstOnActive) {
+                testConfig.isFirstOnActive = false
+            } else {
+                return
+            }
+        }
+
+        params.timer.previewEnd = System.currentTimeMillis()
+
+        when (testConfig.currentRunningTest) {
+            TestType.PREVIEW -> {
+                testConfig.testFinished = true
+                closeCameraX(activity, params, testConfig)
+            }
+
+            TestType.SWITCH_CAMERA, TestType.MULTI_SWITCH -> {
+                if (testConfig.switchTestCurrentCamera == testConfig.switchTestCameras.get(0)) {
+                    if (testConfig.testFinished) {
+                        params.timer.switchToFirstEnd = System.currentTimeMillis()
+                        Thread.sleep(PrefHelper.getPreviewBuffer(activity)) // Let preview run
+                        closePreviewAndCamera(activity, params, testConfig)
+                    } else {
+                        Thread.sleep(PrefHelper.getPreviewBuffer(activity)) // Let preview run
+                        params.timer.switchToSecondStart = System.currentTimeMillis()
+                        closePreviewAndCamera(activity, params, testConfig)
+                    }
+                } else {
+                    params.timer.switchToSecondEnd = System.currentTimeMillis()
+                    Thread.sleep(PrefHelper.getPreviewBuffer(activity)) // Let preview run
+                    params.timer.switchToFirstStart = System.currentTimeMillis()
+                    closePreviewAndCamera(activity, params, testConfig)
+                }
+            }
+
+            else -> {
+                cameraXTakePicture(activity, params, testConfig)
+            }
+        }
+        if (null != session)
+            super.onActive(session)
+    }
+
+    /**
+     * Preview session has been configured. Camera X handles the next step.
+     */
+    override fun onConfigured(@NonNull cameraCaptureSession: CameraCaptureSession) {
+        MainActivity.logd("In onConfigured: CaptureSession configured!")
+    }
+
+    /**
+     * Configuration of the preview stream failed, try again.
+     */
+    override fun onConfigureFailed(@NonNull cameraCaptureSession: CameraCaptureSession) {
+        MainActivity.logd("CameraX preview initialization failed. Closing camera.")
+        closeCameraX(activity, params, testConfig)
+    }
+
+    /**
+     * Preview session has been closed. Camera X handles the next step.
+     */
+    override fun onClosed(session: CameraCaptureSession) {
+        MainActivity.logd("In CameraXPreviewSessionStateCallback onClosed.")
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Common.kt b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Common.kt
new file mode 100644
index 0000000..589e0df
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/antelope/cameracontrollers/Common.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2019 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.integration.antelope.cameracontrollers
+
+import androidx.camera.integration.antelope.CameraAPI
+import androidx.camera.integration.antelope.CameraParams
+import androidx.camera.integration.antelope.MainActivity
+import androidx.camera.integration.antelope.TestConfig
+
+/**
+ * Cross-API function to close the currently active preview stream and camera device
+ */
+fun closePreviewAndCamera(activity: MainActivity, params: CameraParams?, testConfig: TestConfig) {
+    if (null == params)
+        return
+
+    when (testConfig.api) {
+        CameraAPI.CAMERA1 -> camera1CloseCamera(activity, params, testConfig)
+        CameraAPI.CAMERA2 -> camera2CloseCamera(activity, params, testConfig)
+        CameraAPI.CAMERAX -> closeCameraX(activity, params, testConfig)
+    }
+}
+
+/**
+ * Convenience method to close all cameras on a device.
+ */
+fun closeAllCameras(activity: MainActivity, testConfig: TestConfig) {
+    MainActivity.logd("Closing all cameras.")
+    for (tempCameraParams: CameraParams in MainActivity.cameraParams.values) {
+        closePreviewAndCamera(activity, tempCameraParams, testConfig)
+    }
+}
+
+/**
+ * Close the first open camera on the device. This can be used if a ERROR_MAX_CAMERAS_IN_USE is
+ * occurring.
+ */
+fun closeACamera(activity: MainActivity, testConfig: TestConfig) {
+    var closedACamera = false
+    MainActivity.logd("In closeACamera, looking for open camera.")
+    for (tempCameraParams: CameraParams in MainActivity.cameraParams.values) {
+        if (tempCameraParams.isOpen) {
+            MainActivity.logd("In closeACamera, found open camera, closing: " + tempCameraParams.id)
+            closedACamera = true
+            closePreviewAndCamera(activity, tempCameraParams, testConfig)
+            break
+        }
+    }
+
+    // We couldn't find an open camera, let's close everything
+    if (!closedACamera) {
+        closeAllCameras(activity, testConfig)
+    }
+}
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/BaseActivity.java b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/BaseActivity.java
deleted file mode 100644
index 9ca2686..0000000
--- a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/BaseActivity.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.integration.timing;
-
-import android.os.Bundle;
-
-import androidx.appcompat.app.AppCompatActivity;
-
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
-
-/**
- * An activity used to run performance test case.
- *
- * <p>To run performance test case, please implement this Activity. Camerax Use Case can be
- * implement in prepareUseCase and runUseCase. For performance result, you can set currentTimeMillis
- * to startTime and store the execution time into totalTime. At the end of test case, please call
- * onUseCaseFinish() to notify the lock.
- */
-public abstract class BaseActivity extends AppCompatActivity {
-    public static final long MICROS_IN_SECOND = TimeUnit.SECONDS.toMillis(1);
-    public static final long PREVIEW_FILL_BUFFER_TIME = 1500;
-    private static final String TAG = "BaseActivity";
-    public long startTime;
-    public long totalTime;
-    public long openCameraStartTime;
-    public long openCameraTotalTime;
-    public long startRreviewTime;
-    public long startPreviewTotalTime;
-    public long previewFrameRate;
-    public long closeCameraStartTime;
-    public long closeCameraTotalTime;
-    public String imageResolution;
-    public CountDownLatch latch;
-
-    /**
-     * Prepares the use case.
-     */
-    public abstract void prepareUseCase();
-
-    /**
-     * Activates use case so it will receive data from camera.
-     *
-     * @throws InterruptedException on fatal errors.
-     */
-    public abstract void runUseCase() throws InterruptedException;
-
-    /**
-     * Called when the test case finishes.
-     * <p>Could be called in CameraDevice's state callbacks.
-     */
-    public void onUseCaseFinish() {
-        latch.countDown();
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        latch = new CountDownLatch(1);
-    }
-}
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/CustomLifecycle.java b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/CustomLifecycle.java
deleted file mode 100644
index fd5308a..0000000
--- a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/CustomLifecycle.java
+++ /dev/null
@@ -1,75 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.integration.timing;
-
-import android.os.Handler;
-import android.os.Looper;
-
-import androidx.annotation.NonNull;
-import androidx.lifecycle.Lifecycle;
-import androidx.lifecycle.LifecycleOwner;
-import androidx.lifecycle.LifecycleRegistry;
-
-/** A customized lifecycle owner which obeys the lifecycle transition rules. */
-public final class CustomLifecycle implements LifecycleOwner {
-    private final LifecycleRegistry mLifecycleRegistry;
-    private final Handler mMainHandler = new Handler(Looper.getMainLooper());
-
-    public CustomLifecycle() {
-        mLifecycleRegistry = new LifecycleRegistry(this);
-        mLifecycleRegistry.setCurrentState(Lifecycle.State.INITIALIZED);
-        mLifecycleRegistry.setCurrentState(Lifecycle.State.CREATED);
-    }
-
-    @NonNull
-    @Override
-    public Lifecycle getLifecycle() {
-        return mLifecycleRegistry;
-    }
-
-    /**
-     * Called when activity resumes.
-     */
-    public void doOnResume() {
-        if (Looper.getMainLooper() != Looper.myLooper()) {
-            mMainHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    CustomLifecycle.this.doOnResume();
-                }
-            });
-            return;
-        }
-        mLifecycleRegistry.setCurrentState(Lifecycle.State.RESUMED);
-    }
-
-    /**
-     * Called when activity is destroyed.
-     */
-    public void doDestroyed() {
-        if (Looper.getMainLooper() != Looper.myLooper()) {
-            mMainHandler.post(new Runnable() {
-                @Override
-                public void run() {
-                    CustomLifecycle.this.doDestroyed();
-                }
-            });
-            return;
-        }
-        mLifecycleRegistry.setCurrentState(Lifecycle.State.DESTROYED);
-    }
-}
diff --git a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/TakePhotoActivity.java b/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/TakePhotoActivity.java
deleted file mode 100644
index dca2bd2..0000000
--- a/camera/integration-tests/timingtestapp/src/main/java/androidx/camera/integration/timing/TakePhotoActivity.java
+++ /dev/null
@@ -1,280 +0,0 @@
-/*
- * Copyright (C) 2019 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package androidx.camera.integration.timing;
-
-import android.graphics.SurfaceTexture;
-import android.hardware.camera2.CameraCaptureSession;
-import android.hardware.camera2.CameraDevice;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.TextureView;
-import android.view.TextureView.SurfaceTextureListener;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-
-import androidx.camera.camera2.Camera2Config;
-import androidx.camera.core.CameraX;
-import androidx.camera.core.CameraX.LensFacing;
-import androidx.camera.core.ImageCapture;
-import androidx.camera.core.ImageCapture.CaptureMode;
-import androidx.camera.core.ImageCaptureConfig;
-import androidx.camera.core.ImageProxy;
-import androidx.camera.core.Preview;
-import androidx.camera.core.PreviewConfig;
-
-/** This Activity is used to run image capture performance test in mobileharness. */
-public class TakePhotoActivity extends BaseActivity {
-
-    private static final String TAG = "TakePhotoActivity";
-    // How many sample frames we should use to calculate framerate.
-    private static final int FRAMERATE_SAMPLE_WINDOW = 5;
-    private static final String EXTRA_CAPTURE_MODE = "capture_mode";
-    private static final String EXTRA_CAMERA_FACING = "camera_facing";
-    private static final String CAMERA_FACING_FRONT = "FRONT";
-    private static final String CAMERA_FACING_BACK = "BACK";
-    private final String mDefaultCameraFacing = CAMERA_FACING_BACK;
-    private final CameraDevice.StateCallback mDeviceStateCallback =
-            new CameraDevice.StateCallback() {
-
-                @Override
-                public void onOpened(CameraDevice cameraDevice) {
-                    openCameraTotalTime = System.currentTimeMillis() - openCameraStartTime;
-                    Log.d(TAG, "[onOpened] openCameraTotalTime: " + openCameraTotalTime);
-                    startRreviewTime = System.currentTimeMillis();
-                }
-
-                @Override
-                public void onClosed(CameraDevice camera) {
-                    super.onClosed(camera);
-                    closeCameraTotalTime = System.currentTimeMillis() - closeCameraStartTime;
-                    Log.d(TAG, "[onClosed] closeCameraTotalTime: " + closeCameraTotalTime);
-                    onUseCaseFinish();
-                }
-
-                @Override
-                public void onDisconnected(CameraDevice cameraDevice) {
-                }
-
-                @Override
-                public void onError(CameraDevice cameraDevice, int i) {
-                    Log.e(TAG, "[onError] open camera failed, error code: " + i);
-                }
-            };
-    private final CameraCaptureSession.StateCallback mCaptureSessionStateCallback =
-            new CameraCaptureSession.StateCallback() {
-
-                @Override
-                public void onActive(CameraCaptureSession session) {
-                    super.onActive(session);
-                    startPreviewTotalTime = System.currentTimeMillis() - startRreviewTime;
-                    Log.d(TAG, "[onActive] previewStartTotalTime: " + startPreviewTotalTime);
-                }
-
-                @Override
-                public void onConfigured(CameraCaptureSession cameraCaptureSession) {
-                    Log.d(TAG, "[onConfigured] CaptureSession configured!");
-                }
-
-                @Override
-                public void onConfigureFailed(CameraCaptureSession cameraCaptureSession) {
-                    Log.e(TAG, "[onConfigureFailed] CameraX preview initialization failed.");
-                }
-            };
-    /** The default cameraId to use. */
-    private LensFacing mCurrentCameraLensFacing = LensFacing.BACK;
-    private ImageCapture mImageCapture;
-    private Preview mPreview;
-    private int mFrameCount;
-    private long mPreviewSampleStartTime;
-    private CaptureMode mCaptureMode = CaptureMode.MIN_LATENCY;
-    private CustomLifecycle mCustomLifecycle;
-
-    @Override
-    public void runUseCase() throws InterruptedException {
-
-        // Length of time to let the preview stream run before capturing the first image.
-        // This can help ensure capture latency is real latency and not merely the device
-        // filling the buffer.
-        Thread.sleep(PREVIEW_FILL_BUFFER_TIME);
-
-        startTime = System.currentTimeMillis();
-        mImageCapture.takePicture(
-                new ImageCapture.OnImageCapturedListener() {
-                    @Override
-                    public void onCaptureSuccess(ImageProxy image, int rotationDegrees) {
-                        totalTime = System.currentTimeMillis() - startTime;
-                        if (image != null) {
-                            imageResolution = image.getWidth() + "x" + image.getHeight();
-                        } else {
-                            Log.e(TAG, "[onCaptureSuccess] image is null");
-                        }
-                    }
-                });
-    }
-
-    @Override
-    public void prepareUseCase() {
-        createPreview();
-        createImageCapture();
-    }
-
-    void createPreview() {
-        PreviewConfig.Builder configBuilder =
-                new PreviewConfig.Builder()
-                        .setLensFacing(mCurrentCameraLensFacing)
-                        .setTargetName("Preview");
-
-        new Camera2Config.Extender(configBuilder)
-                .setDeviceStateCallback(mDeviceStateCallback)
-                .setSessionStateCallback(mCaptureSessionStateCallback);
-
-        mPreview = new Preview(configBuilder.build());
-        openCameraStartTime = System.currentTimeMillis();
-
-        mPreview.setOnPreviewOutputUpdateListener(
-                new Preview.OnPreviewOutputUpdateListener() {
-                    @Override
-                    public void onUpdated(Preview.PreviewOutput previewOutput) {
-                        TextureView textureView = TakePhotoActivity.this.findViewById(
-                                R.id.textureView);
-                        ViewGroup viewGroup = (ViewGroup) textureView.getParent();
-                        viewGroup.removeView(textureView);
-                        viewGroup.addView(textureView);
-                        textureView.setSurfaceTexture(previewOutput.getSurfaceTexture());
-                        textureView.setSurfaceTextureListener(
-                                new SurfaceTextureListener() {
-                                    @Override
-                                    public void onSurfaceTextureAvailable(
-                                            SurfaceTexture surfaceTexture, int i, int i1) {
-                                    }
-
-                                    @Override
-                                    public void onSurfaceTextureSizeChanged(
-                                            SurfaceTexture surfaceTexture, int i, int i1) {
-                                    }
-
-                                    @Override
-                                    public boolean onSurfaceTextureDestroyed(
-                                            SurfaceTexture surfaceTexture) {
-                                        return false;
-                                    }
-
-                                    @Override
-                                    public void onSurfaceTextureUpdated(
-                                            SurfaceTexture surfaceTexture) {
-                                        Log.d(TAG, "[onSurfaceTextureUpdated]");
-                                        if (0 == totalTime) {
-                                            return;
-                                        }
-
-                                        if (0 == mFrameCount) {
-                                            mPreviewSampleStartTime = System.currentTimeMillis();
-                                        } else if (FRAMERATE_SAMPLE_WINDOW == mFrameCount) {
-                                            final long duration =
-                                                    System.currentTimeMillis()
-                                                            - mPreviewSampleStartTime;
-                                            previewFrameRate =
-                                                    (MICROS_IN_SECOND
-                                                            * FRAMERATE_SAMPLE_WINDOW
-                                                            / duration);
-                                            closeCameraStartTime = System.currentTimeMillis();
-                                            mCustomLifecycle.doDestroyed();
-                                        }
-                                        mFrameCount++;
-                                    }
-                                });
-                    }
-                });
-
-        CameraX.bindToLifecycle(mCustomLifecycle, mPreview);
-    }
-
-    void createImageCapture() {
-        ImageCaptureConfig config =
-                new ImageCaptureConfig.Builder()
-                        .setTargetName("ImageCapture")
-                        .setLensFacing(mCurrentCameraLensFacing)
-                        .setCaptureMode(mCaptureMode)
-                        .build();
-
-        mImageCapture = new ImageCapture(config);
-        CameraX.bindToLifecycle(mCustomLifecycle, mImageCapture);
-
-        final Button button = this.findViewById(R.id.Picture);
-        button.setOnClickListener(
-                new View.OnClickListener() {
-                    @Override
-                    public void onClick(View view) {
-                        startTime = System.currentTimeMillis();
-                        mImageCapture.takePicture(
-                                new ImageCapture.OnImageCapturedListener() {
-                                    @Override
-                                    public void onCaptureSuccess(
-                                            ImageProxy image, int rotationDegrees) {
-                                        totalTime = System.currentTimeMillis() - startTime;
-                                        if (image != null) {
-                                            imageResolution =
-                                                    image.getWidth() + "x" + image.getHeight();
-                                        } else {
-                                            Log.e(TAG, "[onCaptureSuccess] image is null");
-                                        }
-                                    }
-                                });
-                    }
-                });
-    }
-
-    @Override
-    protected void onCreate(Bundle savedInstanceState) {
-        super.onCreate(savedInstanceState);
-        setContentView(R.layout.activity_main);
-
-        final Bundle bundle = getIntent().getExtras();
-        if (bundle != null) {
-            final String captureModeString = bundle.getString(EXTRA_CAPTURE_MODE);
-            if (captureModeString != null) {
-                mCaptureMode = CaptureMode.valueOf(captureModeString.toUpperCase());
-            }
-            final String cameraLensFacing = bundle.getString(EXTRA_CAMERA_FACING);
-            if (cameraLensFacing != null) {
-                setupCamera(cameraLensFacing);
-            } else {
-                setupCamera(mDefaultCameraFacing);
-            }
-        }
-        mCustomLifecycle = new CustomLifecycle();
-        prepareUseCase();
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-        mCustomLifecycle.doOnResume();
-    }
-
-    void setupCamera(String cameraFacing) {
-        Log.d(TAG, "Camera Facing: " + cameraFacing);
-        if (CAMERA_FACING_BACK.equalsIgnoreCase(cameraFacing)) {
-            mCurrentCameraLensFacing = LensFacing.BACK;
-        } else if (CAMERA_FACING_FRONT.equalsIgnoreCase(cameraFacing)) {
-            mCurrentCameraLensFacing = LensFacing.FRONT;
-        } else {
-            throw new RuntimeException("Invalid lens facing: " + cameraFacing);
-        }
-    }
-}
diff --git a/camera/integration-tests/timingtestapp/src/main/res/drawable-v24/ic_launcher_foreground.xml b/camera/integration-tests/timingtestapp/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..18c76bb
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,50 @@
+<!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:aapt="http://schemas.android.com/aapt"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillType="evenOdd"
+        android:pathData="M32,64C32,64 38.39,52.99 44.13,50.95C51.37,48.37 70.14,49.57 70.14,49.57L108.26,87.69L108,109.01L75.97,107.97L32,64Z"
+        android:strokeWidth="1"
+        android:strokeColor="#00000000">
+        <aapt:attr name="android:fillColor">
+            <gradient
+                android:endX="78.5885"
+                android:endY="90.9159"
+                android:startX="48.7653"
+                android:startY="61.0927"
+                android:type="linear">
+                <item
+                    android:color="#44000000"
+                    android:offset="0.0" />
+                <item
+                    android:color="#00000000"
+                    android:offset="1.0" />
+            </gradient>
+        </aapt:attr>
+    </path>
+    <path
+        android:fillColor="#FFFFFF"
+        android:fillType="nonZero"
+        android:pathData="M66.94,46.02L66.94,46.02C72.44,50.07 76,56.61 76,64L32,64C32,56.61 35.56,50.11 40.98,46.06L36.18,41.19C35.45,40.45 35.45,39.3 36.18,38.56C36.91,37.81 38.05,37.81 38.78,38.56L44.25,44.05C47.18,42.57 50.48,41.71 54,41.71C57.48,41.71 60.78,42.57 63.68,44.05L69.11,38.56C69.84,37.81 70.98,37.81 71.71,38.56C72.44,39.3 72.44,40.45 71.71,41.19L66.94,46.02ZM62.94,56.92C64.08,56.92 65,56.01 65,54.88C65,53.76 64.08,52.85 62.94,52.85C61.8,52.85 60.88,53.76 60.88,54.88C60.88,56.01 61.8,56.92 62.94,56.92ZM45.06,56.92C46.2,56.92 47.13,56.01 47.13,54.88C47.13,53.76 46.2,52.85 45.06,52.85C43.92,52.85 43,53.76 43,54.88C43,56.01 43.92,56.92 45.06,56.92Z"
+        android:strokeWidth="1"
+        android:strokeColor="#00000000" />
+</vector>
diff --git a/camera/integration-tests/timingtestapp/src/main/res/drawable/ic_launcher_background.xml b/camera/integration-tests/timingtestapp/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..cc52369
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,185 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+    android:width="108dp"
+    android:height="108dp"
+    android:viewportWidth="108"
+    android:viewportHeight="108">
+    <path
+        android:fillColor="#008577"
+        android:pathData="M0,0h108v108h-108z" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M9,0L9,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,0L19,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,0L29,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,0L39,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,0L49,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,0L59,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,0L69,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,0L79,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M89,0L89,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M99,0L99,108"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,9L108,9"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,19L108,19"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,29L108,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,39L108,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,49L108,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,59L108,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,69L108,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,79L108,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,89L108,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M0,99L108,99"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,29L89,29"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,39L89,39"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,49L89,49"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,59L89,59"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,69L89,69"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M19,79L89,79"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M29,19L29,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M39,19L39,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M49,19L49,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M59,19L59,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M69,19L69,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+    <path
+        android:fillColor="#00000000"
+        android:pathData="M79,19L79,89"
+        android:strokeWidth="0.8"
+        android:strokeColor="#33FFFFFF" />
+</vector>
diff --git a/camera/integration-tests/timingtestapp/src/main/res/drawable/rounded_frame.xml b/camera/integration-tests/timingtestapp/src/main/res/drawable/rounded_frame.xml
new file mode 100644
index 0000000..1347875
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/drawable/rounded_frame.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android">
+    <solid android:color="@android:color/transparent" />
+    <stroke
+        android:width="2dp"
+        android:color="@color/colorAccent" />
+    <corners android:radius="5dp" />
+    <padding
+        android:bottom="10dp"
+        android:left="10dp"
+        android:right="10dp"
+        android:top="10dp" />
+</shape>
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/res/layout-land/activity_main.xml b/camera/integration-tests/timingtestapp/src/main/res/layout-land/activity_main.xml
new file mode 100644
index 0000000..9de951f
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/layout-land/activity_main.xml
@@ -0,0 +1,155 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/constraint_main"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".MainActivity">
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline_horizontal"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintGuide_percent="0.60" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline_vertical"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="vertical"
+        app:layout_constraintGuide_percent="0.65" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline_progress"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintGuide_percent="0.96" />
+
+    <ScrollView
+        android:id="@+id/scroll_log"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginStart="8dp"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="8dp"
+        android:layout_marginBottom="8dp"
+        android:background="@drawable/rounded_frame"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="@id/guideline_vertical"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/text_log"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:ems="10"
+                android:text="@string/log_initial" />
+        </LinearLayout>
+    </ScrollView>
+
+    <ProgressBar
+        android:id="@+id/progress_test"
+        style="@android:style/Widget.DeviceDefault.Light.ProgressBar.Horizontal"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="8dp"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="8dp"
+        android:layout_marginBottom="8dp"
+        android:indeterminate="false"
+        android:visibility="invisible"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="@id/guideline_vertical"
+        app:layout_constraintTop_toBottomOf="@+id/guideline_progress" />
+
+    <androidx.camera.integration.antelope.AutoFitSurfaceView
+        android:id="@+id/surface_preview"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginStart="16dp"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="8dp"
+        android:layout_marginBottom="8dp"
+        app:layout_constraintBottom_toTopOf="@id/button_single"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="@id/guideline_vertical"
+        app:layout_constraintTop_toTopOf="parent" />
+
+    <androidx.camera.integration.antelope.AutoFitTextureView
+        android:id="@+id/texture_preview"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginStart="16dp"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="8dp"
+        android:layout_marginBottom="8dp"
+        android:visibility="invisible"
+        app:layout_constraintBottom_toTopOf="@id/button_single"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="@id/guideline_vertical"
+        app:layout_constraintTop_toTopOf="parent" />
+
+
+    <Button
+        android:id="@+id/button_single"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="8dp"
+        android:includeFontPadding="false"
+        android:minHeight="20dp"
+        android:text="@string/button_single_test"
+        app:layout_constraintBottom_toTopOf="@+id/button_multi"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="@+id/button_multi" />
+
+    <Button
+        android:id="@+id/button_multi"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="8dp"
+        android:includeFontPadding="false"
+        android:minHeight="20dp"
+        android:text="@string/button_multi_test"
+        app:layout_constraintBottom_toTopOf="@id/button_abort"
+        app:layout_constraintEnd_toEndOf="parent" />
+
+    <Button
+        android:id="@+id/button_abort"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp"
+        android:includeFontPadding="false"
+        android:minHeight="20dp"
+        android:text="@string/button_abort"
+        app:layout_constraintBottom_toTopOf="@id/guideline_progress"
+        app:layout_constraintEnd_toEndOf="parent" />
+
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/res/layout/activity_main.xml b/camera/integration-tests/timingtestapp/src/main/res/layout/activity_main.xml
index 3dc76a9..9615209 100644
--- a/camera/integration-tests/timingtestapp/src/main/res/layout/activity_main.xml
+++ b/camera/integration-tests/timingtestapp/src/main/res/layout/activity_main.xml
@@ -1,52 +1,155 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2019 The Android Open Source Project
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
 
-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.
--->
 <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
     xmlns:app="http://schemas.android.com/apk/res-auto"
     xmlns:tools="http://schemas.android.com/tools"
+    android:id="@+id/constraint_main"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
-    tools:context="androidx.camera.integration.timing">
+    tools:context=".MainActivity">
 
-    <TextureView
-        android:id="@+id/textureView"
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline_horizontal"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintGuide_percent="0.60" />
+
+    <ScrollView
+        android:id="@+id/scroll_log"
         android:layout_width="0dp"
         android:layout_height="0dp"
+        android:layout_marginStart="8dp"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="8dp"
+        android:layout_marginBottom="8dp"
+        android:background="@drawable/rounded_frame"
+        app:layout_constraintBottom_toTopOf="@id/guideline_horizontal"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+        <LinearLayout
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/text_log"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:ems="10"
+                android:text="@string/log_initial" />
+        </LinearLayout>
+    </ScrollView>
+
+    <ProgressBar
+        android:id="@+id/progress_test"
+        style="@android:style/Widget.DeviceDefault.Light.ProgressBar.Horizontal"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginStart="8dp"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="8dp"
+        android:layout_marginBottom="8dp"
+        android:indeterminate="false"
+        android:visibility="invisible"
         app:layout_constraintBottom_toBottomOf="parent"
         app:layout_constraintEnd_toEndOf="parent"
         app:layout_constraintStart_toStartOf="parent"
-        app:layout_constraintTop_toTopOf="parent" />
+        app:layout_constraintTop_toBottomOf="@+id/guideline_progress" />
+
+    <androidx.camera.integration.antelope.AutoFitSurfaceView
+        android:id="@+id/surface_preview"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginStart="16dp"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="8dp"
+        android:layout_marginBottom="8dp"
+        app:layout_constraintBottom_toTopOf="@id/guideline_progress"
+        app:layout_constraintEnd_toStartOf="@id/guideline_vertical"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/guideline_horizontal" />
+
+    <androidx.camera.integration.antelope.AutoFitTextureView
+        android:id="@+id/texture_preview"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_marginStart="16dp"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="8dp"
+        android:layout_marginBottom="8dp"
+        android:visibility="invisible"
+        app:layout_constraintBottom_toTopOf="@id/guideline_progress"
+        app:layout_constraintEnd_toStartOf="@id/guideline_vertical"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/guideline_horizontal" />
 
     <androidx.constraintlayout.widget.Guideline
-        android:id="@+id/takepicture"
+        android:id="@+id/guideline_progress"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal"
+        app:layout_constraintGuide_percent="0.96" />
+
+
+    <Button
+        android:id="@+id/button_single"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="8dp"
+        android:includeFontPadding="false"
+        android:minHeight="20dp"
+        android:text="@string/button_single_test"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="@+id/button_multi"
+        app:layout_constraintTop_toBottomOf="@+id/guideline_horizontal" />
+
+    <Button
+        android:id="@+id/button_multi"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="8dp"
+        android:includeFontPadding="false"
+        android:minHeight="20dp"
+        android:text="@string/button_multi_test"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintTop_toBottomOf="@+id/button_single" />
+
+    <Button
+        android:id="@+id/button_abort"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp"
+        android:includeFontPadding="false"
+        android:minHeight="20dp"
+        android:text="@string/button_abort"
+        app:layout_constraintBottom_toTopOf="@id/guideline_progress"
+        app:layout_constraintEnd_toEndOf="parent" />
+
+    <androidx.constraintlayout.widget.Guideline
+        android:id="@+id/guideline_vertical"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:orientation="vertical"
-        app:layout_constraintGuide_begin="0dp"
-        app:layout_constraintGuide_percent="0.1" />
+        app:layout_constraintGuide_percent="0.5" />
 
-    <Button
-        android:id="@+id/Picture"
-        android:layout_width="wrap_content"
-        android:layout_height="wrap_content"
-        android:scaleType="fitXY"
-        android:text="Picture"
-        app:layout_constraintBottom_toBottomOf="parent"
-        app:layout_constraintEnd_toEndOf="parent"
-        app:layout_constraintHorizontal_bias="0.0"
-        app:layout_constraintStart_toStartOf="@+id/takepicture"
-        app:layout_constraintTop_toTopOf="parent"
-        app:layout_constraintVertical_bias="1.0" />
-</androidx.constraintlayout.widget.ConstraintLayout>
+
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/res/layout/settings_dialog.xml b/camera/integration-tests/timingtestapp/src/main/res/layout/settings_dialog.xml
new file mode 100644
index 0000000..d3e5d000
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/layout/settings_dialog.xml
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    android:id="@+id/constraint_settings_dialog"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+
+    <ScrollView
+        android:id="@+id/scroll_settings_dialog"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_marginStart="8dp"
+        android:layout_marginTop="8dp"
+        android:layout_marginEnd="8dp"
+        android:scrollbarFadeDuration="0"
+        app:layout_constraintBottom_toTopOf="@id/button_start"
+        app:layout_constraintEnd_toEndOf="parent"
+        app:layout_constraintStart_toStartOf="parent"
+        app:layout_constraintTop_toTopOf="parent">
+
+    </ScrollView>
+
+    <Button
+        android:id="@+id/button_start"
+        style="@style/Widget.AppCompat.Button.ButtonBar.AlertDialog"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp"
+        android:layout_marginBottom="8dp"
+        android:text="@string/settings_single_go"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toEndOf="parent" />
+
+    <Button
+        android:id="@+id/button_cancel"
+        style="@style/Widget.AppCompat.Button.ButtonBar.AlertDialog"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_marginEnd="8dp"
+        android:layout_marginBottom="8dp"
+        android:text="@string/settings_single_cancel"
+        app:layout_constraintBottom_toBottomOf="parent"
+        app:layout_constraintEnd_toStartOf="@+id/button_start"
+        app:layout_constraintTop_toTopOf="@+id/button_start" />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/res/menu/main_menu.xml b/camera/integration-tests/timingtestapp/src/main/res/menu/main_menu.xml
new file mode 100644
index 0000000..0b000f91
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/menu/main_menu.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<menu xmlns:android="http://schemas.android.com/apk/res/android">
+    <item
+        android:id="@+id/menu_logcat"
+        android:checkable="true"
+        android:title="@string/menu_logcat" />
+    <item
+        android:id="@+id/menu_delete_photos"
+        android:title="@string/menu_delete_photos" />
+    <item
+        android:id="@+id/menu_delete_logs"
+        android:title="@string/menu_delete_logs" />
+</menu>
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/camera/integration-tests/timingtestapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..25efb7b
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@mipmap/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/camera/integration-tests/timingtestapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..25efb7b
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
+    <background android:drawable="@drawable/ic_launcher_background" />
+    <foreground android:drawable="@mipmap/ic_launcher_foreground" />
+</adaptive-icon>
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-hdpi/ic_launcher.png b/camera/integration-tests/timingtestapp/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..8db5361
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-hdpi/ic_launcher.png
Binary files differ
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/camera/integration-tests/timingtestapp/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..c9fb04d
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-hdpi/ic_launcher_round.png b/camera/integration-tests/timingtestapp/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..a462e63
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-hdpi/ic_launcher_round.png
Binary files differ
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-mdpi/ic_launcher.png b/camera/integration-tests/timingtestapp/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..fba050b
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-mdpi/ic_launcher.png
Binary files differ
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/camera/integration-tests/timingtestapp/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..45ff1c3
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-mdpi/ic_launcher_round.png b/camera/integration-tests/timingtestapp/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..a7299aa
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-mdpi/ic_launcher_round.png
Binary files differ
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-xhdpi/ic_launcher.png b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..6ffe739
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xhdpi/ic_launcher.png
Binary files differ
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..2a09046
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..17901edb
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png
Binary files differ
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxhdpi/ic_launcher.png b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..e45eb2f
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..12891cd
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..f1ffb89
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..cf9ffa6
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Binary files differ
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000..ff76659
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
Binary files differ
diff --git a/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..e757795
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
Binary files differ
diff --git a/camera/integration-tests/timingtestapp/src/main/res/values/arrays.xml b/camera/integration-tests/timingtestapp/src/main/res/values/arrays.xml
new file mode 100644
index 0000000..872ac89
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/values/arrays.xml
@@ -0,0 +1,83 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <string-array name="array_numtests">
+        <item>5</item>
+        <item>10</item>
+        <item>15</item>
+        <item>30</item>
+        <item>50</item>
+        <item>100</item>
+        <item>250</item>
+        <item>500</item>
+    </string-array>
+
+    <string-array name="array_previewbuffer">
+        <item>250</item>
+        <item>500</item>
+        <item>750</item>
+        <item>1000</item>
+        <item>1500</item>
+        <item>2000</item>
+        <item>2500</item>
+        <item>5000</item>
+    </string-array>
+
+    <string-array name="array_settings_api">
+        <item>Camera1</item>
+        <item>Camera2</item>
+        <item>CameraX</item>
+    </string-array>
+
+    <!-- Don't turn CameraX on by default until it is more stable -->
+    <string-array name="array_settings_api_defaults">
+        <item>Camera1</item>
+        <item>Camera2</item>
+        <item>CameraX</item>
+    </string-array>
+
+    <string-array name="array_settings_imagesize">
+        <item>Min</item>
+        <item>Max</item>
+    </string-array>
+
+    <string-array name="array_settings_focus">
+        <item>Auto</item>
+        <item>Continuous</item>
+    </string-array>
+
+    <string-array name="array_single_test_types">
+        <item>Camera Open/Close</item>
+        <item>Preview Start</item>
+        <item>Switch Cameras</item>
+        <item>Switch Cameras (Multiple)</item>
+        <item>Single Capture</item>
+        <item>Multiple Captures</item>
+        <item>Multiple Captures (Chained)</item>
+    </string-array>
+
+    <string-array name="array_single_test_type_values">
+        <item>INIT</item>
+        <item>PREVIEW</item>
+        <item>SWITCH_CAMERA</item>
+        <item>MULTI_SWITCH</item>
+        <item>PHOTO</item>
+        <item>MULTI_PHOTO</item>
+        <item>MULTI_PHOTO_CHAIN</item>
+    </string-array>
+
+</resources>
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/res/values/colors.xml b/camera/integration-tests/timingtestapp/src/main/res/values/colors.xml
new file mode 100644
index 0000000..b956ca4
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/values/colors.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <color name="colorPrimary">#0088BB</color>
+    <color name="colorPrimaryDark">#00574B</color>
+    <color name="colorAccent">#55DD11FF</color>
+</resources>
diff --git a/camera/integration-tests/timingtestapp/src/main/res/values/ic_launcher_background.xml b/camera/integration-tests/timingtestapp/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000..09803e6
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+    <color name="ic_launcher_background">#A617A1</color>
+</resources>
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/res/values/strings.xml b/camera/integration-tests/timingtestapp/src/main/res/values/strings.xml
index 2df0565..3af8838 100644
--- a/camera/integration-tests/timingtestapp/src/main/res/values/strings.xml
+++ b/camera/integration-tests/timingtestapp/src/main/res/values/strings.xml
@@ -1,17 +1,118 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2019 The Android Open Source Project
+<!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
 
-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="log_initial">Android Camera Performance Tool</string>
+
+    <string name="app_name">Antelope</string>
+    <string name="label_camera1">Camera1</string>
+    <string name="label_camera2">Camera2</string>
+    <string name="label_camerax">CameraX</string>
+    <string name="label_size_max">Max image size</string>
+    <string name="label_size_min">Min image size</string>
+    <string name="label_focus_auto">Auto-focus</string>
+    <string name="label_focus_continuous">Continuous focus</string>
+    <string name="label_camera">Camera:</string>
+    <string name="button_single_test">Single Test</string>
+    <string name="button_multi_test">Multiple Tests</string>
+    <string name="button_abort">Abort</string>
+    <string name="label_log">Log: </string>
+    <string name="log_copied">Log copied to the clipboard</string>
+
+    <string name="menu_logcat">Output logcat</string>
+    <string name="menu_delete_photos">Delete images</string>
+    <string name="menu_delete_logs">Delete CSV logs</string>
+
+    <string name="settings_autodelete_key">settings_autodelete</string>
+    <string name="settings_autodelete_title">Auto-delete images</string>
+    <string name="settings_autodelete_summary">Automatically delete images immediately after test runs</string>
+    <string name="settings_autodelete_summary_off">Do not automatically delete images immediately after test runs, images will be stored in the Antelope directory of your device\'s photos area</string>
+
+    <string name="settings_numtests_key">settings_numtests</string>
+    <string name="settings_numtests_title">Number of repetitions</string>
+    <string name="settings_numtests_summary">Number of repetitions to do for repeated tests</string>
+
+    <string name="settings_previewbuffer_key">settings_previewbuffer</string>
+    <string name="settings_previewbuffer_title">Preview buffer time (ms)</string>
+    <string name="settings_previewbuffer_summary">Length of time to allow the preview buffer to run before trying to capture an image. Longer values will ensure the hardware frame buffer is full to offer accurate capture-only latency.</string>
+
+    <string name="settings_autotest_header">Auto-test settings</string>
+    <string name="settings_autotest_header_key">settings_autotest_header_key</string>
+
+    <string name="settings_autotest_api_key">settings_autotest_api</string>
+    <string name="settings_autotest_api_title">APIs</string>
+    <string name="settings_autotest_api_summary">APIs to include in test</string>
+
+    <string name="settings_autotest_imagesize_key">settings_autotest_imagesize</string>
+    <string name="settings_autotest_imagesize_title">Image Capture Sizes</string>
+    <string name="settings_autotest_imagesize_summary">Capture sizes to include in test</string>
+
+    <string name="settings_autotest_focus_key">settings_autotest_focus</string>
+    <string name="settings_autotest_focus_title">Focus Modes</string>
+    <string name="settings_autotest_focus_summary">Focus modes to include in test</string>
+
+    <string name="settings_autotest_switchtest_key">settings_autotest_switchtest</string>
+    <string name="settings_autotest_switchtest_title">Perform Switch Test</string>
+    <string name="settings_autotest_switchtest_summary_on">Time switching from primary->secondary->primary camera.</string>
+    <string name="settings_autotest_switchtest_summmary_off">Do not perform switch test.</string>
+
+    <string name="settings_autotest_cameras_key">settings_autotest_cameras</string>
+    <string name="settings_autotest_cameras_title">Only Logical Cameras</string>
+    <string name="settings_autotest_cameras_summary_on">Only test logical cameras (default front and back camera).</string>
+    <string name="settings_autotest_cameras_summary_off">Test all physical cameras on the device.</string>
+
+    <!-- Single test settings -->
+    <string name="settings_single_test_dialog_title">Single Test</string>
+
+    <string name="settings_single_test_type_key">settings_single_test_type</string>
+    <string name="settings_single_test_type_title">Test Type</string>
+
+    <string name="settings_single_test_api_key">settings_single_test_api</string>
+    <string name="settings_single_test_api_title">API</string>
+
+    <string name="settings_single_test_focus_key">settings_single_test_focus</string>
+    <string name="settings_single_test_focus_title">Focus Mode</string>
+
+    <string name="settings_single_test_imagesize_key">settings_single_test_imagesize</string>
+    <string name="settings_single_test_imagesize_title">Image Capture Size</string>
+
+    <string name="settings_single_test_camera_key">settings_single_test_camera</string>
+    <string name="settings_single_test_camera_title">Camera</string>
+
+    <string name="settings_single_go">Begin Test</string>
+    <string name="settings_single_cancel">Cancel</string>
+    <string name="settings_multi_go">Begin Testing</string>
+    <string name="settings_multi_cancel">Cancel</string>
+
+    <!-- Multi Test settings -->
+    <string name="settings_multi_test_dialog_title">Auto Test</string>
+
+
+    <!--
+    <string name="settings__key">settings_</string>
+    <string name="settings__title"></string>
+    <string name="settings__summary"></string>
+
+    <string name="settings__key">settings_</string>
+    <string name="settings__title"></string>
+    <string name="settings__summary"></string>
+-->
+    <!--
+    <string name="settings__key">settings_</string>
+    <string name="settings__title"></string>
+    <string name="settings__summary"></string>
+    -->
 </resources>
diff --git a/camera/integration-tests/timingtestapp/src/main/res/values/style.xml b/camera/integration-tests/timingtestapp/src/main/res/values/style.xml
deleted file mode 100644
index 7503cc0..0000000
--- a/camera/integration-tests/timingtestapp/src/main/res/values/style.xml
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!-- Copyright (C) 2019 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-  http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<resources>
-    <!-- Base application theme. -->
-    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
-        <!-- Customize your theme here. -->
-    </style>
-</resources>
diff --git a/camera/integration-tests/timingtestapp/src/main/res/values/styles.xml b/camera/integration-tests/timingtestapp/src/main/res/values/styles.xml
new file mode 100644
index 0000000..9d7ebc1
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/values/styles.xml
@@ -0,0 +1,30 @@
+<!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<resources>
+
+    <!-- Base application theme. -->
+    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
+        <!-- Customize your theme here. -->
+        <item name="colorPrimary">@color/colorPrimary</item>
+        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
+        <item name="colorAccent">@color/colorAccent</item>
+    </style>
+
+    <style name="SettingsDialogTheme" parent="@style/Theme.AppCompat.Light.Dialog">
+        <item name="android:windowNoTitle">false</item>
+    </style>
+</resources>
diff --git a/camera/integration-tests/timingtestapp/src/main/res/xml/multi_test_settings.xml b/camera/integration-tests/timingtestapp/src/main/res/xml/multi_test_settings.xml
new file mode 100644
index 0000000..dccf223
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/xml/multi_test_settings.xml
@@ -0,0 +1,130 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <!-- Doesn't work as expected
+    <SeekBarPreference
+        android:id="@+id/seekbar_numtests"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:key="@string/settings_numtests_key"
+        android:title="@string/settings_numtests_title"
+        android:summary="@string/settings_numtests_summary"
+        android:defaultValue="30"
+        android:max="500"
+        app:min="5"
+        app:showSeekBarValue="true"
+        app:adjustable="true"
+        app:seekBarIncrement="5"
+        />
+
+    <SeekBarPreference
+        android:id="@+id/seekbar_numtests"
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:key="@string/settings_previewbuffer_key"
+        android:title="@string/settings_previewbuffer_title"
+        android:summary="@string/settings_previewbuffer_summary"
+        android:defaultValue="1500"
+        android:max="5000"
+        app:min="250"
+        app:showSeekBarValue="true"
+        app:adjustable="true"
+        app:seekBarIncrement="250"
+        />
+-->
+
+    <MultiSelectListPreference
+        android:id="@+id/multiselect_apis"
+        android:defaultValue="@array/array_settings_api_defaults"
+        android:entries="@array/array_settings_api"
+        android:entryValues="@array/array_settings_api"
+        android:key="@string/settings_autotest_api_key"
+        android:summary="@string/settings_autotest_api_summary"
+        android:title="@string/settings_autotest_api_title"
+        app:iconSpaceReserved="false" />
+
+    <MultiSelectListPreference
+        android:id="@+id/multiselect_imagesize"
+        android:defaultValue="@array/array_settings_imagesize"
+        android:entries="@array/array_settings_imagesize"
+        android:entryValues="@array/array_settings_imagesize"
+        android:key="@string/settings_autotest_imagesize_key"
+        android:summary="@string/settings_autotest_imagesize_summary"
+        android:title="@string/settings_autotest_imagesize_title"
+        app:iconSpaceReserved="false" />
+
+    <MultiSelectListPreference
+        android:id="@+id/multiselect_focus"
+        android:defaultValue="@array/array_settings_focus"
+        android:entries="@array/array_settings_focus"
+        android:entryValues="@array/array_settings_focus"
+        android:key="@string/settings_autotest_focus_key"
+        android:summary="@string/settings_autotest_focus_summary"
+        android:title="@string/settings_autotest_focus_title"
+        app:iconSpaceReserved="false" />
+
+    <CheckBoxPreference
+        android:id="@+id/checkbox_switchtest"
+        android:defaultValue="true"
+        android:key="@string/settings_autotest_switchtest_key"
+        android:summaryOff="@string/settings_autotest_switchtest_summmary_off"
+        android:summaryOn="@string/settings_autotest_switchtest_summary_on"
+        android:title="@string/settings_autotest_switchtest_title"
+        app:iconSpaceReserved="false" />
+
+    <CheckBoxPreference
+        android:id="@+id/checkbox_logical_cameras"
+        android:defaultValue="true"
+        android:key="@string/settings_autotest_cameras_key"
+        android:summaryOff="@string/settings_autotest_cameras_summary_off"
+        android:summaryOn="@string/settings_autotest_cameras_summary_on"
+        android:title="@string/settings_autotest_cameras_title"
+        app:iconSpaceReserved="false" />
+
+
+    <ListPreference
+        android:id="@+id/list_numtests"
+        android:defaultValue="30"
+        android:entries="@array/array_numtests"
+        android:entryValues="@array/array_numtests"
+        android:key="@string/settings_numtests_key"
+        android:summary="%s"
+        android:title="@string/settings_numtests_title"
+        app:iconSpaceReserved="false" />
+
+    <ListPreference
+        android:id="@+id/list_previewbuffer"
+        android:defaultValue="1500"
+        android:entries="@array/array_previewbuffer"
+        android:entryValues="@array/array_previewbuffer"
+        android:key="@string/settings_previewbuffer_key"
+        android:summary="%s"
+        android:title="@string/settings_previewbuffer_title"
+        app:iconSpaceReserved="false" />
+
+    <CheckBoxPreference
+        android:id="@+id/checkbox_autodelete"
+        android:defaultValue="true"
+        android:key="@string/settings_autodelete_key"
+        android:summaryOff="@string/settings_autodelete_summary_off"
+        android:summaryOn="@string/settings_autodelete_summary"
+        android:title="@string/settings_autodelete_title"
+        app:iconSpaceReserved="false" />
+
+</PreferenceScreen>
\ No newline at end of file
diff --git a/camera/integration-tests/timingtestapp/src/main/res/xml/single_test_settings.xml b/camera/integration-tests/timingtestapp/src/main/res/xml/single_test_settings.xml
new file mode 100644
index 0000000..0a22ccb
--- /dev/null
+++ b/camera/integration-tests/timingtestapp/src/main/res/xml/single_test_settings.xml
@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+  ~ Copyright 2019 The Android Open Source Project
+  ~
+  ~ Licensed under the Apache License, Version 2.0 (the "License");
+  ~ you may not use this file except in compliance with the License.
+  ~ You may obtain a copy of the License at
+  ~
+  ~     http://www.apache.org/licenses/LICENSE-2.0
+  ~
+  ~ Unless required by applicable law or agreed to in writing, software
+  ~ distributed under the License is distributed on an "AS IS" BASIS,
+  ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  ~ See the License for the specific language governing permissions and
+  ~ limitations under the License.
+  -->
+
+<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+
+    <ListPreference
+        android:id="@+id/list_single_test_type"
+        android:defaultValue="PHOTO"
+        android:entries="@array/array_single_test_types"
+        android:entryValues="@array/array_single_test_type_values"
+        android:key="@string/settings_single_test_type_key"
+        android:summary="%s"
+        android:title="@string/settings_single_test_type_title"
+        app:iconSpaceReserved="false" />
+
+    <ListPreference
+        android:id="@+id/list_single_test_api"
+        android:defaultValue="Camera2"
+        android:entries="@array/array_settings_api"
+        android:entryValues="@array/array_settings_api"
+        android:key="@string/settings_single_test_api_key"
+        android:summary="%s"
+        android:title="@string/settings_single_test_api_title"
+        app:iconSpaceReserved="false" />
+
+    <ListPreference
+        android:id="@+id/list_single_test_focus"
+        android:defaultValue="Auto"
+        android:entries="@array/array_settings_focus"
+        android:entryValues="@array/array_settings_focus"
+        android:key="@string/settings_single_test_focus_key"
+        android:summary="%s"
+        android:title="@string/settings_single_test_focus_title"
+        app:iconSpaceReserved="false" />
+
+    <ListPreference
+        android:id="@+id/list_single_test_imagesize"
+        android:defaultValue="Max"
+        android:entries="@array/array_settings_imagesize"
+        android:entryValues="@array/array_settings_imagesize"
+        android:key="@string/settings_single_test_imagesize_key"
+        android:summary="%s"
+        android:title="@string/settings_single_test_imagesize_title"
+        app:iconSpaceReserved="false" />
+
+    <ListPreference
+        android:id="@+id/list_single_test_camera"
+        android:key="@string/settings_single_test_camera_key"
+        android:summary="%s"
+        android:title="@string/settings_single_test_camera_title"
+        app:iconSpaceReserved="false" />
+
+    <ListPreference
+        android:id="@+id/list_numtests"
+        android:defaultValue="30"
+        android:entries="@array/array_numtests"
+        android:entryValues="@array/array_numtests"
+        android:key="@string/settings_numtests_key"
+        android:summary="%s"
+        android:title="@string/settings_numtests_title"
+        app:iconSpaceReserved="false" />
+    <!--    android:summary="@string/settings_numtests_summary" -->
+    <!--        android:summary="@string/settings_previewbuffer_summary" -->
+
+    <ListPreference
+        android:id="@+id/list_previewbuffer"
+        android:defaultValue="1500"
+        android:entries="@array/array_previewbuffer"
+        android:entryValues="@array/array_previewbuffer"
+        android:key="@string/settings_previewbuffer_key"
+        android:summary="%s"
+        android:title="@string/settings_previewbuffer_title"
+        app:iconSpaceReserved="false" />
+
+    <CheckBoxPreference
+        android:id="@+id/checkbox_autodelete"
+        android:defaultValue="true"
+        android:key="@string/settings_autodelete_key"
+        android:summaryOff="@string/settings_autodelete_summary_off"
+        android:summaryOn="@string/settings_autodelete_summary"
+        android:title="@string/settings_autodelete_title"
+        app:iconSpaceReserved="false" />
+
+</PreferenceScreen>
\ No newline at end of file