2020-01-08 00:45:45 -08:00
package cash.z.ecc.android.ui.scan
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
2020-06-10 04:49:38 -07:00
import android.util.DisplayMetrics
2020-01-08 00:45:45 -08:00
import android.view.LayoutInflater
import android.view.View
2020-06-10 04:49:38 -07:00
import androidx.camera.core.*
2020-01-08 00:45:45 -08:00
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentScanBinding
import cash.z.ecc.android.di.viewmodel.activityViewModel
import cash.z.ecc.android.di.viewmodel.viewModel
import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.ext.onClickNavTo
2020-02-21 15:52:57 -08:00
import cash.z.ecc.android.feedback.Report
2020-06-10 04:49:38 -07:00
import cash.z.ecc.android.feedback.Report.Tap.SCAN_BACK
import cash.z.ecc.android.feedback.Report.Tap.SCAN_RECEIVE
import cash.z.ecc.android.sdk.ext.twig
2020-01-08 00:45:45 -08:00
import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.send.SendViewModel
2020-06-10 04:49:38 -07:00
import com.crashlytics.android.Crashlytics
2020-01-08 00:45:45 -08:00
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.coroutines.launch
2020-06-10 04:49:38 -07:00
import java.util.concurrent.ExecutorService
2020-01-08 00:45:45 -08:00
import java.util.concurrent.Executors
class ScanFragment : BaseFragment < FragmentScanBinding > ( ) {
2020-02-21 15:52:57 -08:00
override val screen = Report . Screen . SCAN
2020-01-08 00:45:45 -08:00
private val viewModel : ScanViewModel by viewModel ( )
private val sendViewModel : SendViewModel by activityViewModel ( )
private lateinit var cameraProviderFuture : ListenableFuture < ProcessCameraProvider >
2020-06-10 04:49:38 -07:00
private var cameraExecutor : ExecutorService ? = null
2020-01-08 00:45:45 -08:00
override fun inflate ( inflater : LayoutInflater ) : FragmentScanBinding =
FragmentScanBinding . inflate ( inflater )
override fun onViewCreated ( view : View , savedInstanceState : Bundle ? ) {
super . onViewCreated ( view , savedInstanceState )
2020-06-10 04:49:38 -07:00
if ( cameraExecutor != null ) cameraExecutor ?. shutdown ( )
cameraExecutor = Executors . newSingleThreadExecutor ( )
2020-01-08 00:45:45 -08:00
2020-02-21 15:52:57 -08:00
binding . buttonReceive . onClickNavTo ( R . id . action _nav _scan _to _nav _receive ) { tapped ( SCAN _RECEIVE ) }
binding . backButtonHitArea . onClickNavBack ( ) { tapped ( SCAN _BACK ) }
2020-01-08 00:45:45 -08:00
}
override fun onActivityCreated ( savedInstanceState : Bundle ? ) {
super . onActivityCreated ( savedInstanceState )
if ( ! allPermissionsGranted ( ) ) getRuntimePermissions ( )
}
override fun onAttach ( context : Context ) {
super . onAttach ( context )
cameraProviderFuture = ProcessCameraProvider . getInstance ( context )
cameraProviderFuture . addListener ( Runnable {
bindPreview ( cameraProviderFuture . get ( ) )
} , ContextCompat . getMainExecutor ( context ) )
}
2020-06-10 04:49:38 -07:00
override fun onDestroyView ( ) {
super . onDestroyView ( )
cameraExecutor ?. shutdown ( )
cameraExecutor = null
}
2020-01-08 00:45:45 -08:00
private fun bindPreview ( cameraProvider : ProcessCameraProvider ) {
2020-06-10 04:49:38 -07:00
// Most of the code here is adapted from: https://github.com/android/camera-samples/blob/master/CameraXBasic/app/src/main/java/com/android/example/cameraxbasic/fragments/CameraFragment.kt
// it's worth keeping tabs on that implementation because they keep making breaking changes to these APIs!
// Get screen metrics used to setup camera for full screen resolution
val metrics = DisplayMetrics ( ) . also { binding . preview . display . getRealMetrics ( it ) }
val screenAspectRatio = aspectRatio ( metrics . widthPixels , metrics . heightPixels )
val rotation = binding . preview . display . rotation
val preview =
Preview . Builder ( ) . setTargetName ( " Preview " ) . setTargetAspectRatio ( screenAspectRatio )
. setTargetRotation ( rotation ) . build ( )
2020-01-08 00:45:45 -08:00
2020-06-10 04:49:38 -07:00
val cameraSelector = CameraSelector . Builder ( )
. requireLensFacing ( CameraSelector . LENS _FACING _BACK )
. build ( )
2020-01-08 00:45:45 -08:00
2020-06-10 04:49:38 -07:00
val imageAnalysis = ImageAnalysis . Builder ( ) . setTargetAspectRatio ( screenAspectRatio )
. setTargetRotation ( rotation )
. setBackpressureStrategy ( ImageAnalysis . STRATEGY _KEEP _ONLY _LATEST )
. build ( )
2020-01-08 00:45:45 -08:00
2020-06-10 04:49:38 -07:00
imageAnalysis . setAnalyzer ( cameraExecutor !! , QrAnalyzer { q , i ->
onQrScanned ( q , i )
} )
// Must unbind the use-cases before rebinding them
cameraProvider . unbindAll ( )
try {
2020-01-08 00:45:45 -08:00
cameraProvider . bindToLifecycle ( this , cameraSelector , preview , imageAnalysis )
2020-06-10 04:49:38 -07:00
preview . setSurfaceProvider ( binding . preview . createSurfaceProvider ( ) )
} catch ( t : Throwable ) {
// TODO: consider bubbling this up to the user
Crashlytics . logException ( t )
twig ( " Error while opening the camera: $t " )
2020-01-08 00:45:45 -08:00
}
}
2020-06-10 04:49:38 -07:00
/ * *
* Adapted from : https : //github.com/android/camera-samples/blob/master/CameraXBasic/app/src/main/java/com/android/example/cameraxbasic/fragments/CameraFragment.kt#L350
* /
private fun aspectRatio ( width : Int , height : Int ) : Int {
val previewRatio = kotlin . math . max ( width , height ) . toDouble ( ) / kotlin . math . min (
width ,
height
)
if ( kotlin . math . abs ( previewRatio - ( 4.0 / 3.0 ) )
<= kotlin . math . abs ( previewRatio - ( 16.0 / 9.0 ) ) ) {
return AspectRatio . RATIO _4 _3
}
return AspectRatio . RATIO _16 _9
}
2020-01-08 00:45:45 -08:00
private fun onQrScanned ( qrContent : String , image : ImageProxy ) {
resumedScope . launch {
if ( viewModel . isNotValid ( qrContent ) ) image . close ( ) // continue scanning
else {
sendViewModel . toAddress = qrContent
2020-02-12 04:58:41 -08:00
mainActivity ?. safeNavigate ( R . id . action _nav _scan _to _nav _send _address )
2020-01-08 00:45:45 -08:00
}
}
}
// private fun updateOverlay(detectedObjects: DetectedObjects) {
// if (detectedObjects.objects.isEmpty()) {
// return
// }
//
// overlay.setSize(detectedObjects.imageWidth, detectedObjects.imageHeight)
// val list = mutableListOf<BoxData>()
// for (obj in detectedObjects.objects) {
// val box = obj.boundingBox
// val name = "${categoryNames[obj.classificationCategory]}"
// val confidence =
// if (obj.classificationCategory != FirebaseVisionObject.CATEGORY_UNKNOWN) {
// val confidence: Int = obj.classificationConfidence!!.times(100).toInt()
// "$confidence%"
// } else {
// ""
// }
// list.add(BoxData("$name $confidence", box))
// }
// overlay.set(list)
// }
//
// Permissions
//
private val requiredPermissions : Array < String ? >
get ( ) {
return try {
val info = mainActivity ?. packageManager
?. getPackageInfo ( mainActivity ?. packageName , PackageManager . GET _PERMISSIONS )
val ps = info ?. requestedPermissions
if ( ps != null && ps . isNotEmpty ( ) ) {
ps
} else {
arrayOfNulls ( 0 )
}
} catch ( e : Exception ) {
arrayOfNulls ( 0 )
}
}
private fun allPermissionsGranted ( ) : Boolean {
for ( permission in requiredPermissions ) {
if ( !is PermissionGranted ( mainActivity !! , permission !! ) ) {
return false
}
}
return true
}
private fun getRuntimePermissions ( ) {
val allNeededPermissions = arrayListOf < String > ( )
for ( permission in requiredPermissions ) {
if ( !is PermissionGranted ( mainActivity !! , permission !! ) ) {
allNeededPermissions . add ( permission )
}
}
if ( allNeededPermissions . isNotEmpty ( ) ) {
requestPermissions ( allNeededPermissions . toTypedArray ( ) , CAMERA _PERMISSION _REQUEST )
}
}
override fun onRequestPermissionsResult (
requestCode : Int ,
permissions : Array < out String > ,
grantResults : IntArray
) {
super . onRequestPermissionsResult ( requestCode , permissions , grantResults )
if ( allPermissionsGranted ( ) ) {
// view!!.postDelayed(
// {
// onStartCamera()
// },
// 2000L
// ) // TODO: remove this temp hack to sidestep crash when permissions were not available
}
}
companion object {
private const val CAMERA _PERMISSION _REQUEST = 1002
private fun isPermissionGranted ( context : Context , permission : String ) : Boolean {
return ContextCompat . checkSelfPermission ( context , permission ) == PackageManager . PERMISSION _GRANTED
}
}
}