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