Skip to content

Commit

Permalink
Update dependencies and safe camera close
Browse files Browse the repository at this point in the history
- Update Gradle dependencies
- Perform safer camera close in onStop()
- Migrate Camera2Formats sample to ViewPager2
- Refactor openCamera() and createCaptureSession()

Change-Id: I7703a2c14567730f1b8cc5fe851174e4d5a5e7c1
  • Loading branch information
owahltinez committed Dec 6, 2019
1 parent 917b4a3 commit f5a03b0
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 148 deletions.
7 changes: 4 additions & 3 deletions Camera2Formats/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -72,15 +72,16 @@ dependencies {

// App compat and UI things
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-beta01'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.2.0-rc03'
implementation "androidx.viewpager2:viewpager2:1.0.0"

// Navigation library
def nav_version = "2.1.0"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"

// EXIF Interface
implementation 'androidx.exifinterface:exifinterface:1.1.0-rc01'
implementation 'androidx.exifinterface:exifinterface:1.1.0'

// Glide
implementation 'com.github.bumptech.glide:glide:4.10.0'
Expand All @@ -91,7 +92,7 @@ dependencies {
testImplementation 'androidx.test:rules:1.2.0'
testImplementation 'androidx.test:runner:1.2.0'
testImplementation 'androidx.test.espresso:espresso-core:3.2.0'
testImplementation "org.robolectric:robolectric:4.3"
testImplementation "org.robolectric:robolectric:4.3.1"

// Instrumented testing
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
Expand Down
5 changes: 3 additions & 2 deletions Camera2Formats/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@
<!-- Permission declarations -->
<uses-permission android:name="android.permission.CAMERA" />

<!-- A camera with RAW capability is required to use this application -->
<!-- A camera with (optional) RAW capability is required to use this application -->
<uses-feature android:name="android.hardware.camera.any" />
<uses-feature android:name="android.hardware.camera.raw" />
<uses-feature android:name="android.hardware.camera.raw" android:required="false" />

<application
android:allowBackup="true"
android:fullBackupContent="true"
android:label="@string/app_name"
android:icon="@drawable/ic_launcher"
tools:ignore="GoogleAppIndexingWarning">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import android.view.Display
import android.view.LayoutInflater
import android.view.Surface
import android.view.SurfaceHolder
Expand All @@ -55,6 +54,7 @@ import com.example.android.camera2.common.OrientationLiveData
import com.example.android.camera2.formats.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import java.io.Closeable
import java.io.File
import java.io.FileOutputStream
Expand Down Expand Up @@ -108,6 +108,9 @@ class CameraFragment : Fragment() {
/** Where the camera preview is displayed */
private lateinit var viewFinder: SurfaceView

/** The [CameraDevice] that will be opened in this fragment */
private lateinit var camera: CameraDevice

/** Internal reference to the ongoing [CameraCaptureSession] configured with our parameters */
private lateinit var session: CameraCaptureSession

Expand Down Expand Up @@ -149,7 +152,7 @@ class CameraFragment : Fragment() {
}
})

// Used to rotate the output video to match device orientation
// Used to rotate the output media to match device orientation
relativeOrientation = OrientationLiveData(requireContext(), characteristics).apply {
observe(this@CameraFragment, Observer {
orientation -> Log.d(TAG, "Orientation changed: $orientation")
Expand All @@ -162,11 +165,25 @@ class CameraFragment : Fragment() {
* - Opens the camera
* - Configures the camera session
* - Starts the preview by dispatching a repeating capture request
* - Sets up the image capture listeners
* - Sets up the still image capture listeners
*/
private fun initializeCamera() = lifecycleScope.launch(Dispatchers.Main) {
val camera = openCamera()
session = startCaptureSession(camera)
// Open the selected camera
camera = openCamera(cameraManager, args.cameraId, cameraHandler)

// Initialize an image reader which will be used to capture still photos
val size = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
.getOutputSizes(args.pixelFormat).maxBy { it.height * it.width }!!
imageReader = ImageReader.newInstance(
size.width, size.height, args.pixelFormat, IMAGE_BUFFER_SIZE)

// Creates list of Surfaces where the camera will output frames
val targets = listOf(viewFinder.holder.surface, imageReader.surface)

// Start a capture session using our open camera and list of Surfaces where frames will go
session = createCaptureSession(camera, targets, cameraHandler)

val captureRequest = camera.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW).apply { addTarget(viewFinder.holder.surface) }

Expand Down Expand Up @@ -215,58 +232,57 @@ class CameraFragment : Fragment() {

/** Opens the camera and returns the opened device (as the result of the suspend coroutine) */
@SuppressLint("MissingPermission")
private suspend fun openCamera(): CameraDevice = suspendCoroutine { cont ->
val cameraManager =
requireContext().getSystemService(Context.CAMERA_SERVICE) as CameraManager
private suspend fun openCamera(
manager: CameraManager,
cameraId: String,
handler: Handler? = null
): CameraDevice = suspendCancellableCoroutine { cont ->
manager.openCamera(cameraId, object : CameraDevice.StateCallback() {
override fun onOpened(device: CameraDevice) = cont.resume(device)

val cameraId = args.cameraId
cameraManager.openCamera(cameraId, object : CameraDevice.StateCallback() {
override fun onDisconnected(device: CameraDevice) {
Log.w(TAG, "Camera $cameraId has been disconnected")
requireActivity().finish()
}

override fun onError(device: CameraDevice, error: Int) {
val exc = RuntimeException("Camera $cameraId open error: $error")
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"
}
val exc = RuntimeException("Camera $cameraId error: ($error) $msg")
Log.e(TAG, exc.message, exc)
cont.resumeWithException(exc)
if (cont.isActive) cont.resumeWithException(exc)
}

override fun onOpened(device: CameraDevice) = cont.resume(device)

}, cameraHandler)
}, handler)
}

/**
* Starts a [CameraCaptureSession] and returns the configured session (as the result of the
* suspend coroutine
*/
private suspend fun startCaptureSession(device: CameraDevice):
CameraCaptureSession = suspendCoroutine { cont ->
val size = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)!!
.getOutputSizes(args.pixelFormat).maxBy { it.height * it.width }!!

imageReader =
ImageReader.newInstance(size.width, size.height, args.pixelFormat, IMAGE_BUFFER_SIZE)
// Create list of Surfaces where the camera will output frames
val targets: MutableList<Surface> =
arrayOf(viewFinder.holder.surface, imageReader.surface).toMutableList()
private suspend fun createCaptureSession(
device: CameraDevice,
targets: List<Surface>,
handler: Handler? = null
): CameraCaptureSession = 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() {

override fun onConfigured(session: CameraCaptureSession) = cont.resume(session)

override fun onConfigureFailed(session: CameraCaptureSession) {
val exc = RuntimeException(
"Camera ${device.id} session configuration failed, see log for details")
val exc = RuntimeException("Camera ${device.id} session configuration failed")
Log.e(TAG, exc.message, exc)
cont.resumeWithException(exc)
}

override fun onConfigured(session: CameraCaptureSession) = cont.resume(session)

}, cameraHandler)
}, handler)
}

/**
Expand All @@ -278,6 +294,7 @@ class CameraFragment : Fragment() {
CombinedCaptureResult = suspendCoroutine { cont ->

// Flush any images left in the image reader
@Suppress("ControlFlowWithEmptyBody")
while (imageReader.acquireNextImage() != null) {}

// Start a new image queue
Expand Down Expand Up @@ -307,6 +324,7 @@ class CameraFragment : Fragment() {
// 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) {

Expand Down Expand Up @@ -347,25 +365,24 @@ class CameraFragment : Fragment() {

/** Helper function used to save a [CombinedCaptureResult] into a [File] */
private suspend fun saveResult(result: CombinedCaptureResult): File = suspendCoroutine { cont ->
when {
when (result.format) {

// When the format is JPEG or DEPTH JPEG we can simply save the bytes as-is
result.format == ImageFormat.JPEG || result.format == ImageFormat.DEPTH_JPEG -> {
ImageFormat.JPEG, ImageFormat.DEPTH_JPEG -> {
val buffer = result.image.planes[0].buffer
val bytes = ByteArray(buffer.remaining()).apply { buffer.get(this) }
try {
val output = createFile(requireContext(), "jpg")
FileOutputStream(output).use { it.write(bytes) }
cont.resume(output)
} catch (exc: IOException) {
Log.e(TAG, "Unable to write RAW image to file", exc)
Log.e(TAG, "Unable to write JPEG image to file", exc)
cont.resumeWithException(exc)
}

}

// When the format is RAW we use the DngCreator utility library
result.format == ImageFormat.RAW_SENSOR -> {
ImageFormat.RAW_SENSOR -> {
val dngCreator = DngCreator(characteristics, result.metadata)
try {
val output = createFile(requireContext(), "dng")
Expand All @@ -388,8 +405,11 @@ class CameraFragment : Fragment() {

override fun onStop() {
super.onStop()
session.close()
session.device.close()
try {
camera.close()
} catch (exc: Throwable) {
Log.e(TAG, "Error closing camera", exc)
}
}

override fun onDestroy() {
Expand All @@ -407,7 +427,7 @@ class CameraFragment : Fragment() {
/** Maximum time allowed to wait for the result of an image capture */
private const val IMAGE_CAPTURE_TIMEOUT_MILLIS: Long = 5000

/** Helper data class used to pass around capture results with their associated image */
/** Helper data class used to hold capture metadata with their associated image */
data class CombinedCaptureResult(
val image: Image,
val metadata: CaptureResult,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.util.Log
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.widget.ImageView
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.navArgs
import androidx.viewpager.widget.PagerAdapter
import androidx.viewpager.widget.ViewPager
import androidx.viewpager2.widget.ViewPager2
import com.bumptech.glide.Glide
import com.example.android.camera2.common.GenericListAdapter
import com.example.android.camera2.common.decodeExifOrientation
import com.example.android.camera2.formats.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.BufferedInputStream
Expand Down Expand Up @@ -65,35 +65,29 @@ class ImageViewerFragment : Fragment() {
/** Data backing our Bitmap viewpager */
private val bitmapList: MutableList<Bitmap> = mutableListOf()

/** Adapter class used to present a view containing one photo or video as a page */
inner class BitmapPagerAdapter : PagerAdapter() {
override fun getCount(): Int = bitmapList.size
override fun instantiateItem(container: ViewGroup, idx: Int): Any {
return ImageView(container.context).apply {
container.addView(this)
Glide.with(this).load(bitmapList[idx]).into(this)
}
}
override fun destroyItem(container: ViewGroup, position: Int, obj: Any) =
container.removeView(obj as View)
override fun isViewFromObject(view: View, obj: Any): Boolean = view == obj
private fun imageViewFactory() = ImageView(requireContext()).apply {
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.fragment_viewpager, container, false).apply {
): View? = ViewPager2(requireContext()).apply {
// Populate the ViewPager and implement a cache of two media items
this as ViewPager
offscreenPageLimit = 2
adapter = BitmapPagerAdapter()
adapter = GenericListAdapter(
bitmapList,
itemViewFactory = { imageViewFactory() }) { view, item, _ ->
view as ImageView
Glide.with(view).load(item).into(view)
}
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

view as ViewPager
view as ViewPager2
lifecycleScope.launch(Dispatchers.IO) {

// Load input image file
Expand All @@ -104,9 +98,7 @@ class ImageViewerFragment : Fragment() {

// If we have depth data attached, attempt to load it
if (isDepth) {

try {

val depthStart = findNextJpegEndMarker(inputBuffer, 2)
addItemToViewPager(view, decodeBitmap(
inputBuffer, depthStart, inputBuffer.size - depthStart))
Expand All @@ -119,7 +111,6 @@ class ImageViewerFragment : Fragment() {
Log.e(TAG, "Invalid start marker for depth or confidence data")
}
}

}
}

Expand All @@ -135,7 +126,7 @@ class ImageViewerFragment : Fragment() {
}

/** Utility function used to add an item to the viewpager and notify it, in the main thread */
private fun addItemToViewPager(view: ViewPager, item: Bitmap) = view.post {
private fun addItemToViewPager(view: ViewPager2, item: Bitmap) = view.post {
bitmapList.add(item)
view.adapter!!.notifyDataSetChanged()
}
Expand All @@ -160,8 +151,9 @@ class ImageViewerFragment : Fragment() {
/** These are the magic numbers used to separate the different JPG data chunks */
private val JPEG_DELIMITER_BYTES = arrayOf(-1, -39)


/** Utility function used to find the markers indicating separation between JPEG data chunks */
/**
* Utility function used to find the markers indicating separation between JPEG data chunks
*/
private fun findNextJpegEndMarker(jpegBuffer: ByteArray, start: Int): Int {

// Sanitize input arguments
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class SelectorFragment : Fragment() {
val cameraList = enumerateCameras(cameraManager)

val layoutId = android.R.layout.simple_list_item_1
adapter = GenericListAdapter(cameraList, layoutId) { view, item, _ ->
adapter = GenericListAdapter(cameraList, itemLayoutId = layoutId) { view, item, _ ->
view.findViewById<TextView>(android.R.id.text1).text = item.title
view.setOnClickListener {
Navigation.findNavController(requireActivity(), R.id.fragment_container)
Expand Down
21 changes: 0 additions & 21 deletions Camera2Formats/app/src/main/res/layout/fragment_viewpager.xml

This file was deleted.

Loading

0 comments on commit f5a03b0

Please sign in to comment.