1 /*
<lambda>null2  * 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 package androidx.camera.integration.antelope
18 
19 import android.Manifest
20 import android.content.ClipData
21 import android.content.Context
22 import android.content.pm.ActivityInfo
23 import android.content.pm.PackageManager
24 import android.content.res.Configuration
25 import android.os.Build
26 import android.os.Bundle
27 import android.os.Environment
28 import android.os.Handler
29 import android.os.HandlerThread
30 import android.util.Log
31 import android.view.Menu
32 import android.view.MenuInflater
33 import android.view.MenuItem
34 import android.view.View
35 import android.view.WindowManager
36 import android.widget.Toast
37 import androidx.activity.result.contract.ActivityResultContracts.RequestPermission
38 import androidx.appcompat.app.AppCompatActivity
39 import androidx.camera.integration.antelope.cameracontrollers.camera2Abort
40 import androidx.camera.integration.antelope.cameracontrollers.cameraXAbort
41 import androidx.camera.integration.antelope.cameracontrollers.closeAllCameras
42 import androidx.camera.integration.antelope.databinding.ActivityMainBinding
43 import androidx.core.content.ContextCompat
44 import androidx.lifecycle.Observer
45 import androidx.lifecycle.ViewModelProvider
46 import androidx.test.espresso.idling.CountingIdlingResource
47 import java.io.File
48 
49 /** Main Antelope Activity */
50 class MainActivity : AppCompatActivity() {
51 
52     companion object {
53         /** Directory to save image files under sdcard/DCIM */
54         const val PHOTOS_DIR: String = "Antelope"
55         /** Directory to save .csv log files to under sdcard/Documents */
56         const val LOG_DIR: String = "Antelope"
57         /** Tag to include when using the logd function */
58         val LOG_TAG = "Antelope"
59 
60         /** Define "normal" focal length as 50.0mm */
61         const val NORMAL_FOCAL_LENGTH: Float = 50f
62         /** No aperture reference */
63         const val NO_APERTURE: Float = 0f
64         /** Fixed-focus lenses have a value of 0 */
65         const val FIXED_FOCUS_DISTANCE: Float = 0f
66         /** Constant for invalid focal length */
67         val INVALID_FOCAL_LENGTH: Float = Float.MAX_VALUE
68         /** For single tests, percentage completion to show in progress bar when test is running */
69         const val PROGRESS_SINGLE_PERCENTAGE = 25
70 
71         /** List of test results for current test run */
72         internal val testRun: ArrayList<TestResults> = ArrayList<TestResults>()
73         /** List of test configurations for a multiple test run */
74         internal val autoTestConfigs: ArrayList<TestConfig> = ArrayList()
75 
76         /** Flag if a single test is running */
77         var isSingleTestRunning = false
78         /** Number of test remaining in a multiple test run */
79         var testsRemaining = 0
80 
81         /** View model that contains state data for the application */
82         lateinit var camViewModel: CamViewModel
83 
84         /** Hashmap of CameraParams for all cameras on the device */
85         lateinit var cameraParams: HashMap<String, CameraParams>
86         /** Convenience access to device information, OS build, etc. */
87         lateinit var deviceInfo: DeviceInfo
88 
89         /** Array of human-readable information for each camera on this device */
90         val cameras: ArrayList<String> = ArrayList<String>()
91         /** Array of camera ids for this device */
92         val cameraIds: ArrayList<String> = ArrayList<String>()
93 
94         /** Idling Resource used for Espresso tests */
95         public val antelopeIdlingResource = CountingIdlingResource("AntelopeIdlingResource")
96 
97         val PHOTOS_PATH = Environment.DIRECTORY_DCIM + File.separatorChar + PHOTOS_DIR
98         val LOG_PATH = Environment.DIRECTORY_DOCUMENTS + File.separatorChar + LOG_DIR
99 
100         /** Convenience wrapper for Log.d that can be toggled on/off */
101         fun logd(message: String) {
102             if (camViewModel.getShouldOutputLog().value ?: false) Log.d(LOG_TAG, message)
103         }
104     }
105 
106     private val requestPermission =
107         registerForActivityResult(RequestPermission()) { granted ->
108             if (granted) {
109                 // We now have permission, restart the app
110                 val intent = this.intent
111                 finish()
112                 startActivity(intent)
113             }
114         }
115 
116     lateinit var binding: ActivityMainBinding
117 
118     /** Check camera permissions and set up UI */
119     override fun onCreate(savedInstanceState: Bundle?) {
120         super.onCreate(savedInstanceState)
121 
122         binding = ActivityMainBinding.inflate(layoutInflater)
123         setContentView(binding.root)
124 
125         camViewModel = ViewModelProvider(this).get(CamViewModel::class.java)
126         cameraParams = camViewModel.getCameraParams()
127         deviceInfo = DeviceInfo()
128 
129         if (checkCameraPermissions()) {
130             initializeCameras(this)
131             setupCameraNames()
132         }
133 
134         binding.buttonSingle.setOnClickListener {
135             val testDiag =
136                 SettingsDialog.newInstance(
137                     SettingsDialog.DIALOG_TYPE_SINGLE,
138                     getString(R.string.settings_single_test_dialog_title),
139                     cameras.toTypedArray(),
140                     cameraIds.toTypedArray()
141                 )
142             testDiag.show(supportFragmentManager, SettingsDialog.DIALOG_TYPE_SINGLE)
143         }
144 
145         binding.buttonMulti.setOnClickListener {
146             val testDiag =
147                 SettingsDialog.newInstance(
148                     SettingsDialog.DIALOG_TYPE_MULTI,
149                     getString(R.string.settings_multi_test_dialog_title),
150                     cameras.toTypedArray(),
151                     cameraIds.toTypedArray()
152                 )
153             testDiag.show(supportFragmentManager, SettingsDialog.DIALOG_TYPE_MULTI)
154         }
155 
156         binding.buttonAbort.setOnClickListener { abortTests() }
157 
158         // Human readable report
159         val humanReadableReportObserver =
160             Observer<String> { newReport -> binding.textLog.text = newReport }
161         camViewModel.getHumanReadableReport().observe(this, humanReadableReportObserver)
162     }
163 
164     /** Set up options menu to allow debug logging and clearing cache'd data */
165     override fun onCreateOptionsMenu(menu: Menu): Boolean {
166         val inflater: MenuInflater = menuInflater
167         inflater.inflate(R.menu.main_menu, menu)
168         if (camViewModel.getShouldOutputLog().value != null)
169             menu.getItem(0).isChecked = camViewModel.getShouldOutputLog().value!!
170         return true
171     }
172 
173     /** Handle menu presses */
174     override fun onOptionsItemSelected(item: MenuItem): Boolean {
175         return when (item.itemId) {
176             R.id.menu_logcat -> {
177                 item.isChecked = !item.isChecked
178                 camViewModel.getShouldOutputLog().value = item.isChecked
179                 true
180             }
181             R.id.menu_delete_photos -> {
182                 deleteTestPhotos(this)
183                 true
184             }
185             R.id.menu_delete_logs -> {
186                 deleteCSVFiles(this)
187                 true
188             }
189             else -> super.onOptionsItemSelected(item)
190         }
191     }
192 
193     /**
194      * Update the main scrollview text
195      *
196      * @param log The new text
197      * @param append Whether to append the new text or to replace the old
198      * @param copyToClipboard Whether or not to copy the text to the system clipboard
199      */
200     fun updateLog(log: String, append: Boolean = false, copyToClipboard: Boolean = true) {
201         runOnUiThread {
202             if (append)
203                 camViewModel.getHumanReadableReport().value =
204                     camViewModel.getHumanReadableReport().value + log
205             else camViewModel.getHumanReadableReport().value = log
206         }
207 
208         if (copyToClipboard) {
209             runOnUiThread {
210                 // Copy to clipboard
211                 val clipboard =
212                     getSystemService(Context.CLIPBOARD_SERVICE) as android.content.ClipboardManager
213                 val clip = ClipData.newPlainText("Log", log)
214                 clipboard.setPrimaryClip(clip)
215                 Toast.makeText(this, getString(R.string.log_copied), Toast.LENGTH_SHORT).show()
216             }
217         }
218     }
219 
220     /** Create human readable names for the camera devices */
221     private fun setupCameraNames() {
222         cameras.clear()
223         cameraIds.clear()
224         for (param in cameraParams) {
225             var camera = ""
226 
227             camera += param.value.id
228             cameraIds += param.value.id
229 
230             if (param.value.isFront) camera += " (Front)"
231             else if (param.value.isExternal) camera += " (External)" else camera += " (Back)"
232 
233             camera += " " + param.value.megapixels + "MP"
234 
235             if (!param.value.hasAF) camera += " fixed-focus"
236 
237             camera += " (min FL: " + param.value.smallestFocalLength + "mm)"
238             cameras.add(camera)
239         }
240     }
241 
242     /** Check if we have been granted the need camera and file-system permissions */
243     fun checkCameraPermissions(): Boolean {
244         if (
245             ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) !=
246                 PackageManager.PERMISSION_GRANTED
247         ) {
248             // Launch the permission request for CAMERA
249             requestPermission.launch(Manifest.permission.CAMERA)
250             return false
251         } else if (
252             Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU &&
253                 ContextCompat.checkSelfPermission(
254                     this,
255                     Manifest.permission.WRITE_EXTERNAL_STORAGE
256                 ) != PackageManager.PERMISSION_GRANTED
257         ) {
258             // Launch the permission request for WRITE_EXTERNAL_STORAGE. From Android T, skips to
259             // request WRITE_EXTERNAL_STORAGE permission since it won't be granted any more.
260             requestPermission.launch(Manifest.permission.WRITE_EXTERNAL_STORAGE)
261             return false
262         }
263 
264         return true
265     }
266 
267     /** Start the background threads associated with the given camera device/params */
268     fun startBackgroundThread(params: CameraParams) {
269         if (params.backgroundThread == null) {
270             params.backgroundThread =
271                 HandlerThread(LOG_TAG).apply {
272                     this.start()
273                     params.backgroundHandler = Handler(this.looper)
274                 }
275         }
276     }
277 
278     /** Stop the background threads associated with the given camera device/params */
279     fun stopBackgroundThread(params: CameraParams) {
280         params.backgroundThread?.quitSafely()
281         try {
282             params.backgroundThread?.join()
283             params.backgroundThread = null
284             params.backgroundHandler = null
285         } catch (e: InterruptedException) {
286             logd("Interrupted while shutting background thread down: " + e)
287         }
288     }
289 
290     /** Resume all background threads associated with any given camera devices/params */
291     override fun onResume() {
292         super.onResume()
293         for (tempCameraParams in cameraParams) {
294             startBackgroundThread(tempCameraParams.value)
295         }
296     }
297 
298     /** Pause all background threads associated with any camera devices/params */
299     override fun onPause() {
300         for (tempCameraParams in cameraParams) {
301             stopBackgroundThread(tempCameraParams.value)
302         }
303         super.onPause()
304     }
305 
306     /** Show/hide the progress bar during a test */
307     fun showProgressBar(visible: Boolean = true, percentage: Int = PROGRESS_SINGLE_PERCENTAGE) {
308         runOnUiThread {
309             if (visible) {
310                 binding.progressTest.progress = percentage
311                 binding.progressTest.visibility = View.VISIBLE
312             } else {
313                 binding.progressTest.progress = 0
314                 binding.progressTest.visibility = View.INVISIBLE
315             }
316         }
317     }
318 
319     /** Enable/disable controls during a test run */
320     fun toggleControls(enabled: Boolean = true) {
321         runOnUiThread {
322             binding.buttonMulti.isEnabled = enabled
323             binding.buttonSingle.isEnabled = enabled
324             binding.buttonSingle.isEnabled = enabled
325             binding.buttonAbort.isEnabled = !enabled // note: inverse of others
326         }
327     }
328 
329     /** Lock orientation during a test so the camera doesn't get re-initialized mid-capture */
330     fun toggleRotationLock(lockRotation: Boolean = true) {
331         if (lockRotation) {
332             val currentOrientation = resources.configuration.orientation
333             if (currentOrientation == Configuration.ORIENTATION_LANDSCAPE) {
334                 requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE
335             } else {
336                 requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT
337             }
338         } else {
339             requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_USER
340         }
341     }
342 
343     /** Launch a single test based on the current configuration */
344     fun startSingleTest() {
345         testRun.clear()
346         testsRemaining = 1
347         isSingleTestRunning = true
348 
349         val config = createSingleTestConfig(this)
350         setupUIForTest(config, false)
351 
352         // Tell Espresso to wait until test run is complete
353         logd("Incrementing AntelopeIdlingResource")
354         antelopeIdlingResource.increment()
355 
356         initializeTest(this, cameraParams.get(config.camera), config)
357     }
358 
359     /** Launch a series of tests based on the current configuration */
360     fun startMultiTest() {
361         isSingleTestRunning = false
362         setupAutoTestRunner(this)
363 
364         // Tell Espresso to wait until test run is complete
365         logd("Incrementing AntelopeIdlingResource")
366         antelopeIdlingResource.increment()
367 
368         autoTestRunner(this)
369     }
370 
371     /** User has requested to abort the test run. Close cameras and reset the UI. */
372     fun abortTests() {
373         val currentConfig: TestConfig = createTestConfig("ABORT")
374 
375         val currentCamera = camViewModel.getCurrentCamera().value ?: 0
376         val currentParams = cameraParams.get(currentCamera.toString())
377 
378         when (currentConfig.api) {
379             CameraAPI.CAMERA1 -> closeAllCameras(this, currentConfig)
380             CameraAPI.CAMERAX -> {
381                 if (null != currentParams) cameraXAbort(this, currentParams, currentConfig)
382             }
383             CameraAPI.CAMERA2 -> {
384                 if (null != currentParams) camera2Abort(this, currentParams)
385             }
386         }
387 
388         testsRemaining = 0
389         multiCounter = 0
390 
391         runOnUiThread {
392             toggleControls(true)
393             toggleRotationLock(false)
394             window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
395             binding.progressTest.progress = 0
396             showProgressBar(false)
397             updateLog("\nABORTED", true)
398         }
399 
400         // Indicate to Espresso that a test run has ended
401         try {
402             logd("Decrementing AntelopeIdlingResource")
403             antelopeIdlingResource.decrement()
404         } catch (ex: IllegalStateException) {
405             logd("Antelope idling resource decremented below 0. This should never happen.")
406         }
407     }
408 
409     /** After tests are completed, reset the UI to the initial state */
410     fun resetUIAfterTest() {
411         runOnUiThread {
412             toggleControls(true)
413             toggleRotationLock(false)
414             showProgressBar(false)
415             window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
416         }
417     }
418 
419     /**
420      * Prepare the main UI for a test run. This includes showing/hiding the appropriate preview
421      * surface depending on if the test is Camera 1/2/X
422      */
423     internal fun setupUIForTest(testConfig: TestConfig, append: Boolean = true) {
424         with(testConfig) {
425             MainActivity.camViewModel.getCurrentAPI().postValue(this.api)
426             MainActivity.camViewModel.getCurrentImageCaptureSize().postValue(imageCaptureSize)
427             MainActivity.camViewModel.getCurrentCamera().postValue(camera.toInt())
428 
429             if (FocusMode.FIXED == focusMode)
430                 MainActivity.camViewModel.getCurrentFocusMode().postValue(FocusMode.AUTO)
431             else MainActivity.camViewModel.getCurrentFocusMode().postValue(focusMode)
432 
433             if (CameraAPI.CAMERAX == api) {
434                 binding.surfacePreview.visibility = View.INVISIBLE
435                 binding.texturePreview.visibility = View.VISIBLE
436             } else {
437                 binding.surfacePreview.visibility = View.VISIBLE
438                 binding.texturePreview.visibility = View.INVISIBLE
439             }
440 
441             toggleControls(false)
442             toggleRotationLock(true)
443             updateLog("Running: $testName\n", append, false)
444             binding.scrollLog.fullScroll(View.FOCUS_DOWN)
445             window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
446         }
447     }
448 }
449