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