Switching to CameraX

This commit is contained in:
Matéo Duparc 2020-12-19 19:55:54 +01:00
parent b23cb7b8ce
commit eefca5ee0a
Signed by: hardcoresushi
GPG Key ID: 007F84120107191E
12 changed files with 209 additions and 117 deletions

View File

@ -47,7 +47,7 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "androidx.core:core-ktx:1.3.2"
implementation "androidx.appcompat:appcompat:1.2.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.3"
implementation "androidx.constraintlayout:constraintlayout:2.0.4"
implementation "androidx.sqlite:sqlite-ktx:2.1.0"
implementation "androidx.preference:preference-ktx:1.1.1"
@ -56,6 +56,12 @@ dependencies {
implementation "com.github.bumptech.glide:glide:4.11.0"
implementation "com.google.android.exoplayer:exoplayer-core:2.11.7"
implementation "com.google.android.exoplayer:exoplayer-ui:2.11.7"
implementation "com.otaliastudios:cameraview:2.6.4"
implementation "androidx.biometric:biometric:1.0.1"
def camerax_version = "1.0.0-rc01"
implementation "androidx.camera:camera-camera2:$camerax_version"
implementation "androidx.camera:camera-lifecycle:$camerax_version"
implementation "androidx.camera:camera-view:1.0.0-alpha20"
implementation "androidx.camera:camera-extensions:1.0.0-alpha20"
}

View File

@ -24,5 +24,4 @@
-keep class sushi.hardcore.droidfs.SettingsActivity$** {
*;
}
-keep class sushi.hardcore.droidfs.explorers.ExplorerElement
-keep class com.otaliastudios.cameraview.markers.DefaultAutoFocusMarker
-keep class sushi.hardcore.droidfs.explorers.ExplorerElement

View File

@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature
android:name="android.hardware.camera.any"

View File

@ -3,8 +3,15 @@ package sushi.hardcore.droidfs
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.hardware.camera2.CameraCharacteristics
import android.hardware.camera2.CameraManager
import android.os.Build
import android.os.Bundle
import android.text.InputType
import android.util.DisplayMetrics
import android.util.Size
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import android.view.WindowManager
import android.view.animation.Animation
@ -12,35 +19,34 @@ import android.view.animation.LinearInterpolator
import android.view.animation.RotateAnimation
import android.widget.EditText
import android.widget.ImageView
import android.widget.RelativeLayout
import android.widget.Toast
import androidx.camera.camera2.interop.Camera2CameraInfo
import androidx.camera.core.*
import androidx.camera.extensions.HdrImageCaptureExtender
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.core.content.ContextCompat
import com.otaliastudios.cameraview.CameraListener
import com.otaliastudios.cameraview.PictureResult
import com.otaliastudios.cameraview.controls.Facing
import com.otaliastudios.cameraview.controls.Flash
import com.otaliastudios.cameraview.controls.Grid
import com.otaliastudios.cameraview.controls.Hdr
import kotlinx.android.synthetic.main.activity_camera.*
import sushi.hardcore.droidfs.adapters.DialogSingleChoiceAdapter
import sushi.hardcore.droidfs.provider.RestrictedFileProvider
import sushi.hardcore.droidfs.util.GocryptfsVolume
import sushi.hardcore.droidfs.util.MiscUtils
import sushi.hardcore.droidfs.util.PathUtils
import sushi.hardcore.droidfs.widgets.ColoredAlertDialogBuilder
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
companion object {
private val flashModes = listOf(Flash.AUTO, Flash.ON, Flash.OFF)
private val gridTitles = listOf(R.string.grid_none, R.string.grid_3x3, R.string.grid_4x4)
private val gridValues = listOf(Grid.OFF, Grid.DRAW_3X3, Grid.DRAW_4X4)
private const val CAMERA_PERMISSION_REQUEST_CODE = 1
private const val fileNameRandomMin = 100000
private const val fileNameRandomMax = 999999
private val dateFormat = SimpleDateFormat("yyyyMMdd_HHmmss")
private val random = Random()
}
private var currentFlashModeIndex = 0
private var timerDuration = 0
set(value) {
field = value
@ -57,22 +63,140 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
private lateinit var outputDirectory: String
private lateinit var fileName: String
private var isFinishingIntentionally = false
private lateinit var context: Context
private lateinit var cameraExecutor: ExecutorService
private var imageCapture: ImageCapture? = null
private var resolutions: Array<Size>? = null
private var currentResolutionIndex: Int = 0
private var isBackCamera = true
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_camera)
gocryptfsVolume = GocryptfsVolume(intent.getIntExtra("sessionID", -1))
outputDirectory = intent.getStringExtra("path")!!
context = this
camera.setLifecycleOwner(this)
camera.addCameraListener(object: CameraListener(){
override fun onPictureTaken(result: PictureResult) {
take_photo_button.onPhotoTaken()
val inputStream = ByteArrayInputStream(result.data)
if (gocryptfsVolume.importFile(inputStream, PathUtils.pathJoin(outputDirectory, fileName))){
Toast.makeText(context, getString(R.string.picture_save_success, fileName), Toast.LENGTH_SHORT).show()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED){
setupCamera()
} else {
requestPermissions(arrayOf(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE)
}
} else {
setupCamera()
}
cameraExecutor = Executors.newSingleThreadExecutor()
take_photo_button.onClick = ::onClickTakePhoto
orientedIcons = listOf(image_ratio, image_timer, image_close, image_flash, image_camera_switch)
sensorOrientationListener = SensorOrientationListener(this)
val scaleGestureDetector = ScaleGestureDetector(this, object : ScaleGestureDetector.SimpleOnScaleGestureListener(){
override fun onScale(detector: ScaleGestureDetector): Boolean {
val currentZoomRatio = imageCapture?.camera?.cameraInfo?.zoomState?.value?.zoomRatio ?: 0F
imageCapture?.camera?.cameraControl?.setZoomRatio(currentZoomRatio*detector.scaleFactor)
return true
}
})
camera_preview.setOnTouchListener { _, motionEvent: MotionEvent ->
when (motionEvent.action) {
MotionEvent.ACTION_DOWN -> true
MotionEvent.ACTION_UP -> {
val factory = camera_preview.meteringPointFactory
val point = factory.createPoint(motionEvent.x, motionEvent.y)
val action = FocusMeteringAction.Builder(point).build()
imageCapture?.camera?.cameraControl?.startFocusAndMetering(action)
true
}
MotionEvent.ACTION_MOVE -> scaleGestureDetector.onTouchEvent(motionEvent)
else -> false
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
when (requestCode) {
CAMERA_PERMISSION_REQUEST_CODE -> if (grantResults.size == 1) {
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.error)
.setMessage(R.string.camera_perm_needed)
.setCancelable(false)
.setPositiveButton(R.string.ok) { _, _ ->
isFinishingIntentionally = true
finish()
}.show()
} else {
ColoredAlertDialogBuilder(context)
setupCamera()
}
}
}
}
private fun adaptPreviewSize(resolution: Size){
val metrics = DisplayMetrics()
windowManager.defaultDisplay.getMetrics(metrics)
//resolution.width and resolution.height seem to be inverted
val width = resolution.height
val height = resolution.width
camera_preview.layoutParams = if (metrics.widthPixels < width){
RelativeLayout.LayoutParams(
metrics.widthPixels,
(height * (metrics.widthPixels.toFloat() / width)).toInt()
)
} else {
RelativeLayout.LayoutParams(width, height)
}
(camera_preview.layoutParams as RelativeLayout.LayoutParams).addRule(RelativeLayout.CENTER_IN_PARENT)
}
private fun setupCamera(resolution: Size? = null){
val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
cameraProviderFuture.addListener({
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
val preview = Preview.Builder()
.build()
.also {
it.setSurfaceProvider(camera_preview.surfaceProvider)
}
val builder = ImageCapture.Builder()
.setFlashMode(ImageCapture.FLASH_MODE_AUTO)
resolution?.let {
builder.setTargetResolution(it)
}
val hdrImageCapture = HdrImageCaptureExtender.create(builder)
val cameraSelector = if (isBackCamera){ CameraSelector.DEFAULT_BACK_CAMERA } else { CameraSelector.DEFAULT_FRONT_CAMERA }
if (hdrImageCapture.isExtensionAvailable(cameraSelector)){
hdrImageCapture.enableExtension(cameraSelector)
}
imageCapture = builder.build()
cameraProvider.unbindAll()
val camera = cameraProvider.bindToLifecycle(this, cameraSelector, preview, imageCapture)
adaptPreviewSize(imageCapture!!.attachedSurfaceResolution!!)
val info = Camera2CameraInfo.from(camera.cameraInfo)
val cameraManager = getSystemService(Context.CAMERA_SERVICE) as CameraManager
val characteristics = cameraManager.getCameraCharacteristics(info.cameraId)
characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)?.let { streamConfigurationMap ->
resolutions = streamConfigurationMap.getOutputSizes(imageCapture!!.imageFormat)
}
}, ContextCompat.getMainExecutor(this))
}
private fun takePhoto() {
val imageCapture = imageCapture ?: return
val outputBuff = ByteArrayOutputStream()
val outputOptions = ImageCapture.OutputFileOptions.Builder(outputBuff).build()
imageCapture.takePicture(outputOptions, ContextCompat.getMainExecutor(this), object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
take_photo_button.onPhotoTaken()
if (gocryptfsVolume.importFile(ByteArrayInputStream(outputBuff.toByteArray()), PathUtils.pathJoin(outputDirectory, fileName))){
Toast.makeText(applicationContext, getString(R.string.picture_save_success, fileName), Toast.LENGTH_SHORT).show()
} else {
ColoredAlertDialogBuilder(applicationContext)
.setTitle(R.string.error)
.setMessage(R.string.picture_save_failed)
.setCancelable(false)
@ -83,10 +207,11 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
.show()
}
}
override fun onError(exception: ImageCaptureException) {
take_photo_button.onPhotoTaken()
Toast.makeText(applicationContext, exception.message, Toast.LENGTH_SHORT).show()
}
})
take_photo_button.onClick = ::onClickTakePhoto
orientedIcons = listOf(image_hdr, image_timer, image_grid, image_close, image_flash, image_camera_switch)
sensorOrientationListener = SensorOrientationListener(this)
}
private fun onClickTakePhoto() {
@ -102,49 +227,41 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
Thread.sleep(1000)
}
runOnUiThread {
camera.takePicture()
takePhoto()
text_timer.visibility = View.GONE
}
}.start()
} else {
camera.takePicture()
takePhoto()
}
}
fun onClickFlash(view: View) {
currentFlashModeIndex = MiscUtils.incrementIndex(currentFlashModeIndex, flashModes)
camera.flash = flashModes[currentFlashModeIndex]
image_flash.setImageResource(when (camera.flash) {
Flash.AUTO -> R.drawable.icon_flash_auto
Flash.ON -> R.drawable.icon_flash_on
else -> R.drawable.icon_flash_off
image_flash.setImageResource(when (imageCapture?.flashMode) {
ImageCapture.FLASH_MODE_AUTO -> {
imageCapture?.flashMode = ImageCapture.FLASH_MODE_ON
R.drawable.icon_flash_on
}
ImageCapture.FLASH_MODE_ON -> {
imageCapture?.flashMode = ImageCapture.FLASH_MODE_OFF
R.drawable.icon_flash_off
}
else -> {
imageCapture?.flashMode = ImageCapture.FLASH_MODE_AUTO
R.drawable.icon_flash_auto
}
})
}
fun onClickCameraSwitch(view: View) {
camera.toggleFacing()
if (camera.facing == Facing.FRONT){
image_camera_switch.setImageResource(R.drawable.icon_camera_back)
} else {
isBackCamera = if (isBackCamera) {
image_camera_switch.setImageResource(R.drawable.icon_camera_front)
Thread {
Thread.sleep(25)
camera.flash = flashModes[currentFlashModeIndex] //refresh flash mode after switching camera
}.start()
}
}
fun onClickHDR(view: View) {
camera.hdr = when (camera.hdr){
Hdr.ON -> {
image_hdr.setImageResource(R.drawable.icon_hdr_off)
Hdr.OFF
}
Hdr.OFF -> {
image_hdr.setImageResource(R.drawable.icon_hdr_on)
Hdr.ON
}
false
} else {
image_camera_switch.setImageResource(R.drawable.icon_camera_back)
true
}
setupCamera()
}
fun onClickTimer(view: View) {
@ -173,16 +290,18 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
dialog.show()
}
fun onClickGrid(view: View) {
ColoredAlertDialogBuilder(this)
.setTitle(getString(R.string.choose_grid))
.setSingleChoiceItems(gridTitles.map { getString(it) }.toTypedArray(), gridValues.indexOf(camera.grid)){ dialog, which ->
camera.grid = gridValues[which]
image_grid.setImageResource(if (camera.grid == Grid.OFF){ R.drawable.icon_grid_off } else { R.drawable.icon_grid_on })
dialog.dismiss()
}
.setNegativeButton(R.string.cancel, null)
.show()
fun onClickRatio(view: View) {
resolutions?.let {
ColoredAlertDialogBuilder(this)
.setTitle(R.string.choose_resolution)
.setSingleChoiceItems(DialogSingleChoiceAdapter(this, it.map { size -> size.toString() }.toTypedArray()), currentResolutionIndex) { dialog, which ->
setupCamera(resolutions!![which])
dialog.dismiss()
currentResolutionIndex = which
}
.setNegativeButton(R.string.cancel, null)
.show()
}
}
fun onClickClose(view: View) {
@ -192,6 +311,7 @@ class CameraActivity : BaseActivity(), SensorOrientationListener.Listener {
override fun onDestroy() {
super.onDestroy()
cameraExecutor.shutdown()
if (!isFinishingIntentionally) {
gocryptfsVolume.close()
RestrictedFileProvider.wipeAll(this)

View File

@ -419,7 +419,7 @@ open class BaseExplorerActivity : BaseActivity() {
setCurrentPath(currentDirectoryPath)
dialog.dismiss()
}
.setNegativeButton(R.string.cancel) { dialog, _ -> dialog.dismiss() }
.setNegativeButton(R.string.cancel, null)
.show()
true
}

View File

@ -1,5 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M20,2L4,2c-1.1,0 -2,0.9 -2,2v16c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM8,20L4,20v-4h4v4zM8,14L4,14v-4h4v4zM8,8L4,8L4,4h4v4zM14,20h-4v-4h4v4zM14,14h-4v-4h4v4zM14,8h-4L10,4h4v4zM20,20h-4v-4h4v4zM20,14h-4v-4h4v4zM20,8h-4L16,4h4v4z"/>
<path android:fillColor="@android:color/white" android:pathData="M19,12h-2v3h-3v2h5v-5zM7,9h3L10,7L5,7v5h2L7,9zM21,3L3,3c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h18c1.1,0 2,-0.9 2,-2L23,5c0,-1.1 -0.9,-2 -2,-2zM21,19.01L3,19.01L3,4.99h18v14.02z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M8,4v1.45l2,2L10,4h4v4h-3.45l2,2L14,10v1.45l2,2L16,10h4v4h-3.45l2,2L20,16v1.45l2,2L22,4c0,-1.1 -0.9,-2 -2,-2L4.55,2l2,2L8,4zM16,4h4v4h-4L16,4zM1.27,1.27L0,2.55l2,2L2,20c0,1.1 0.9,2 2,2h15.46l2,2 1.27,-1.27L1.27,1.27zM10,12.55L11.45,14L10,14v-1.45zM4,6.55L5.45,8L4,8L4,6.55zM8,20L4,20v-4h4v4zM8,14L4,14v-4h3.45l0.55,0.55L8,14zM14,20h-4v-4h3.45l0.55,0.54L14,20zM16,20v-1.46L17.46,20L16,20z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M17.5,15v-2h1.1l0.9,2L21,15l-0.9,-2.1c0.5,-0.2 0.9,-0.8 0.9,-1.4v-1c0,-0.8 -0.7,-1.5 -1.5,-1.5L16,9v4.9l1.1,1.1h0.4zM17.5,10.5h2v1h-2v-1zM13,10.5v0.4l1.5,1.5v-1.9c0,-0.8 -0.7,-1.5 -1.5,-1.5h-1.9l1.5,1.5h0.4zM9.5,9.5l-7,-7 -1.1,1L6.9,9h-0.4v2h-2L4.5,9L3,9v6h1.5v-2.5h2L6.5,15L8,15v-4.9l1.5,1.5L9.5,15h3.4l7.6,7.6 1.1,-1.1 -12.1,-12z"/>
</vector>

View File

@ -1,5 +0,0 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M21,11.5v-1c0,-0.8 -0.7,-1.5 -1.5,-1.5L16,9v6h1.5v-2h1.1l0.9,2L21,15l-0.9,-2.1c0.5,-0.3 0.9,-0.8 0.9,-1.4zM19.5,11.5h-2v-1h2v1zM6.5,11h-2L4.5,9L3,9v6h1.5v-2.5h2L6.5,15L8,15L8,9L6.5,9v2zM13,9L9.5,9v6L13,15c0.8,0 1.5,-0.7 1.5,-1.5v-3c0,-0.8 -0.7,-1.5 -1.5,-1.5zM13,13.5h-2v-3h2v3z"/>
</vector>

View File

@ -6,17 +6,11 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".CameraActivity">
<com.otaliastudios.cameraview.CameraView
android:id="@+id/camera"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cameraGesturePinch="zoom"
app:cameraGestureTap="autoFocus"
app:cameraAutoFocusMarker="@string/cameraview_default_autofocus_marker"
app:cameraHdr="on"
app:cameraPictureFormat="jpeg"
app:cameraPlaySounds="false"
app:cameraAudio="off"/>
<androidx.camera.view.PreviewView
android:id="@+id/camera_preview"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_centerInParent="true"/>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
@ -24,11 +18,11 @@
android:layout_marginTop="20dp">
<ImageView
android:id="@+id/image_hdr"
android:id="@+id/image_ratio"
android:layout_width="30dp"
android:layout_height="30dp"
android:onClick="onClickHDR"
android:src="@drawable/icon_hdr_on"
android:onClick="onClickRatio"
android:src="@drawable/icon_aspect_ratio"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/image_timer"
app:layout_constraintStart_toStartOf="parent"
@ -41,19 +35,8 @@
android:onClick="onClickTimer"
android:src="@drawable/icon_timer_off"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/image_grid"
app:layout_constraintStart_toEndOf="@id/image_hdr"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/image_grid"
android:layout_width="30dp"
android:layout_height="30dp"
android:onClick="onClickGrid"
android:src="@drawable/icon_grid_off"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/image_close"
app:layout_constraintStart_toEndOf="@+id/image_timer"
app:layout_constraintEnd_toStartOf="@id/image_close"
app:layout_constraintStart_toEndOf="@id/image_ratio"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
@ -64,7 +47,7 @@
android:src="@drawable/icon_close"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/image_grid"
app:layout_constraintStart_toEndOf="@id/image_timer"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -162,10 +162,6 @@
<string name="move_success_msg">The selected items have been successfully moved.</string>
<string name="move_success">Move successful !</string>
<string name="enter_timer_duration">Enter the timer duration (in s)</string>
<string name="grid_none">None</string>
<string name="grid_3x3">3x3</string>
<string name="grid_4x4">4x4</string>
<string name="choose_grid">Choose grid</string>
<string name="timer_empty_error_msg">Please enter a numeric value</string>
<string name="path_from_uri_null_error_msg">Failed to retrieve the selected path.</string>
<string name="create_cant_write_error_msg">DroidFS doesn\'t have write access to this path. Please try another location.</string>
@ -193,4 +189,6 @@
<string name="hidden_volume">Hidden Volume</string>
<string name="error_slash_in_name">Volume name cannot contain slashes</string>
<string name="hidden_volume_warning">Hidden volumes are stored in the app\'s internal storage. Other apps can\'t see these volumes without root access. However, if you uninstall DroidFS or clear data of the app, all your hidden volumes will be LOST. Be sure to make backups !</string>
<string name="camera_perm_needed">Camera permission is needed to take photo.</string>
<string name="choose_resolution">Choose a resolution</string>
</resources>

View File

@ -6,7 +6,7 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
classpath 'com.android.tools.build:gradle:4.1.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}