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

827 lines
28 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.api21
import android.Manifest
import android.annotation.TargetApi
import android.content.Context
import android.graphics.ImageFormat
import android.hardware.camera2.*
import android.hardware.camera2.params.StreamConfigurationMap
import android.media.ImageReader
import android.os.Handler
import android.util.Log
import android.util.SparseIntArray
import androidx.annotation.NonNull
import androidx.annotation.RequiresPermission
import cash.z.android.cameraview.CameraView
import cash.z.android.cameraview.base.*
import android.os.HandlerThread
@TargetApi(21)
internal open class Camera2(callback: CameraViewImpl.Callback, preview: PreviewImpl, context: Context) : CameraViewImpl(callback, preview) {
private val mCameraManager: CameraManager
var firebaseCallback: CameraView.FirebaseCallback? = null
private val mCameraDeviceCallback = object : CameraDevice.StateCallback() {
override fun onOpened(@NonNull camera: CameraDevice) {
mCamera = camera
mCallback.onCameraOpened()
startCaptureSession()
}
override fun onClosed(@NonNull camera: CameraDevice) {
mCallback.onCameraClosed()
}
override fun onDisconnected(@NonNull camera: CameraDevice) {
mCamera = null
}
override fun onError(@NonNull camera: CameraDevice, error: Int) {
Log.e(TAG, "onError: " + camera.id + " (" + error + ")")
mCamera = null
}
}
private val mSessionCallback = object : CameraCaptureSession.StateCallback() {
override fun onConfigured(@NonNull session: CameraCaptureSession) {
if (mCamera == null) {
return
}
mCaptureSession = session
updateAutoFocus()
updateFlash()
try {
mCaptureSession!!.setRepeatingRequest(
mPreviewRequestBuilder!!.build(),
mCaptureCallback, backgroundHandler
)
} catch (e: CameraAccessException) {
Log.e(TAG, "Failed to start camera preview because it couldn't access camera", e)
} catch (e: IllegalStateException) {
Log.e(TAG, "Failed to start camera preview.", e)
}
}
override fun onConfigureFailed(@NonNull session: CameraCaptureSession) {
Log.e(TAG, "Failed to configure capture session.")
}
override fun onClosed(@NonNull session: CameraCaptureSession) {
if (mCaptureSession != null && mCaptureSession == session) {
mCaptureSession = null
}
}
}
private var mCaptureCallback: PictureCaptureCallback = object : PictureCaptureCallback() {
override fun onPrecaptureRequired() {
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_START
)
setState(Camera2.PictureCaptureCallback.STATE_PRECAPTURE)
try {
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), this, backgroundHandler)
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER,
CaptureRequest.CONTROL_AE_PRECAPTURE_TRIGGER_IDLE
)
} catch (e: CameraAccessException) {
Log.e(TAG, "Failed to run precapture sequence.", e)
}
}
override fun onReady() {
captureStillPicture()
}
}
private val mOnImageAvailableListener = ImageReader.OnImageAvailableListener { reader ->
reader.acquireNextImage().use { image ->
val planes = image.planes
if (planes.isNotEmpty()) {
System.err.println("camoorah : planes was empty: $firebaseCallback")
firebaseCallback?.onImageAvailable(image)
try{ image.close() } catch(t: Throwable){ System.err.println("camoorah : failed to close")}
} else {
System.err.println("planes was empty")
}
}
}
var cameraId: String? = null
private var mCameraCharacteristics: CameraCharacteristics? = null
var mCamera: CameraDevice? = null
var mCaptureSession: CameraCaptureSession? = null
var mPreviewRequestBuilder: CaptureRequest.Builder? = null
private var mImageReader: ImageReader? = null
private val mPreviewSizes = SizeMap()
private val mPictureSizes = SizeMap()
private var mFacing: Int = 0
private var mAspectRatio = Constants.DEFAULT_ASPECT_RATIO
private var mAutoFocus: Boolean = false
// Revert
override var flash: Int = 0
set(flash) {
if (this.flash == flash) {
return
}
val saved = this.flash
field = flash
if (mPreviewRequestBuilder != null) {
updateFlash()
if (mCaptureSession != null) {
try {
mCaptureSession!!.setRepeatingRequest(
mPreviewRequestBuilder!!.build(),
mCaptureCallback, backgroundHandler
)
} catch (e: CameraAccessException) {
field = saved
}
}
}
}
private var mDisplayOrientation: Int = 0
override val isCameraOpened: Boolean
get() = mCamera != null
override var facing: Int
get() = mFacing
@RequiresPermission(Manifest.permission.CAMERA) set(facing) {
if (mFacing == facing) {
return
}
mFacing = facing
if (isCameraOpened) {
stop()
start()
}
}
override val supportedAspectRatios: Set<AspectRatio>
get() = mPreviewSizes.ratios()
// Revert
override var autoFocus: Boolean
get() = mAutoFocus
set(autoFocus) {
if (mAutoFocus == autoFocus) {
return
}
mAutoFocus = autoFocus
if (mPreviewRequestBuilder != null) {
updateAutoFocus()
if (mCaptureSession != null) {
try {
mCaptureSession!!.setRepeatingRequest(
mPreviewRequestBuilder!!.build(),
mCaptureCallback, backgroundHandler
)
} catch (e: CameraAccessException) {
mAutoFocus = !mAutoFocus
}
}
}
}
init {
mCameraManager = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
mPreview.setCallback(object : PreviewImpl.Callback {
override fun onSurfaceChanged() {
startCaptureSession()
}
})
}
@RequiresPermission(Manifest.permission.CAMERA)
override fun start(): Boolean {
if (!chooseCameraIdByFacing()) {
return false
}
startBackgroundThread()
collectCameraInfo()
prepareImageReader()
startOpeningCamera()
return true
}
override fun stop() {
stopBackgroundThread()
if (mCaptureSession != null) {
mCaptureSession!!.close()
mCaptureSession = null
}
if (mCamera != null) {
mCamera!!.close()
mCamera = null
}
if (mImageReader != null) {
mImageReader!!.close()
mImageReader = null
}
}
override val aspectRatio: AspectRatio get() = mAspectRatio
override fun setAspectRatio(ratio: AspectRatio?): Boolean {
if (ratio == null || ratio == mAspectRatio ||
!mPreviewSizes.ratios().contains(ratio)
) {
// TODO: Better error handling
return false
}
mAspectRatio = ratio
prepareImageReader()
if (mCaptureSession != null) {
mCaptureSession!!.close()
mCaptureSession = null
startCaptureSession()
}
return true
}
override fun takePicture() {
if (mAutoFocus) {
lockFocus()
} else {
captureStillPicture()
}
}
override fun setDisplayOrientation(displayOrientation: Int) {
mDisplayOrientation = displayOrientation
mPreview.setDisplayOrientation(mDisplayOrientation)
}
/**
*
* Chooses a camera ID by the specified camera facing ([.mFacing]).
*
* This rewrites [.mCameraId], [.mCameraCharacteristics], and optionally
* [.mFacing].
*/
private fun chooseCameraIdByFacing(): Boolean {
try {
val internalFacing = INTERNAL_FACINGS.get(mFacing)
val ids = mCameraManager.cameraIdList
if (ids.size == 0) { // No camera
throw RuntimeException("No camera available.")
}
for (id in ids) {
val characteristics = mCameraManager.getCameraCharacteristics(id)
val level = characteristics.get(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
)
if (level == null || level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
continue
}
val internal = characteristics.get(CameraCharacteristics.LENS_FACING)
?: throw NullPointerException("Unexpected state: LENS_FACING null")
if (internal == internalFacing) {
cameraId = id
mCameraCharacteristics = characteristics
return true
}
}
// Not found
cameraId = ids[0]
mCameraCharacteristics = mCameraManager.getCameraCharacteristics(cameraId!!)
val level = mCameraCharacteristics!!.get(
CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL
)
if (level == null || level == CameraCharacteristics.INFO_SUPPORTED_HARDWARE_LEVEL_LEGACY) {
return false
}
val internal = mCameraCharacteristics!!.get(CameraCharacteristics.LENS_FACING)
?: throw NullPointerException("Unexpected state: LENS_FACING null")
var i = 0
val count = INTERNAL_FACINGS.size()
while (i < count) {
if (INTERNAL_FACINGS.valueAt(i) == internal) {
mFacing = INTERNAL_FACINGS.keyAt(i)
return true
}
i++
}
// The operation can reach here when the only camera device is an external one.
// We treat it as facing back.
mFacing = Constants.FACING_BACK
return true
} catch (e: CameraAccessException) {
throw RuntimeException("Failed to get a list of camera devices", e)
}
}
/**
*
* Collects some information from [.mCameraCharacteristics].
*
* This rewrites [.mPreviewSizes], [.mPictureSizes], and optionally,
* [.mAspectRatio].
*/
private fun collectCameraInfo() {
val map = mCameraCharacteristics!!.get(
CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP
) ?: throw IllegalStateException("Failed to get configuration map: " + cameraId!!)
mPreviewSizes.clear()
for (size in map.getOutputSizes(mPreview.outputClass)) {
val width = size.width
val height = size.height
if (width <= MAX_PREVIEW_WIDTH && height <= MAX_PREVIEW_HEIGHT) {
mPreviewSizes.add(Size(width, height))
}
}
mPictureSizes.clear()
collectPictureSizes(mPictureSizes, map)
for (ratio in mPreviewSizes.ratios()) {
if (!mPictureSizes.ratios().contains(ratio)) {
mPreviewSizes.remove(ratio)
}
}
if (!mPreviewSizes.ratios().contains(mAspectRatio)) {
mAspectRatio = mPreviewSizes.ratios().iterator().next()
}
}
protected open fun collectPictureSizes(sizes: SizeMap, map: StreamConfigurationMap) {
val outputSizes = map.getOutputSizes(ImageFormat.JPEG)
for (size in outputSizes) {
mPictureSizes.add(Size(size.width, size.height))
}
}
private fun prepareImageReader() {
if (mImageReader != null) {
mImageReader!!.close()
}
// val largest = mPictureSizes.sizes(mAspectRatio).last()
val previewSize = chooseOptimalSize()
mImageReader = ImageReader.newInstance(
previewSize.width / 4, previewSize.height / 4, ImageFormat.YUV_420_888, 2
)
mImageReader!!.setOnImageAvailableListener(mOnImageAvailableListener, backgroundHandler)
}
/**
*
* Starts opening a camera device.
*
* The result will be processed in [.mCameraDeviceCallback].
*/
@RequiresPermission(Manifest.permission.CAMERA)
private fun startOpeningCamera() {
try {
mCameraManager.openCamera(cameraId!!, mCameraDeviceCallback, backgroundHandler)
} catch (e: CameraAccessException) {
throw RuntimeException("Failed to open camera: " + cameraId!!, e)
}
}
/**
*
* Starts a capture session for camera preview.
*
* This rewrites [.mPreviewRequestBuilder].
*
* The result will be continuously processed in [.mSessionCallback].
*/
fun startCaptureSession() {
if (!isCameraOpened || !mPreview.isReady || mImageReader == null) {
return
}
val previewSize = chooseOptimalSize()
mPreview.setBufferSize(previewSize.width, previewSize.height)
val surface = mPreview.surface
try {
mPreviewRequestBuilder = mCamera!!.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW)
mPreviewRequestBuilder!!.addTarget(surface)
mPreviewRequestBuilder!!.addTarget(mImageReader!!.surface)
mCamera!!.createCaptureSession(
listOf(surface, mImageReader!!.surface),
mSessionCallback, backgroundHandler
)
} catch (e: CameraAccessException) {
throw RuntimeException("Failed to start camera session")
}
}
/**
* Chooses the optimal preview size based on [.mPreviewSizes] and the surface size.
*
* @return The picked size for camera preview.
*/
private fun chooseOptimalSize(): Size {
val surfaceLonger: Int
val surfaceShorter: Int
val surfaceWidth = mPreview.width
val surfaceHeight = mPreview.height
if (surfaceWidth < surfaceHeight) {
surfaceLonger = surfaceHeight
surfaceShorter = surfaceWidth
} else {
surfaceLonger = surfaceWidth
surfaceShorter = surfaceHeight
}
val candidates = mPreviewSizes.sizes(mAspectRatio)
// Pick the smallest of those big enough
for (size in candidates) {
if (size.width >= surfaceLonger && size.height >= surfaceShorter) {
return size
}
}
// If no size is big enough, pick the largest one.
return candidates.last()
}
/**
* Updates the internal state of auto-focus to [.mAutoFocus].
*/
fun updateAutoFocus() {
if (mAutoFocus) {
val modes = mCameraCharacteristics!!.get(
CameraCharacteristics.CONTROL_AF_AVAILABLE_MODES
)
// Auto focus is not supported
if (modes == null || modes.size == 0 ||
modes.size == 1 && modes[0] == CameraCharacteristics.CONTROL_AF_MODE_OFF
) {
mAutoFocus = false
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_OFF
)
} else {
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_CONTINUOUS_PICTURE
)
}
} else {
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AF_MODE,
CaptureRequest.CONTROL_AF_MODE_OFF
)
}
}
/**
* Updates the internal state of flash to [.mFlash].
*/
fun updateFlash() {
when (flash) {
Constants.FLASH_OFF -> {
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON
)
mPreviewRequestBuilder!!.set(
CaptureRequest.FLASH_MODE,
CaptureRequest.FLASH_MODE_OFF
)
}
Constants.FLASH_ON -> {
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH
)
mPreviewRequestBuilder!!.set(
CaptureRequest.FLASH_MODE,
CaptureRequest.FLASH_MODE_OFF
)
}
Constants.FLASH_TORCH -> {
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON
)
mPreviewRequestBuilder!!.set(
CaptureRequest.FLASH_MODE,
CaptureRequest.FLASH_MODE_TORCH
)
}
Constants.FLASH_AUTO -> {
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
)
mPreviewRequestBuilder!!.set(
CaptureRequest.FLASH_MODE,
CaptureRequest.FLASH_MODE_OFF
)
}
Constants.FLASH_RED_EYE -> {
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH_REDEYE
)
mPreviewRequestBuilder!!.set(
CaptureRequest.FLASH_MODE,
CaptureRequest.FLASH_MODE_OFF
)
}
}
}
/**
* Locks the focus as the first step for a still image capture.
*/
private fun lockFocus() {
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AF_TRIGGER,
CaptureRequest.CONTROL_AF_TRIGGER_START
)
try {
mCaptureCallback.setState(PictureCaptureCallback.STATE_LOCKING)
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, backgroundHandler)
} catch (e: CameraAccessException) {
Log.e(TAG, "Failed to lock focus.", e)
}
}
/**
* Captures a still picture.
*/
fun captureStillPicture() {
Log.e("camoorah", "capturing still picture")
try {
val captureRequestBuilder = mCamera!!.createCaptureRequest(
CameraDevice.TEMPLATE_STILL_CAPTURE
)
captureRequestBuilder.addTarget(mImageReader!!.surface)
captureRequestBuilder.set(
CaptureRequest.CONTROL_AF_MODE,
mPreviewRequestBuilder!!.get(CaptureRequest.CONTROL_AF_MODE)
)
when (flash) {
Constants.FLASH_OFF -> {
captureRequestBuilder.set(
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON
)
captureRequestBuilder.set(
CaptureRequest.FLASH_MODE,
CaptureRequest.FLASH_MODE_OFF
)
}
Constants.FLASH_ON -> captureRequestBuilder.set(
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_ALWAYS_FLASH
)
Constants.FLASH_TORCH -> {
captureRequestBuilder.set(
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON
)
captureRequestBuilder.set(
CaptureRequest.FLASH_MODE,
CaptureRequest.FLASH_MODE_TORCH
)
}
Constants.FLASH_AUTO -> captureRequestBuilder.set(
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
)
Constants.FLASH_RED_EYE -> captureRequestBuilder.set(
CaptureRequest.CONTROL_AE_MODE,
CaptureRequest.CONTROL_AE_MODE_ON_AUTO_FLASH
)
}
// Calculate JPEG orientation.
val sensorOrientation = mCameraCharacteristics!!.get(
CameraCharacteristics.SENSOR_ORIENTATION
)!!
captureRequestBuilder.set(
CaptureRequest.JPEG_ORIENTATION,
(sensorOrientation +
mDisplayOrientation * (if (mFacing == Constants.FACING_FRONT) 1 else -1) +
360) % 360
)
// Stop preview and capture a still picture.
mCaptureSession!!.stopRepeating()
mCaptureSession!!.capture(captureRequestBuilder.build(),
object : CameraCaptureSession.CaptureCallback() {
override fun onCaptureCompleted(
@NonNull session: CameraCaptureSession,
@NonNull request: CaptureRequest,
@NonNull result: TotalCaptureResult
) {
unlockFocus()
}
}, backgroundHandler
)
} catch (e: CameraAccessException) {
Log.e(TAG, "Cannot capture a still picture.", e)
}
}
/**
* Unlocks the auto-focus and restart camera preview. This is supposed to be called after
* capturing a still picture.
*/
fun unlockFocus() {
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AF_TRIGGER,
CaptureRequest.CONTROL_AF_TRIGGER_CANCEL
)
try {
mCaptureSession!!.capture(mPreviewRequestBuilder!!.build(), mCaptureCallback, backgroundHandler)
updateAutoFocus()
updateFlash()
mPreviewRequestBuilder!!.set(
CaptureRequest.CONTROL_AF_TRIGGER,
CaptureRequest.CONTROL_AF_TRIGGER_IDLE
)
mCaptureSession!!.setRepeatingRequest(mPreviewRequestBuilder!!.build(), mCaptureCallback, backgroundHandler)
mCaptureCallback.setState(PictureCaptureCallback.STATE_PREVIEW)
} catch (e: CameraAccessException) {
Log.e(TAG, "Failed to restart camera preview.", e)
}
}
var backgroundHandlerThread: HandlerThread? = null
var backgroundHandler: Handler? = null
/**
* Starts a background thread and its [Handler].
*/
private fun startBackgroundThread() {
backgroundHandlerThread = HandlerThread("CameraBackgroundProcessor")
backgroundHandlerThread?.start()
backgroundHandler = Handler(backgroundHandlerThread?.looper)
}
/**
* Stops the background thread and its [Handler].
*/
private fun stopBackgroundThread() {
backgroundHandlerThread?.quitSafely()
try {
backgroundHandlerThread?.join()
backgroundHandlerThread = null
backgroundHandler = null
} catch (e: InterruptedException) {
e.printStackTrace()
}
}
/**
* A [CameraCaptureSession.CaptureCallback] for capturing a still picture.
*/
private abstract class PictureCaptureCallback internal constructor() : CameraCaptureSession.CaptureCallback() {
private var mState: Int = 0
internal fun setState(state: Int) {
mState = state
}
override fun onCaptureProgressed(
@NonNull session: CameraCaptureSession,
@NonNull request: CaptureRequest, @NonNull partialResult: CaptureResult
) {
process(partialResult)
}
override fun onCaptureCompleted(
@NonNull session: CameraCaptureSession,
@NonNull request: CaptureRequest, @NonNull result: TotalCaptureResult
) {
process(result)
}
private fun process(@NonNull result: CaptureResult) {
when (mState) {
STATE_LOCKING -> {
val af = result.get(CaptureResult.CONTROL_AF_STATE)
if (af != null) {
if (af == CaptureResult.CONTROL_AF_STATE_FOCUSED_LOCKED || af == CaptureResult.CONTROL_AF_STATE_NOT_FOCUSED_LOCKED) {
val ae = result.get(CaptureResult.CONTROL_AE_STATE)
if (ae == null || ae == CaptureResult.CONTROL_AE_STATE_CONVERGED) {
setState(STATE_CAPTURING)
onReady()
} else {
setState(STATE_LOCKED)
onPrecaptureRequired()
}
}
}
}
STATE_PRECAPTURE -> {
val ae = result.get(CaptureResult.CONTROL_AE_STATE)
if (ae == null || ae == CaptureResult.CONTROL_AE_STATE_PRECAPTURE ||
ae == CaptureRequest.CONTROL_AE_STATE_FLASH_REQUIRED ||
ae == CaptureResult.CONTROL_AE_STATE_CONVERGED
) {
setState(STATE_WAITING)
}
}
STATE_WAITING -> {
val ae = result.get(CaptureResult.CONTROL_AE_STATE)
if (ae == null || ae != CaptureResult.CONTROL_AE_STATE_PRECAPTURE) {
setState(STATE_CAPTURING)
onReady()
}
}
}
}
/**
* Called when it is ready to take a still picture.
*/
abstract fun onReady()
/**
* Called when it is necessary to run the precapture sequence.
*/
abstract fun onPrecaptureRequired()
companion object {
internal val STATE_PREVIEW = 0
internal val STATE_LOCKING = 1
internal val STATE_LOCKED = 2
internal val STATE_PRECAPTURE = 3
internal val STATE_WAITING = 4
internal val STATE_CAPTURING = 5
}
}
companion object {
private val TAG = "Camera2"
private val INTERNAL_FACINGS = SparseIntArray()
init {
INTERNAL_FACINGS.put(Constants.FACING_BACK, CameraCharacteristics.LENS_FACING_BACK)
INTERNAL_FACINGS.put(Constants.FACING_FRONT, CameraCharacteristics.LENS_FACING_FRONT)
}
/**
* Max preview width that is guaranteed by Camera2 API
*/
private val MAX_PREVIEW_WIDTH = 1920
/**
* Max preview height that is guaranteed by Camera2 API
*/
private val MAX_PREVIEW_HEIGHT = 1080
}
}