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 
17 package androidx.camera.integration.view
18 
19 import android.annotation.SuppressLint
20 import android.content.Context
21 import android.content.pm.ActivityInfo
22 import android.os.Bundle
23 import android.view.OrientationEventListener
24 import android.widget.Button
25 import androidx.annotation.OptIn
26 import androidx.appcompat.app.AppCompatActivity
27 import androidx.camera.camera2.Camera2Config
28 import androidx.camera.camera2.pipe.integration.CameraPipeConfig
29 import androidx.camera.core.Camera
30 import androidx.camera.core.CameraSelector
31 import androidx.camera.core.CameraXConfig
32 import androidx.camera.core.ImageAnalysis
33 import androidx.camera.core.ImageCapture
34 import androidx.camera.core.Logger
35 import androidx.camera.core.MirrorMode.MIRROR_MODE_ON_FRONT_ONLY
36 import androidx.camera.core.Preview
37 import androidx.camera.core.UseCase
38 import androidx.camera.core.impl.utils.executor.CameraXExecutors
39 import androidx.camera.lifecycle.ExperimentalCameraProviderConfiguration
40 import androidx.camera.lifecycle.ProcessCameraProvider
41 import androidx.camera.testing.impl.FileUtil.canDeviceWriteToMediaStore
42 import androidx.camera.testing.impl.FileUtil.generateVideoFileOutputOptions
43 import androidx.camera.testing.impl.FileUtil.generateVideoMediaStoreOptions
44 import androidx.camera.testing.impl.FileUtil.writeTextToExternalFile
45 import androidx.camera.video.PendingRecording
46 import androidx.camera.video.Recorder
47 import androidx.camera.video.Recording
48 import androidx.camera.video.VideoCapture
49 import androidx.camera.video.VideoRecordEvent
50 import androidx.camera.view.PreviewView
51 import androidx.camera.view.PreviewView.ImplementationMode
52 import androidx.core.content.ContextCompat
53 import androidx.core.util.Consumer
54 
55 private const val TAG = "StreamSharingActivity"
56 private const val PREFIX_INFORMATION = "test_information"
57 private const val PREFIX_VIDEO = "video"
58 private const val KEY_ORIENTATION = "device_orientation"
59 private const val KEY_STREAM_SHARING_STATE = "is_stream_sharing_enabled"
60 
61 // Possible values for this intent key (case-insensitive): "portrait", "landscape".
62 private const val INTENT_SCREEN_ORIENTATION = "orientation"
63 private const val SCREEN_ORIENTATION_PORTRAIT = "portrait"
64 private const val SCREEN_ORIENTATION_LANDSCAPE = "landscape"
65 
66 // Possible values for this intent key (case-insensitive): "back", "front".
67 private const val INTENT_EXTRA_CAMERA_DIRECTION = "camera_direction"
68 private const val CAMERA_DIRECTION_BACK = "back"
69 private const val CAMERA_DIRECTION_FRONT = "front"
70 
71 // Possible values for this intent key (case-insensitive): "compatible", "performance".
72 private const val INTENT_PREVIEW_VIEW_MODE = "preview_view_mode"
73 private const val PREVIEW_VIEW_COMPATIBLE_MODE = "compatible"
74 private const val PREVIEW_VIEW_PERFORMANCE_MODE = "performance"
75 
76 // Possible values for this intent key (case-insensitive): "camera2", "camera_pipe".
77 private const val INTENT_EXTRA_CAMERA_IMPLEMENTATION = "camera_implementation"
78 private const val CAMERA_IMPLEMENTATION_CAMERA2 = "camera2"
79 private const val CAMERA_IMPLEMENTATION_CAMERA_PIPE = "camera_pipe"
80 
81 class StreamSharingActivity : AppCompatActivity() {
82 
83     private lateinit var previewView: PreviewView
84     private lateinit var exportButton: Button
85     private lateinit var recordButton: Button
86     private lateinit var useCases: Array<UseCase>
87     private var cameraSelector: CameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
88     private var cameraXConfig: CameraXConfig = Camera2Config.defaultConfig()
89     private var camera: Camera? = null
90     private var previewViewMode: ImplementationMode = ImplementationMode.PERFORMANCE
91     private var previewViewScaleType = PreviewView.ScaleType.FILL_CENTER
92 
93     private var activeRecording: Recording? = null
94     private var isUseCasesBound: Boolean = false
95     private var deviceOrientation: Int = -1
96     private val orientationEventListener by lazy {
97         object : OrientationEventListener(applicationContext) {
98             override fun onOrientationChanged(orientation: Int) {
99                 deviceOrientation = orientation
100             }
101         }
102     }
103 
104     override fun onCreate(savedInstanceState: Bundle?) {
105         super.onCreate(savedInstanceState)
106         setContentView(R.layout.activity_stream_sharing)
107 
108         // Apply settings from intent.
109         val bundle = intent.extras
110         if (bundle != null) {
111             parseScreenOrientationAndSetValueIfNeed(bundle)
112             parseCameraSelector(bundle)
113             parseCameraImplementation(bundle)
114             parsePreviewViewMode(bundle)
115         }
116 
117         // Initial view objects.
118         previewView = findViewById(R.id.preview_view)
119         previewView.scaleType = previewViewScaleType
120         previewView.implementationMode = previewViewMode
121         exportButton = findViewById(R.id.export_button)
122         exportButton.setOnClickListener { exportTestInformation() }
123         recordButton = findViewById(R.id.record_button)
124         recordButton.setOnClickListener {
125             if (activeRecording == null) startRecording() else stopRecording()
126         }
127 
128         if (!isCameraXConfigured) {
129             isCameraXConfigured = true
130             configureCameraProvider()
131         }
132         startCamera()
133     }
134 
135     override fun onResume() {
136         super.onResume()
137         orientationEventListener.enable()
138     }
139 
140     override fun onPause() {
141         super.onPause()
142         orientationEventListener.disable()
143     }
144 
145     private fun parseScreenOrientationAndSetValueIfNeed(bundle: Bundle) {
146         val orientationString = bundle.getString(INTENT_SCREEN_ORIENTATION)
147         if (SCREEN_ORIENTATION_PORTRAIT.equals(orientationString, true)) {
148             requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
149         } else if (SCREEN_ORIENTATION_LANDSCAPE.equals(orientationString, true)) {
150             requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
151         }
152     }
153 
154     private fun parseCameraSelector(bundle: Bundle) {
155         val cameraDirection = bundle.getString(INTENT_EXTRA_CAMERA_DIRECTION)
156         if (CAMERA_DIRECTION_BACK.equals(cameraDirection, true)) {
157             cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
158         } else if (CAMERA_DIRECTION_FRONT.equals(cameraDirection, true)) {
159             cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
160         }
161     }
162 
163     private fun parseCameraImplementation(bundle: Bundle) {
164         val implementation = bundle.getString(INTENT_EXTRA_CAMERA_IMPLEMENTATION)
165         if (CAMERA_IMPLEMENTATION_CAMERA2.equals(implementation, true)) {
166             cameraXConfig = Camera2Config.defaultConfig()
167         } else if (CAMERA_IMPLEMENTATION_CAMERA_PIPE.equals(implementation, true)) {
168             cameraXConfig = CameraPipeConfig.defaultConfig()
169         }
170     }
171 
172     private fun parsePreviewViewMode(bundle: Bundle) {
173         val mode = bundle.getString(INTENT_PREVIEW_VIEW_MODE)
174         if (PREVIEW_VIEW_COMPATIBLE_MODE.equals(mode, true)) {
175             previewViewMode = ImplementationMode.COMPATIBLE
176         } else if (PREVIEW_VIEW_PERFORMANCE_MODE.equals(mode, true)) {
177             previewViewMode = ImplementationMode.PERFORMANCE
178         }
179     }
180 
181     @SuppressLint("NullAnnotationGroup")
182     @OptIn(ExperimentalCameraProviderConfiguration::class)
183     private fun configureCameraProvider() {
184         ProcessCameraProvider.configureInstance(cameraXConfig)
185     }
186 
187     private fun startCamera() {
188         val cameraProviderFuture = ProcessCameraProvider.getInstance(applicationContext)
189         cameraProviderFuture.addListener(
190             { bindUseCases(cameraProviderFuture.get()) },
191             ContextCompat.getMainExecutor(applicationContext)
192         )
193     }
194 
195     private fun bindUseCases(cameraProvider: ProcessCameraProvider) {
196         enableRecording(false)
197         isUseCasesBound = false
198         cameraProvider.unbindAll()
199         useCases =
200             arrayOf(
201                 createPreview(),
202                 createImageCapture(),
203                 createImageAnalysis(),
204                 createVideoCapture()
205             )
206         isUseCasesBound =
207             try {
208                 camera = cameraProvider.bindToLifecycle(this, cameraSelector, *useCases)
209                 enableRecording(true)
210                 true
211             } catch (exception: Exception) {
212                 Logger.e(TAG, "Failed to bind use cases.", exception)
213                 false
214             }
215     }
216 
217     private fun createPreview(): Preview {
218         return Preview.Builder().build().apply { setSurfaceProvider(previewView.surfaceProvider) }
219     }
220 
221     private fun createImageCapture(): ImageCapture {
222         return ImageCapture.Builder().build()
223     }
224 
225     private fun createImageAnalysis(): ImageAnalysis {
226         return ImageAnalysis.Builder().build()
227     }
228 
229     private fun createVideoCapture(): VideoCapture<Recorder> {
230         val recorder = Recorder.Builder().build()
231         return VideoCapture.Builder(recorder).setMirrorMode(MIRROR_MODE_ON_FRONT_ONLY).build()
232     }
233 
234     @Suppress("UNCHECKED_CAST")
235     private fun getVideoCapture(): VideoCapture<Recorder>? {
236         return findUseCase(VideoCapture::class.java) as VideoCapture<Recorder>?
237     }
238 
239     private fun <T : UseCase?> findUseCase(useCaseSubclass: Class<T>): T? {
240         for (useCase in useCases) {
241             if (useCaseSubclass.isInstance(useCase)) {
242                 return useCaseSubclass.cast(useCase)
243             }
244         }
245         return null
246     }
247 
248     private fun isStreamSharingEnabled(): Boolean {
249         val isCombinationSupported =
250             camera != null && camera!!.isUseCasesCombinationSupportedByFramework(*useCases)
251         return !isCombinationSupported && isUseCasesBound
252     }
253 
254     private fun enableRecording(enabled: Boolean) {
255         recordButton.isEnabled = enabled
256     }
257 
258     @SuppressLint("MissingPermission")
259     private fun startRecording() {
260         recordButton.text = getString(R.string.btn_video_stop_recording)
261         activeRecording =
262             getVideoCapture()!!.let {
263                 prepareRecording(applicationContext, it.output)
264                     .withAudioEnabled()
265                     .start(CameraXExecutors.directExecutor(), generateVideoRecordEventListener())
266             }
267     }
268 
269     private fun stopRecording() {
270         recordButton.text = getString(R.string.btn_video_record)
271         activeRecording!!.stop()
272         activeRecording = null
273     }
274 
275     private fun prepareRecording(context: Context, recorder: Recorder): PendingRecording {
276         val fileName = generateFileName(PREFIX_VIDEO)
277 
278         return if (canDeviceWriteToMediaStore()) {
279             recorder.prepareRecording(
280                 context,
281                 generateVideoMediaStoreOptions(context.contentResolver, fileName)
282             )
283         } else {
284             recorder.prepareRecording(context, generateVideoFileOutputOptions(fileName))
285         }
286     }
287 
288     private fun exportTestInformation() {
289         val fileName = generateFileName(PREFIX_INFORMATION)
290         val information =
291             "$KEY_ORIENTATION:$deviceOrientation" +
292                 "\n" +
293                 "$KEY_STREAM_SHARING_STATE:${isStreamSharingEnabled()}"
294 
295         writeTextToExternalFile(information, fileName)
296     }
297 
298     private fun generateFileName(prefix: String? = null): String {
299         val timeMillis = System.currentTimeMillis()
300         return if (prefix != null) "${prefix}_$timeMillis" else "$timeMillis"
301     }
302 
303     private fun generateVideoRecordEventListener(): Consumer<VideoRecordEvent> {
304         return Consumer<VideoRecordEvent> { event ->
305             if (event is VideoRecordEvent.Finalize) {
306                 val uri = event.outputResults.outputUri
307                 when (event.error) {
308                     VideoRecordEvent.Finalize.ERROR_NONE,
309                     VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED,
310                     VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED,
311                     VideoRecordEvent.Finalize.ERROR_INSUFFICIENT_STORAGE,
312                     VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE ->
313                         Logger.d(TAG, "Video saved to: $uri")
314                     else ->
315                         Logger.e(TAG, "Failed to save video: uri $uri with code (${event.error})")
316                 }
317             }
318         }
319     }
320 
321     companion object {
322         private var isCameraXConfigured: Boolean = false
323     }
324 }
325