1 /* <lambda>null2 * Copyright 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.camera.integration.extensions.validation 18 19 import android.annotation.SuppressLint 20 import android.content.ContentResolver 21 import android.content.ContentValues 22 import android.content.Context 23 import android.hardware.camera2.CameraCharacteristics 24 import android.hardware.camera2.CameraCharacteristics.LENS_FACING 25 import android.os.Build 26 import android.os.Environment.DIRECTORY_DOCUMENTS 27 import android.provider.MediaStore 28 import android.util.Log 29 import androidx.camera.core.impl.CameraInfoInternal 30 import androidx.camera.extensions.ExtensionsManager 31 import androidx.camera.integration.extensions.ExtensionTestType.TEST_TYPE_CAMERA2_EXTENSION 32 import androidx.camera.integration.extensions.ExtensionTestType.TEST_TYPE_CAMERA2_EXTENSION_STREAM_CONFIG_LATENCY 33 import androidx.camera.integration.extensions.ExtensionTestType.TEST_TYPE_CAMERAX_EXTENSION 34 import androidx.camera.integration.extensions.TestResultType.TEST_RESULT_FAILED 35 import androidx.camera.integration.extensions.TestResultType.TEST_RESULT_NOT_SUPPORTED 36 import androidx.camera.integration.extensions.TestResultType.TEST_RESULT_NOT_TESTED 37 import androidx.camera.integration.extensions.TestResultType.TEST_RESULT_PASSED 38 import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.AVAILABLE_CAMERA2_EXTENSION_MODES 39 import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.getCamera2ExtensionModeIdFromString 40 import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.getCamera2ExtensionModeStringFromId 41 import androidx.camera.integration.extensions.utils.Camera2ExtensionsUtil.isCamera2ExtensionModeSupported 42 import androidx.camera.integration.extensions.utils.CameraSelectorUtil.createCameraSelectorById 43 import androidx.camera.integration.extensions.utils.ExtensionModeUtil.AVAILABLE_EXTENSION_MODES 44 import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeIdFromString 45 import androidx.camera.integration.extensions.utils.ExtensionModeUtil.getExtensionModeStringFromId 46 import androidx.camera.integration.extensions.utils.FileUtil.copyTempFileToOutputLocation 47 import androidx.camera.lifecycle.ProcessCameraProvider 48 import androidx.core.net.toUri 49 import java.io.BufferedReader 50 import java.io.DataInputStream 51 import java.io.File 52 import java.io.FileInputStream 53 import java.io.FileOutputStream 54 import java.io.InputStreamReader 55 import java.text.Format 56 import java.text.SimpleDateFormat 57 import java.util.Calendar 58 import java.util.Locale 59 60 private const val TAG = "TestResults" 61 62 private const val TEST_RESULTS_FILE_NAME = "TestResult.csv" 63 private const val TEST_RESULT_INDEX_TEST_TYPE = 0 64 private const val TEST_RESULT_INDEX_CAMERA_ID = 1 65 private const val TEST_RESULT_INDEX_EXTENSION_MODE = 2 66 private const val TEST_RESULT_INDEX_TEST_RESULT = 3 67 private const val TEST_RESULT_INDEX_DETAILS = 4 68 69 private const val TEST_RESULT_STRING_NOT_SUPPORTED = "NOT_SUPPORTED" 70 private const val TEST_RESULT_STRING_NOT_TESTED = "NOT_TESTED" 71 private const val TEST_RESULT_STRING_PASSED = "PASSED" 72 private const val TEST_RESULT_STRING_FAILED = "FAILED" 73 74 /** A class to load, save and export the test results. */ 75 @SuppressLint("RestrictedApiAndroidX") 76 class TestResults private constructor(val context: Context) { 77 78 /** Camera id to lens facing map. */ 79 private val cameraLensFacingMap = linkedMapOf<String, Int>() 80 81 /** Pair of <test type, camera id> to list of <extension mode, test result> map. */ 82 private val cameraExtensionResultMap = 83 linkedMapOf<Pair<String, String>, LinkedHashMap<Int, Pair<Int, String>>>() 84 85 fun loadTestResults( 86 cameraProvider: ProcessCameraProvider, 87 extensionsManager: ExtensionsManager 88 ) { 89 initTestResult(cameraProvider, extensionsManager) 90 refreshTestResultsFromFile() 91 } 92 93 fun getCameraLensFacingMap() = cameraLensFacingMap 94 95 fun getCameraExtensionResultMap() = cameraExtensionResultMap 96 97 /** Updates test result for specific item and save. */ 98 fun updateTestResultAndSave( 99 testType: String, 100 cameraId: String, 101 extensionMode: Int, 102 testResult: Int, 103 testResultDetails: String = "" 104 ) { 105 Log.d( 106 TAG, 107 "updateTestResultAndSave: testType: $testType, cameraId: $cameraId" + 108 ", extensionMode: $extensionMode, testResult: $testResult" + 109 ", testResultDetails: $testResultDetails" 110 ) 111 val results = cameraExtensionResultMap[Pair(testType, cameraId)] ?: linkedMapOf() 112 results[extensionMode] = Pair(testResult, testResultDetails) 113 saveTestResults() 114 } 115 116 /** 117 * Saves the test results. 118 * 119 * The input parameter is pair of <test type, camera id> to list of <extension mode, test 120 * result> map. 121 */ 122 private fun saveTestResults() { 123 val testResultsFile = File(context.getExternalFilesDir(null), TEST_RESULTS_FILE_NAME) 124 val outputStream = FileOutputStream(testResultsFile) 125 126 val headerString = "Test Type, Camera Id,Extension Mode,Test Result,Test Result Details\n" 127 outputStream.write(headerString.toByteArray()) 128 129 cameraExtensionResultMap.forEach { entry -> 130 val (testType, cameraId) = entry.key 131 entry.value.forEach { 132 val (extensionMode, testResult) = it 133 val extensionModeString = getExtensionModeStringFromId(testType, extensionMode) 134 val testResultString = getTestResultStringFromId(testResult.first) 135 val resultString = 136 "$testType,$cameraId,$extensionModeString,$testResultString" + 137 ",${testResult.second}\n" 138 outputStream.write(resultString.toByteArray()) 139 } 140 } 141 142 outputStream.close() 143 } 144 145 /** 146 * Exports the test results to a CSV file under the Documents folder. 147 * 148 * @return the file path if it is successful to export the test results. Otherwise, null will be 149 * returned. 150 */ 151 fun exportTestResults(contentResolver: ContentResolver): String? { 152 val testResultsFile = File(context.getExternalFilesDir(null), TEST_RESULTS_FILE_NAME) 153 if (!testResultsFile.exists()) { 154 Log.e(TAG, "Test result does not exist!") 155 return null 156 } 157 158 val formatter: Format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US) 159 val savedFileName = "TestResult[${formatter.format(Calendar.getInstance().time)}].csv" 160 161 val contentValues = 162 ContentValues().apply { 163 put(MediaStore.MediaColumns.DISPLAY_NAME, savedFileName) 164 put(MediaStore.MediaColumns.MIME_TYPE, "text/comma-separated-values") 165 put( 166 MediaStore.MediaColumns.RELATIVE_PATH, 167 "$DIRECTORY_DOCUMENTS/ExtensionsValidation" 168 ) 169 } 170 171 if ( 172 copyTempFileToOutputLocation( 173 contentResolver, 174 testResultsFile.toUri(), 175 MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), 176 contentValues 177 ) != null 178 ) { 179 return "$DIRECTORY_DOCUMENTS/ExtensionsValidation/$savedFileName" 180 } 181 182 return null 183 } 184 185 fun resetTestResults( 186 cameraProvider: ProcessCameraProvider, 187 extensionsManager: ExtensionsManager 188 ) { 189 val testResultsFile = File(context.getExternalFilesDir(null), TEST_RESULTS_FILE_NAME) 190 191 if (testResultsFile.exists()) { 192 testResultsFile.delete() 193 } 194 195 cameraExtensionResultMap.clear() 196 cameraLensFacingMap.clear() 197 initTestResult(cameraProvider, extensionsManager) 198 } 199 200 private fun initTestResult( 201 cameraProvider: ProcessCameraProvider, 202 extensionsManager: ExtensionsManager 203 ) { 204 val availableCameraIds = mutableListOf<String>() 205 206 cameraProvider.availableCameraInfos.forEach { 207 val cameraId = (it as CameraInfoInternal).cameraId 208 availableCameraIds.add(cameraId) 209 cameraLensFacingMap[cameraId] = cameraProvider.getLensFacingById(cameraId) 210 } 211 212 // Generates CameraX extension test items 213 availableCameraIds.forEach { cameraId -> 214 val testResultMap = linkedMapOf<Int, Pair<Int, String>>() 215 216 AVAILABLE_EXTENSION_MODES.forEach { mode -> 217 val isSupported = 218 extensionsManager.isExtensionAvailable(createCameraSelectorById(cameraId), mode) 219 220 testResultMap[mode] = 221 if (isSupported) Pair(TEST_RESULT_NOT_TESTED, "") 222 else Pair(TEST_RESULT_NOT_SUPPORTED, "") 223 } 224 225 cameraExtensionResultMap[Pair(TEST_TYPE_CAMERAX_EXTENSION, cameraId)] = testResultMap 226 } 227 228 if (Build.VERSION.SDK_INT < 31) { 229 return 230 } 231 232 // Generates Camera2 extension test items 233 availableCameraIds.forEach { cameraId -> 234 val testResultMap = linkedMapOf<Int, Pair<Int, String>>() 235 236 AVAILABLE_CAMERA2_EXTENSION_MODES.forEach { mode -> 237 val isSupported = isCamera2ExtensionModeSupported(context, cameraId, mode) 238 239 testResultMap[mode] = 240 if (isSupported) Pair(TEST_RESULT_NOT_TESTED, "") 241 else Pair(TEST_RESULT_NOT_SUPPORTED, "") 242 } 243 244 cameraExtensionResultMap[Pair(TEST_TYPE_CAMERA2_EXTENSION, cameraId)] = testResultMap 245 246 // Generates Camera2 extension performance test items 247 cameraExtensionResultMap[ 248 Pair(TEST_TYPE_CAMERA2_EXTENSION_STREAM_CONFIG_LATENCY, cameraId)] = 249 LinkedHashMap(testResultMap) 250 } 251 } 252 253 private fun refreshTestResultsFromFile() { 254 val testResultsFile = File(context.getExternalFilesDir(null), TEST_RESULTS_FILE_NAME) 255 256 if (!testResultsFile.exists()) { 257 return 258 } 259 260 val fileInputStream = FileInputStream(testResultsFile) 261 val dataInputStream = DataInputStream(fileInputStream) 262 val bufferedReader = BufferedReader(InputStreamReader(dataInputStream)) 263 264 var readHeader = false 265 var lineContent = "" 266 while ((bufferedReader.readLine()?.also { lineContent = it }) != null) { 267 if (!readHeader) { 268 readHeader = true 269 continue 270 } 271 272 val values = lineContent.split(",") 273 if (values.size !in (4..5)) { 274 throw IllegalArgumentException("Extensions validation test results parsing error!") 275 } 276 277 val testType = values[TEST_RESULT_INDEX_TEST_TYPE] 278 val cameraId = values[TEST_RESULT_INDEX_CAMERA_ID] 279 val extensionResultMap = cameraExtensionResultMap[Pair(testType, cameraId)] 280 val mode = 281 getExtensionModeIdFromString(testType, values[TEST_RESULT_INDEX_EXTENSION_MODE]) 282 283 val result = getTestResultIdFromString(values[TEST_RESULT_INDEX_TEST_RESULT]) 284 val resultDetails = values.getOrElse(TEST_RESULT_INDEX_DETAILS) { "" } 285 286 extensionResultMap?.set(mode, Pair(result, resultDetails)) 287 } 288 289 fileInputStream.close() 290 } 291 292 private fun ProcessCameraProvider.getLensFacingById(cameraId: String): Int { 293 availableCameraInfos.forEach { 294 val cameraInfoInternal = it as CameraInfoInternal 295 296 if (cameraInfoInternal.cameraId == cameraId) { 297 return (cameraInfoInternal.cameraCharacteristics as CameraCharacteristics).get( 298 LENS_FACING 299 )!! 300 } 301 } 302 303 throw IllegalArgumentException("Can't retrieve lens facing info for camera $cameraId") 304 } 305 306 private fun getTestResultStringFromId(result: Int): String = 307 when (result) { 308 TEST_RESULT_NOT_SUPPORTED -> TEST_RESULT_STRING_NOT_SUPPORTED 309 TEST_RESULT_FAILED -> TEST_RESULT_STRING_FAILED 310 TEST_RESULT_PASSED -> TEST_RESULT_STRING_PASSED 311 else -> TEST_RESULT_STRING_NOT_TESTED 312 } 313 314 private fun getTestResultIdFromString(result: String): Int = 315 when (result) { 316 TEST_RESULT_STRING_NOT_SUPPORTED -> TEST_RESULT_NOT_SUPPORTED 317 TEST_RESULT_STRING_FAILED -> TEST_RESULT_FAILED 318 TEST_RESULT_STRING_PASSED -> TEST_RESULT_PASSED 319 else -> TEST_RESULT_NOT_TESTED 320 } 321 322 companion object { 323 private var instance: TestResults? = null 324 325 @JvmStatic 326 fun getInstance(context: Context): TestResults { 327 if (instance == null) { 328 instance = TestResults(context.applicationContext) 329 } 330 331 return instance!! 332 } 333 334 fun getExtensionModeStringFromId(testType: String, extensionMode: Int) = 335 if (testType == TEST_TYPE_CAMERAX_EXTENSION) { 336 getExtensionModeStringFromId(extensionMode) 337 } else if (testType == TEST_TYPE_CAMERA2_EXTENSION && Build.VERSION.SDK_INT >= 31) { 338 getCamera2ExtensionModeStringFromId(extensionMode) 339 } else if ( 340 testType == TEST_TYPE_CAMERA2_EXTENSION_STREAM_CONFIG_LATENCY && 341 Build.VERSION.SDK_INT >= 31 342 ) { 343 getCamera2ExtensionModeStringFromId(extensionMode) 344 } else { 345 throw RuntimeException( 346 "Something went wrong about testType ($testType) and device API level" + 347 " (${Build.VERSION.SDK_INT})." 348 ) 349 } 350 351 fun getExtensionModeIdFromString(testType: String, extensionModeString: String): Int = 352 if (testType == TEST_TYPE_CAMERAX_EXTENSION) { 353 getExtensionModeIdFromString(extensionModeString) 354 } else if (testType == TEST_TYPE_CAMERA2_EXTENSION && Build.VERSION.SDK_INT >= 31) { 355 getCamera2ExtensionModeIdFromString(extensionModeString) 356 } else if ( 357 testType == TEST_TYPE_CAMERA2_EXTENSION_STREAM_CONFIG_LATENCY && 358 Build.VERSION.SDK_INT >= 31 359 ) { 360 getCamera2ExtensionModeIdFromString(extensionModeString) 361 } else { 362 throw RuntimeException( 363 "Something went wrong about testType ($testType) and device API level" + 364 " (${Build.VERSION.SDK_INT})." 365 ) 366 } 367 } 368 } 369