1 /*
2 * Copyright 2019 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 @file:Suppress("DEPRECATION")
18
19 package androidx.camera.integration.antelope
20
21 import android.content.ContentResolver
22 import android.content.ContentValues
23 import android.content.Intent
24 import android.net.Uri
25 import android.os.Build
26 import android.os.Environment
27 import android.provider.MediaStore
28 import android.widget.Toast
29 import androidx.camera.integration.antelope.MainActivity.Companion.LOG_PATH
30 import androidx.camera.integration.antelope.MainActivity.Companion.logd
31 import com.google.common.math.Quantiles
32 import com.google.common.math.Stats
33 import java.io.BufferedWriter
34 import java.io.File
35 import java.io.FileOutputStream
36 import java.io.IOException
37 import java.io.OutputStream
38 import java.io.OutputStreamWriter
39 import java.text.SimpleDateFormat
40 import java.util.Calendar
41 import java.util.Date
42 import java.util.Locale
43
44 /**
45 * Contains the results for a specific test. Most of the variables are arrays to accommodate tests
46 * with multiple repetitions (MULTI_PHOTO, MULTI_PHOTO_CHAINED, MULTI_SWITCH, etc.)
47 */
48 class TestResults {
49 /** Name of test */
50 var testName: String = ""
51 /** Human readable camera name */
52 var camera: String = ""
53 /** Camera ID */
54 var cameraId: String = ""
55 /** Which API was used (1, 2, or X) */
56 var cameraAPI: CameraAPI = CameraAPI.CAMERA2
57 /** Image size that was requested */
58 var imageCaptureSize: ImageCaptureSize = ImageCaptureSize.MAX
59 /** Auto-focus, continuous focus, or fixed-foxus */
60 var focusMode: FocusMode = FocusMode.AUTO
61 /** Enum type of the test requested */
62 var testType: TestType = TestType.NONE
63 /** Time take to open camera */
64 var initialization: ArrayList<Long> = ArrayList<Long>()
65 /** Time taken for the preview to start */
66 var previewStart: ArrayList<Long> = ArrayList<Long>()
67 /** Time taken for the preview to run before next step in the test */
68 var previewFill: ArrayList<Long> = ArrayList<Long>()
69 /** Time taken to switch from first to second cameras */
70 var switchToSecond: ArrayList<Long> = ArrayList<Long>()
71 /** Time taken to switch from second to first cameras */
72 var switchToFirst: ArrayList<Long> = ArrayList<Long>()
73 /** Time taken for the auto-focus routine to complete */
74 var autofocus: ArrayList<Long> = ArrayList<Long>()
75 /** Time taken for image capture, not including auto-focus delay */
76 var captureNoAF: ArrayList<Long> = ArrayList<Long>()
77 /** Time taken for image capture, including auto-focus delay (if applicable) */
78 var capture: ArrayList<Long> = ArrayList<Long>()
79 /** Time taken after image is captured for it to be ready in the ImageReader */
80 var imageready: ArrayList<Long> = ArrayList<Long>()
81 /** Time taken for capture and the image to appear in the ImageReader */
82 var capturePlusImageReady: ArrayList<Long> = ArrayList<Long>()
83 /** Time taken to save image to disk */
84 var imagesave: ArrayList<Long> = ArrayList<Long>()
85 /** Time taken to close the preview stream */
86 var previewClose: ArrayList<Long> = ArrayList<Long>()
87 /** Time taken to close the camera */
88 var cameraClose: ArrayList<Long> = ArrayList<Long>()
89 /** Time taken for the entire test */
90 var total: ArrayList<Long> = ArrayList<Long>()
91 /** Time taken for the entire test not including filling the preview stream */
92 var totalNoPreview: ArrayList<Long> = ArrayList<Long>()
93 /** Was the image captured an HDR+ image? */
94 var isHDRPlus: ArrayList<Boolean> = ArrayList<Boolean>()
95
96 /** Format results into a human readable string */
toStringnull97 fun toString(activity: MainActivity, header: Boolean): String {
98 var output = ""
99
100 if (header) {
101 val dateFormatter = SimpleDateFormat("d MMM yyyy - kk'h'mm")
102 val cal: Calendar = Calendar.getInstance()
103 output +=
104 "DATE: " +
105 dateFormatter.format(cal.time) +
106 " (Antelope " +
107 getVersionName(activity) +
108 ")\n"
109
110 output += "DEVICE: " + MainActivity.deviceInfo.device + "\n\n"
111 output += "CAMERAS:\n"
112 for (camera in MainActivity.cameras) output += camera + "\n"
113 output += "\n"
114 }
115
116 output += testName + "\n"
117 output += "Camera: " + camera + "\n"
118 output += "API: " + cameraAPI + "\n"
119 output += "Focus Mode: " + focusMode + "\n"
120 output += "Image Capture Size: " + imageCaptureSize + "\n\n"
121
122 output += outputResultLine("Camera open", initialization)
123 output += outputResultLine("Preview start", previewStart)
124 output += outputResultLine("Preview buffer", previewFill)
125
126 when (focusMode) {
127 FocusMode.CONTINUOUS -> {
128 output += outputResultLine("Capture (continuous focus)", capture)
129 }
130 FocusMode.FIXED -> {
131 output += outputResultLine("Capture (fixed-focus)", capture)
132 }
133 else -> {
134 // CameraX doesn't allow us insight into autofocus
135 if (CameraAPI.CAMERAX == cameraAPI) {
136 output += outputResultLine("Capture incl. autofocus", capture)
137 } else {
138 output += outputResultLine("Autofocus", autofocus)
139 output += outputResultLine("Capture", captureNoAF)
140 output += outputResultLine("Capture incl. autofocus", capture)
141 }
142 }
143 }
144
145 output += outputResultLine("Image ready", imageready)
146 output += outputResultLine("Cap + img ready", capturePlusImageReady)
147 output += outputResultLine("Image save", imagesave)
148 output += outputResultLine("Switch to 2nd", switchToSecond)
149 output += outputResultLine("Switch to 1st", switchToFirst)
150 output += outputResultLine("Preview close", previewClose)
151 output += outputResultLine("Camera close", cameraClose)
152 output += outputBooleanResultLine("HDR+", isHDRPlus)
153 output += outputResultLine("Total", total)
154 output += outputResultLine("Total w/o preview buffer", totalNoPreview)
155
156 if (1 < capturePlusImageReady.size) {
157 val captureStats = Stats.of(capturePlusImageReady)
158 output += "Capture range: " + captureStats.min() + " - " + captureStats.max() + "\n"
159 output +=
160 "Capture mean: " +
161 captureStats.mean() +
162 " (" +
163 captureStats.count() +
164 " captures)\n"
165 output += "Capture median: " + Quantiles.median().compute(capturePlusImageReady) + "\n"
166 output += "Capture standard deviation: " + captureStats.sampleStandardDeviation() + "\n"
167 }
168 output += "Total batch time: " + Stats.of(total).sum() + "\n\n\n"
169 return output
170 }
171
172 /** Format results to a comma-based .csv string */
toCSVnull173 fun toCSV(activity: MainActivity, header: Boolean = true): String {
174 val numCommas = PrefHelper.getNumTests(activity)
175
176 var output = ""
177
178 if (header) {
179 val dateFormatter = SimpleDateFormat("d MMM yyyy - kk'h'mm")
180 val cal: Calendar = Calendar.getInstance()
181 output +=
182 "DATE: " +
183 dateFormatter.format(cal.time) +
184 " (Antelope " +
185 getVersionName(activity) +
186 ")" +
187 outputCommas(numCommas) +
188 "\n"
189
190 output +=
191 "DEVICE: " +
192 MainActivity.deviceInfo.device +
193 outputCommas(numCommas) +
194 "\n" +
195 outputCommas(numCommas) +
196 "\n"
197 output += "CAMERAS: " + outputCommas(numCommas) + "\n"
198 for (camera in MainActivity.cameras) output += camera + outputCommas(numCommas) + "\n"
199 output += outputCommas(numCommas) + "\n"
200 }
201
202 output += testName + outputCommas(numCommas) + outputCommas(numCommas) + "\n"
203 output += "Camera: " + camera + outputCommas(numCommas) + "\n"
204 output += "API: " + cameraAPI + outputCommas(numCommas) + "\n"
205 output += "Focus Mode: " + focusMode + outputCommas(numCommas) + "\n"
206 output +=
207 "Image Capture Size: " +
208 imageCaptureSize +
209 outputCommas(numCommas) +
210 "\n" +
211 outputCommas(numCommas) +
212 "\n"
213
214 output += outputResultLine("Camera open", initialization, numCommas, true)
215 output += outputResultLine("Preview start", previewStart, numCommas, true)
216 output += outputResultLine("Preview buffer", previewFill, numCommas, true)
217
218 when (focusMode) {
219 FocusMode.CONTINUOUS -> {
220 output += outputResultLine("Capture (continuous focus)", capture, numCommas, true)
221 }
222 FocusMode.FIXED -> {
223 output += outputResultLine("Capture (fixed-focus)", capture, numCommas, true)
224 }
225 else -> {
226 // CameraX doesn't allow us insight into autofocus
227 if (CameraAPI.CAMERAX == cameraAPI) {
228 output += outputResultLine("Capture incl. autofocus", capture, numCommas, true)
229 } else {
230 output += outputResultLine("Autofocus", autofocus, numCommas, true)
231 output += outputResultLine("Capture", captureNoAF, numCommas, true)
232 output += outputResultLine("Capture incl. autofocus", capture, numCommas, true)
233 }
234 }
235 }
236
237 output += outputResultLine("Image ready", imageready, numCommas, true)
238 output += outputResultLine("Cap + img ready", capturePlusImageReady, numCommas, true)
239 output += outputResultLine("Image save", imagesave, numCommas, true)
240 output += outputResultLine("Switch to 2nd", switchToSecond, numCommas, true)
241 output += outputResultLine("Switch to 1st", switchToFirst, numCommas, true)
242 output += outputResultLine("Preview close", previewClose, numCommas, true)
243 output += outputResultLine("Camera close", cameraClose, numCommas, true)
244 output += outputBooleanResultLine("HDR+", isHDRPlus, numCommas, true)
245 output += outputResultLine("Total", total, numCommas, true)
246 output += outputResultLine("Total w/o preview buffer", totalNoPreview, numCommas, true)
247
248 if (1 < capturePlusImageReady.size) {
249 val captureStats = Stats.of(capturePlusImageReady)
250 output +=
251 "Capture range:," +
252 captureStats.min() +
253 " - " +
254 captureStats.max() +
255 outputCommas(numCommas) +
256 "\n"
257 output +=
258 "Capture mean " +
259 " (" +
260 captureStats.count() +
261 " captures):," +
262 Stats.of(capturePlusImageReady).mean() +
263 outputCommas(numCommas) +
264 "\n"
265 output +=
266 "Capture median:," +
267 Quantiles.median().compute(capturePlusImageReady) +
268 outputCommas(numCommas) +
269 "\n"
270 output +=
271 "Capture standard deviation:," +
272 captureStats.sampleStandardDeviation() +
273 outputCommas(numCommas) +
274 "\n"
275 }
276
277 output += "Total batch time:," + Stats.of(total).sum() + outputCommas(numCommas) + "\n"
278
279 output += outputCommas(numCommas) + "\n"
280 output += outputCommas(numCommas) + "\n"
281 output += outputCommas(numCommas) + "\n"
282
283 return output
284 }
285 }
286
287 /**
288 * Write all results to disk in a .csv file
289 *
290 * @param activity The main activity
291 * @param filePrefix The prefix for the .csv file
292 * @param csv The comma-based csv string
293 */
writeCSVnull294 fun writeCSV(activity: MainActivity, filePrefix: String, csv: String) {
295 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
296 writeCSVAfterQ(activity, filePrefix, csv)
297 } else {
298 writeCSVBeforeQ(activity, filePrefix, csv)
299 }
300 }
301
302 /**
303 * When the platform is Android Pie and Pie below, Environment.getExternalStoragePublicDirectory
304 * (Environment.DIRECTORY_DOCUMENTS) can work. For Q, set requestLegacyExternalStorage = true to
305 * make it workable. Ref:
306 * https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage
307 */
writeCSVBeforeQnull308 fun writeCSVBeforeQ(activity: MainActivity, prefix: String, csv: String) {
309 val csvFile =
310 File(
311 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),
312 File.separatorChar +
313 MainActivity.LOG_DIR +
314 File.separatorChar +
315 prefix +
316 "_" +
317 generateCSVTimestamp() +
318 ".csv"
319 )
320
321 val csvDir =
322 File(
323 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),
324 MainActivity.LOG_DIR
325 )
326 val docsDir =
327 File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "")
328
329 if (!docsDir.exists()) {
330 val createSuccess = docsDir.mkdir()
331 if (!createSuccess) {
332 activity.runOnUiThread {
333 Toast.makeText(activity, "Documents" + " creation failed.", Toast.LENGTH_SHORT)
334 .show()
335 }
336 MainActivity.logd("Log storage directory Documents" + " creation failed!!")
337 } else {
338 MainActivity.logd("Log storage directory Documents" + " did not exist. Created.")
339 }
340 }
341
342 if (!csvDir.exists()) {
343 val createSuccess = csvDir.mkdir()
344 if (!createSuccess) {
345 activity.runOnUiThread {
346 Toast.makeText(
347 activity,
348 "Documents/" + MainActivity.LOG_DIR + " creation failed.",
349 Toast.LENGTH_SHORT
350 )
351 .show()
352 }
353 MainActivity.logd(
354 "Log storage directory Documents/" + MainActivity.LOG_DIR + " creation failed!!"
355 )
356 } else {
357 MainActivity.logd(
358 "Log storage directory Documents/" +
359 MainActivity.LOG_DIR +
360 " did not exist. Created."
361 )
362 }
363 }
364
365 val output = BufferedWriter(OutputStreamWriter(FileOutputStream(csvFile)))
366 try {
367 output.write(csv)
368 logd("CSV write completed successfully.")
369
370 // File is written, let media scanner know
371 val scannerIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
372 scannerIntent.data = Uri.fromFile(csvFile)
373 activity.sendBroadcast(scannerIntent)
374 } catch (e: IOException) {
375 logd("IOException Fail on CSV write: " + e.printStackTrace())
376 } finally {
377 try {
378 output.close()
379 } catch (e: IOException) {
380 logd("IOException Fail on CSV close: " + e.printStackTrace())
381 e.printStackTrace()
382 }
383 }
384 }
385
386 /**
387 * R and R above, change to use MediaStore to access the shared media files.
388 * https://developer.android.com/training/data-storage/shared
389 *
390 * @param activity The main activity
391 * @param prefix The prefix for the .csv file
392 * @param csv The comma-based csv string
393 */
writeCSVAfterQnull394 fun writeCSVAfterQ(activity: MainActivity, prefix: String, csv: String) {
395 var output: OutputStream?
396 val resolver: ContentResolver = activity.contentResolver
397 val contentValues =
398 ContentValues().apply {
399 put(
400 MediaStore.MediaColumns.DISPLAY_NAME,
401 prefix + "_" + generateCSVTimestamp() + ".csv"
402 )
403 put(MediaStore.MediaColumns.MIME_TYPE, "text/comma-separated-values")
404 put(MediaStore.MediaColumns.RELATIVE_PATH, LOG_PATH)
405 }
406
407 val csvUri =
408 resolver.insert(MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL), contentValues)
409 if (csvUri != null) {
410 lateinit var bufferWriter: BufferedWriter
411 try {
412 output = activity.contentResolver.openOutputStream(csvUri)
413 bufferWriter = BufferedWriter(OutputStreamWriter(output))
414 bufferWriter.write(csv)
415 logd("CSV write completed successfully.")
416 } catch (e: IOException) {
417 logd("IOException Fail on CSV write: " + e.printStackTrace())
418 } finally {
419 try {
420 bufferWriter.close()
421 } catch (e: IOException) {
422 logd("IOException Fail on CSV close: " + e.printStackTrace())
423 e.printStackTrace()
424 }
425 }
426 } else {
427 activity.runOnUiThread {
428 Toast.makeText(activity, "CSV log file creation failed.", Toast.LENGTH_SHORT).show()
429 }
430 }
431 }
432
433 /** Delete all Antelope .csv files in the Documents directory */
deleteCSVFilesnull434 fun deleteCSVFiles(activity: MainActivity) {
435 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
436 deleteCSVFilesAfterQ(activity)
437 } else {
438 deleteCSVFilesBeforeQ(activity)
439 }
440
441 activity.runOnUiThread {
442 Toast.makeText(activity, "CSV logs deleted", Toast.LENGTH_SHORT).show()
443 }
444 logd("All csv logs in directory DOCUMENTS/" + MainActivity.LOG_DIR + " deleted.")
445 }
446
447 /**
448 * R and R above, change to use MediaStore to delete the log files. It will delete records in media
449 * store and the physical log files.
450 */
deleteCSVFilesAfterQnull451 fun deleteCSVFilesAfterQ(activity: MainActivity) {
452 val logDirUri = MediaStore.Files.getContentUri(MediaStore.VOLUME_EXTERNAL)
453 val resolver: ContentResolver = activity.contentResolver
454 val selection = MediaStore.MediaColumns.RELATIVE_PATH + " like ?"
455 val selectionArgs = arrayOf("%$LOG_PATH%")
456
457 resolver.delete(logDirUri, selection, selectionArgs)
458 }
459
460 /**
461 * When the platform is Android Pie and Pie below, Environment.getExternalStoragePublicDirectory
462 * (Environment.DIRECTORY_DOCUMENTS) can work. For Q, set requestLegacyExternalStorage = true to
463 * make it workable. Ref:
464 * https://developer.android.com/training/data-storage/use-cases#opt-out-scoped-storage
465 */
deleteCSVFilesBeforeQnull466 fun deleteCSVFilesBeforeQ(activity: MainActivity) {
467 val csvDir =
468 File(
469 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS),
470 MainActivity.LOG_DIR
471 )
472
473 if (csvDir.exists()) {
474
475 for (csv in csvDir.listFiles()!!) csv.delete()
476
477 // Files are deleted, let media scanner know
478 val scannerIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
479 scannerIntent.data = Uri.fromFile(csvDir)
480 activity.sendBroadcast(scannerIntent)
481 }
482 }
483
484 /** Generate a timestamp for csv filenames */
generateCSVTimestampnull485 fun generateCSVTimestamp(): String {
486 val sdf = SimpleDateFormat("yyyy-MM-dd-HH'h'mm", Locale.US)
487 return sdf.format(Date())
488 }
489
490 /**
491 * Create a string consisting solely of the number of commas indicated
492 *
493 * Handy for properly formatting comma-based .csv files as the number of columns will depend on the
494 * user configurable number of test repetitions.
495 */
outputCommasnull496 fun outputCommas(numCommas: Int): String {
497 var output = ""
498 for (i in 1..numCommas) output += ","
499 return output
500 }
501
502 /** For a list of Longs, output a comma separated .csv line */
outputResultLinenull503 fun outputResultLine(
504 name: String,
505 results: ArrayList<Long>,
506 numCommas: Int = 30,
507 isCSV: Boolean = false
508 ): String {
509 var output = ""
510
511 if (!results.isEmpty()) {
512 output += name + ": "
513 for ((index, result) in results.withIndex()) {
514 if (isCSV || (0 != index)) output += ","
515 output += result
516 }
517 if (isCSV) output += outputCommas(numCommas - results.size)
518 output += "\n"
519 }
520
521 return output
522 }
523
524 /** For a list of Booleans, output a comma separated .csv line */
outputBooleanResultLinenull525 fun outputBooleanResultLine(
526 name: String,
527 results: ArrayList<Boolean>,
528 numCommas: Int = 30,
529 isCSV: Boolean = false
530 ): String {
531 var output = ""
532
533 // If every result is false, don't output this line at all
534 if (!results.isEmpty() && results.contains(true)) {
535 output += name + ": "
536 for ((index, result) in results.withIndex()) {
537 if (isCSV || (0 != index)) output += ","
538 if (result) output += "HDR+" else output += " - "
539 }
540 if (isCSV) output += outputCommas(numCommas - results.size)
541 output += "\n"
542 }
543
544 return output
545 }
546