diff options
author | Treehugger Robot <treehugger-gerrit@google.com> | 2019-04-27 02:20:34 +0000 |
---|---|---|
committer | Gerrit Code Review <noreply-gerritcodereview@google.com> | 2019-04-27 02:20:34 +0000 |
commit | a243337dccbc9496d9f916fc93b70af9ece39e4e (patch) | |
tree | 7c3e181e78a78d8f1554e0016a5b90b63780f41a | |
parent | 1679bdb835f6a9a39fa2f04803a5ece7c61fe939 (diff) | |
parent | e128705fd2524480a7364a9fa223c88b8907925e (diff) | |
download | support-a243337dccbc9496d9f916fc93b70af9ece39e4e.tar.gz |
Merge "Initial commit for AOSP merge" into androidx-master-dev
68 files changed, 7194 insertions, 536 deletions
diff --git a/camera/integration-tests/timingtestapp/build.gradle b/camera/integration-tests/timingtestapp/build.gradle index 3aa3257d6d7..e6d2fea5514 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 @@ import static androidx.build.dependencies.DependenciesKt.* 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 @@ android { 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")) + + // Lifecycle and LiveData + implementation(ARCH_LIFECYCLE_EXTENSIONS) - // Android Support Library - api(CONSTRAINT_LAYOUT, { transitive = true }) - implementation(project(":appcompat")) + // 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) - // Guava + // 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 921b5cd0df4..20539cc9505 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-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> - <uses-permission android:name="android.permission.RECORD_AUDIO" /> - <uses-feature android:name="android.hardware.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" /> - <application android:theme="@style/AppTheme"> - <activity - android:name=".TakePhotoActivity" - android:label="Taking Photo"> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> + + <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 Binary files differnew file mode 100644 index 00000000000..ae4c1099d32 --- /dev/null +++ b/camera/integration-tests/timingtestapp/src/main/ic_launcher-web.png 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 00000000000..e9967108a51 --- /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 00000000000..7f814b556e8 --- /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 00000000000..3c031e72215 --- /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 00000000000..4c84e6c23ab --- /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 00000000000..17ab0fd9d4d --- /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 00000000000..19d3b130e68 --- /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 00000000000..4f3b0d4e9c8 --- /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 00000000000..e34dddcfbba --- /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 00000000000..69b8b896bfa --- /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 00000000000..3d3d351fb67 --- /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 00000000000..a85901e287b --- /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 00000000000..b9b41665ff9 --- /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 00000000000..533b8704f1b --- /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 00000000000..b7da33951e8 --- /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 00000000000..6288ffb30ec --- /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 00000000000..1e259893972 --- /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 00000000000..70453d5c2fe --- /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 00000000000..fae19ae3b72 --- /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 00000000000..b16d784cede --- /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 00000000000..ca8bbfabef4 --- /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 00000000000..84c94ac7f42 --- /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 00000000000..458eaa233ba --- /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 00000000000..97212167de6 --- /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 00000000000..340b424254d --- /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 00000000000..5c3037b94ab --- /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 00000000000..76af9de2828 --- /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 00000000000..87df30d6b2d --- /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 00000000000..13be3e706a2 --- /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 00000000000..e011f80366f --- /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 00000000000..589e0df9f49 --- /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 9ca268603a4..00000000000 --- 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 fd5308a08f6..00000000000 --- 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 dca2bd21844..00000000000 --- 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 00000000000..18c76bbcaf0 --- /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 00000000000..cc5236921eb --- /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 00000000000..13478759023 --- /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 00000000000..9de951f4697 --- /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 3dc76a981bd..96152090f0c 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"> + + <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" /> - <TextureView - android:id="@+id/textureView" + <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="vertical" - app:layout_constraintGuide_begin="0dp" - app:layout_constraintGuide_percent="0.1" /> + android:orientation="horizontal" + app:layout_constraintGuide_percent="0.96" /> + <Button - android:id="@+id/Picture" - android:layout_width="wrap_content" + android:id="@+id/button_single" + android:layout_width="0dp" android:layout_height="wrap_content" - android:scaleType="fitXY" - android:text="Picture" - app:layout_constraintBottom_toBottomOf="parent" + 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_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> + 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_percent="0.5" /> + + +</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 00000000000..d3e5d000d3d --- /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 00000000000..0b000f91ebd --- /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 00000000000..25efb7bcf6b --- /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 00000000000..25efb7bcf6b --- /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 Binary files differnew file mode 100644 index 00000000000..8db53615434 --- /dev/null +++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-hdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 00000000000..c9fb04df7e1 --- /dev/null +++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-hdpi/ic_launcher_foreground.png 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 Binary files differnew file mode 100644 index 00000000000..a462e63b025 --- /dev/null +++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-hdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 00000000000..fba050b0221 --- /dev/null +++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-mdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 00000000000..45ff1c30223 --- /dev/null +++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-mdpi/ic_launcher_foreground.png 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 Binary files differnew file mode 100644 index 00000000000..a7299aa2fd5 --- /dev/null +++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-mdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 00000000000..6ffe739e07f --- /dev/null +++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 00000000000..2a09046539d --- /dev/null +++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png 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 Binary files differnew file mode 100644 index 00000000000..17901edb85b --- /dev/null +++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xhdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 00000000000..e45eb2fc377 --- /dev/null +++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 00000000000..12891cd9229 --- /dev/null +++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png 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 Binary files differnew file mode 100644 index 00000000000..f1ffb895743 --- /dev/null +++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxhdpi/ic_launcher_round.png 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 Binary files differnew file mode 100644 index 00000000000..cf9ffa64c5d --- /dev/null +++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxxhdpi/ic_launcher.png 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 Binary files differnew file mode 100644 index 00000000000..ff76659db41 --- /dev/null +++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png 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 Binary files differnew file mode 100644 index 00000000000..e757795076a --- /dev/null +++ b/camera/integration-tests/timingtestapp/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png 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 00000000000..872ac892082 --- /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 00000000000..b956ca4d80d --- /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 00000000000..09803e6e421 --- /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 2df05657350..3af8838b542 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 +<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> - http://www.apache.org/licenses/LICENSE-2.0 + <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> -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. + <!-- 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> --> -<resources> + <!-- + <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 7503cc0b615..00000000000 --- 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 00000000000..9d7ebc15f79 --- /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 00000000000..dccf2231f2e --- /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 00000000000..0a22ccb5c9c --- /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 |