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.uiwidgets.foldable 18 19 import android.content.ContentValues 20 import android.content.Context 21 import android.content.pm.PackageManager 22 import android.content.res.Configuration 23 import android.graphics.Point 24 import android.graphics.Rect 25 import android.hardware.camera2.CameraCharacteristics 26 import android.hardware.camera2.CameraManager 27 import android.hardware.display.DisplayManager 28 import android.os.Build 29 import android.os.Bundle 30 import android.provider.MediaStore 31 import android.util.Log 32 import android.view.Display 33 import android.view.GestureDetector 34 import android.view.GestureDetector.SimpleOnGestureListener 35 import android.view.Menu 36 import android.view.MenuItem 37 import android.view.MotionEvent 38 import android.view.ScaleGestureDetector 39 import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener 40 import android.view.Surface 41 import android.view.View 42 import android.view.ViewGroup.LayoutParams.MATCH_PARENT 43 import android.widget.Toast 44 import androidx.annotation.OptIn 45 import androidx.appcompat.app.AppCompatActivity 46 import androidx.appcompat.widget.PopupMenu 47 import androidx.camera.camera2.interop.Camera2CameraInfo 48 import androidx.camera.camera2.interop.ExperimentalCamera2Interop 49 import androidx.camera.core.CameraInfo 50 import androidx.camera.core.CameraSelector 51 import androidx.camera.core.FocusMeteringAction 52 import androidx.camera.core.ImageCapture 53 import androidx.camera.core.ImageCaptureException 54 import androidx.camera.core.MeteringPointFactory 55 import androidx.camera.integration.uiwidgets.R 56 import androidx.camera.integration.uiwidgets.databinding.ActivityFoldableCameraBinding 57 import androidx.camera.integration.uiwidgets.rotations.CameraActivity.Companion.PERMISSIONS 58 import androidx.camera.view.LifecycleCameraController 59 import androidx.camera.view.PreviewView 60 import androidx.core.app.ActivityCompat 61 import androidx.core.content.ContextCompat 62 import androidx.lifecycle.lifecycleScope 63 import androidx.window.layout.DisplayFeature 64 import androidx.window.layout.FoldingFeature 65 import androidx.window.layout.WindowInfoTracker 66 import androidx.window.layout.WindowLayoutInfo 67 import androidx.window.layout.WindowMetrics 68 import androidx.window.layout.WindowMetricsCalculator 69 import kotlinx.coroutines.flow.collect 70 import kotlinx.coroutines.launch 71 72 class FoldableCameraActivity : AppCompatActivity() { 73 companion object { 74 private const val TAG = "FoldableCameraActivity" 75 private const val REQUEST_CODE_PERMISSIONS = 20 76 private const val KEY_CAMERA_SELECTOR = "CameraSelectorStr" 77 private const val KEY_SCALETYPE = "ScaleType" 78 private const val BACK_CAMERA_STR = "Back camera" 79 private const val FRONT_CAMERA_STR = "Front camera" 80 } 81 82 private lateinit var binding: ActivityFoldableCameraBinding 83 private lateinit var windowInfoTracker: WindowInfoTracker 84 private var currentCameraSelectorString = BACK_CAMERA_STR 85 private lateinit var cameraController: LifecycleCameraController 86 private var isPreviewInLeftTop = true 87 private var activeWindowLayoutInfo: WindowLayoutInfo? = null 88 private val lastWindowMetrics: WindowMetrics 89 get() = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(this) 90 91 override fun onCreate(savedInstanceState: Bundle?) { 92 super.onCreate(savedInstanceState) 93 binding = ActivityFoldableCameraBinding.inflate(layoutInflater) 94 cameraController = LifecycleCameraController(this) 95 binding.previewView.controller = cameraController 96 setContentView(binding.root) 97 savedInstanceState?.let { 98 currentCameraSelectorString = it.getString(KEY_CAMERA_SELECTOR) ?: BACK_CAMERA_STR 99 cameraController.cameraSelector = 100 getCameraSelectorFromString(currentCameraSelectorString) 101 binding.previewView.scaleType = 102 PreviewView.ScaleType.valueOf(it.getString(KEY_SCALETYPE)!!) 103 } 104 windowInfoTracker = WindowInfoTracker.getOrCreate(this) 105 106 if (shouldRequestPermissionsAtRuntime() && !hasPermissions()) { 107 ActivityCompat.requestPermissions(this, PERMISSIONS, REQUEST_CODE_PERMISSIONS) 108 } else { 109 startCamera() 110 } 111 } 112 113 override fun onConfigurationChanged(newConfig: Configuration) { 114 super.onConfigurationChanged(newConfig) 115 showCamerasAndDisplayInfo() 116 } 117 118 override fun onSaveInstanceState(outState: Bundle) { 119 super.onSaveInstanceState(outState) 120 outState.putString(KEY_CAMERA_SELECTOR, currentCameraSelectorString) 121 outState.putString(KEY_SCALETYPE, binding.previewView.scaleType.toString()) 122 } 123 124 override fun onRequestPermissionsResult( 125 requestCode: Int, 126 permissions: Array<out String>, 127 grantResults: IntArray 128 ) { 129 super.onRequestPermissionsResult(requestCode, permissions, grantResults) 130 if (requestCode == REQUEST_CODE_PERMISSIONS) { 131 if (hasPermissions()) { 132 startCamera() 133 } else { 134 Log.d(TAG, "Camera permission is required") 135 finish() 136 } 137 } 138 } 139 140 override fun onCreateOptionsMenu(menu: Menu): Boolean { 141 menuInflater.inflate(R.menu.foldable_menu, menu) 142 return super.onCreateOptionsMenu(menu) 143 } 144 145 override fun onPrepareOptionsMenu(menu: Menu): Boolean { 146 menu 147 .findItem(R.id.implementationMode) 148 ?.setTitle("Current impl: ${binding.previewView.implementationMode}") 149 return super.onPrepareOptionsMenu(menu) 150 } 151 152 override fun onOptionsItemSelected(item: MenuItem): Boolean { 153 when (item.itemId) { 154 R.id.implementationMode -> { 155 binding.previewView.implementationMode = 156 when (binding.previewView.implementationMode) { 157 PreviewView.ImplementationMode.PERFORMANCE -> 158 PreviewView.ImplementationMode.COMPATIBLE 159 else -> PreviewView.ImplementationMode.PERFORMANCE 160 } 161 // Reset controller so the new implementation mode will be effective. 162 binding.previewView.controller = null 163 binding.previewView.controller = cameraController 164 } 165 R.id.fitCenter -> binding.previewView.scaleType = PreviewView.ScaleType.FIT_CENTER 166 R.id.fillCenter -> binding.previewView.scaleType = PreviewView.ScaleType.FILL_CENTER 167 R.id.fitStart -> binding.previewView.scaleType = PreviewView.ScaleType.FIT_START 168 R.id.fitEnd -> binding.previewView.scaleType = PreviewView.ScaleType.FIT_END 169 } 170 return super.onOptionsItemSelected(item) 171 } 172 173 private fun startCamera() { 174 lifecycleScope.launch { 175 showCamerasAndDisplayInfo() 176 cameraController.bindToLifecycle(this@FoldableCameraActivity) 177 setupUI() 178 } 179 180 showCamerasAndDisplayInfo() 181 182 // Runs Flow.collect in separate coroutine because it will block the coroutine. 183 lifecycleScope.launch { 184 windowInfoTracker.windowLayoutInfo(this@FoldableCameraActivity).collect { newLayoutInfo 185 -> 186 Log.d(TAG, "newLayoutInfo: $newLayoutInfo") 187 activeWindowLayoutInfo = newLayoutInfo 188 adjustPreviewByFoldingState() 189 } 190 } 191 } 192 193 private fun setupUI() { 194 binding.btnTakePicture.setOnClickListener { 195 val contentValues = ContentValues() 196 contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") 197 val outputFileOptions = 198 ImageCapture.OutputFileOptions.Builder( 199 contentResolver, 200 MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 201 contentValues 202 ) 203 .build() 204 205 cameraController.takePicture( 206 outputFileOptions, 207 ContextCompat.getMainExecutor(this), 208 object : ImageCapture.OnImageSavedCallback { 209 override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { 210 Toast.makeText( 211 this@FoldableCameraActivity, 212 "Image captured successfully", 213 Toast.LENGTH_SHORT 214 ) 215 .show() 216 } 217 218 override fun onError(exception: ImageCaptureException) { 219 Toast.makeText( 220 this@FoldableCameraActivity, 221 "Failed to capture", 222 Toast.LENGTH_SHORT 223 ) 224 .show() 225 } 226 } 227 ) 228 } 229 230 binding.btnSwitchCamera.setOnClickListener { showSwitchCameraMenu() } 231 232 val tapGestureDetector = GestureDetector(this, onTapGestureListener) 233 val scaleDetector = ScaleGestureDetector(this, mScaleGestureListener) 234 binding.previewView.setOnTouchListener { _, event -> 235 val tapEventProcessed = tapGestureDetector.onTouchEvent(event) 236 val scaleEventProcessed = scaleDetector.onTouchEvent(event) 237 tapEventProcessed || scaleEventProcessed 238 } 239 240 binding.btnSwitchArea.setOnClickListener { 241 isPreviewInLeftTop = !isPreviewInLeftTop 242 adjustPreviewByFoldingState() 243 } 244 } 245 246 private val mScaleGestureListener: SimpleOnScaleGestureListener = 247 object : SimpleOnScaleGestureListener() { 248 override fun onScale(detector: ScaleGestureDetector): Boolean { 249 val cameraInfo = cameraController.cameraInfo 250 val newZoom = cameraInfo!!.zoomState.value!!.zoomRatio * detector.scaleFactor 251 cameraController.setZoomRatio(newZoom) 252 return true 253 } 254 } 255 private val onTapGestureListener: GestureDetector.OnGestureListener = 256 object : SimpleOnGestureListener() { 257 override fun onSingleTapUp(e: MotionEvent): Boolean { 258 val factory: MeteringPointFactory = binding.previewView.meteringPointFactory 259 val action = FocusMeteringAction.Builder(factory.createPoint(e.x, e.y)).build() 260 261 val future = cameraController.cameraControl!!.startFocusAndMetering(action) 262 future.addListener({}, { v -> v.run() }) 263 return true 264 } 265 } 266 267 private fun adjustPreviewByFoldingState() { 268 val previewView = binding.previewView 269 val btnSwitchArea = binding.btnSwitchArea 270 activeWindowLayoutInfo 271 ?.displayFeatures 272 ?.firstOrNull { it is FoldingFeature } 273 ?.let { 274 val rect = 275 getFeaturePositionInViewRect(it, previewView.parent as View) ?: return@let 276 val foldingFeature = it as FoldingFeature 277 if (foldingFeature.state == FoldingFeature.State.HALF_OPENED) { 278 btnSwitchArea.visibility = View.VISIBLE 279 when (foldingFeature.orientation) { 280 FoldingFeature.Orientation.VERTICAL -> { 281 if (isPreviewInLeftTop) { 282 previewView.moveToLeftOf(rect) 283 val blankAreaWidth = 284 (btnSwitchArea.parent as View).width - rect.right 285 btnSwitchArea.x = 286 rect.right + (blankAreaWidth - btnSwitchArea.width) / 2f 287 btnSwitchArea.y = (previewView.height - btnSwitchArea.height) / 2f 288 } else { 289 previewView.moveToRightOf(rect) 290 btnSwitchArea.x = (rect.left - btnSwitchArea.width) / 2f 291 btnSwitchArea.y = (previewView.height - btnSwitchArea.height) / 2f 292 } 293 } 294 FoldingFeature.Orientation.HORIZONTAL -> { 295 if (isPreviewInLeftTop) { 296 previewView.moveToTopOf(rect) 297 val blankAreaHeight = 298 (btnSwitchArea.parent as View).height - rect.bottom 299 btnSwitchArea.x = (previewView.width - btnSwitchArea.width) / 2f 300 btnSwitchArea.y = 301 rect.bottom + (blankAreaHeight - btnSwitchArea.height) / 2f 302 } else { 303 previewView.moveToBottomOf(rect) 304 btnSwitchArea.x = (previewView.width - btnSwitchArea.width) / 2f 305 btnSwitchArea.y = (rect.top - btnSwitchArea.height) / 2f 306 } 307 } 308 } 309 } else { 310 previewView.restore() 311 btnSwitchArea.x = 0f 312 btnSwitchArea.y = 0f 313 btnSwitchArea.visibility = View.INVISIBLE 314 } 315 showCamerasAndDisplayInfo() 316 } 317 } 318 319 private fun View.moveToLeftOf(foldingFeatureRect: Rect) { 320 x = 0f 321 layoutParams = layoutParams.apply { width = foldingFeatureRect.left } 322 } 323 324 private fun View.moveToRightOf(foldingFeatureRect: Rect) { 325 x = foldingFeatureRect.left.toFloat() 326 layoutParams = 327 layoutParams.apply { width = (parent as View).width - foldingFeatureRect.left } 328 } 329 330 private fun View.moveToTopOf(foldingFeatureRect: Rect) { 331 y = 0f 332 layoutParams = layoutParams.apply { height = foldingFeatureRect.top } 333 } 334 335 private fun View.moveToBottomOf(foldingFeatureRect: Rect) { 336 y = foldingFeatureRect.top.toFloat() 337 layoutParams = 338 layoutParams.apply { height = (parent as View).height - foldingFeatureRect.top } 339 } 340 341 private fun View.restore() { 342 // Restore to full view 343 layoutParams = 344 layoutParams.apply { 345 width = MATCH_PARENT 346 height = MATCH_PARENT 347 } 348 y = 0f 349 x = 0f 350 } 351 352 private fun shouldRequestPermissionsAtRuntime(): Boolean { 353 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M 354 } 355 356 private fun hasPermissions(): Boolean { 357 return PERMISSIONS.all { 358 ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED 359 } 360 } 361 362 private val Display.rotationString: String 363 get() { 364 return when (rotation) { 365 Surface.ROTATION_0 -> "0" 366 Surface.ROTATION_90 -> "90" 367 Surface.ROTATION_180 -> "180" 368 Surface.ROTATION_270 -> "270" 369 else -> "unknown:$rotation" 370 } 371 } 372 373 @Suppress("DEPRECATION") 374 private fun showCamerasAndDisplayInfo() { 375 var totalMsg = "" 376 val cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager 377 val displayManager = getSystemService(Context.DISPLAY_SERVICE) as DisplayManager 378 for (display in displayManager.displays) { 379 val realPt = Point() 380 display?.getRealSize(realPt) 381 totalMsg += 382 "Display(${display.displayId}) size=(${realPt.x},${realPt.y}) " + 383 "rot=${display.rotationString}\n" 384 } 385 386 totalMsg += "WindowMetrics=${lastWindowMetrics.bounds}\n" 387 388 for (id in cameraManager.cameraIdList) { 389 val characteristics = cameraManager.getCameraCharacteristics(id) 390 val msg = 391 "[$id] ${characteristics.lensFacing} " + 392 "${characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION)} degrees\n" + 393 " array = " + 394 "${characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE)}\n" + 395 " focal length = [${characteristics.focalLength}]\n" 396 totalMsg += msg 397 } 398 399 binding.cameraInfo.text = totalMsg 400 } 401 402 private fun showSwitchCameraMenu() { 403 val popup = PopupMenu(this, binding.btnSwitchCamera) 404 popup.menu.add(0, 0, 0, BACK_CAMERA_STR) 405 popup.menu.add(0, 0, 0, FRONT_CAMERA_STR) 406 val cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager 407 for (id in cameraManager.cameraIdList) { 408 popup.menu.add(0, 0, 0, "$id") 409 } 410 popup.show() 411 412 popup.setOnMenuItemClickListener { menuItem -> 413 currentCameraSelectorString = menuItem.title as String 414 cameraController.cameraSelector = 415 getCameraSelectorFromString(currentCameraSelectorString) 416 true 417 } 418 } 419 420 @OptIn(ExperimentalCamera2Interop::class) 421 private fun getCameraSelectorFromString(cameraSelectorStr: String): CameraSelector = 422 when (cameraSelectorStr) { 423 BACK_CAMERA_STR -> CameraSelector.DEFAULT_BACK_CAMERA 424 FRONT_CAMERA_STR -> CameraSelector.DEFAULT_FRONT_CAMERA 425 else -> 426 CameraSelector.Builder() 427 .addCameraFilter { 428 for (cameraInfo in it) { 429 if (Camera2CameraInfo.from(cameraInfo).cameraId == cameraSelectorStr) { 430 return@addCameraFilter listOf(cameraInfo) 431 } 432 } 433 return@addCameraFilter emptyList<CameraInfo>() 434 } 435 .build() 436 } 437 438 private val CameraCharacteristics.lensFacing: String 439 get() = 440 when (this.get(CameraCharacteristics.LENS_FACING)) { 441 CameraCharacteristics.LENS_FACING_BACK -> "BACK" 442 CameraCharacteristics.LENS_FACING_FRONT -> "FRONT" 443 CameraCharacteristics.LENS_FACING_EXTERNAL -> "EXTERNAL" 444 else -> "UNKNOWN" 445 } 446 447 private val CameraCharacteristics.focalLength: String 448 get() { 449 val focalLengths = this.get(CameraCharacteristics.LENS_INFO_AVAILABLE_FOCAL_LENGTHS) 450 if (focalLengths == null || focalLengths.isEmpty()) { 451 return "NONE" 452 } 453 return focalLengths.joinToString(",") 454 } 455 456 /** 457 * Gets the bounds of the display feature translated to the View's coordinate space and current 458 * position in the window. This will also include view padding in the calculations. 459 * 460 * Copied from windowManager Jetpack library sample codes. 461 * https://github.com/android/user-interface-samples/tree/main/WindowManager 462 */ 463 fun getFeaturePositionInViewRect( 464 displayFeature: DisplayFeature, 465 view: View, 466 includePadding: Boolean = true 467 ): Rect? { 468 // The location of the view in window to be in the same coordinate space as the feature. 469 val viewLocationInWindow = IntArray(2) 470 view.getLocationInWindow(viewLocationInWindow) 471 472 // Intersect the feature rectangle in window with view rectangle to clip the bounds. 473 val viewRect = 474 Rect( 475 viewLocationInWindow[0], 476 viewLocationInWindow[1], 477 viewLocationInWindow[0] + view.width, 478 viewLocationInWindow[1] + view.height 479 ) 480 481 // Include padding if needed 482 if (includePadding) { 483 viewRect.left += view.paddingLeft 484 viewRect.top += view.paddingTop 485 viewRect.right -= view.paddingRight 486 viewRect.bottom -= view.paddingBottom 487 } 488 489 val featureRectInView = Rect(displayFeature.bounds) 490 val intersects = featureRectInView.intersect(viewRect) 491 if ((featureRectInView.width() == 0 && featureRectInView.height() == 0) || !intersects) { 492 return null 493 } 494 495 // Offset the feature coordinates to view coordinate space start point 496 featureRectInView.offset(-viewLocationInWindow[0], -viewLocationInWindow[1]) 497 498 return featureRectInView 499 } 500 } 501