zcash-android-wallet-poc/zcash-android-wallet-app/qrecycler/src/main/java/cash/z/android/cameraview/CameraView.kt

596 lines
20 KiB
Kotlin

/*
* Copyright (C) 2016 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 cash.z.android.cameraview
import android.app.Activity
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.hardware.camera2.CameraAccessException
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.media.Image
import android.os.Build
import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.view.Surface
import android.view.View
import android.widget.FrameLayout
import androidx.annotation.IntDef
import androidx.annotation.NonNull
import androidx.annotation.Nullable
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import cash.z.android.cameraview.api21.Camera2
import cash.z.android.cameraview.base.AspectRatio
import cash.z.android.cameraview.base.CameraViewImpl
import cash.z.android.cameraview.base.Constants
import cash.z.android.cameraview.base.PreviewImpl
import cash.z.android.qrecycler.R
import com.google.android.cameraview.Camera2Api23
import com.google.android.cameraview.TextureViewPreview
import com.google.firebase.ml.vision.barcode.FirebaseVisionBarcode
import com.google.firebase.ml.vision.common.FirebaseVisionImageMetadata
import java.util.*
open class CameraView(context: Context, attrs: AttributeSet?, defStyleAttr: Int) :
FrameLayout(context, attrs, defStyleAttr) {
@JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : this(context, attrs, 0)
internal lateinit var mImpl: CameraViewImpl
private var mCallbacks: CallbackBridge?
private var mAdjustViewBounds: Boolean = false
private var mDisplayOrientationDetector: DisplayOrientationDetector?
var firebaseCallback: FirebaseCallback? = null
set(value) {
(mImpl as? Camera2)?.firebaseCallback = value
field = value
}
lateinit var cameraId: String
/**
* @return `true` if the camera is opened.
*/
val isCameraOpened: Boolean
get() = mImpl.isCameraOpened
/**
* @return True when this CameraView is adjusting its bounds to preserve the aspect ratio of
* camera.
* @see .setAdjustViewBounds
*/
/**
* @param adjustViewBounds `true` if you want the CameraView to adjust its bounds to
* preserve the aspect ratio of camera.
* @see .getAdjustViewBounds
*/
var adjustViewBounds: Boolean
get() = mAdjustViewBounds
set(adjustViewBounds) {
if (mAdjustViewBounds != adjustViewBounds) {
mAdjustViewBounds = adjustViewBounds
requestLayout()
}
}
/**
* Gets the direction that the current camera faces.
*
* @return The camera facing.
*/
/**
* Chooses camera by the direction it faces.
*
* @param facing The camera facing. Must be either [.FACING_BACK] or
* [.FACING_FRONT].
*/
var facing: Int
@Facing
get() = mImpl.facing
set(@Facing facing) {
mImpl.facing = facing
}
/**
* Gets all the aspect ratios supported by the current camera.
*/
val supportedAspectRatios: Set<AspectRatio>
get() = mImpl.supportedAspectRatios
/**
* Gets the current aspect ratio of camera.
*
* @return The current [AspectRatio]. Can be `null` if no camera is opened yet.
*/
/**
* Sets the aspect ratio of camera.
*
* @param ratio The [AspectRatio] to be set.
*/
var aspectRatio: AspectRatio?
@Nullable
get() = mImpl.aspectRatio
set(@NonNull ratio) {
if (mImpl.setAspectRatio(ratio)) {
requestLayout()
}
}
/**
* Returns whether the continuous auto-focus mode is enabled.
*
* @return `true` if the continuous auto-focus mode is enabled. `false` if it is
* disabled, or if it is not supported by the current camera.
*/
/**
* Enables or disables the continuous auto-focus mode. When the current camera doesn't support
* auto-focus, calling this method will be ignored.
*
* @param autoFocus `true` to enable continuous auto-focus mode. `false` to
* disable it.
*/
var autoFocus: Boolean
get() = mImpl.autoFocus
set(autoFocus) {
mImpl.autoFocus = autoFocus
}
/**
* Gets the current flash mode.
*
* @return The current flash mode.
*/
/**
* Sets the flash mode.
*
* @param flash The desired flash mode.
*/
var flash: Int
@Flash
get() = mImpl.flash
set(@Flash flash) {
mImpl.flash = flash
}
/** Direction the camera faces relative to device screen. */
@IntDef(FACING_BACK, FACING_FRONT)
@kotlin.annotation.Retention(AnnotationRetention.SOURCE)
annotation class Facing
/** The mode for for the camera device's flash control */
@IntDef(FLASH_OFF, FLASH_ON, FLASH_TORCH, FLASH_AUTO, FLASH_RED_EYE)
annotation class Flash
init {
if (isInEditMode) {
mCallbacks = null
mDisplayOrientationDetector = null
} else {
// Internal setup
val preview = createPreviewImpl(context)
mCallbacks = CallbackBridge()
if (Build.VERSION.SDK_INT < 23) {
mImpl = Camera2(mCallbacks!!, preview, context)
} else {
mImpl = Camera2Api23(mCallbacks!!, preview, context)
}
// Attributes
val a = context.obtainStyledAttributes(
attrs, R.styleable.CameraView, defStyleAttr,
R.style.Widget_CameraView
)
mAdjustViewBounds = a.getBoolean(R.styleable.CameraView_android_adjustViewBounds, false)
facing = a.getInt(R.styleable.CameraView_facing, FACING_BACK)
var aspectRatioString = a.getString(R.styleable.CameraView_aspectRatio)
if (aspectRatioString != null) {
aspectRatio = AspectRatio.parse(aspectRatioString)
} else {
aspectRatio = Constants.DEFAULT_ASPECT_RATIO
}
autoFocus = a.getBoolean(R.styleable.CameraView_autoFocus, true)
flash = a.getInt(R.styleable.CameraView_flash, Constants.FLASH_AUTO)
a.recycle()
// Display orientation detector
mDisplayOrientationDetector = object : DisplayOrientationDetector(context) {
override fun onDisplayOrientationChanged(displayOrientation: Int) {
mImpl.setDisplayOrientation(displayOrientation)
}
}
}
}
@NonNull
private fun createPreviewImpl(context: Context): PreviewImpl {
return TextureViewPreview(context, this)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
if (!isInEditMode) {
mDisplayOrientationDetector!!.enable(ViewCompat.getDisplay(this)!!)
}
}
override fun onDetachedFromWindow() {
if (!isInEditMode) {
mDisplayOrientationDetector!!.disable()
}
super.onDetachedFromWindow()
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
if (isInEditMode) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
return
}
// Handle android:adjustViewBounds
if (mAdjustViewBounds) {
if (!isCameraOpened) {
mCallbacks!!.reserveRequestLayoutOnOpen()
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
return
}
val widthMode = View.MeasureSpec.getMode(widthMeasureSpec)
val heightMode = View.MeasureSpec.getMode(heightMeasureSpec)
if (widthMode == View.MeasureSpec.EXACTLY && heightMode != View.MeasureSpec.EXACTLY) {
val ratio = aspectRatio!!
var height = (View.MeasureSpec.getSize(widthMeasureSpec) * ratio!!.toFloat()) as Int
if (heightMode == View.MeasureSpec.AT_MOST) {
height = Math.min(height, View.MeasureSpec.getSize(heightMeasureSpec))
}
super.onMeasure(
widthMeasureSpec,
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
)
} else if (widthMode != View.MeasureSpec.EXACTLY && heightMode == View.MeasureSpec.EXACTLY) {
val ratio = aspectRatio!!
var width = (View.MeasureSpec.getSize(heightMeasureSpec) * ratio!!.toFloat()) as Int
if (widthMode == View.MeasureSpec.AT_MOST) {
width = Math.min(width, View.MeasureSpec.getSize(widthMeasureSpec))
}
super.onMeasure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
heightMeasureSpec
)
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
// Measure the TextureView
val width = measuredWidth
val height = measuredHeight
var ratio = aspectRatio
if (mDisplayOrientationDetector!!.lastKnownDisplayOrientation % 180 == 0) {
ratio = ratio!!.inverse()
}
assert(ratio != null)
if (height < width * ratio!!.y / ratio!!.x) {
mImpl.view.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(
width * ratio!!.y / ratio!!.x,
View.MeasureSpec.EXACTLY
)
)
} else {
mImpl.view.measure(
View.MeasureSpec.makeMeasureSpec(
height * ratio!!.x / ratio!!.y,
View.MeasureSpec.EXACTLY
),
View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
)
}
}
private var rectPaint = Paint().apply {
color = Color.GREEN
style = Paint.Style.FILL
strokeWidth = 8f
}
override fun draw(canvas: Canvas) {
super.draw(canvas)
val rect = RectF(0f,0f,canvas.width.toFloat(),canvas.height.toFloat())
canvas.drawRect(rect, rectPaint)
}
override fun onSaveInstanceState(): Parcelable? {
val state = SavedState(super.onSaveInstanceState())
state.facing = facing
state.ratio = aspectRatio
state.autoFocus = autoFocus
state.flash = flash
return state
}
override fun onRestoreInstanceState(state: Parcelable?) {
if (state !is SavedState) {
super.onRestoreInstanceState(state)
return
}
val ss = state as SavedState?
super.onRestoreInstanceState(ss!!.getSuperState())
facing = ss.facing
aspectRatio = ss.ratio
autoFocus = ss.autoFocus
flash = ss.flash
}
/**
* Open a camera device and start showing camera preview. This is typically called from
* [Activity.onResume].
*/
fun start() {
if (!mImpl.start()) {
throw IllegalStateException("failed to start even though we're on API 21+")
// //store the state ,and restore this state after fall back o Camera1
// val state = onSaveInstanceState()
// // Camera2 uses legacy hardware layer; fall back to Camera1
// mImpl = Camera1(mCallbacks, createPreviewImpl(context))
// onRestoreInstanceState(state)
// mImpl.start()
}
// start results in cameraId being set so bubble that up for firebase rotation use
when(mImpl) {
is Camera2 -> cameraId = (mImpl as Camera2).cameraId!!
}
}
/**
* Stop camera preview and close the device. This is typically called from
* [Activity.onPause].
*/
fun stop() {
mImpl.stop()
}
/**
* Add a new callback.
*
* @param callback The [Callback] to add.
* @see .removeCallback
*/
fun addCallback(@NonNull callback: Callback) {
mCallbacks!!.add(callback)
}
/**
* Remove a callback.
*
* @param callback The [Callback] to remove.
* @see .addCallback
*/
fun removeCallback(@NonNull callback: Callback) {
mCallbacks!!.remove(callback)
}
/**
* Take a picture. The result will be returned to
* [Callback.onPictureTaken].
*/
fun takePicture() {
mImpl.takePicture()
}
fun setBarcode(barcode: FirebaseVisionBarcode) {
}
interface FirebaseCallback {
fun onImageAvailable(image: Image)
// TODO: attribute this code. The library I found it in has no attribution but it clearly came from somewhere. Modified it to not require instantiating a sparsearray of orientations (just use when instead) also simplified method signature
// one source : https://github.com/firebase/snippets-android/blob/master/mlkit/app/src/main/java/com/google/firebase/example/mlkit/VisionImage.java
/**
* Get the angle by which an image must be rotated given the device's current
* orientation.
*/
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
@Throws(CameraAccessException::class)
fun getRotationCompensation(cameraId: String, activity: Activity): Int {
// Get the device's current rotation relative to its "native" orientation.
// Then, from the ORIENTATIONS table, look up the angle the image must be
// rotated to compensate for the device's rotation.
val deviceRotation = activity.windowManager.defaultDisplay.rotation
var rotationCompensation = when(deviceRotation) {
Surface.ROTATION_0 -> 90
Surface.ROTATION_90 -> 0
Surface.ROTATION_180 -> 270
Surface.ROTATION_270 -> 180
else -> throw IllegalArgumentException("Unsupported rotation value! Expected [0|90|180|270] but got: $deviceRotation")
}
// On most devices, the sensor orientation is 90 degrees, but for some
// devices it is 270 degrees. For devices with a sensor orientation of
// 270, rotate the image an additional 180 ((270 + 270) % 360) degrees.
val cameraManager = activity.getSystemService(AppCompatActivity.CAMERA_SERVICE) as CameraManager
val sensorOrientation = cameraManager
.getCameraCharacteristics(cameraId)
.get(CameraCharacteristics.SENSOR_ORIENTATION)!!
rotationCompensation = (rotationCompensation + sensorOrientation + 270) % 360
// Return the corresponding FirebaseVisionImageMetadata rotation value.
val result: Int
when (rotationCompensation) {
0 -> result = FirebaseVisionImageMetadata.ROTATION_0
90 -> result = FirebaseVisionImageMetadata.ROTATION_90
180 -> result = FirebaseVisionImageMetadata.ROTATION_180
270 -> result = FirebaseVisionImageMetadata.ROTATION_270
else -> throw IllegalArgumentException("Unsupported rotation value! Expected [0|90|180|270] but got: $deviceRotation") // this should be impossible, given that we would have already thrown an exception
}
return result
}
}
private inner class CallbackBridge internal constructor() : CameraViewImpl.Callback {
private val mCallbacks = ArrayList<Callback>()
private var mRequestLayoutOnOpen: Boolean = false
fun add(callback: Callback) {
mCallbacks.add(callback)
}
fun remove(callback: Callback) {
mCallbacks.remove(callback)
}
override fun onCameraOpened() {
if (mRequestLayoutOnOpen) {
mRequestLayoutOnOpen = false
requestLayout()
}
for (callback in mCallbacks) {
callback.onCameraOpened(this@CameraView)
}
}
override fun onCameraClosed() {
for (callback in mCallbacks) {
callback.onCameraClosed(this@CameraView)
}
}
override fun onPictureTaken(data: ByteArray) {
for (callback in mCallbacks) {
callback.onPictureTaken(this@CameraView, data)
}
}
fun reserveRequestLayoutOnOpen() {
mRequestLayoutOnOpen = true
}
}
protected class SavedState : View.BaseSavedState {
@Facing
internal var facing: Int = 0
internal var ratio: AspectRatio? = null
internal var autoFocus: Boolean = false
@Flash
internal var flash: Int = 0
constructor(source: Parcel) : this(source, AspectRatio::class.java.classLoader!!)
constructor(source: Parcel, loader: ClassLoader) : super(source) {
facing = source.readInt()
ratio = source.readParcelable(loader)
autoFocus = source.readByte().toInt() != 0
flash = source.readInt()
}
constructor(parcelable: Parcelable) : super(parcelable)
override fun writeToParcel(out: Parcel, flags: Int) {
super.writeToParcel(out, flags)
out.writeInt(facing)
out.writeParcelable(ratio, 0)
out.writeByte((if (autoFocus) 1 else 0).toByte())
out.writeInt(flash)
}
override fun describeContents(): Int {
return 0
}
companion object CREATOR : Parcelable.Creator<SavedState> {
override fun createFromParcel(parcel: Parcel): SavedState {
return SavedState(parcel)
}
override fun newArray(size: Int): Array<SavedState?> {
return arrayOfNulls(size)
}
}
}
/**
* Callback for monitoring events about [CameraView].
*/
abstract class Callback {
/**
* Called when camera is opened.
*
* @param cameraView The associated [CameraView].
*/
open fun onCameraOpened(cameraView: CameraView) {}
/**
* Called when camera is closed.
*
* @param cameraView The associated [CameraView].
*/
open fun onCameraClosed(cameraView: CameraView) {}
/**
* Called when a picture is taken.
*
* @param cameraView The associated [CameraView].
* @param data JPEG data.
*/
open fun onPictureTaken(cameraView: CameraView, data: ByteArray) {}
}
companion object {
/** The camera device faces the opposite direction as the device's screen. */
const val FACING_BACK = Constants.FACING_BACK
/** The camera device faces the same direction as the device's screen. */
const val FACING_FRONT = Constants.FACING_FRONT
/** Flash will not be fired. */
const val FLASH_OFF = Constants.FLASH_OFF
/** Flash will always be fired during snapshot. */
const val FLASH_ON = Constants.FLASH_ON
/** Constant emission of light during preview, auto-focus and snapshot. */
const val FLASH_TORCH = Constants.FLASH_TORCH
/** Flash will be fired automatically when required. */
const val FLASH_AUTO = Constants.FLASH_AUTO
/** Flash will be fired in red-eye reduction mode. */
const val FLASH_RED_EYE = Constants.FLASH_RED_EYE
}
}