|
|
package com.br_technology.securitytrain_master.ui.home.activity
|
|
|
|
|
|
import android.content.Context
|
|
|
import android.content.Intent
|
|
|
import android.graphics.Color
|
|
|
import android.graphics.drawable.ColorDrawable
|
|
|
import android.media.MediaScannerConnection
|
|
|
import android.net.Uri
|
|
|
import android.os.Build
|
|
|
import android.util.Log
|
|
|
import android.view.LayoutInflater
|
|
|
import android.webkit.MimeTypeMap
|
|
|
import android.widget.Toast
|
|
|
import androidx.camera.core.*
|
|
|
import androidx.camera.lifecycle.ProcessCameraProvider
|
|
|
import androidx.core.content.ContentProviderCompat.requireContext
|
|
|
import androidx.core.content.ContextCompat
|
|
|
import androidx.core.net.toFile
|
|
|
import androidx.lifecycle.lifecycleScope
|
|
|
import com.br_technology.securitytrain_master.R
|
|
|
import com.br_technology.securitytrain_master.base.common.Constant.ANIMATION_FAST_MILLIS
|
|
|
import com.br_technology.securitytrain_master.base.common.Constant.ANIMATION_SLOW_MILLIS
|
|
|
import com.br_technology.securitytrain_master.databinding.ActivityVerifyCameraBinding
|
|
|
import com.br_technology.securitytrain_master.expand.screenHeight
|
|
|
import com.br_technology.securitytrain_master.expand.screenWidth
|
|
|
import com.br_technology.securitytrain_master.ui.home.viewmodel.VerifyCameraViewModel
|
|
|
import com.wjx.android.wanandroidmvvm.base.view.BaseLifeCycleActivity
|
|
|
import kotlinx.coroutines.Dispatchers
|
|
|
import kotlinx.coroutines.launch
|
|
|
import java.io.File
|
|
|
import java.nio.ByteBuffer
|
|
|
import java.text.SimpleDateFormat
|
|
|
import java.util.*
|
|
|
import java.util.concurrent.ExecutorService
|
|
|
import java.util.concurrent.Executors
|
|
|
import kotlin.math.abs
|
|
|
import kotlin.math.max
|
|
|
import kotlin.math.min
|
|
|
|
|
|
/**
|
|
|
* Time: 8/20/2021 14:27
|
|
|
* Author: Captain
|
|
|
* Description: 初见时你很迷人
|
|
|
*/
|
|
|
|
|
|
/** Helper type alias used for analysis use case callbacks */
|
|
|
typealias LumaListener = (luma: Double) -> Unit
|
|
|
class VerifyCameraActivity :
|
|
|
BaseLifeCycleActivity<VerifyCameraViewModel, ActivityVerifyCameraBinding>(
|
|
|
ActivityVerifyCameraBinding::inflate
|
|
|
) {
|
|
|
|
|
|
private lateinit var outputDirectory: File
|
|
|
private var cameraProvider: ProcessCameraProvider? = null
|
|
|
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
|
|
|
private var preview: Preview? = null
|
|
|
private var camera: Camera? = null
|
|
|
private var imageCapture: ImageCapture? = null
|
|
|
private var imageAnalyzer: ImageAnalysis? = null
|
|
|
/** Blocking camera operations are performed using this executor */
|
|
|
private lateinit var cameraExecutor: ExecutorService
|
|
|
override fun initDataObserver() {
|
|
|
|
|
|
}
|
|
|
|
|
|
override fun initData() {
|
|
|
super.initData()
|
|
|
cameraExecutor = Executors.newSingleThreadExecutor()
|
|
|
outputDirectory = getOutputDirectory(this)
|
|
|
updateCameraUi()
|
|
|
setUpCamera()
|
|
|
binding.apply {
|
|
|
ivBack.setOnClickListener {
|
|
|
finish()
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private fun updateCameraUi() {
|
|
|
// In the background, load latest photo taken (if any) for gallery thumbnail
|
|
|
// lifecycleScope.launch(Dispatchers.IO) {
|
|
|
// outputDirectory.listFiles { file ->
|
|
|
// EXTENSION_WHITELIST.contains(file.extension.toUpperCase(Locale.ROOT))
|
|
|
// }?.maxOrNull()?.let {
|
|
|
// setGalleryThumbnail(Uri.fromFile(it))
|
|
|
// }
|
|
|
// }
|
|
|
|
|
|
// Listener for button used to capture photo
|
|
|
binding.ivTakePhoto.setOnClickListener {
|
|
|
|
|
|
Toast.makeText(baseContext, "aaaaa", Toast.LENGTH_SHORT).show()
|
|
|
// Get a stable reference of the modifiable image capture use case
|
|
|
imageCapture?.let { imageCapture ->
|
|
|
|
|
|
// Create output file to hold the image
|
|
|
val photoFile = createFile(outputDirectory, FILENAME, PHOTO_EXTENSION)
|
|
|
|
|
|
// Setup image capture metadata
|
|
|
val metadata = ImageCapture.Metadata().apply {
|
|
|
|
|
|
// Mirror image when using the front camera
|
|
|
isReversedHorizontal = lensFacing == CameraSelector.LENS_FACING_FRONT
|
|
|
}
|
|
|
|
|
|
// Create output options object which contains file + metadata
|
|
|
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile)
|
|
|
.setMetadata(metadata)
|
|
|
.build()
|
|
|
|
|
|
// Setup image capture listener which is triggered after photo has been taken
|
|
|
imageCapture.takePicture(
|
|
|
outputOptions, cameraExecutor, object : ImageCapture.OnImageSavedCallback {
|
|
|
override fun onError(exc: ImageCaptureException) {
|
|
|
Log.e(TAG, "Photo capture failed: ${exc.message}", exc)
|
|
|
}
|
|
|
|
|
|
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
|
|
|
val savedUri = output.savedUri ?: Uri.fromFile(photoFile)
|
|
|
Log.d(TAG, "Photo capture succeeded: $savedUri")
|
|
|
|
|
|
// We can only change the foreground Drawable using API level 23+ API
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
|
// Update the gallery thumbnail with latest picture taken
|
|
|
// setGalleryThumbnail(savedUri)
|
|
|
//todo 拿到照片的uri上传服务器
|
|
|
|
|
|
}
|
|
|
|
|
|
// Implicit broadcasts will be ignored for devices running API level >= 24
|
|
|
// so if you only target API level 24+ you can remove this statement
|
|
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
|
|
sendBroadcast(
|
|
|
Intent(android.hardware.Camera.ACTION_NEW_PICTURE, savedUri)
|
|
|
)
|
|
|
}
|
|
|
|
|
|
// If the folder selected is an external media directory, this is
|
|
|
// unnecessary but otherwise other apps will not be able to access our
|
|
|
// images unless we scan them using [MediaScannerConnection]
|
|
|
val mimeType = MimeTypeMap.getSingleton()
|
|
|
.getMimeTypeFromExtension(savedUri.toFile().extension)
|
|
|
MediaScannerConnection.scanFile(
|
|
|
this@VerifyCameraActivity,
|
|
|
arrayOf(savedUri.toFile().absolutePath),
|
|
|
arrayOf(mimeType)
|
|
|
) { _, uri ->
|
|
|
Log.d(TAG, "Image capture scanned into media store: $uri")
|
|
|
}
|
|
|
}
|
|
|
})
|
|
|
|
|
|
// We can only change the foreground Drawable using API level 23+ API
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
|
|
|
|
// Display flash animation to indicate that photo was captured
|
|
|
binding.root.postDelayed({
|
|
|
binding.root.foreground = ColorDrawable(Color.WHITE)
|
|
|
binding.root.postDelayed(
|
|
|
{ binding.root.foreground = null }, ANIMATION_FAST_MILLIS)
|
|
|
}, ANIMATION_SLOW_MILLIS)
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Setup for button used to switch cameras
|
|
|
binding.ivSwitchScreen.let {
|
|
|
|
|
|
// Disable the button until the camera is set up
|
|
|
it.isEnabled = false
|
|
|
|
|
|
// Listener for button used to switch cameras. Only called if the button is enabled
|
|
|
it.setOnClickListener {
|
|
|
lensFacing = if (CameraSelector.LENS_FACING_FRONT == lensFacing) {
|
|
|
CameraSelector.LENS_FACING_BACK
|
|
|
} else {
|
|
|
CameraSelector.LENS_FACING_FRONT
|
|
|
}
|
|
|
// Re-bind use cases to update selected camera
|
|
|
bindCameraUseCases()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Listener for button used to view the most recent photo
|
|
|
// cameraUiContainerBinding?.photoViewButton?.setOnClickListener {
|
|
|
// // Only navigate when the gallery has photos
|
|
|
// if (true == outputDirectory.listFiles()?.isNotEmpty()) {
|
|
|
// Navigation.findNavController(
|
|
|
// requireActivity(), R.id.fragment_container
|
|
|
// ).navigate(CameraFragmentDirections
|
|
|
// .actionCameraToGallery(outputDirectory.absolutePath))
|
|
|
// }
|
|
|
// }
|
|
|
}
|
|
|
|
|
|
fun getOutputDirectory(context: Context): File {
|
|
|
val appContext = context.applicationContext
|
|
|
val mediaDir = context.externalMediaDirs.firstOrNull()?.let {
|
|
|
File(it, appContext.resources.getString(R.string.app_name)).apply { mkdirs() } }
|
|
|
return if (mediaDir != null && mediaDir.exists())
|
|
|
mediaDir else appContext.filesDir
|
|
|
}
|
|
|
|
|
|
/** Initialize CameraX, and prepare to bind the camera use cases */
|
|
|
private fun setUpCamera() {
|
|
|
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
|
|
|
cameraProviderFuture.addListener(Runnable {
|
|
|
|
|
|
// CameraProvider
|
|
|
cameraProvider = cameraProviderFuture.get()
|
|
|
|
|
|
// Select lensFacing depending on the available cameras
|
|
|
lensFacing = when {
|
|
|
hasBackCamera() -> CameraSelector.LENS_FACING_BACK
|
|
|
hasFrontCamera() -> CameraSelector.LENS_FACING_FRONT
|
|
|
else -> throw IllegalStateException("Back and front camera are unavailable")
|
|
|
}
|
|
|
|
|
|
// Enable or disable switching between cameras
|
|
|
updateCameraSwitchButton()
|
|
|
|
|
|
// Build and bind the camera use cases
|
|
|
bindCameraUseCases()
|
|
|
}, ContextCompat.getMainExecutor(this))
|
|
|
}
|
|
|
|
|
|
/** Enabled or disabled a button to switch cameras depending on the available cameras */
|
|
|
private fun updateCameraSwitchButton() {
|
|
|
try {
|
|
|
binding.ivSwitchScreen.isEnabled = hasBackCamera() && hasFrontCamera()
|
|
|
} catch (exception: CameraInfoUnavailableException) {
|
|
|
binding.ivSwitchScreen.isEnabled = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/** Returns true if the device has an available back camera. False otherwise */
|
|
|
private fun hasBackCamera(): Boolean {
|
|
|
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_BACK_CAMERA) ?: false
|
|
|
}
|
|
|
|
|
|
/** Returns true if the device has an available front camera. False otherwise */
|
|
|
private fun hasFrontCamera(): Boolean {
|
|
|
return cameraProvider?.hasCamera(CameraSelector.DEFAULT_FRONT_CAMERA) ?: false
|
|
|
}
|
|
|
|
|
|
private fun bindCameraUseCases() {
|
|
|
val screenAspectRatio = aspectRatio(binding.root.screenWidth(), binding.root.screenHeight())
|
|
|
Log.d(TAG, "Preview aspect ratio: $screenAspectRatio")
|
|
|
|
|
|
// val rotation = binding.viewFinder.display.rotation
|
|
|
|
|
|
// CameraProvider
|
|
|
val cameraProvider = cameraProvider
|
|
|
?: throw IllegalStateException("Camera initialization failed.")
|
|
|
|
|
|
// CameraSelector
|
|
|
val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
|
|
|
|
|
|
// Preview
|
|
|
preview = Preview.Builder()
|
|
|
// We request aspect ratio but no resolution
|
|
|
.setTargetAspectRatio(screenAspectRatio)
|
|
|
// Set initial target rotation
|
|
|
// .setTargetRotation(rotation)
|
|
|
.build()
|
|
|
|
|
|
// ImageCapture
|
|
|
imageCapture = ImageCapture.Builder()
|
|
|
.setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY)
|
|
|
// We request aspect ratio but no resolution to match preview config, but letting
|
|
|
// CameraX optimize for whatever specific resolution best fits our use cases
|
|
|
.setTargetAspectRatio(screenAspectRatio)
|
|
|
// Set initial target rotation, we will have to call this again if rotation changes
|
|
|
// during the lifecycle of this use case
|
|
|
// .setTargetRotation(rotation)
|
|
|
.build()
|
|
|
|
|
|
// ImageAnalysis
|
|
|
imageAnalyzer = ImageAnalysis.Builder()
|
|
|
// We request aspect ratio but no resolution
|
|
|
.setTargetAspectRatio(screenAspectRatio)
|
|
|
// Set initial target rotation, we will have to call this again if rotation changes
|
|
|
// during the lifecycle of this use case
|
|
|
// .setTargetRotation(rotation)
|
|
|
.build()
|
|
|
// The analyzer can then be assigned to the instance
|
|
|
.also {
|
|
|
it.setAnalyzer(cameraExecutor, LuminosityAnalyzer { luma ->
|
|
|
// Values returned from our analyzer are passed to the attached listener
|
|
|
// We log image analysis results here - you should do something useful
|
|
|
// instead!
|
|
|
Log.d(TAG, "Average luminosity: $luma")
|
|
|
})
|
|
|
}
|
|
|
|
|
|
// Must unbind the use-cases before rebinding them
|
|
|
cameraProvider.unbindAll()
|
|
|
|
|
|
try {
|
|
|
// A variable number of use-cases can be passed here -
|
|
|
// camera provides access to CameraControl & CameraInfo
|
|
|
camera = cameraProvider.bindToLifecycle(
|
|
|
this, cameraSelector, preview, imageCapture, imageAnalyzer)
|
|
|
|
|
|
// Attach the viewfinder's surface provider to preview use case
|
|
|
preview?.setSurfaceProvider(binding.viewFinder.surfaceProvider)
|
|
|
} catch (exc: Exception) {
|
|
|
Log.e(TAG, "Use case binding failed", exc)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
private fun aspectRatio(width: Int, height: Int): Int {
|
|
|
val previewRatio = max(width, height).toDouble() / min(width, height)
|
|
|
if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) {
|
|
|
return AspectRatio.RATIO_4_3
|
|
|
}
|
|
|
return AspectRatio.RATIO_16_9
|
|
|
}
|
|
|
|
|
|
private class LuminosityAnalyzer(listener: LumaListener? = null) : ImageAnalysis.Analyzer {
|
|
|
private val frameRateWindow = 8
|
|
|
private val frameTimestamps = ArrayDeque<Long>(5)
|
|
|
private val listeners = ArrayList<LumaListener>().apply { listener?.let { add(it) } }
|
|
|
private var lastAnalyzedTimestamp = 0L
|
|
|
var framesPerSecond: Double = -1.0
|
|
|
private set
|
|
|
|
|
|
/**
|
|
|
* Used to add listeners that will be called with each luma computed
|
|
|
*/
|
|
|
fun onFrameAnalyzed(listener: LumaListener) = listeners.add(listener)
|
|
|
|
|
|
/**
|
|
|
* Helper extension function used to extract a byte array from an image plane buffer
|
|
|
*/
|
|
|
private fun ByteBuffer.toByteArray(): ByteArray {
|
|
|
rewind() // Rewind the buffer to zero
|
|
|
val data = ByteArray(remaining())
|
|
|
get(data) // Copy the buffer into a byte array
|
|
|
return data // Return the byte array
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Analyzes an image to produce a result.
|
|
|
*
|
|
|
* <p>The caller is responsible for ensuring this analysis method can be executed quickly
|
|
|
* enough to prevent stalls in the image acquisition pipeline. Otherwise, newly available
|
|
|
* images will not be acquired and analyzed.
|
|
|
*
|
|
|
* <p>The image passed to this method becomes invalid after this method returns. The caller
|
|
|
* should not store external references to this image, as these references will become
|
|
|
* invalid.
|
|
|
*
|
|
|
* @param image image being analyzed VERY IMPORTANT: Analyzer method implementation must
|
|
|
* call image.close() on received images when finished using them. Otherwise, new images
|
|
|
* may not be received or the camera may stall, depending on back pressure setting.
|
|
|
*
|
|
|
*/
|
|
|
override fun analyze(image: ImageProxy) {
|
|
|
// If there are no listeners attached, we don't need to perform analysis
|
|
|
if (listeners.isEmpty()) {
|
|
|
image.close()
|
|
|
return
|
|
|
}
|
|
|
|
|
|
// Keep track of frames analyzed
|
|
|
val currentTime = System.currentTimeMillis()
|
|
|
frameTimestamps.push(currentTime)
|
|
|
|
|
|
// Compute the FPS using a moving average
|
|
|
while (frameTimestamps.size >= frameRateWindow) frameTimestamps.removeLast()
|
|
|
val timestampFirst = frameTimestamps.peekFirst() ?: currentTime
|
|
|
val timestampLast = frameTimestamps.peekLast() ?: currentTime
|
|
|
framesPerSecond = 1.0 / ((timestampFirst - timestampLast) /
|
|
|
frameTimestamps.size.coerceAtLeast(1).toDouble()) * 1000.0
|
|
|
|
|
|
// Analysis could take an arbitrarily long amount of time
|
|
|
// Since we are running in a different thread, it won't stall other use cases
|
|
|
|
|
|
lastAnalyzedTimestamp = frameTimestamps.first
|
|
|
|
|
|
// Since format in ImageAnalysis is YUV, image.planes[0] contains the luminance plane
|
|
|
val buffer = image.planes[0].buffer
|
|
|
|
|
|
// Extract image data from callback object
|
|
|
val data = buffer.toByteArray()
|
|
|
|
|
|
// Convert the data into an array of pixel values ranging 0-255
|
|
|
val pixels = data.map { it.toInt() and 0xFF }
|
|
|
|
|
|
// Compute average luminance for the image
|
|
|
val luma = pixels.average()
|
|
|
|
|
|
// Call all listeners with new value
|
|
|
listeners.forEach { it(luma) }
|
|
|
|
|
|
image.close()
|
|
|
}
|
|
|
}
|
|
|
|
|
|
companion object {
|
|
|
|
|
|
private const val TAG = "CameraXBasic"
|
|
|
private const val FILENAME = "yyyy-MM-dd-HH-mm-ss-SSS"
|
|
|
private const val PHOTO_EXTENSION = ".jpg"
|
|
|
private const val RATIO_4_3_VALUE = 4.0 / 3.0
|
|
|
private const val RATIO_16_9_VALUE = 16.0 / 9.0
|
|
|
|
|
|
/** Helper function used to create a timestamped file */
|
|
|
private fun createFile(baseFolder: File, format: String, extension: String) =
|
|
|
File(baseFolder, SimpleDateFormat(format, Locale.US)
|
|
|
.format(System.currentTimeMillis()) + extension)
|
|
|
}
|
|
|
|
|
|
} |
|
|
\ No newline at end of file |
...
|
...
|
|