[CameraViewfinder] Add supend function support for CameraViewfinder requestSurface API
RelNote: Add CoroutineCameraViewfinder to support configuring viewfinder using suspending functions in Kotlin.
Bug: b/264471649
Test: run integration test on real device
Change-Id: I657bc25a2908414c44a700829f1706988c0c89ea
diff --git a/camera/camera-viewfinder/api/current.txt b/camera/camera-viewfinder/api/current.txt
index 0023b9c..48504db 100644
--- a/camera/camera-viewfinder/api/current.txt
+++ b/camera/camera-viewfinder/api/current.txt
@@ -27,6 +27,11 @@
enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
}
+ @RequiresApi(21) public final class CameraViewfinderExt {
+ method public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
+ field public static final androidx.camera.viewfinder.CameraViewfinderExt INSTANCE;
+ }
+
@RequiresApi(21) public class ViewfinderSurfaceRequest {
method public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode? getImplementationMode();
method public int getLensFacing();
diff --git a/camera/camera-viewfinder/api/public_plus_experimental_current.txt b/camera/camera-viewfinder/api/public_plus_experimental_current.txt
index 0023b9c..48504db 100644
--- a/camera/camera-viewfinder/api/public_plus_experimental_current.txt
+++ b/camera/camera-viewfinder/api/public_plus_experimental_current.txt
@@ -27,6 +27,11 @@
enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
}
+ @RequiresApi(21) public final class CameraViewfinderExt {
+ method public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
+ field public static final androidx.camera.viewfinder.CameraViewfinderExt INSTANCE;
+ }
+
@RequiresApi(21) public class ViewfinderSurfaceRequest {
method public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode? getImplementationMode();
method public int getLensFacing();
diff --git a/camera/camera-viewfinder/api/restricted_current.txt b/camera/camera-viewfinder/api/restricted_current.txt
index 0023b9c..48504db 100644
--- a/camera/camera-viewfinder/api/restricted_current.txt
+++ b/camera/camera-viewfinder/api/restricted_current.txt
@@ -27,6 +27,11 @@
enum_constant public static final androidx.camera.viewfinder.CameraViewfinder.ScaleType FIT_START;
}
+ @RequiresApi(21) public final class CameraViewfinderExt {
+ method public suspend Object? requestSurface(androidx.camera.viewfinder.CameraViewfinder, androidx.camera.viewfinder.ViewfinderSurfaceRequest viewfinderSurfaceRequest, kotlin.coroutines.Continuation<? super android.view.Surface>);
+ field public static final androidx.camera.viewfinder.CameraViewfinderExt INSTANCE;
+ }
+
@RequiresApi(21) public class ViewfinderSurfaceRequest {
method public androidx.camera.viewfinder.CameraViewfinder.ImplementationMode? getImplementationMode();
method public int getLensFacing();
diff --git a/camera/camera-viewfinder/build.gradle b/camera/camera-viewfinder/build.gradle
index 449b256..ea7bfde 100644
--- a/camera/camera-viewfinder/build.gradle
+++ b/camera/camera-viewfinder/build.gradle
@@ -29,6 +29,7 @@
implementation(libs.guavaListenableFuture)
implementation("androidx.core:core:1.3.2")
implementation("androidx.concurrent:concurrent-futures:1.0.0")
+ implementation(project(":concurrent:concurrent-futures-ktx"))
implementation(libs.autoValueAnnotations)
implementation("androidx.appcompat:appcompat:1.1.0")
implementation("androidx.test.espresso:espresso-idling-resource:3.1.0")
diff --git a/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinderExt.kt b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinderExt.kt
new file mode 100644
index 0000000..1193fc1
--- /dev/null
+++ b/camera/camera-viewfinder/src/main/java/androidx/camera/viewfinder/CameraViewfinderExt.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.camera.viewfinder
+
+import android.view.Surface
+import androidx.annotation.RequiresApi
+import androidx.concurrent.futures.await
+
+/**
+ * Provides a suspending function of [CameraViewfinder.requestSurfaceAsync] to request
+ * a [Surface] by sending a [ViewfinderSurfaceRequest].
+ */
+@RequiresApi(21) // TODO(b/200306659): Remove and replace with annotation on package-info.java
+object CameraViewfinderExt {
+ suspend fun CameraViewfinder.requestSurface(
+ viewfinderSurfaceRequest: ViewfinderSurfaceRequest
+ ): Surface = requestSurfaceAsync(viewfinderSurfaceRequest).await()
+}
diff --git a/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt b/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
index 04b25e4..cc628ac 100644
--- a/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
+++ b/camera/integration-tests/viewfindertestapp/src/main/java/androidx/camera/integration/viewfinder/CameraViewfinderFoldableFragment.kt
@@ -53,12 +53,14 @@
import android.view.Surface
import android.view.View
import android.view.ViewGroup
+import android.view.ViewTreeObserver
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.camera.core.impl.utils.CompareSizesByArea
-import androidx.camera.core.impl.utils.futures.FutureCallback
-import androidx.camera.core.impl.utils.futures.Futures
import androidx.camera.viewfinder.CameraViewfinder
+import androidx.camera.viewfinder.CameraViewfinder.ImplementationMode
+import androidx.camera.viewfinder.CameraViewfinder.ScaleType
+import androidx.camera.viewfinder.CameraViewfinderExt.requestSurface
import androidx.camera.viewfinder.ViewfinderSurfaceRequest
import androidx.camera.viewfinder.populateFromCharacteristics
import androidx.core.app.ActivityCompat
@@ -72,7 +74,6 @@
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowLayoutInfo
import com.google.common.base.Objects
-import com.google.common.util.concurrent.ListenableFuture
import java.io.Closeable
import java.io.File
import java.io.FileOutputStream
@@ -82,7 +83,6 @@
import java.util.Date
import java.util.Locale
import java.util.concurrent.ArrayBlockingQueue
-import java.util.concurrent.Semaphore
import java.util.concurrent.TimeoutException
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
@@ -90,6 +90,7 @@
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext
/**
@@ -98,47 +99,45 @@
class CameraViewfinderFoldableFragment : Fragment(), View.OnClickListener,
ActivityCompat.OnRequestPermissionsResultCallback {
- private lateinit var cameraThread: HandlerThread
-
- private lateinit var cameraHandler: Handler
-
- private lateinit var imageReaderThread: HandlerThread
-
- private lateinit var imageReaderHandler: Handler
-
- private val cameraOpenCloseLock = Semaphore(1)
+ private val cameraOpenCloseLock = Mutex()
private val onImageAvailableListener = ImageReader.OnImageAvailableListener {
- cameraHandler.post(
+ cameraHandler?.post(
ImageSaver(
it.acquireNextImage(),
- file
+ checkNotNull(file) { "file cannot be null when saving image" }
)
)
}
- private lateinit var camera: CameraDevice
-
- private lateinit var characteristics: CameraCharacteristics
-
- private lateinit var cameraId: String
-
private lateinit var cameraManager: CameraManager
private lateinit var cameraViewfinder: CameraViewfinder
- private lateinit var file: File
-
- private lateinit var imageReader: ImageReader
-
- private lateinit var relativeOrientation: OrientationLiveData
-
- private lateinit var session: CameraCaptureSession
-
- private lateinit var surfaceListenableFuture: ListenableFuture<Surface>
-
private lateinit var windowInfoTracker: WindowInfoTracker
+ private var cameraThread: HandlerThread? = null
+
+ private var cameraHandler: Handler? = null
+
+ private var imageReaderThread: HandlerThread? = null
+
+ private var imageReaderHandler: Handler? = null
+
+ private var camera: CameraDevice? = null
+
+ private var characteristics: CameraCharacteristics? = null
+
+ private var cameraId: String? = null
+
+ private var file: File? = null
+
+ private var imageReader: ImageReader? = null
+
+ private var relativeOrientation: OrientationLiveData? = null
+
+ private var session: CameraCaptureSession? = null
+
private var activeWindowLayoutInfo: WindowLayoutInfo? = null
private var isViewfinderInLeftTop = true
@@ -147,6 +146,8 @@
private var resolution: Size? = null
+ private var layoutChangedListener: ViewTreeObserver.OnGlobalLayoutListener? = null
+
@Deprecated("Deprecated in Java")
@Suppress("DEPRECATION")
override fun onCreate(savedInstanceState: Bundle?) {
@@ -176,21 +177,20 @@
R.id.implementationMode -> {
val implementationMode =
when (cameraViewfinder.implementationMode) {
- CameraViewfinder.ImplementationMode.PERFORMANCE ->
- CameraViewfinder.ImplementationMode.COMPATIBLE
- else -> CameraViewfinder.ImplementationMode.PERFORMANCE
+ ImplementationMode.PERFORMANCE ->
+ ImplementationMode.COMPATIBLE
+ else -> ImplementationMode.PERFORMANCE
}
- val viewfinderSurfaceRequest = ViewfinderSurfaceRequest.Builder(resolution!!)
- .populateFromCharacteristics(characteristics)
- .setImplementationMode(implementationMode)
- .build()
- sendSurfaceRequest(viewfinderSurfaceRequest)
+ lifecycleScope.launch {
+ closeCamera()
+ sendSurfaceRequest(implementationMode, false)
+ }
}
- R.id.fitCenter -> cameraViewfinder.scaleType = CameraViewfinder.ScaleType.FIT_CENTER
- R.id.fillCenter -> cameraViewfinder.scaleType = CameraViewfinder.ScaleType.FILL_CENTER
- R.id.fitStart -> cameraViewfinder.scaleType = CameraViewfinder.ScaleType.FIT_START
- R.id.fitEnd -> cameraViewfinder.scaleType = CameraViewfinder.ScaleType.FIT_END
+ R.id.fitCenter -> cameraViewfinder.scaleType = ScaleType.FIT_CENTER
+ R.id.fillCenter -> cameraViewfinder.scaleType = ScaleType.FILL_CENTER
+ R.id.fitStart -> cameraViewfinder.scaleType = ScaleType.FIT_START
+ R.id.fitEnd -> cameraViewfinder.scaleType = ScaleType.FIT_END
}
return super.onOptionsItemSelected(item)
}
@@ -216,9 +216,13 @@
override fun onResume() {
super.onResume()
cameraThread = HandlerThread("CameraThread").apply { start() }
- cameraHandler = Handler(cameraThread.looper)
+ cameraHandler = Handler(checkNotNull(cameraThread) {
+ "camera thread cannot be null"
+ }.looper)
imageReaderThread = HandlerThread("ImageThread").apply { start() }
- imageReaderHandler = Handler(imageReaderThread.looper)
+ imageReaderHandler = Handler(checkNotNull(imageReaderThread) {
+ "image reader thread cannot be null"
+ }.looper)
// Request Permission
val cameraPermission = activity?.let {
@@ -241,12 +245,13 @@
}
}
- setUpCameraOutputs(false)
+ layoutChangedListener = ViewTreeObserver.OnGlobalLayoutListener {
+ cameraViewfinder.viewTreeObserver.removeOnGlobalLayoutListener(layoutChangedListener)
+ layoutChangedListener = null
- val viewfinderSurfaceRequest = ViewfinderSurfaceRequest.Builder(resolution!!)
- .populateFromCharacteristics(characteristics)
- .build()
- sendSurfaceRequest(viewfinderSurfaceRequest)
+ sendSurfaceRequest(null, false)
+ }
+ cameraViewfinder.viewTreeObserver.addOnGlobalLayoutListener(layoutChangedListener)
lifecycleScope.launch {
windowInfoTracker.windowLayoutInfo(requireActivity())
@@ -259,10 +264,12 @@
}
override fun onPause() {
- closeCamera()
- cameraThread.quitSafely()
- imageReaderThread.quitSafely()
- viewfinderSurfaceRequest?.markSurfaceSafeToRelease()
+ lifecycleScope.launch {
+ closeCamera()
+ cameraThread?.quitSafely()
+ imageReaderThread?.quitSafely()
+ viewfinderSurfaceRequest?.markSurfaceSafeToRelease()
+ }
super.onPause()
}
@@ -314,27 +321,20 @@
}
// ------------- Create Capture Session --------------
- private fun sendSurfaceRequest(request: ViewfinderSurfaceRequest) {
- cameraViewfinder.post {
- if (isAdded && context != null) {
- val context = requireContext()
- this.viewfinderSurfaceRequest = request
- surfaceListenableFuture =
- cameraViewfinder.requestSurfaceAsync(request)
-
- Futures.addCallback(surfaceListenableFuture, object : FutureCallback<Surface?> {
- override fun onSuccess(surface: Surface?) {
- Log.d(TAG, "request onSurfaceAvailable surface = $surface")
- if (surface != null) {
- initializeCamera(surface)
- }
- }
-
- override fun onFailure(t: Throwable) {
- Log.e(TAG, "request onSurfaceClosed")
- }
- }, ContextCompat.getMainExecutor(context))
+ private fun sendSurfaceRequest(
+ implementationMode: ImplementationMode?,
+ toggleCamera: Boolean
+ ) = lifecycleScope.launch {
+ if (isAdded && context != null) {
+ setUpCameraOutputs(toggleCamera)
+ val builder = ViewfinderSurfaceRequest.Builder(resolution!!)
+ .populateFromCharacteristics(characteristics!!)
+ if (implementationMode != null) {
+ builder.setImplementationMode(implementationMode)
}
+ viewfinderSurfaceRequest = builder.build()
+ val surface = cameraViewfinder.requestSurface(viewfinderSurfaceRequest!!)
+ initializeCamera(surface)
}
}
@@ -342,24 +342,34 @@
try {
for (cameraId in cameraManager.cameraIdList) {
characteristics = cameraManager.getCameraCharacteristics(cameraId)
- relativeOrientation = OrientationLiveData(requireContext(), characteristics).apply {
+ relativeOrientation = OrientationLiveData(requireContext(),
+ checkNotNull(characteristics) {
+ "camera characteristics cannot be null"
+ }).apply {
observe(viewLifecycleOwner, Observer { orientation ->
Log.d(TAG, "Orientation changed: $orientation")
})
}
- val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
+ val facing = checkNotNull(characteristics) {
+ "camera characteristics cannot be null"
+ }.get(CameraCharacteristics.LENS_FACING)
// Toggle the front and back camera
if (toggleCamera) {
- val currentFacing: Int? = cameraManager.getCameraCharacteristics(this.cameraId)
+ val currentFacing: Int? = cameraManager.getCameraCharacteristics(
+ checkNotNull(this.cameraId) {
+ "camera id cannot be null"
+ })
.get<Int>(CameraCharacteristics.LENS_FACING)
if (Objects.equal(currentFacing, facing)) {
continue
}
}
- val map = characteristics.get(
+ val map = checkNotNull(characteristics) {
+ "camera characteristics cannot be null"
+ }.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
) ?: continue
@@ -384,31 +394,42 @@
}
}
- private fun initializeCamera(surface: Surface) = lifecycleScope.launch(Dispatchers.IO) {
- cameraOpenCloseLock.acquire()
+ private suspend fun initializeCamera(surface: Surface) {
+ cameraOpenCloseLock.lock()
- // Open the selected camera
- camera = openCamera(cameraManager, cameraId, cameraHandler)
+ withContext(Dispatchers.IO) {
+ // Open the selected camera
+ camera = openCamera(cameraManager, checkNotNull(cameraId) {
+ "camera id cannot be null"
+ }, cameraHandler)
- // Creates list of Surfaces where the camera will output frames
- val targets = listOf(surface, imageReader.surface)
+ // Creates list of Surfaces where the camera will output frames
+ val targets = listOf(surface, checkNotNull(imageReader?.surface) {
+ "image reader surface cannot be null"
+ })
- try {
- // Start a capture session using our open camera and list of Surfaces where frames will go
- session = createCaptureSession(camera, targets, cameraHandler)
+ try {
+ // Start a capture session using our open camera and list of Surfaces where frames will go
+ session = createCaptureSession(checkNotNull(camera) {
+ "camera cannot be null"
+ }, targets, cameraHandler)
- val captureRequest = camera.createCaptureRequest(
- CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(surface) }
+ val captureRequest = checkNotNull(camera) {
+ "camera cannot be null"
+ }.createCaptureRequest(
+ CameraDevice.TEMPLATE_PREVIEW
+ ).apply { addTarget(surface) }
- // This will keep sending the capture request as frequently as possible until the
- // session is torn down or session.stopRepeating() is called
- session.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
- } catch (e: CameraAccessException) {
- Log.e(TAG, "createCaptureSession CameraAccessException")
- } catch (e: IllegalArgumentException) {
- Log.e(TAG, "createCaptureSession IllegalArgumentException")
- } catch (e: SecurityException) {
- Log.e(TAG, "createCaptureSession SecurityException")
+ // This will keep sending the capture request as frequently as possible until the
+ // session is torn down or session.stopRepeating() is called
+ session?.setRepeatingRequest(captureRequest.build(), null, cameraHandler)
+ } catch (e: CameraAccessException) {
+ Log.e(TAG, "createCaptureSession CameraAccessException")
+ } catch (e: IllegalArgumentException) {
+ Log.e(TAG, "createCaptureSession IllegalArgumentException")
+ } catch (e: SecurityException) {
+ Log.e(TAG, "createCaptureSession SecurityException")
+ }
}
}
@@ -417,58 +438,55 @@
manager: CameraManager,
cameraId: String,
handler: Handler? = null
- ): CameraDevice = suspendCancellableCoroutine { cont ->
- try {
- manager.openCamera(cameraId, object : CameraDevice.StateCallback() {
- override fun onOpened(device: CameraDevice) {
- cameraOpenCloseLock.release()
- cont.resume(device)
- }
-
- override fun onDisconnected(device: CameraDevice) {
- Log.w(TAG, "Camera $cameraId has been disconnected")
- cameraOpenCloseLock.release()
- }
-
- override fun onError(device: CameraDevice, error: Int) {
- val msg = when (error) {
- ERROR_CAMERA_DEVICE -> "Fatal (device)"
- ERROR_CAMERA_DISABLED -> "Device policy"
- ERROR_CAMERA_IN_USE -> "Camera in use"
- ERROR_CAMERA_SERVICE -> "Fatal (service)"
- ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
- else -> "Unknown"
+ ): CameraDevice = withContext(Dispatchers.IO) {
+ suspendCancellableCoroutine { cont ->
+ try {
+ manager.openCamera(cameraId, object : CameraDevice.StateCallback() {
+ override fun onOpened(device: CameraDevice) {
+ cameraOpenCloseLock.unlock()
+ cont.resume(device)
}
- Log.e(TAG, "Camera $cameraId error: ($error) $msg")
- }
- }, handler)
- } catch (e: CameraAccessException) {
- Log.e(TAG, "openCamera CameraAccessException")
- } catch (e: IllegalArgumentException) {
- Log.e(TAG, "openCamera IllegalArgumentException")
- } catch (e: SecurityException) {
- Log.e(TAG, "openCamera SecurityException")
+
+ override fun onDisconnected(device: CameraDevice) {
+ Log.w(TAG, "Camera $cameraId has been disconnected")
+ cameraOpenCloseLock.unlock()
+ }
+
+ override fun onError(device: CameraDevice, error: Int) {
+ val msg = when (error) {
+ ERROR_CAMERA_DEVICE -> "Fatal (device)"
+ ERROR_CAMERA_DISABLED -> "Device policy"
+ ERROR_CAMERA_IN_USE -> "Camera in use"
+ ERROR_CAMERA_SERVICE -> "Fatal (service)"
+ ERROR_MAX_CAMERAS_IN_USE -> "Maximum cameras in use"
+ else -> "Unknown"
+ }
+ Log.e(TAG, "Camera $cameraId error: ($error) $msg")
+ }
+ }, handler)
+ } catch (e: CameraAccessException) {
+ Log.e(TAG, "openCamera CameraAccessException")
+ } catch (e: IllegalArgumentException) {
+ Log.e(TAG, "openCamera IllegalArgumentException")
+ } catch (e: SecurityException) {
+ Log.e(TAG, "openCamera SecurityException")
+ }
}
}
- private fun closeCamera() {
+ private suspend fun closeCamera() = withContext(Dispatchers.IO) {
try {
- cameraOpenCloseLock.acquire()
- if (::session.isInitialized) {
- session.close()
- }
-
- if (::camera.isInitialized) {
- camera.close()
- }
-
- if (::imageReader.isInitialized) {
- imageReader.close()
- }
+ cameraOpenCloseLock.lock()
+ session?.close()
+ camera?.close()
+ imageReader?.close()
+ session = null
+ camera = null
+ imageReader = null
} catch (exc: Throwable) {
Log.e(TAG, "Error closing camera", exc)
} finally {
- cameraOpenCloseLock.release()
+ cameraOpenCloseLock.unlock()
}
}
@@ -477,30 +495,30 @@
device: CameraDevice,
targets: List<Surface>,
handler: Handler? = null
- ): CameraCaptureSession = suspendCoroutine { cont ->
+ ): CameraCaptureSession = withContext(Dispatchers.IO) {
+ suspendCoroutine { cont ->
- // Create a capture session using the predefined targets; this also involves defining the
- // session state callback to be notified of when the session is ready
- device.createCaptureSession(targets, object : CameraCaptureSession.StateCallback() {
+ // Create a capture session using the predefined targets; this also involves defining the
+ // session state callback to be notified of when the session is ready
+ device.createCaptureSession(targets, object : CameraCaptureSession.StateCallback() {
- override fun onConfigured(session: CameraCaptureSession) = cont.resume(session)
+ override fun onConfigured(session: CameraCaptureSession) = cont.resume(session)
- override fun onConfigureFailed(session: CameraCaptureSession) {
- val exc = RuntimeException("Camera ${device.id} session configuration failed")
- Log.e(TAG, exc.message, exc)
- cont.resumeWithException(exc)
- }
- }, handler)
+ override fun onConfigureFailed(session: CameraCaptureSession) {
+ val exc = RuntimeException("Camera ${device.id} session configuration failed")
+ Log.e(TAG, exc.message, exc)
+ cont.resumeWithException(exc)
+ }
+ }, handler)
+ }
}
// ------------- Toggle Camera -----------
private fun toggleCamera() {
- closeCamera()
- setUpCameraOutputs(true)
- val viewfinderSurfaceRequest = ViewfinderSurfaceRequest.Builder(resolution!!)
- .populateFromCharacteristics(characteristics)
- .build()
- sendSurfaceRequest(viewfinderSurfaceRequest)
+ lifecycleScope.launch {
+ closeCamera()
+ sendSurfaceRequest(null, true)
+ }
}
// ------------- Save Bitmap ------------
@@ -521,8 +539,10 @@
val contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val uri = resolver.insert(contentUri, values)
try {
- val fos = resolver.openOutputStream(uri!!)
- bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos!!)
+ val fos = resolver.openOutputStream(checkNotNull(uri) { "uri cannot be null" })
+ bitmap.compress(Bitmap.CompressFormat.PNG, 100, checkNotNull(fos) {
+ "fos cannot be null"
+ })
fos.close()
showToast("Saved: $displayName")
} catch (e: IOException) {
@@ -757,83 +777,89 @@
// Flush any images left in the image reader
@Suppress("ControlFlowWithEmptyBody")
- while (imageReader.acquireNextImage() != null) {
+ while (imageReader?.acquireNextImage() != null) {
}
// Start a new image queue
val imageQueue = ArrayBlockingQueue<Image>(IMAGE_BUFFER_SIZE)
- imageReader.setOnImageAvailableListener({ reader ->
+ imageReader?.setOnImageAvailableListener({ reader ->
val image = reader.acquireNextImage()
Log.d(TAG, "Image available in queue: ${image.timestamp}")
imageQueue.add(image)
}, imageReaderHandler)
- val captureRequest = session.device.createCaptureRequest(
- CameraDevice.TEMPLATE_STILL_CAPTURE).apply { addTarget(imageReader.surface) }
- session.capture(captureRequest.build(), object : CameraCaptureSession.CaptureCallback() {
+ val captureRequest = session?.device?.createCaptureRequest(
+ CameraDevice.TEMPLATE_STILL_CAPTURE).apply {
+ imageReader?.surface?.let { this?.addTarget(it) } }
+ if (captureRequest != null) {
+ session?.capture(captureRequest.build(),
+ object : CameraCaptureSession.CaptureCallback() {
- override fun onCaptureStarted(
- session: CameraCaptureSession,
- request: CaptureRequest,
- timestamp: Long,
- frameNumber: Long
- ) {
- super.onCaptureStarted(session, request, timestamp, frameNumber)
- }
+ override fun onCaptureStarted(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ timestamp: Long,
+ frameNumber: Long
+ ) {
+ super.onCaptureStarted(session, request, timestamp, frameNumber)
+ }
- override fun onCaptureCompleted(
- session: CameraCaptureSession,
- request: CaptureRequest,
- result: TotalCaptureResult
- ) {
- super.onCaptureCompleted(session, request, result)
- val resultTimestamp = result.get(CaptureResult.SENSOR_TIMESTAMP)
- Log.d(TAG, "Capture result received: $resultTimestamp")
+ override fun onCaptureCompleted(
+ session: CameraCaptureSession,
+ request: CaptureRequest,
+ result: TotalCaptureResult
+ ) {
+ super.onCaptureCompleted(session, request, result)
+ val resultTimestamp = result.get(CaptureResult.SENSOR_TIMESTAMP)
+ Log.d(TAG, "Capture result received: $resultTimestamp")
- // Set a timeout in case image captured is dropped from the pipeline
- val exc = TimeoutException("Image dequeuing took too long")
- val timeoutRunnable = Runnable { cont.resumeWithException(exc) }
- imageReaderHandler.postDelayed(timeoutRunnable, IMAGE_CAPTURE_TIMEOUT_MILLIS)
+ // Set a timeout in case image captured is dropped from the pipeline
+ val exc = TimeoutException("Image dequeuing took too long")
+ val timeoutRunnable = Runnable { cont.resumeWithException(exc) }
+ imageReaderHandler?.postDelayed(timeoutRunnable, IMAGE_CAPTURE_TIMEOUT_MILLIS)
- // Loop in the coroutine's context until an image with matching timestamp comes
- // We need to launch the coroutine context again because the callback is done in
- // the handler provided to the `capture` method, not in our coroutine context
- @Suppress("BlockingMethodInNonBlockingContext")
- lifecycleScope.launch(cont.context) {
- while (true) {
+ // Loop in the coroutine's context until an image with matching timestamp comes
+ // We need to launch the coroutine context again because the callback is done in
+ // the handler provided to the `capture` method, not in our coroutine context
+ @Suppress("BlockingMethodInNonBlockingContext")
+ lifecycleScope.launch(cont.context) {
+ while (true) {
- // Dequeue images while timestamps don't match
- val image = imageQueue.take()
- // if (image.timestamp != resultTimestamp) continue
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
- image.format != ImageFormat.DEPTH_JPEG &&
- image.timestamp != resultTimestamp) continue
- Log.d(TAG, "Matching image dequeued: ${image.timestamp}")
+ // Dequeue images while timestamps don't match
+ val image = imageQueue.take()
+ // if (image.timestamp != resultTimestamp) continue
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q &&
+ image.format != ImageFormat.DEPTH_JPEG &&
+ image.timestamp != resultTimestamp) continue
+ Log.d(TAG, "Matching image dequeued: ${image.timestamp}")
- // Unset the image reader listener
- imageReaderHandler.removeCallbacks(timeoutRunnable)
- imageReader.setOnImageAvailableListener(null, null)
+ // Unset the image reader listener
+ imageReaderHandler?.removeCallbacks(timeoutRunnable)
+ imageReader?.setOnImageAvailableListener(null, null)
- // Clear the queue of images, if there are left
- while (imageQueue.size > 0) {
- imageQueue.take().close()
+ // Clear the queue of images, if there are left
+ while (imageQueue.size > 0) {
+ imageQueue.take().close()
+ }
+
+ // Compute EXIF orientation metadata
+ val rotation = relativeOrientation?.value ?: 0
+ val mirrored = characteristics?.get(
+ CameraCharacteristics.LENS_FACING) ==
+ CameraCharacteristics.LENS_FACING_FRONT
+ val exifOrientation = computeExifOrientation(rotation, mirrored)
+
+ // Build the result and resume progress
+ cont.resume(CombinedCaptureResult(
+ image, result, exifOrientation, checkNotNull(imageReader) {
+ "image reader cannot be null"
+ }.imageFormat))
+ // There is no need to break out of the loop, this coroutine will suspend
}
-
- // Compute EXIF orientation metadata
- val rotation = relativeOrientation.value ?: 0
- val mirrored = characteristics.get(CameraCharacteristics.LENS_FACING) ==
- CameraCharacteristics.LENS_FACING_FRONT
- val exifOrientation = computeExifOrientation(rotation, mirrored)
-
- // Build the result and resume progress
- cont.resume(CombinedCaptureResult(
- image, result, exifOrientation, imageReader.imageFormat))
-
- // There is no need to break out of the loop, this coroutine will suspend
}
}
- }
- }, cameraHandler)
+ }, cameraHandler)
+ }
}
/** Helper function used to save a [CombinedCaptureResult] into a [File] */
@@ -856,7 +882,9 @@
// When the format is RAW we use the DngCreator utility library
ImageFormat.RAW_SENSOR -> {
- val dngCreator = DngCreator(characteristics, result.metadata)
+ val dngCreator = DngCreator(checkNotNull(characteristics) {
+ "camera characteristics cannot be null"
+ }, result.metadata)
try {
val output = createFile("dng")
FileOutputStream(output).use { dngCreator.writeImage(it, result.image) }