Browse Source

QR scanning fixes

pull/15/head
fekt 1 year ago
parent
commit
5696b62c8b
  1. 15
      app/build.gradle
  2. 12
      app/src/main/AndroidManifest.xml
  3. 397
      app/src/main/java/cash/z/ecc/android/ui/scan/ScanFragment.kt
  4. 4
      app/src/main/java/cash/z/ecc/android/ui/scan/ScanViewModel.kt
  5. 2
      app/src/main/res/layout/fragment_scan.xml
  6. 14
      buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt

15
app/build.gradle

@ -143,11 +143,16 @@ dependencies {
implementation Deps.AndroidX.LEGACY implementation Deps.AndroidX.LEGACY
implementation Deps.AndroidX.PAGING implementation Deps.AndroidX.PAGING
implementation Deps.AndroidX.RECYCLER implementation Deps.AndroidX.RECYCLER
implementation Deps.AndroidX.CameraX.CAMERA2
implementation Deps.AndroidX.CameraX.CORE def camerax_version = "1.2.0-rc01"
implementation Deps.AndroidX.CameraX.LIFECYCLE implementation "androidx.camera:camera-core:${camerax_version}"
implementation Deps.AndroidX.CameraX.View.EXT implementation "androidx.camera:camera-camera2:${camerax_version}"
implementation Deps.AndroidX.CameraX.View.VIEW implementation "androidx.camera:camera-lifecycle:${camerax_version}"
implementation "androidx.camera:camera-view:${camerax_version}"
//WindowManager
implementation "androidx.window:window:1.1.0-alpha01"
implementation Deps.AndroidX.Lifecycle.LIFECYCLE_RUNTIME_KTX implementation Deps.AndroidX.Lifecycle.LIFECYCLE_RUNTIME_KTX
implementation Deps.AndroidX.Navigation.FRAGMENT_KTX implementation Deps.AndroidX.Navigation.FRAGMENT_KTX
implementation Deps.AndroidX.Navigation.UI_KTX implementation Deps.AndroidX.Navigation.UI_KTX

12
app/src/main/AndroidManifest.xml

@ -1,5 +1,5 @@
<manifest <manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:android="http://schemas.android.com/apk/res/android"> xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.VIBRATE"/> <uses-permission android:name="android.permission.VIBRATE"/>
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.CAMERA" />
@ -11,8 +11,12 @@
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/ZcashTheme"> android:theme="@style/ZcashTheme"
<activity android:name=".ui.MainActivity" android:screenOrientation="portrait"> tools:targetApi="31">
<activity
android:name=".ui.MainActivity"
android:exported="true"
android:screenOrientation="portrait">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />

397
app/src/main/java/cash/z/ecc/android/ui/scan/ScanFragment.kt

@ -1,216 +1,341 @@
package cash.z.ecc.android.ui.scan package cash.z.ecc.android.ui.scan
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.camera.core.* import androidx.camera.core.*
import androidx.camera.lifecycle.ProcessCameraProvider import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.fragment.app.viewModels import androidx.fragment.app.viewModels
import androidx.window.layout.WindowInfoTracker
import androidx.window.layout.WindowMetricsCalculator
import cash.z.ecc.android.R import cash.z.ecc.android.R
import cash.z.ecc.android.databinding.FragmentScanBinding import cash.z.ecc.android.databinding.FragmentScanBinding
import cash.z.ecc.android.ext.onClickNavBack import cash.z.ecc.android.ext.onClickNavBack
import cash.z.ecc.android.feedback.Report
import cash.z.ecc.android.feedback.Report.Tap.SCAN_BACK
import cash.z.ecc.android.ui.base.BaseFragment import cash.z.ecc.android.ui.base.BaseFragment
import cash.z.ecc.android.ui.send.SendViewModel import cash.z.ecc.android.ui.send.SendViewModel
import cash.z.ecc.android.util.twig
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors import java.util.concurrent.Executors
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
class ScanFragment : BaseFragment<FragmentScanBinding>() {
override val screen = Report.Screen.SCAN class ScanFragment : BaseFragment<FragmentScanBinding>() {
private var _fragmentCameraBinding: FragmentScanBinding? = null
private val fragmentCameraBinding get() = _fragmentCameraBinding!!
private var displayId: Int = -1
private var lensFacing: Int = CameraSelector.LENS_FACING_BACK
private var preview: Preview? = null
private var imageCapture: ImageCapture? = null
private var imageAnalyzer: ImageAnalysis? = null
private var camera: Camera? = null
private var cameraProvider: ProcessCameraProvider? = null
private val viewModel: ScanViewModel by viewModels() private val viewModel: ScanViewModel by viewModels()
private val sendViewModel: SendViewModel by activityViewModels() private val sendViewModel: SendViewModel by activityViewModels()
private lateinit var windowManager: WindowInfoTracker
private lateinit var cameraProviderFuture: ListenableFuture<ProcessCameraProvider> /** Blocking camera operations are performed using this executor */
private lateinit var cameraExecutor: ExecutorService
private var cameraExecutor: ExecutorService? = null
override fun inflate(inflater: LayoutInflater): FragmentScanBinding = override fun inflate(inflater: LayoutInflater): FragmentScanBinding =
FragmentScanBinding.inflate(inflater) FragmentScanBinding.inflate(inflater)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
if (cameraExecutor != null) cameraExecutor?.shutdown()
// Initialize our background executor
cameraExecutor = Executors.newSingleThreadExecutor() cameraExecutor = Executors.newSingleThreadExecutor()
binding.backButtonHitArea.onClickNavBack() { tapped(SCAN_BACK) } //Initialize WindowManager to retrieve display metrics
} windowManager = WindowInfoTracker.getOrCreate(view.context)
override fun onCreate(savedInstanceState: Bundle?) { // Wait for the views to be properly laid out
super.onCreate(savedInstanceState) fragmentCameraBinding.viewFinder.post {
if (!allPermissionsGranted()) getRuntimePermissions()
}
override fun onAttach(context: Context) { // Keep track of the display in which this view is attached
super.onAttach(context) displayId = fragmentCameraBinding.viewFinder.display.displayId
cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener( // Set up the camera and its use cases
Runnable { setUpCamera()
bindPreview(cameraProviderFuture.get()) }
},
ContextCompat.getMainExecutor(context) // Initialize back button
) _fragmentCameraBinding?.backButtonHitArea?.onClickNavBack()
} }
override fun onDestroyView() { override fun onDestroyView() {
_fragmentCameraBinding = null
super.onDestroyView() super.onDestroyView()
cameraExecutor?.shutdown()
cameraExecutor = null // Shut down our background executor
cameraExecutor.shutdown()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_fragmentCameraBinding = FragmentScanBinding.inflate(inflater, container, false)
return fragmentCameraBinding.root
}
/** Initialize CameraX, and prepare to bind the camera use cases */
private fun setUpCamera() {
val cameraProviderFuture = ProcessCameraProvider.getInstance(requireContext())
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")
}
// Build and bind the camera use cases
bindCameraUseCases()
}, ContextCompat.getMainExecutor(requireContext()))
} }
private fun bindPreview(cameraProvider: ProcessCameraProvider) { /** Declare and bind preview, capture and analysis use cases */
// 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 private fun bindCameraUseCases() {
// 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 // 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 metrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(requireActivity()).bounds
val rotation = binding.preview.display.rotation Log.d("SilentDragon", "Screen metrics: ${metrics.width()} x ${metrics.height()}")
val screenAspectRatio = aspectRatio(metrics.width(), metrics.height())
*/
// Hardcode to square for now otherwise scanning doesn't work
val screenAspectRatio = aspectRatio(1, 1)
Log.d("SilentDragon", "Preview aspect ratio: $screenAspectRatio")
val rotation = fragmentCameraBinding.viewFinder.display.rotation
// CameraProvider
val cameraProvider = cameraProvider
?: throw IllegalStateException("Camera initialization failed.")
val preview = // CameraSelector
Preview.Builder().setTargetName("Preview").setTargetAspectRatio(screenAspectRatio) val cameraSelector = CameraSelector.Builder().requireLensFacing(lensFacing).build()
.setTargetRotation(rotation).build()
val cameraSelector = CameraSelector.Builder() // Preview
.requireLensFacing(CameraSelector.LENS_FACING_BACK) 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() .build()
val imageAnalysis = ImageAnalysis.Builder().setTargetAspectRatio(screenAspectRatio) // 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) .setTargetRotation(rotation)
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST) .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
.build() .build()
imageAnalysis.setAnalyzer(
cameraExecutor!!, // The analyzer can then be assigned to the instance
QrAnalyzer { q, i -> .also {
onQrScanned(q, i) it.setAnalyzer(cameraExecutor,
QrAnalyzer { q, i ->
onQrScanned(q, i)
}
)
} }
)
// Must unbind the use-cases before rebinding them // Must unbind the use-cases before rebinding them
cameraProvider.unbindAll() cameraProvider.unbindAll()
if (camera != null) {
// Must remove observers from the previous camera instance
removeCameraStateObservers(camera!!.cameraInfo)
}
try { try {
cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageAnalysis) // A variable number of use-cases can be passed here -
preview.setSurfaceProvider(binding.preview.surfaceProvider) // camera provides access to CameraControl & CameraInfo
} catch (t: Throwable) { camera = cameraProvider.bindToLifecycle(
// TODO: consider bubbling this up to the user this, cameraSelector, preview, imageCapture, imageAnalyzer)
mainActivity?.feedback?.report(t)
twig("Error while opening the camera: $t") // Attach the viewfinder's surface provider to preview use case
preview?.setSurfaceProvider(fragmentCameraBinding.viewFinder.surfaceProvider)
observeCameraState(camera?.cameraInfo!!)
} catch (exc: Exception) {
Log.e(TAG, "Use case binding failed", exc)
}
}
private fun removeCameraStateObservers(cameraInfo: CameraInfo) {
cameraInfo.cameraState.removeObservers(viewLifecycleOwner)
}
private fun observeCameraState(cameraInfo: CameraInfo) {
cameraInfo.cameraState.observe(viewLifecycleOwner) { cameraState ->
run {
when (cameraState.type) {
CameraState.Type.PENDING_OPEN -> {
// Ask the user to close other camera apps
Toast.makeText(context,
"CameraState: Pending Open",
Toast.LENGTH_SHORT).show()
}
CameraState.Type.OPENING -> {
// Show the Camera UI
Toast.makeText(context,
"CameraState: Opening",
Toast.LENGTH_SHORT).show()
}
CameraState.Type.OPEN -> {
// Setup Camera resources and begin processing
Toast.makeText(context,
"CameraState: Open",
Toast.LENGTH_SHORT).show()
}
CameraState.Type.CLOSING -> {
// Close camera UI
Toast.makeText(context,
"CameraState: Closing",
Toast.LENGTH_SHORT).show()
}
CameraState.Type.CLOSED -> {
// Free camera resources
Toast.makeText(context,
"CameraState: Closed",
Toast.LENGTH_SHORT).show()
}
}
}
cameraState.error?.let { error ->
when (error.code) {
// Open errors
CameraState.ERROR_STREAM_CONFIG -> {
// Make sure to setup the use cases properly
Toast.makeText(context,
"Stream config error",
Toast.LENGTH_SHORT).show()
}
// Opening errors
CameraState.ERROR_CAMERA_IN_USE -> {
// Close the camera or ask user to close another camera app that's using the
// camera
Toast.makeText(context,
"Camera in use",
Toast.LENGTH_SHORT).show()
}
CameraState.ERROR_MAX_CAMERAS_IN_USE -> {
// Close another open camera in the app, or ask the user to close another
// camera app that's using the camera
Toast.makeText(context,
"Max cameras in use",
Toast.LENGTH_SHORT).show()
}
CameraState.ERROR_OTHER_RECOVERABLE_ERROR -> {
Toast.makeText(context,
"Other recoverable error",
Toast.LENGTH_SHORT).show()
}
// Closing errors
CameraState.ERROR_CAMERA_DISABLED -> {
// Ask the user to enable the device's cameras
Toast.makeText(context,
"Camera disabled",
Toast.LENGTH_SHORT).show()
}
CameraState.ERROR_CAMERA_FATAL_ERROR -> {
// Ask the user to reboot the device to restore camera function
Toast.makeText(context,
"Fatal error",
Toast.LENGTH_SHORT).show()
}
// Closed errors
CameraState.ERROR_DO_NOT_DISTURB_MODE_ENABLED -> {
// Ask the user to disable the "Do Not Disturb" mode, then reopen the camera
Toast.makeText(context,
"Do not disturb mode enabled",
Toast.LENGTH_SHORT).show()
}
}
}
} }
} }
/** /**
* Adapted from: https://github.com/android/camera-samples/blob/master/CameraXBasic/app/src/main/java/com/android/example/cameraxbasic/fragments/CameraFragment.kt#L350 * [androidx.camera.core.ImageAnalysis.Builder] requires enum value of
* [androidx.camera.core.AspectRatio]. Currently it has values of 4:3 & 16:9.
*
* Detecting the most suitable ratio for dimensions provided in @params by counting absolute
* of preview ratio to one of the provided values.
*
* @param width - preview width
* @param height - preview height
* @return suitable aspect ratio
*/ */
private fun aspectRatio(width: Int, height: Int): Int { private fun aspectRatio(width: Int, height: Int): Int {
val previewRatio = kotlin.math.max(width, height).toDouble() / kotlin.math.min( val previewRatio = max(width, height).toDouble() / min(width, height)
width, if (abs(previewRatio - RATIO_4_3_VALUE) <= abs(previewRatio - RATIO_16_9_VALUE)) {
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_4_3
} }
return AspectRatio.RATIO_16_9 return AspectRatio.RATIO_16_9
} }
/** 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
}
companion object {
private const val TAG = "SilentDragon"
private const val RATIO_4_3_VALUE = 4.0 / 3.0
private const val RATIO_16_9_VALUE = 16.0 / 9.0
}
private fun onQrScanned(qrContent: String, image: ImageProxy) { private fun onQrScanned(qrContent: String, image: ImageProxy) {
//Log.d("SilentDragon", "QR scanned: $qrContent")
resumedScope.launch { resumedScope.launch {
val parsed = viewModel.parse(qrContent) val parsed = viewModel.parse(qrContent)
if (parsed == null) { if (parsed == null) {
val network = viewModel.networkName val network = viewModel.networkName
binding.textScanError.text = getString(R.string.scan_invalid_address, network, qrContent) _fragmentCameraBinding?.textScanError?.text =
getString(R.string.scan_invalid_address, network, qrContent)
image.close() image.close()
} else { /* continue scanning*/ } else { /* continue scanning*/
binding.textScanError.text = "" _fragmentCameraBinding?.textScanError?.text = ""
sendViewModel.toAddress = parsed sendViewModel.toAddress = parsed
mainActivity?.safeNavigate(R.id.action_nav_scan_to_nav_send) mainActivity?.safeNavigate(R.id.action_nav_scan_to_nav_send)
} }
} }
} }
// 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 (!isPermissionGranted(mainActivity!!, permission!!)) {
return false
}
}
return true
}
private fun getRuntimePermissions() {
val allNeededPermissions = arrayListOf<String>()
for (permission in requiredPermissions) {
if (!isPermissionGranted(mainActivity!!, permission!!)) {
allNeededPermissions.add(permission)
}
}
if (allNeededPermissions.isNotEmpty()) {
requestPermissions(allNeededPermissions.toTypedArray(), CAMERA_PERMISSION_REQUEST)
}
}
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
}
}
} }

4
app/src/main/java/cash/z/ecc/android/ui/scan/ScanViewModel.kt

@ -12,9 +12,9 @@ class ScanViewModel : ViewModel() {
val networkName get() = synchronizer.network.networkName val networkName get() = synchronizer.network.networkName
suspend fun parse(qrCode: String): String? { suspend fun parse(qrCode: String): String? {
// temporary parse code to allow both plain addresses and those that start with zcash: // temporary parse code to allow both plain addresses and those that start with hush:
// TODO: replace with more robust ZIP-321 handling of QR codes // TODO: replace with more robust ZIP-321 handling of QR codes
val address = if (qrCode.startsWith("zcash:")) { val address = if (qrCode.startsWith("hush:")) {
qrCode.substring(6, qrCode.indexOf("?").takeUnless { it == -1 } ?: qrCode.length) qrCode.substring(6, qrCode.indexOf("?").takeUnless { it == -1 } ?: qrCode.length)
} else { } else {
qrCode qrCode

2
app/src/main/res/layout/fragment_scan.xml

@ -46,7 +46,7 @@
tools:background="@color/zcashRed" /> tools:background="@color/zcashRed" />
<androidx.camera.view.PreviewView <androidx.camera.view.PreviewView
android:id="@+id/preview" android:id="@+id/viewFinder"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"

14
buildSrc/src/main/java/cash/z/ecc/android/Dependencies.kt

@ -6,9 +6,9 @@ object Deps {
const val kotlinVersion = "1.7.20" const val kotlinVersion = "1.7.20"
const val navigationVersion = "2.5.2" const val navigationVersion = "2.5.2"
const val compileSdkVersion = 31 const val compileSdkVersion = 33
const val minSdkVersion = 21 const val minSdkVersion = 21
const val targetSdkVersion = 30 const val targetSdkVersion = 33
const val versionName = "1.0.0" const val versionName = "1.0.0"
const val versionCode = 1_00_00 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release. const val versionCode = 1_00_00 // last digits are alpha(0XX) beta(2XX) rc(4XX) release(8XX). Ex: 1_08_04_401 is an release candidate build of version 1.8.4 and 1_08_04_800 would be the final release.
const val packageName = "hush.android" const val packageName = "hush.android"
@ -25,15 +25,7 @@ object Deps {
const val MULTIDEX = "androidx.multidex:multidex:2.0.1" const val MULTIDEX = "androidx.multidex:multidex:2.0.1"
const val PAGING = "androidx.paging:paging-runtime-ktx:2.1.2" const val PAGING = "androidx.paging:paging-runtime-ktx:2.1.2"
const val RECYCLER = "androidx.recyclerview:recyclerview:1.2.1" const val RECYCLER = "androidx.recyclerview:recyclerview:1.2.1"
object CameraX : Version("1.1.0-alpha05") {
val CAMERA2 = "androidx.camera:camera-camera2:$version"
val CORE = "androidx.camera:camera-core:$version"
val LIFECYCLE = "androidx.camera:camera-lifecycle:$version"
object View : Version("1.0.0-alpha27") {
val EXT = "androidx.camera:camera-extensions:$version"
val VIEW = "androidx.camera:camera-view:$version"
}
}
object Lifecycle : Version("2.4.0-alpha02") { object Lifecycle : Version("2.4.0-alpha02") {
val LIFECYCLE_RUNTIME_KTX = "androidx.lifecycle:lifecycle-runtime-ktx:$version" val LIFECYCLE_RUNTIME_KTX = "androidx.lifecycle:lifecycle-runtime-ktx:$version"
} }

Loading…
Cancel
Save