1 /*
<lambda>null2  * Copyright 2023 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 package androidx.camera.integration.view
17 
18 import android.annotation.SuppressLint
19 import android.content.ContentValues
20 import android.os.Bundle
21 import android.os.Environment
22 import android.provider.MediaStore
23 import android.view.LayoutInflater
24 import android.view.View
25 import android.view.ViewGroup
26 import android.widget.Button
27 import android.widget.RadioButton
28 import android.widget.RadioGroup
29 import android.widget.Toast
30 import androidx.annotation.VisibleForTesting
31 import androidx.camera.core.CameraEffect
32 import androidx.camera.core.CameraEffect.IMAGE_CAPTURE
33 import androidx.camera.core.CameraEffect.PREVIEW
34 import androidx.camera.core.CameraEffect.VIDEO_CAPTURE
35 import androidx.camera.core.CameraSelector
36 import androidx.camera.core.ImageCapture
37 import androidx.camera.core.ImageCapture.OutputFileOptions
38 import androidx.camera.core.ImageCaptureException
39 import androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor
40 import androidx.camera.video.MediaStoreOutputOptions
41 import androidx.camera.video.Recording
42 import androidx.camera.video.VideoRecordEvent
43 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED
44 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED
45 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INSUFFICIENT_STORAGE
46 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE
47 import androidx.camera.view.CameraController
48 import androidx.camera.view.LifecycleCameraController
49 import androidx.camera.view.PreviewView
50 import androidx.camera.view.video.AudioConfig
51 import androidx.fragment.app.Fragment
52 
53 /** Fragment for testing effects integration. */
54 class EffectsFragment : Fragment() {
55 
56     private lateinit var cameraController: LifecycleCameraController
57     lateinit var previewView: PreviewView
58     private lateinit var surfaceEffectForPreviewVideo: RadioButton
59     lateinit var surfaceEffectForImageCapture: RadioButton
60     private lateinit var imageEffectForImageCapture: RadioButton
61     private lateinit var previewVideoGroup: RadioGroup
62     private lateinit var imageGroup: RadioGroup
63     private lateinit var capture: Button
64     private lateinit var record: Button
65     private lateinit var flip: Button
66     private var recording: Recording? = null
67     private lateinit var surfaceProcessor: ToneMappingSurfaceProcessor
68     private var imageEffect: ToneMappingImageEffect? = null
69     private var isBackCamera = true
70 
71     override fun onCreateView(
72         inflater: LayoutInflater,
73         container: ViewGroup?,
74         savedInstanceState: Bundle?
75     ): View? {
76         // Inflate the layout for this fragment.
77         val view = inflater.inflate(R.layout.effects_view, container, false)
78         previewView = view.findViewById(R.id.preview_view)
79         surfaceEffectForPreviewVideo = view.findViewById(R.id.surface_effect_for_preview_video)
80         surfaceEffectForImageCapture = view.findViewById(R.id.surface_effect_for_image_capture)
81         imageEffectForImageCapture = view.findViewById(R.id.image_effect_for_image_capture)
82         previewVideoGroup = view.findViewById(R.id.preview_and_video_effect_group)
83         imageGroup = view.findViewById(R.id.image_effect_group)
84         capture = view.findViewById(R.id.capture)
85         record = view.findViewById(R.id.record)
86         flip = view.findViewById(R.id.flip)
87         // Set up  UI events.
88         previewView.implementationMode = PreviewView.ImplementationMode.COMPATIBLE
89         previewVideoGroup.setOnCheckedChangeListener { _, _ -> updateEffects() }
90         imageGroup.setOnCheckedChangeListener { _, _ -> updateEffects() }
91         capture.setOnClickListener { takePicture() }
92         record.setOnClickListener {
93             if (recording == null) {
94                 startRecording()
95             } else {
96                 stopRecording()
97             }
98         }
99         flip.setOnClickListener { toggleCamera() }
100         // Set up the surface processor.
101         surfaceProcessor = ToneMappingSurfaceProcessor()
102         // Set up the camera controller.
103         cameraController = LifecycleCameraController(requireContext())
104         cameraController.setEnabledUseCases(
105             CameraController.IMAGE_CAPTURE or CameraController.VIDEO_CAPTURE
106         )
107         previewView.controller = cameraController
108         updateEffects()
109         cameraController.bindToLifecycle(viewLifecycleOwner)
110         return view
111     }
112 
113     private fun updateEffects() {
114         try {
115             val effects = mutableSetOf<CameraEffect>()
116             var surfaceEffectTarget = 0
117             if (surfaceEffectForPreviewVideo.isChecked) {
118                 surfaceEffectTarget = surfaceEffectTarget or PREVIEW or VIDEO_CAPTURE
119             }
120             if (surfaceEffectForImageCapture.isChecked) {
121                 surfaceEffectTarget = surfaceEffectTarget or IMAGE_CAPTURE
122             }
123             if (surfaceEffectTarget != 0) {
124                 effects.add(ToneMappingSurfaceEffect(surfaceEffectTarget, surfaceProcessor))
125             }
126             if (imageEffectForImageCapture.isChecked) {
127                 // Use ImageEffect for image capture
128                 imageEffect = ToneMappingImageEffect()
129                 effects.add(imageEffect!!)
130             } else {
131                 imageEffect = null
132             }
133             cameraController.setEffects(effects)
134         } catch (e: RuntimeException) {
135             toast("Failed to set effects: $e")
136         }
137     }
138 
139     override fun onDestroyView() {
140         super.onDestroyView()
141         surfaceProcessor.release()
142     }
143 
144     private fun toast(message: String?) {
145         requireActivity().runOnUiThread {
146             Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
147         }
148     }
149 
150     private fun takePicture() {
151         takePicture(
152             object : ImageCapture.OnImageSavedCallback {
153                 override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
154                     toast("Image saved successfully.")
155                 }
156 
157                 override fun onError(exception: ImageCaptureException) {
158                     toast("Image capture failed. $exception")
159                 }
160             }
161         )
162     }
163 
164     fun takePicture(onImageSavedCallback: ImageCapture.OnImageSavedCallback) {
165         createDefaultPictureFolderIfNotExist()
166         val contentValues = ContentValues()
167         contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
168         val outputFileOptions =
169             OutputFileOptions.Builder(
170                     requireContext().contentResolver,
171                     MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
172                     contentValues
173                 )
174                 .build()
175         cameraController.takePicture(outputFileOptions, directExecutor(), onImageSavedCallback)
176     }
177 
178     @SuppressLint("MissingPermission")
179     private fun startRecording() {
180         record.text = "Stop recording"
181         val outputOptions: MediaStoreOutputOptions = getNewVideoOutputMediaStoreOptions()
182         val audioConfig = AudioConfig.create(true)
183         recording =
184             cameraController.startRecording(outputOptions, audioConfig, directExecutor()) {
185                 if (it is VideoRecordEvent.Finalize) {
186                     val uri = it.outputResults.outputUri
187                     when (it.error) {
188                         VideoRecordEvent.Finalize.ERROR_NONE,
189                         ERROR_FILE_SIZE_LIMIT_REACHED,
190                         ERROR_DURATION_LIMIT_REACHED,
191                         ERROR_INSUFFICIENT_STORAGE,
192                         ERROR_SOURCE_INACTIVE -> toast("Video saved to: $uri")
193                         else -> toast("Failed to save video: uri $uri with code (${it.error})")
194                     }
195                 }
196             }
197     }
198 
199     private fun stopRecording() {
200         record.text = "Record"
201         recording?.stop()
202         recording = null
203     }
204 
205     private fun getNewVideoOutputMediaStoreOptions(): MediaStoreOutputOptions {
206         val videoFileName = "video_" + System.currentTimeMillis()
207         val resolver = requireContext().contentResolver
208         val contentValues = ContentValues()
209         contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4")
210         contentValues.put(MediaStore.Video.Media.TITLE, videoFileName)
211         contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName)
212         return MediaStoreOutputOptions.Builder(
213                 resolver,
214                 MediaStore.Video.Media.EXTERNAL_CONTENT_URI
215             )
216             .setContentValues(contentValues)
217             .build()
218     }
219 
220     private fun createDefaultPictureFolderIfNotExist() {
221         val pictureFolder =
222             Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
223         if (!pictureFolder.exists()) {
224             if (!pictureFolder.mkdir()) {
225                 toast("Failed to create directory: $pictureFolder")
226             }
227         }
228     }
229 
230     fun toggleCamera() {
231         cameraController.cameraSelector =
232             if (isBackCamera) {
233                 isBackCamera = false
234                 CameraSelector.DEFAULT_FRONT_CAMERA
235             } else {
236                 isBackCamera = true
237                 CameraSelector.DEFAULT_BACK_CAMERA
238             }
239     }
240 
241     @VisibleForTesting
242     fun getImageEffect(): ToneMappingImageEffect? {
243         return imageEffect
244     }
245 
246     @VisibleForTesting
247     fun getSurfaceProcessor(): ToneMappingSurfaceProcessor {
248         return surfaceProcessor
249     }
250 }
251