1 /*
<lambda>null2  * Copyright 2020 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.rotations
18 
19 import android.Manifest
20 import android.content.ContentValues
21 import android.content.Intent
22 import android.content.pm.PackageManager
23 import android.os.Build
24 import android.os.Bundle
25 import android.provider.MediaStore
26 import android.text.TextUtils
27 import android.util.Log
28 import android.util.Size
29 import androidx.annotation.OptIn
30 import androidx.annotation.VisibleForTesting
31 import androidx.appcompat.app.AppCompatActivity
32 import androidx.camera.camera2.Camera2Config
33 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
34 import androidx.camera.core.Camera
35 import androidx.camera.core.CameraSelector
36 import androidx.camera.core.ImageAnalysis
37 import androidx.camera.core.ImageCapture
38 import androidx.camera.core.ImageCaptureException
39 import androidx.camera.core.ImageProxy
40 import androidx.camera.core.Preview
41 import androidx.camera.core.impl.utils.executor.CameraXExecutors
42 import androidx.camera.integration.uiwidgets.databinding.ActivityRotationsMainBinding
43 import androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration
44 import androidx.camera.lifecycle.ProcessCameraProvider
45 import androidx.core.app.ActivityCompat
46 import androidx.core.content.ContextCompat
47 import androidx.lifecycle.Lifecycle
48 import java.io.File
49 import java.io.FileOutputStream
50 import java.util.concurrent.ExecutorService
51 import java.util.concurrent.Executors
52 import java.util.concurrent.Semaphore
53 
54 open class CameraActivity : AppCompatActivity() {
55 
56     private lateinit var mBinding: ActivityRotationsMainBinding
57     private lateinit var mCamera: Camera
58     protected lateinit var mImageAnalysis: ImageAnalysis
59     protected lateinit var mImageCapture: ImageCapture
60     private lateinit var mAnalysisExecutor: ExecutorService
61 
62     override fun onCreate(savedInstanceState: Bundle?) {
63         super.onCreate(savedInstanceState)
64         mBinding = ActivityRotationsMainBinding.inflate(layoutInflater)
65         setContentView(mBinding.root)
66         mAnalysisExecutor = Executors.newSingleThreadExecutor()
67         if (shouldRequestPermissionsAtRuntime() && !hasPermissions()) {
68             ActivityCompat.requestPermissions(this, PERMISSIONS, REQUEST_CODE_PERMISSIONS)
69         } else {
70             setUpCamera()
71         }
72     }
73 
74     override fun onDestroy() {
75         super.onDestroy()
76         mAnalysisExecutor.shutdown()
77     }
78 
79     override fun onRequestPermissionsResult(
80         requestCode: Int,
81         permissions: Array<out String>,
82         grantResults: IntArray
83     ) {
84         super.onRequestPermissionsResult(requestCode, permissions, grantResults)
85         if (requestCode == REQUEST_CODE_PERMISSIONS) {
86             if (hasPermissions()) {
87                 setUpCamera()
88             } else {
89                 Log.d(TAG, "Camera permission is required")
90                 finish()
91             }
92         }
93     }
94 
95     private fun shouldRequestPermissionsAtRuntime(): Boolean {
96         return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
97     }
98 
99     private fun hasPermissions(): Boolean {
100         return PERMISSIONS.all {
101             ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED
102         }
103     }
104 
105     @OptIn(ExperimentalCameraProviderConfiguration::class)
106     private fun setUpCamera() {
107         val newCameraImpl = intent.getStringExtra(KEY_CAMERA_IMPLEMENTATION)
108         Log.d(TAG, "Set up cameraImpl: $newCameraImpl")
109         if (!TextUtils.isEmpty(newCameraImpl) && newCameraImpl != cameraImpl) {
110             try {
111                 Log.d(TAG, "ProcessCameraProvider initialize using $newCameraImpl")
112                 ProcessCameraProvider.configureInstance(
113                     when (newCameraImpl) {
114                         CAMERA2_IMPLEMENTATION_OPTION -> Camera2Config.defaultConfig()
115                         CAMERA_PIPE_IMPLEMENTATION_OPTION -> CameraPipeConfig.defaultConfig()
116                         else -> Camera2Config.defaultConfig()
117                     }
118                 )
119                 cameraImpl = newCameraImpl
120             } catch (e: IllegalStateException) {
121                 throw IllegalStateException(
122                     "WARNING: CameraX is currently configured to a different implementation " +
123                         "this would have resulted in unexpected behavior.",
124                     e
125                 )
126             }
127         }
128 
129         if (intent.getBooleanExtra(KEY_CAMERA_IMPLEMENTATION_NO_HISTORY, false)) {
130             intent =
131                 Intent(intent).apply {
132                     removeExtra(KEY_CAMERA_IMPLEMENTATION)
133                     removeExtra(KEY_CAMERA_IMPLEMENTATION_NO_HISTORY)
134                 }
135             cameraImpl = null
136         }
137 
138         val cameraProcessFuture = ProcessCameraProvider.getInstance(this)
139         cameraProcessFuture.addListener(
140             Runnable {
141                 val cameraProvider = cameraProcessFuture.get()
142                 if (lifecycle.currentState != Lifecycle.State.DESTROYED) {
143                     setUpCamera(cameraProvider)
144                 } else {
145                     Log.d(TAG, "Skip camera setup since activity is closed")
146                 }
147             },
148             ContextCompat.getMainExecutor(this)
149         )
150     }
151 
152     private fun setUpCamera(cameraProvider: ProcessCameraProvider) {
153         val preview =
154             Preview.Builder().build().apply {
155                 setSurfaceProvider(mBinding.previewView.getSurfaceProvider())
156             }
157         mImageAnalysis =
158             ImageAnalysis.Builder().build().apply {
159                 setAnalyzer(mAnalysisExecutor, createAnalyzer())
160             }
161         mImageCapture = ImageCapture.Builder().build().also { it.setCallback() }
162         mCamera =
163             cameraProvider.bindToLifecycle(
164                 this,
165                 getCameraSelector(),
166                 preview,
167                 mImageAnalysis,
168                 mImageCapture
169             )
170     }
171 
172     private fun getCameraSelector(): CameraSelector {
173         val lensFacing = intent.getIntExtra(KEY_LENS_FACING, CameraSelector.LENS_FACING_BACK)
174         return CameraSelector.Builder().requireLensFacing(lensFacing).build()
175     }
176 
177     private fun createAnalyzer(): ImageAnalysis.Analyzer {
178         return ImageAnalysis.Analyzer { imageProxy ->
179             mAnalysisImageRotation = imageProxy.imageInfo.rotationDegrees
180             mAnalysisRunning.release()
181             Log.d(TAG, "Analyzed image rotation = $mAnalysisImageRotation, $mAnalysisRunning")
182             imageProxy.close()
183         }
184     }
185 
186     private fun ImageCapture.setCallback() {
187         mBinding.previewView.setOnClickListener {
188             val imageCaptureMode =
189                 intent.getIntExtra(KEY_IMAGE_CAPTURE_MODE, IMAGE_CAPTURE_MODE_IN_MEMORY)
190             when (imageCaptureMode) {
191                 IMAGE_CAPTURE_MODE_IN_MEMORY -> setInMemoryCallback()
192                 IMAGE_CAPTURE_MODE_FILE -> setFileCallback()
193                 IMAGE_CAPTURE_MODE_OUTPUT_STREAM -> setOutputStreamCallback()
194                 IMAGE_CAPTURE_MODE_MEDIA_STORE -> setMediaStoreCallback()
195             }
196         }
197     }
198 
199     private fun ImageCapture.setInMemoryCallback() {
200         takePicture(
201             CameraXExecutors.mainThreadExecutor(),
202             object : ImageCapture.OnImageCapturedCallback() {
203                 override fun onCaptureSuccess(image: ImageProxy) {
204                     mCaptureResult = ImageCaptureResult.InMemory(image)
205                     mCaptureDone.release()
206                     image.close()
207                     Log.d(TAG, "InMemory image capture successful")
208                 }
209 
210                 override fun onError(exception: ImageCaptureException) {
211                     mCaptureDone.release()
212                     Log.e(TAG, "InMemory image capture failed", exception)
213                 }
214             }
215         )
216     }
217 
218     private fun ImageCapture.setFileCallback() {
219         val imageFile = File("${cacheDir.absolutePath}/${System.currentTimeMillis()}.jpg")
220         val outputFileOptions = ImageCapture.OutputFileOptions.Builder(imageFile).build()
221         takePicture(
222             outputFileOptions,
223             CameraXExecutors.mainThreadExecutor(),
224             object : ImageCapture.OnImageSavedCallback {
225                 override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
226                     mCaptureResult = ImageCaptureResult.FileOrOutputStream(imageFile)
227                     mCaptureDone.release()
228                     Log.d(TAG, "File image capture successful")
229                 }
230 
231                 override fun onError(exception: ImageCaptureException) {
232                     mCaptureDone.release()
233                     Log.e(TAG, "File image capture failed", exception)
234                 }
235             }
236         )
237     }
238 
239     private fun ImageCapture.setOutputStreamCallback() {
240         val imageFile = File("${cacheDir.absolutePath}/${System.currentTimeMillis()}.jpg")
241         val outputStream = FileOutputStream(imageFile)
242         val outputFileOptions = ImageCapture.OutputFileOptions.Builder(outputStream).build()
243         takePicture(
244             outputFileOptions,
245             CameraXExecutors.mainThreadExecutor(),
246             object : ImageCapture.OnImageSavedCallback {
247                 override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
248                     mCaptureResult = ImageCaptureResult.FileOrOutputStream(imageFile)
249                     mCaptureDone.release()
250                     Log.d(TAG, "OutputStream image capture successful")
251                 }
252 
253                 override fun onError(exception: ImageCaptureException) {
254                     mCaptureDone.release()
255                     Log.e(TAG, "OutputStream image capture failed", exception)
256                 }
257             }
258         )
259     }
260 
261     private fun ImageCapture.setMediaStoreCallback() {
262         val contentValues =
263             ContentValues().apply {
264                 put(MediaStore.MediaColumns.DISPLAY_NAME, "${System.currentTimeMillis()}")
265                 put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
266             }
267         val outputFileOptions =
268             ImageCapture.OutputFileOptions.Builder(
269                     contentResolver,
270                     MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
271                     contentValues
272                 )
273                 .build()
274         takePicture(
275             outputFileOptions,
276             CameraXExecutors.mainThreadExecutor(),
277             object : ImageCapture.OnImageSavedCallback {
278                 override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
279                     mCaptureResult =
280                         ImageCaptureResult.MediaStore(contentResolver, outputFileResults.savedUri!!)
281                     mCaptureDone.release()
282                     Log.d(TAG, "MediaStore image capture successful")
283                 }
284 
285                 override fun onError(exception: ImageCaptureException) {
286                     mCaptureDone.release()
287                     Log.e(TAG, "MediaStore image capture failed", exception)
288                 }
289             }
290         )
291     }
292 
293     protected fun isImageAnalysisInitialized(): Boolean {
294         return ::mImageAnalysis.isInitialized
295     }
296 
297     protected fun isImageCaptureInitialized(): Boolean {
298         return ::mImageCapture.isInitialized
299     }
300 
301     // region For testing
302     @VisibleForTesting val mAnalysisRunning = Semaphore(0)
303 
304     @VisibleForTesting var mAnalysisImageRotation = -1
305 
306     @VisibleForTesting val mCaptureDone = Semaphore(0)
307 
308     @VisibleForTesting var mCaptureResult: ImageCaptureResult? = null
309 
310     @VisibleForTesting
311     fun getSensorRotationRelativeToAnalysisTargetRotation(): Int {
312         val targetRotation = mImageAnalysis.targetRotation
313         return mCamera.cameraInfo.getSensorRotationDegrees(targetRotation)
314     }
315 
316     @VisibleForTesting
317     fun getSensorRotationRelativeToCaptureTargetRotation(): Int {
318         val targetRotation = mImageCapture.targetRotation
319         return mCamera.cameraInfo.getSensorRotationDegrees(targetRotation)
320     }
321 
322     @VisibleForTesting
323     fun getCaptureResolution(): Size {
324         val resolution =
325             mImageCapture.attachedSurfaceResolution
326                 ?: throw IllegalStateException("ImageCapture surface resolution is null")
327 
328         val rotation = getSensorRotationRelativeToCaptureTargetRotation()
329         return if (rotation == 90 || rotation == 270) {
330             Size(resolution.height, resolution.width)
331         } else {
332             resolution
333         }
334     }
335 
336     // endregion
337 
338     companion object {
339         const val KEY_LENS_FACING = "lens-facing"
340         const val KEY_IMAGE_CAPTURE_MODE = "image-capture-mode"
341         const val KEY_CAMERA_IMPLEMENTATION = "camera_implementation"
342         const val KEY_CAMERA_IMPLEMENTATION_NO_HISTORY = "camera_implementation_no_history"
343         const val CAMERA2_IMPLEMENTATION_OPTION = "camera2"
344         const val CAMERA_PIPE_IMPLEMENTATION_OPTION = "camera_pipe"
345         const val IMAGE_CAPTURE_MODE_IN_MEMORY = 0
346         const val IMAGE_CAPTURE_MODE_FILE = 1
347         const val IMAGE_CAPTURE_MODE_OUTPUT_STREAM = 2
348         const val IMAGE_CAPTURE_MODE_MEDIA_STORE = 3
349 
350         private const val TAG = "CameraActivity"
351         private const val REQUEST_CODE_PERMISSIONS = 20
352         val PERMISSIONS =
353             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
354             // in android 10 or later, we don't actually need WRITE_EXTERNAL_STORAGE to write to
355             // the external storage.
356             arrayOf(Manifest.permission.CAMERA)
357             else arrayOf(Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE)
358         private var cameraImpl: String? = null
359     }
360 }
361