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