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