Skip to content

Instantly share code, notes, and snippets.

@agent10
Created September 20, 2019 08:13
Show Gist options
  • Save agent10/cb84df27bed0dc068343abc56e617135 to your computer and use it in GitHub Desktop.
Save agent10/cb84df27bed0dc068343abc56e617135 to your computer and use it in GitHub Desktop.
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.graphics.*
import android.hardware.camera2.*
import android.media.ImageReader
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.util.Log
import android.util.Size
import android.util.SparseIntArray
import android.view.*
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.reactivex.disposables.CompositeDisposable
import timber.log.Timber
import java.util.*
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.collections.ArrayList
class CameraFragment : BaseFragment(),
ActivityCompat.OnRequestPermissionsResultCallback {
/**
* [TextureView.SurfaceTextureListener] handles several lifecycle events on a
* [TextureView].
*/
private val surfaceTextureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) {
openCamera(width, height)
}
override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) {
configureTransform(width, height)
}
override fun onSurfaceTextureDestroyed(texture: SurfaceTexture) = true
override fun onSurfaceTextureUpdated(texture: SurfaceTexture) = Unit
}
/**
* ID of the current [CameraDevice].
*/
private lateinit var cameraId: String
/**
* An [AutoFitTextureView] for camera preview.
*/
private lateinit var textureView: AutoFitTextureView
/**
* A [CameraCaptureSession] for camera preview.
*/
private var captureSession: CameraCaptureSession? = null
private var previewRequestBuilder: CaptureRequest.Builder? = null
/**
* A reference to the opened [CameraDevice].
*/
private var cameraDevice: CameraDevice? = null
/**
* The [android.util.Size] of camera preview.
*/
private lateinit var previewSize: Size
/**
* [CameraDevice.StateCallback] is called when [CameraDevice] changes its state.
*/
private val stateCallback = object : CameraDevice.StateCallback() {
override fun onOpened(cameraDevice: CameraDevice) {
cameraOpenCloseLock.release()
this@CameraFragment.cameraDevice = cameraDevice
createCameraPreviewSession()
}
override fun onDisconnected(cameraDevice: CameraDevice) {
cameraOpenCloseLock.release()
cameraDevice.close()
this@CameraFragment.cameraDevice = null
}
override fun onError(cameraDevice: CameraDevice, error: Int) {
onDisconnected(cameraDevice)
this@CameraFragment.activity?.finish()
}
}
/**
* An additional thread for running tasks that shouldn't block the UI.
*/
private var backgroundThread: HandlerThread? = null
/**
* A [Handler] for running tasks in the background.
*/
private var backgroundHandler: Handler? = null
/**
* A [Semaphore] to prevent the app from exiting before closing the camera.
*/
private val cameraOpenCloseLock = Semaphore(1)
private lateinit var imageReader: ImageReader
@Inject
lateinit var recognizeImageUseCase: RecognizeImageUseCase
@Inject
lateinit var sharedMainViewModel: SharedMainViewModel
private val disposable = CompositeDisposable()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.camera_fragment_layout, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
textureView = view.findViewById(R.id.texture)
disposable.add(sharedMainViewModel.uiEvents.subscribe {
if (it is SharedMainViewModel.UIEvent.FlashTurn) {
Timber.w("Flash switch todo")
setTorch()
}
})
}
override fun onDestroyView() {
super.onDestroyView()
disposable.clear()
}
private var isTorchEnabled = false
private fun setTorch() {
isTorchEnabled = !isTorchEnabled
previewRequestBuilder?.let { builder ->
builder.set(CaptureRequest.FLASH_MODE, if (isTorchEnabled) CaptureRequest.FLASH_MODE_TORCH else CaptureRequest.FLASH_MODE_OFF)
val previewRequest = builder.build()
captureSession?.setRepeatingRequest(previewRequest, null, backgroundHandler)
}
}
override fun onResume() {
super.onResume()
startBackgroundThread()
// When the screen is turned off and turned back on, the SurfaceTexture is already
// available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open
// a camera and start preview from here (otherwise, we wait until the surface is ready in
// the SurfaceTextureListener).
if (textureView.isAvailable) {
openCamera(textureView.width, textureView.height)
} else {
textureView.surfaceTextureListener = surfaceTextureListener
}
}
override fun onPause() {
closeCamera()
stopBackgroundThread()
super.onPause()
}
private fun requestCameraPermission() {
if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
ConfirmationDialog()
.show(
childFragmentManager,
FRAGMENT_DIALOG
)
} else {
requestPermissions(arrayOf(Manifest.permission.CAMERA), 1)
}
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray
) {
if (requestCode == 1) {
if (grantResults.size != 1 || grantResults[0] != PackageManager.PERMISSION_GRANTED) {
// ErrorDialog.newInstance(getString(R.string.request_permission))
// .show(childFragmentManager,
// FRAGMENT_DIALOG
// )
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
}
/**
* Sets up member variables related to camera.
*
* @param width The width of available size for camera preview
* @param height The height of available size for camera preview
*/
private fun setUpCameraOutputs(width: Int, height: Int) {
val manager = activity!!.getSystemService(Context.CAMERA_SERVICE) as CameraManager
try {
for (cameraId in manager.cameraIdList) {
val characteristics = manager.getCameraCharacteristics(cameraId)
// We don't use a front facing camera in this sample.
val cameraDirection = characteristics.get(CameraCharacteristics.LENS_FACING)
if (cameraDirection != null &&
cameraDirection == CameraCharacteristics.LENS_FACING_FRONT
) {
continue
}
val map = characteristics.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
) ?: continue
// Find out if we need to swap dimension to get the preview size relative to sensor
// coordinate.
val displaySize = Point()
activity!!.windowManager.defaultDisplay.getSize(displaySize)
val swappedDimensions = displaySize.x < displaySize.y
val rotatedPreviewWidth = if (swappedDimensions) height else width
val rotatedPreviewHeight = if (swappedDimensions) width else height
val maxPreviewWidth = rotatedPreviewWidth
val maxPreviewHeight = rotatedPreviewHeight
previewSize =
chooseOptimalSize(
map.getOutputSizes(SurfaceTexture::class.java),
rotatedPreviewWidth, rotatedPreviewHeight,
maxPreviewWidth, maxPreviewHeight,
Size(maxPreviewWidth, maxPreviewHeight)
)
imageReader =
ImageReader.newInstance(
previewSize.width,
previewSize.height,
ImageFormat.YUV_420_888,
2
)
this.cameraId = cameraId
return
}
} catch (e: CameraAccessException) {
Log.e(TAG, e.toString())
} catch (e: NullPointerException) {
// Currently an NPE is thrown when the Camera2API is used but not supported on the
// device this code runs.
// ErrorDialog.newInstance(getString(R.string.camera_error))
// .show(childFragmentManager, FRAGMENT_DIALOG)
}
}
/**
* Opens the camera specified by [CameraFragment.cameraId].
*/
private fun openCamera(width: Int, height: Int) {
val permission = ContextCompat.checkSelfPermission(activity!!, Manifest.permission.CAMERA)
if (permission != PackageManager.PERMISSION_GRANTED) {
requestCameraPermission()
return
}
setUpCameraOutputs(width, height)
configureTransform(width, height)
val manager = activity!!.getSystemService(Context.CAMERA_SERVICE) as CameraManager
try {
// Wait for camera to open - 2.5 seconds is sufficient
if (!cameraOpenCloseLock.tryAcquire(2500, TimeUnit.MILLISECONDS)) {
throw RuntimeException("Time out waiting to lock camera opening.")
}
manager.openCamera(cameraId, stateCallback, backgroundHandler)
} catch (e: CameraAccessException) {
Log.e(TAG, e.toString())
} catch (e: InterruptedException) {
throw RuntimeException("Interrupted while trying to lock camera opening.", e)
}
}
/**
* Closes the current [CameraDevice].
*/
private fun closeCamera() {
try {
cameraOpenCloseLock.acquire()
captureSession?.close()
captureSession = null
cameraDevice?.close()
cameraDevice = null
} catch (e: InterruptedException) {
throw RuntimeException("Interrupted while trying to lock camera closing.", e)
} finally {
cameraOpenCloseLock.release()
}
}
/**
* Starts a background thread and its [Handler].
*/
private fun startBackgroundThread() {
backgroundThread = HandlerThread("CameraBackground").also { it.start() }
backgroundHandler = Handler(backgroundThread?.looper)
}
/**
* Stops the background thread and its [Handler].
*/
private fun stopBackgroundThread() {
backgroundThread?.quitSafely()
try {
backgroundThread?.join()
backgroundThread = null
backgroundHandler = null
} catch (e: InterruptedException) {
Log.e(TAG, e.toString())
}
}
/**
* Creates a new [CameraCaptureSession] for camera preview.
*/
private fun createCameraPreviewSession() {
try {
val texture = textureView.surfaceTexture
imageReader.setOnImageAvailableListener({ reader ->
val img = reader.acquireLatestImage()
img?.let {
val buf = img.planes[0].buffer
val data = ByteArray(buf.remaining())
buf.get(data)
val w = img.width
val h = img.height
img.close()
recognizeImageUseCase.recognize(data, w, h)
}
}, backgroundHandler)
// We configure the size of default buffer to be the size of camera preview we want.
texture.setDefaultBufferSize(previewSize.width, previewSize.height)
// This is the output Surface we need to start preview.
val surface = Surface(texture)
val imgSurface = imageReader.surface
// We set up a CaptureRequest.Builder with the output Surface.
previewRequestBuilder = cameraDevice!!.createCaptureRequest(
CameraDevice.TEMPLATE_PREVIEW
)
previewRequestBuilder?.addTarget(surface)
previewRequestBuilder?.addTarget(imgSurface)
// Here, we create a CameraCaptureSession for camera preview.
cameraDevice?.createCaptureSession(
Arrays.asList(surface, imgSurface),
object : CameraCaptureSession.StateCallback() {
override fun onConfigured(cameraCaptureSession: CameraCaptureSession) {
// The camera is already closed
if (cameraDevice == null) return
// When the session is ready, we start displaying the preview.
captureSession = cameraCaptureSession
try {
previewRequestBuilder?.let { builder ->
// Auto focus should be continuous for camera preview.
builder.set(
CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
)
builder.set(
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON
)
// Finally, we start displaying the camera preview.
val previewRequest = builder.build()
captureSession?.setRepeatingRequest(
previewRequest,
null, backgroundHandler
)
}
} catch (e: CameraAccessException) {
Log.e(TAG, e.toString())
}
}
override fun onConfigureFailed(session: CameraCaptureSession) {
// activity.showToast("Failed")
Log.e("CAMERA", "kiol failed")
}
}, null
)
} catch (e: CameraAccessException) {
Log.e(TAG, e.toString())
}
}
/**
* Configures the necessary [android.graphics.Matrix] transformation to `textureView`.
* This method should be called after the camera preview size is determined in
* setUpCameraOutputs and also the size of `textureView` is fixed.
*
* @param viewWidth The width of `textureView`
* @param viewHeight The height of `textureView`
*/
private fun configureTransform(viewWidth: Int, viewHeight: Int) {
activity ?: return
val rotation = activity!!.windowManager.defaultDisplay.rotation
val matrix = Matrix()
val viewRect = RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat())
val bufferRect = RectF(0f, 0f, previewSize.width.toFloat(), previewSize.height.toFloat())
val centerX = viewRect.centerX()
val centerY = viewRect.centerY()
if (Surface.ROTATION_90 == rotation || Surface.ROTATION_270 == rotation) {
with(matrix) {
postRotate((90 * (rotation - 2)).toFloat(), centerX, centerY)
}
} else if (Surface.ROTATION_180 == rotation) {
matrix.postRotate(180f, centerX, centerY)
}
val bufferRatio = previewSize.height / previewSize.width.toFloat()
val scaledWidth: Float
val scaledHeight: Float
// Match longest sides together -- i.e. apply center-crop transformation
if (viewRect.width() > viewRect.height()) {
scaledHeight = viewRect.width()
scaledWidth = viewRect.width() * bufferRatio
} else {
scaledHeight = viewRect.height()
scaledWidth = viewRect.height() * bufferRatio
}
// Compute the relative scale value
val xScale = scaledWidth / viewRect.width()
val yScale = scaledHeight / viewRect.height()
// Scale input buffers to fill the view finder
matrix.preScale(xScale, yScale, centerX, centerY)
matrix.mapRect(viewRect)
val scale = if (viewRect.width().toFloat() * viewHeight.toFloat() > viewWidth.toFloat() * viewRect.height().toFloat()) {
viewHeight.toFloat() / viewRect.height().toFloat()
} else {
viewWidth.toFloat() / viewRect.width().toFloat()
}
matrix.preScale(scale, scale, centerX, centerY)
textureView.setTransform(matrix)
}
companion object {
/**
* Conversion from screen rotation to JPEG orientation.
*/
private val ORIENTATIONS = SparseIntArray()
private val FRAGMENT_DIALOG = "dialog"
init {
ORIENTATIONS.append(Surface.ROTATION_0, 90)
ORIENTATIONS.append(Surface.ROTATION_90, 0)
ORIENTATIONS.append(Surface.ROTATION_180, 270)
ORIENTATIONS.append(Surface.ROTATION_270, 180)
}
/**
* Tag for the [Log].
*/
private val TAG = "CameraFragment"
/**
* Given `choices` of `Size`s supported by a camera, choose the smallest one that
* is at least as large as the respective texture view size, and that is at most as large as
* the respective max size, and whose aspect ratio matches with the specified value. If such
* size doesn't exist, choose the largest one that is at most as large as the respective max
* size, and whose aspect ratio matches with the specified value.
*
* @param choices The list of sizes that the camera supports for the intended
* output class
* @param textureViewWidth The width of the texture view relative to sensor coordinate
* @param textureViewHeight The height of the texture view relative to sensor coordinate
* @param maxWidth The maximum width that can be chosen
* @param maxHeight The maximum height that can be chosen
* @param aspectRatio The aspect ratio
* @return The optimal `Size`, or an arbitrary one if none were big enough
*/
@JvmStatic
private fun chooseOptimalSize(
choices: Array<Size>,
textureViewWidth: Int,
textureViewHeight: Int,
maxWidth: Int,
maxHeight: Int,
aspectRatio: Size
): Size {
// Collect the supported resolutions that are at least as big as the preview Surface
val bigEnough = ArrayList<Size>()
// Collect the supported resolutions that are smaller than the preview Surface
val notBigEnough = ArrayList<Size>()
val w = aspectRatio.width
val h = aspectRatio.height
for (option in choices) {
if (option.width <= maxWidth && option.height <= maxHeight
) {
if (option.width >= textureViewWidth && option.height >= textureViewHeight) {
bigEnough.add(option)
} else {
notBigEnough.add(option)
}
}
}
// Pick the smallest of those big enough. If there is no one big enough, pick the
// largest of those not big enough.
if (bigEnough.size > 0) {
return Collections.min(
bigEnough,
CompareSizesByArea()
)
} else if (notBigEnough.size > 0) {
return Collections.max(
notBigEnough,
CompareSizesByArea()
)
} else {
Log.e(TAG, "Couldn't find any suitable preview size")
return choices[0]
}
}
@JvmStatic
fun newInstance(): CameraFragment =
CameraFragment()
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment