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