Browse Source

QR scanning fixes

pull/15/head
fekt 2 years ago
parent
commit
5696b62c8b
  1. 15
      app/build.gradle
  2. 12
      app/src/main/AndroidManifest.xml
  3. 383
      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" />

383
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)
// Wait for the views to be properly laid out
fragmentCameraBinding.viewFinder.post {
// Keep track of the display in which this view is attached
displayId = fragmentCameraBinding.viewFinder.display.displayId
override fun onCreate(savedInstanceState: Bundle?) { // Set up the camera and its use cases
super.onCreate(savedInstanceState) setUpCamera()
if (!allPermissionsGranted()) getRuntimePermissions()
} }
override fun onAttach(context: Context) { // Initialize back button
super.onAttach(context) _fragmentCameraBinding?.backButtonHitArea?.onClickNavBack()
cameraProviderFuture = ProcessCameraProvider.getInstance(context)
cameraProviderFuture.addListener(
Runnable {
bindPreview(cameraProviderFuture.get())
},
ContextCompat.getMainExecutor(context)
)
} }
override fun onDestroyView() { override fun onDestroyView() {
_fragmentCameraBinding = null
super.onDestroyView() super.onDestroyView()
cameraExecutor?.shutdown()
cameraExecutor = null // Shut down our background executor
cameraExecutor.shutdown()
} }
private fun bindPreview(cameraProvider: ProcessCameraProvider) { override fun onCreateView(
// 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 inflater: LayoutInflater,
// it's worth keeping tabs on that implementation because they keep making breaking changes to these APIs! 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()))
}
/** Declare and bind preview, capture and analysis use cases */
private fun bindCameraUseCases() {
// 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 preview = val rotation = fragmentCameraBinding.viewFinder.display.rotation
Preview.Builder().setTargetName("Preview").setTargetAspectRatio(screenAspectRatio)
.setTargetRotation(rotation).build()
val cameraSelector = CameraSelector.Builder() // CameraProvider
.requireLensFacing(CameraSelector.LENS_FACING_BACK) 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() .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
.also {
it.setAnalyzer(cameraExecutor,
QrAnalyzer { q, i -> QrAnalyzer { q, i ->
onQrScanned(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) {
* Adapted from: https://github.com/android/camera-samples/blob/master/CameraXBasic/app/src/main/java/com/android/example/cameraxbasic/fragments/CameraFragment.kt#L350 cameraInfo.cameraState.removeObservers(viewLifecycleOwner)
*/
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
} }
private fun onQrScanned(qrContent: String, image: ImageProxy) { private fun observeCameraState(cameraInfo: CameraInfo) {
resumedScope.launch { cameraInfo.cameraState.observe(viewLifecycleOwner) { cameraState ->
val parsed = viewModel.parse(qrContent) run {
if (parsed == null) { when (cameraState.type) {
val network = viewModel.networkName CameraState.Type.PENDING_OPEN -> {
binding.textScanError.text = getString(R.string.scan_invalid_address, network, qrContent) // Ask the user to close other camera apps
image.close() Toast.makeText(context,
} else { /* continue scanning*/ "CameraState: Pending Open",
binding.textScanError.text = "" Toast.LENGTH_SHORT).show()
sendViewModel.toAddress = parsed
mainActivity?.safeNavigate(R.id.action_nav_scan_to_nav_send)
} }
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 -> {
// private fun updateOverlay(detectedObjects: DetectedObjects) { // Close camera UI
// if (detectedObjects.objects.isEmpty()) { Toast.makeText(context,
// return "CameraState: Closing",
// } Toast.LENGTH_SHORT).show()
// }
// overlay.setSize(detectedObjects.imageWidth, detectedObjects.imageHeight) CameraState.Type.CLOSED -> {
// val list = mutableListOf<BoxData>() // Free camera resources
// for (obj in detectedObjects.objects) { Toast.makeText(context,
// val box = obj.boundingBox "CameraState: Closed",
// val name = "${categoryNames[obj.classificationCategory]}" Toast.LENGTH_SHORT).show()
// 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 { cameraState.error?.let { error ->
for (permission in requiredPermissions) { when (error.code) {
if (!isPermissionGranted(mainActivity!!, permission!!)) { // Open errors
return false 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()
}
}
} }
} }
return true
} }
private fun getRuntimePermissions() { /**
val allNeededPermissions = arrayListOf<String>() * [androidx.camera.core.ImageAnalysis.Builder] requires enum value of
for (permission in requiredPermissions) { * [androidx.camera.core.AspectRatio]. Currently it has values of 4:3 & 16:9.
if (!isPermissionGranted(mainActivity!!, permission!!)) { *
allNeededPermissions.add(permission) * 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 {
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
} }
if (allNeededPermissions.isNotEmpty()) { /** Returns true if the device has an available back camera. False otherwise */
requestPermissions(allNeededPermissions.toTypedArray(), CAMERA_PERMISSION_REQUEST) 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 { companion object {
private const val CAMERA_PERMISSION_REQUEST = 1002
private fun isPermissionGranted(context: Context, permission: String): Boolean { private const val TAG = "SilentDragon"
return ContextCompat.checkSelfPermission(context, permission) == PackageManager.PERMISSION_GRANTED 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) {
//Log.d("SilentDragon", "QR scanned: $qrContent")
resumedScope.launch {
val parsed = viewModel.parse(qrContent)
if (parsed == null) {
val network = viewModel.networkName
_fragmentCameraBinding?.textScanError?.text =
getString(R.string.scan_invalid_address, network, qrContent)
image.close()
} else { /* continue scanning*/
_fragmentCameraBinding?.textScanError?.text = ""
sendViewModel.toAddress = parsed
mainActivity?.safeNavigate(R.id.action_nav_scan_to_nav_send)
}
} }
} }
} }

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