From aa6fb439a18b1e4813614d33c721edbf9538f913 Mon Sep 17 00:00:00 2001 From: A117870935 Date: Tue, 21 Nov 2023 15:21:56 +0530 Subject: [PATCH] Scanbot migration 1.89.0 -> 4.0.0 --- app/build.gradle | 2 +- .../nmc/android/jobs/ScanDocUploadWorker.kt | 33 +- .../android/ui/CropScannedDocumentFragment.kt | 50 ++- .../nmc/android/ui/ScanDocumentFragment.kt | 339 +++++++++--------- .../com/nmc/android/ui/ScanPagerFragment.java | 4 +- .../nmc/android/utils/CheckableThemeUtils.kt | 97 +++-- .../java/com/owncloud/android/MainApp.java | 2 +- .../ui/activity/FolderPickerActivity.kt | 18 +- .../ui/activity/NotificationsActivity.kt | 1 + .../main/res/layout/files_folder_picker.xml | 7 - .../res/layout/fragment_scan_document.xml | 4 +- .../main/res/layout/fragment_scan_save.xml | 12 +- app/src/main/res/values-night/colors.xml | 6 +- app/src/main/res/values/colors.xml | 6 +- app/src/main/res/values/strings.xml | 2 + build.gradle | 2 +- 16 files changed, 305 insertions(+), 280 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 283d7570ff3c..403c9be70348 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -396,7 +396,7 @@ dependencies { // splash screen dependency ref: https://developer.android.com/develop/ui/views/launch/splash-screen/migrate implementation 'androidx.core:core-splashscreen:1.0.1' - //scanbot sdk + //scanbot sdk: https://github.com/doo/scanbot-sdk-example-android implementation "io.scanbot:sdk-package-2:$scanbotSdkVersion" //apache pdf-box for encrypting pdf files diff --git a/app/src/main/java/com/nmc/android/jobs/ScanDocUploadWorker.kt b/app/src/main/java/com/nmc/android/jobs/ScanDocUploadWorker.kt index 155c3892d76b..70368f5bd9e3 100644 --- a/app/src/main/java/com/nmc/android/jobs/ScanDocUploadWorker.kt +++ b/app/src/main/java/com/nmc/android/jobs/ScanDocUploadWorker.kt @@ -20,14 +20,15 @@ import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.operations.UploadFileOperation import com.owncloud.android.ui.notifications.NotificationUtils import com.owncloud.android.utils.StringUtils +import io.scanbot.ocr.model.OcrPage +import io.scanbot.pdf.model.PageSize +import io.scanbot.pdf.model.PdfConfig import io.scanbot.sdk.ScanbotSDK -import io.scanbot.sdk.core.contourdetector.DetectionResult -import io.scanbot.sdk.entity.Language +import io.scanbot.sdk.core.contourdetector.DetectionStatus import io.scanbot.sdk.ocr.OpticalCharacterRecognizer import io.scanbot.sdk.ocr.process.OcrResult import io.scanbot.sdk.persistence.Page import io.scanbot.sdk.persistence.PageFileStorage -import io.scanbot.sdk.process.PDFPageSize import io.scanbot.sdk.process.PDFRenderer import org.apache.pdfbox.pdmodel.PDDocument import org.apache.pdfbox.pdmodel.encryption.AccessPermission @@ -37,7 +38,7 @@ import java.io.FileNotFoundException import java.io.FileOutputStream import java.security.SecureRandom -class ScanDocUploadWorker constructor( +class ScanDocUploadWorker( private val context: Context, params: WorkerParameters, private val notificationManager: NotificationManager, @@ -78,15 +79,19 @@ class ScanDocUploadWorker constructor( SaveScannedDocumentFragment.SAVE_TYPE_JPG -> { saveJPGImageFiles(docFileName, bitmapList) } + SaveScannedDocumentFragment.SAVE_TYPE_PNG -> { savePNGImageFiles(docFileName, bitmapList) } + SaveScannedDocumentFragment.SAVE_TYPE_PDF -> { saveNonOCRPDFFile(docFileName, bitmapList, scanDocPdfPwd) } + SaveScannedDocumentFragment.SAVE_TYPE_PDF_OCR -> { savePDFWithOCR(docFileName, bitmapList, scanDocPdfPwd) } + SaveScannedDocumentFragment.SAVE_TYPE_TXT -> { saveTextFile(docFileName, bitmapList) } @@ -148,7 +153,8 @@ class ScanDocUploadWorker constructor( private fun saveNonOCRPDFFile(fileName: String?, bitmapList: List, pdfPassword: String?) { val pageList = getScannedPages(bitmapList) - val pdfFile: File? = pdfRenderer.renderDocumentFromPages(pageList, PDFPageSize.A4) + val pdfFile: File? = + pdfRenderer.renderDocumentFromPages(pageList, PdfConfig.defaultConfig().copy(pageSize = PageSize.A4)) if (pdfFile != null) { val renamedFile = File(pdfFile.parent + OCFile.PATH_SEPARATOR + fileName + ".pdf") if (pdfFile.renameTo(renamedFile)) { @@ -200,21 +206,19 @@ class ScanDocUploadWorker constructor( private fun getScannedPages(bitmapList: List): List { val pageList: MutableList = ArrayList() for (bitmap in bitmapList) { - val page = Page(pageFileStorage.add(bitmap), emptyList(), DetectionResult.OK) + val page = Page(pageFileStorage.add(bitmap), listOf(), DetectionStatus.OK) pageList.add(page) } return pageList } private fun savePDFWithOCR(fileName: String?, bitmapList: List, pdfPassword: String?) { - val languages = setOf(Language.ENG) val ocrResult: OcrResult = opticalCharacterRecognizer.recognizeTextWithPdfFromPages( getScannedPages(bitmapList), - PDFPageSize.A4, - languages + PdfConfig.defaultConfig().copy(pageSize = PageSize.A4) ) - val ocrPageList: List = ocrResult.ocrPages + val ocrPageList: List = ocrResult.ocrPages if (ocrPageList.isNotEmpty()) { val ocrText = ocrResult.recognizedText } @@ -229,18 +233,17 @@ class ScanDocUploadWorker constructor( } private fun saveTextFile(fileName: String?, bitmapList: List) { - val languages = setOf(Language.ENG) for (i in bitmapList.indices) { var newFileName = fileName val bitmap = bitmapList[i] if (i > 0) { newFileName += "($i)" } - val page = Page(pageFileStorage.add(bitmap), emptyList(), DetectionResult.OK) + val page = Page(pageFileStorage.add(bitmap), emptyList(), DetectionStatus.OK) val pageList: MutableList = ArrayList() pageList.add(page) - val ocrResult: OcrResult = opticalCharacterRecognizer.recognizeTextFromPages(pageList, languages) - val ocrPageList: List = ocrResult.ocrPages + val ocrResult: OcrResult = opticalCharacterRecognizer.recognizeTextFromPages(pageList) + val ocrPageList: List = ocrResult.ocrPages if (ocrPageList.isNotEmpty()) { val ocrText = ocrResult.recognizedText val txtFile = FileUtils.writeTextToFile(context, ocrText, newFileName) @@ -256,11 +259,9 @@ class ScanDocUploadWorker constructor( } FileUploader.uploadNewFile( - context, accountManager.user, savedFiles.toTypedArray(), remotePaths, - null, // MIME type will be detected from file name FileUploader.LOCAL_BEHAVIOUR_DELETE, false, // do not create parent folder if not existent UploadFileOperation.CREATED_BY_USER, diff --git a/app/src/main/java/com/nmc/android/ui/CropScannedDocumentFragment.kt b/app/src/main/java/com/nmc/android/ui/CropScannedDocumentFragment.kt index f6a5a3394151..2a00b6c66fd2 100644 --- a/app/src/main/java/com/nmc/android/ui/CropScannedDocumentFragment.kt +++ b/app/src/main/java/com/nmc/android/ui/CropScannedDocumentFragment.kt @@ -18,9 +18,11 @@ import com.nmc.android.utils.ScanBotSdkUtils import com.owncloud.android.R import com.owncloud.android.databinding.FragmentCropScanBinding import io.scanbot.sdk.ScanbotSDK -import io.scanbot.sdk.core.contourdetector.DetectionResult +import io.scanbot.sdk.core.contourdetector.ContourDetector +import io.scanbot.sdk.core.contourdetector.DetectionStatus import io.scanbot.sdk.core.contourdetector.Line2D import io.scanbot.sdk.process.CropOperation +import io.scanbot.sdk.process.ImageProcessor import java.util.concurrent.Executors import kotlin.math.absoluteValue @@ -29,11 +31,12 @@ class CropScannedDocumentFragment : Fragment() { private lateinit var onFragmentChangeListener: OnFragmentChangeListener private lateinit var onDocScanListener: OnDocScanListener - private var scannedDocIndex: Int = -1 private lateinit var scanbotSDK: ScanbotSDK + private lateinit var imageProcessor: ImageProcessor + private lateinit var contourDetector: ContourDetector + private var scannedDocIndex: Int = -1 private lateinit var originalBitmap: Bitmap - private var rotationDegrees = 0 private var polygonPoints: List? = null @@ -72,6 +75,9 @@ class CropScannedDocumentFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) scanbotSDK = (requireActivity() as ScanActivity).scanbotSDK + contourDetector = scanbotSDK.createContourDetector() + imageProcessor = scanbotSDK.imageProcessor() + detectDocument() binding.cropBtnResetBorders.setOnClickListener { onClickListener(it) @@ -155,28 +161,26 @@ class CropScannedDocumentFragment : Fragment() { InitImageViewTask().executeOnExecutor(Executors.newSingleThreadExecutor()) } - // We use AsyncTask only for simplicity here. Avoid using it in your production app due to memory leaks, etc! @SuppressLint("StaticFieldLeak") internal inner class InitImageViewTask : AsyncTask() { private var previewBitmap: Bitmap? = null override fun doInBackground(vararg params: Void?): InitImageResult { - //originalBitmap = FileUtils.convertFileToBitmap(File(scannedDocPath)) originalBitmap = onDocScanListener.scannedDocs[scannedDocIndex] previewBitmap = ScanBotSdkUtils.resizeForPreview(originalBitmap) - val detector = scanbotSDK.createContourDetector() - val detectionResult = detector.detect(originalBitmap) - val linesPair = Pair(detector.horizontalLines, detector.verticalLines) - val polygon = detector.polygonF - - return when (detectionResult) { - DetectionResult.OK, - DetectionResult.OK_BUT_BAD_ANGLES, - DetectionResult.OK_BUT_TOO_SMALL, - DetectionResult.OK_BUT_BAD_ASPECT_RATIO -> { - InitImageResult(linesPair, polygon!!) + val result = contourDetector.detect(originalBitmap) + return when (result?.status) { + DetectionStatus.OK, + DetectionStatus.OK_BUT_BAD_ANGLES, + DetectionStatus.OK_BUT_TOO_SMALL, + DetectionStatus.OK_BUT_BAD_ASPECT_RATIO -> { + val linesPair = Pair(result.horizontalLines, result.verticalLines) + val polygon = result.polygonF + + InitImageResult(linesPair, polygon) } + else -> InitImageResult(Pair(listOf(), listOf()), listOf()) } } @@ -190,7 +194,7 @@ class CropScannedDocumentFragment : Fragment() { binding.cropPolygonView.polygon = initImageResult.polygon binding.cropPolygonView.setLines(initImageResult.linesPair.first, initImageResult.linesPair.second) - if (initImageResult.polygon.isNullOrEmpty()) { + if (initImageResult.polygon.isEmpty()) { resetCrop() } else { onCropDragListener() @@ -204,7 +208,7 @@ class CropScannedDocumentFragment : Fragment() { // crop & warp image by selected polygon (editPolygonView.getPolygon()) val operations = listOf(CropOperation(binding.cropPolygonView.polygon)) - var documentImage = scanbotSDK.imageProcessor().processBitmap(originalBitmap, operations, false) + var documentImage = imageProcessor.processBitmap(originalBitmap, operations, false) documentImage?.let { if (rotationDegrees > 0) { // rotate the final cropped image result based on current rotation value: @@ -213,17 +217,11 @@ class CropScannedDocumentFragment : Fragment() { documentImage = Bitmap.createBitmap(it, 0, 0, it.width, it.height, matrix, true) } onDocScanListener.replaceScannedDoc(scannedDocIndex, documentImage, false) - /* onDocScanListener.replaceScannedDoc( - scannedDocIndex, FileUtils.saveImage( - requireContext(), - documentImage, null - ) - )*/ + onFragmentChangeListener.onReplaceFragment( EditScannedDocumentFragment.newInstance(scannedDocIndex), ScanActivity - .FRAGMENT_EDIT_SCAN_TAG, false + .FRAGMENT_EDIT_SCAN_TAG, false ) - // resultImageView.setImageBitmap(resizeForPreview(documentImage!!)) } } diff --git a/app/src/main/java/com/nmc/android/ui/ScanDocumentFragment.kt b/app/src/main/java/com/nmc/android/ui/ScanDocumentFragment.kt index 5c67e0bf45c7..3fb7e57a8e71 100644 --- a/app/src/main/java/com/nmc/android/ui/ScanDocumentFragment.kt +++ b/app/src/main/java/com/nmc/android/ui/ScanDocumentFragment.kt @@ -2,62 +2,43 @@ package com.nmc.android.ui import android.Manifest import android.content.Context -import android.content.pm.ActivityInfo import android.content.pm.PackageManager import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Matrix -import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.ProgressBar import android.widget.Toast -import androidx.annotation.RequiresApi -import androidx.appcompat.widget.AppCompatTextView import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.core.content.res.ResourcesCompat import androidx.fragment.app.Fragment -import com.google.android.material.button.MaterialButton import com.nmc.android.interfaces.OnDocScanListener import com.nmc.android.interfaces.OnFragmentChangeListener import com.owncloud.android.R +import com.owncloud.android.databinding.FragmentScanDocumentBinding import io.scanbot.sdk.ScanbotSDK -import io.scanbot.sdk.SdkLicenseError -import io.scanbot.sdk.camera.CameraOpenCallback import io.scanbot.sdk.camera.CaptureInfo import io.scanbot.sdk.camera.FrameHandlerResult -import io.scanbot.sdk.camera.PictureCallback -import io.scanbot.sdk.camera.ScanbotCameraView import io.scanbot.sdk.contourdetector.ContourDetectorFrameHandler -import io.scanbot.sdk.contourdetector.DocumentAutoSnappingController -import io.scanbot.sdk.core.contourdetector.DetectionResult +import io.scanbot.sdk.core.contourdetector.ContourDetector +import io.scanbot.sdk.core.contourdetector.DetectionStatus +import io.scanbot.sdk.docdetection.ui.IDocumentScannerViewCallback import io.scanbot.sdk.docprocessing.PageProcessor import io.scanbot.sdk.ocr.OpticalCharacterRecognizer import io.scanbot.sdk.persistence.PageFileStorage import io.scanbot.sdk.process.CropOperation -import io.scanbot.sdk.process.Operation -import io.scanbot.sdk.ui.PolygonView -import io.scanbot.sdk.ui.camera.ShutterButton -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import java.util.ArrayList - -class ScanDocumentFragment : Fragment(), ContourDetectorFrameHandler.ResultHandler { - - private lateinit var cameraView: ScanbotCameraView - private lateinit var polygonView: PolygonView - private lateinit var userGuidanceHint: AppCompatTextView - private lateinit var autoSnappingToggleButton: MaterialButton - private lateinit var flashToggleButton: MaterialButton - private lateinit var shutterButton: ShutterButton - private lateinit var progressBar: ProgressBar - - private lateinit var contourDetectorFrameHandler: ContourDetectorFrameHandler - private lateinit var autoSnappingController: DocumentAutoSnappingController +import io.scanbot.sdk.process.ImageProcessor +import io.scanbot.sdk.ui.camera.CameraUiSettings +import io.scanbot.sdk.ui.view.base.configuration.CameraOrientationMode + +class ScanDocumentFragment : Fragment() { private lateinit var scanbotSDK: ScanbotSDK + private lateinit var contourDetector: ContourDetector + private lateinit var imageProcessor: ImageProcessor private var lastUserGuidanceHintTs = 0L private var flashEnabled = false @@ -69,20 +50,18 @@ class ScanDocumentFragment : Fragment(), ContourDetectorFrameHandler.ResultHandl private lateinit var pageFileStorage: PageFileStorage private lateinit var pageProcessor: PageProcessor - private val uiScope = CoroutineScope(Dispatchers.Main) - private lateinit var onDocScanListener: OnDocScanListener private lateinit var onFragmentChangeListener: OnFragmentChangeListener private lateinit var calledFrom: String + private lateinit var binding: FragmentScanDocumentBinding + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) arguments?.getString(ARG_CALLED_FROM)?.let { calledFrom = it } - // Fragment locked in portrait screen orientation - requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT } override fun onAttach(context: Context) { @@ -96,102 +75,117 @@ class ScanDocumentFragment : Fragment(), ContourDetectorFrameHandler.ResultHandl } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { - //supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY) if (requireActivity() is ScanActivity) { (requireActivity() as ScanActivity).showHideToolbar(false) (requireActivity() as ScanActivity).showHideDefaultToolbarDivider(false) } - return inflater.inflate(R.layout.fragment_scan_document, container, false) + binding = FragmentScanDocumentBinding.inflate(inflater, container, false) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) askPermission() - scanbotSDK = (requireActivity() as ScanActivity).scanbotSDK - initOCR() - cameraView = view.findViewById(R.id.camera) as ScanbotCameraView - - // In this example we demonstrate how to lock the orientation of the UI (Activity) - // as well as the orientation of the taken picture to portrait. - //cameraView.lockToPortrait(true) - - // See https://github.com/doo/scanbot-sdk-example-android/wiki/Using-ScanbotCameraView#preview-mode - //cameraView.setPreviewMode(io.scanbot.sdk.camera.CameraPreviewMode.FIT_IN); - cameraView.setCameraOpenCallback(object : CameraOpenCallback { - override fun onCameraOpened() { - cameraView.postDelayed({ - cameraView.setAutoFocusSound(false) + initDependencies() + + binding.camera.apply { + initCamera(CameraUiSettings(true)) + initDetectionBehavior(contourDetector, + { result -> + // Here you are continuously notified about contour detection results. + // For example, you can show a user guidance text depending on the current detection status. + // don't update the text if fragment is removing + if (!isRemoving) { + // Here you are continuously notified about contour detection results. + // For example, you can show a user guidance text depending on the current detection status. + binding.userGuidanceHint.post { + if (result is FrameHandlerResult.Success<*>) { + showUserGuidance((result as FrameHandlerResult.Success).value.detectionStatus) + } + } + } + false // typically you need to return false + }, + object : IDocumentScannerViewCallback { + override fun onCameraOpen() { + // In this example we demonstrate how to lock the orientation of the UI (Activity) + // as well as the orientation of the taken picture to portrait. + binding.camera.cameraConfiguration.setCameraOrientationMode(CameraOrientationMode.PORTRAIT) + + binding.camera.viewController.useFlash(flashEnabled) + binding.camera.viewController.continuousFocus() + } - // Shutter sound is ON by default. You can disable it: - // cameraView.setShutterSound(false); + override fun onPictureTaken(image: ByteArray, captureInfo: CaptureInfo) { + processPictureTaken(image, captureInfo.imageOrientation) - cameraView.continuousFocus() - cameraView.useFlash(flashEnabled) - }, 700) - } - }) - flashToggleButton = view.findViewById(R.id.scan_doc_btn_flash) - progressBar = view.findViewById(R.id.scan_doc_progress_bar) - polygonView = view.findViewById(R.id.polygonView) as PolygonView - // polygonView.setFillColor(POLYGON_FILL_COLOR) - //polygonView.setFillColorOK(POLYGON_FILL_COLOR_OK) - - contourDetectorFrameHandler = ContourDetectorFrameHandler.attach(cameraView, scanbotSDK.createContourDetector()) + // continue scanning + /*binding.camera.postDelayed({ + binding.camera.viewController.startPreview() + }, 1000)*/ + } + } + ) - // Please note: https://github.com/doo/Scanbot-SDK-Examples/wiki/Detecting-and-drawing-contours#contour-detection-parameters - contourDetectorFrameHandler.setAcceptedAngleScore(60.0) - contourDetectorFrameHandler.setAcceptedSizeScore(75.0) - contourDetectorFrameHandler.addResultHandler(polygonView.contourDetectorResultHandler) - contourDetectorFrameHandler.addResultHandler(this) + // See https://docs.scanbot.io/document-scanner-sdk/android/features/document-scanner/using-scanbot-camera-view/#preview-mode + // cameraConfiguration.setCameraPreviewMode(io.scanbot.sdk.camera.CameraPreviewMode.FIT_IN) + } - autoSnappingController = DocumentAutoSnappingController.attach(cameraView, contourDetectorFrameHandler) - autoSnappingController.setIgnoreBadAspectRatio(ignoreBadAspectRatio) + binding.camera.viewController.apply { + setAcceptedAngleScore(60.0) + setAcceptedSizeScore(75.0) + setIgnoreBadAspectRatio(ignoreBadAspectRatio) - // Please note: https://github.com/doo/Scanbot-SDK-Examples/wiki/Autosnapping#sensitivity - autoSnappingController.setSensitivity(0.85f) - cameraView.addPictureCallback(object : PictureCallback() { - override fun onPictureTaken(image: ByteArray, captureInfo: CaptureInfo) { - processPictureTaken(image, captureInfo.imageOrientation) - } - }) - userGuidanceHint = view.findViewById(R.id.userGuidanceHint) + // Please note: https://docs.scanbot.io/document-scanner-sdk/android/features/document-scanner/autosnapping/#sensitivity + setAutoSnappingSensitivity(0.85f) + } - shutterButton = view.findViewById(R.id.shutterButton) - shutterButton.setOnClickListener { cameraView.takePicture(false) } - shutterButton.visibility = View.VISIBLE + binding.shutterButton.setOnClickListener { binding.camera.viewController.takePicture(false) } + binding.shutterButton.visibility = View.VISIBLE - flashToggleButton.setOnClickListener { + binding.scanDocBtnFlash.setOnClickListener { flashEnabled = !flashEnabled - cameraView.useFlash(flashEnabled) + binding.camera.viewController.useFlash(flashEnabled) toggleFlashButtonUI() } - view.findViewById(R.id.scan_doc_btn_cancel).setOnClickListener { - //if fragment opened from Edit Scan Fragment then on cancel click it should go to that fragment + binding.scanDocBtnCancel.setOnClickListener { + // if fragment opened from Edit Scan Fragment then on cancel click it should go to that fragment if (calledFrom == EditScannedDocumentFragment.TAG) { openEditScanFragment() } else { - //else default behaviour + // else default behaviour (requireActivity() as ScanActivity).onBackPressed() } } - autoSnappingToggleButton = view.findViewById(R.id.scan_doc_btn_automatic) - autoSnappingToggleButton.setOnClickListener { + binding.scanDocBtnAutomatic.setOnClickListener { autoSnappingEnabled = !autoSnappingEnabled setAutoSnapEnabled(autoSnappingEnabled) } - autoSnappingToggleButton.post { setAutoSnapEnabled(autoSnappingEnabled) } + binding.scanDocBtnAutomatic.post { setAutoSnapEnabled(autoSnappingEnabled) } toggleFlashButtonUI() } private fun toggleFlashButtonUI() { if (flashEnabled) { - flashToggleButton.setIconTintResource(R.color.primary) - flashToggleButton.setTextColor(resources.getColor(R.color.primary)) + binding.scanDocBtnFlash.setIconTintResource(R.color.primary) + binding.scanDocBtnFlash.setTextColor( + ResourcesCompat.getColor( + resources, + R.color.primary, + requireContext().theme + ) + ) } else { - flashToggleButton.setIconTintResource(R.color.grey_60) - flashToggleButton.setTextColor(resources.getColor(R.color.grey_60)) + binding.scanDocBtnFlash.setIconTintResource(R.color.grey_60) + binding.scanDocBtnFlash.setTextColor( + ResourcesCompat.getColor( + resources, + R.color.grey_60, + requireContext().theme + ) + ) } } @@ -221,7 +215,10 @@ class ScanDocumentFragment : Fragment(), ContourDetectorFrameHandler.ResultHandl } } - private fun initOCR() { + private fun initDependencies() { + scanbotSDK = (requireActivity() as ScanActivity).scanbotSDK + contourDetector = scanbotSDK.createContourDetector() + imageProcessor = scanbotSDK.imageProcessor() opticalCharacterRecognizer = scanbotSDK.createOcrRecognizer() pageFileStorage = scanbotSDK.createPageFileStorage() pageProcessor = scanbotSDK.createPageProcessor() @@ -229,32 +226,16 @@ class ScanDocumentFragment : Fragment(), ContourDetectorFrameHandler.ResultHandl override fun onResume() { super.onResume() - cameraView.onResume() - if (this::progressBar.isInitialized) { - progressBar.visibility = View.GONE - } + binding.camera.viewController.onResume() + binding.scanDocProgressBar.visibility = View.GONE } override fun onPause() { super.onPause() - cameraView.onPause() - } - - override fun handle(result: FrameHandlerResult): Boolean { - // Here you are continuously notified about contour detection results. - // For example, you can show a user guidance text depending on the current detection status. - //don't update the text if fragment is removing - if (!isRemoving) { - userGuidanceHint.post { - if (result is FrameHandlerResult.Success<*>) { - showUserGuidance((result as FrameHandlerResult.Success).value.detectionResult) - } - } - } - return false // typically you need to return false + binding.camera.viewController.onPause() } - private fun showUserGuidance(result: DetectionResult) { + private fun showUserGuidance(result: DetectionStatus) { if (!autoSnappingEnabled) { return } @@ -263,46 +244,53 @@ class ScanDocumentFragment : Fragment(), ContourDetectorFrameHandler.ResultHandl } // Make sure to reset the default polygon fill color (see the ignoreBadAspectRatio case). - //polygonView.setFillColor(POLYGON_FILL_COLOR) - //fragment should be added and visible because this method is being called from handler - //it can be called when fragment is not attached or visible - if(isAdded && isVisible) { + // polygonView.setFillColor(POLYGON_FILL_COLOR) + // fragment should be added and visible because this method is being called from handler + // it can be called when fragment is not attached or visible + if (isAdded && isVisible) { when (result) { - DetectionResult.OK -> { - userGuidanceHint.text = resources.getString(R.string.result_scan_doc_dont_move) - userGuidanceHint.visibility = View.VISIBLE + DetectionStatus.OK -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_dont_move) + binding.userGuidanceHint.visibility = View.VISIBLE } - DetectionResult.OK_BUT_TOO_SMALL -> { - userGuidanceHint.text = resources.getString(R.string.result_scan_doc_move_closer) - userGuidanceHint.visibility = View.VISIBLE + + DetectionStatus.OK_BUT_TOO_SMALL -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_move_closer) + binding.userGuidanceHint.visibility = View.VISIBLE } - DetectionResult.OK_BUT_BAD_ANGLES -> { - userGuidanceHint.text = resources.getString(R.string.result_scan_doc_perspective) - userGuidanceHint.visibility = View.VISIBLE + + DetectionStatus.OK_BUT_BAD_ANGLES -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_perspective) + binding.userGuidanceHint.visibility = View.VISIBLE } - DetectionResult.ERROR_NOTHING_DETECTED -> { - userGuidanceHint.text = resources.getString(R.string.result_scan_doc_no_doc) - userGuidanceHint.visibility = View.VISIBLE + + DetectionStatus.ERROR_NOTHING_DETECTED -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_no_doc) + binding.userGuidanceHint.visibility = View.VISIBLE } - DetectionResult.ERROR_TOO_NOISY -> { - userGuidanceHint.text = resources.getString(R.string.result_scan_doc_bg_noisy) - userGuidanceHint.visibility = View.VISIBLE + + DetectionStatus.ERROR_TOO_NOISY -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_bg_noisy) + binding.userGuidanceHint.visibility = View.VISIBLE } - DetectionResult.OK_BUT_BAD_ASPECT_RATIO -> { + + DetectionStatus.OK_BUT_BAD_ASPECT_RATIO -> { if (ignoreBadAspectRatio) { - userGuidanceHint.text = resources.getString(R.string.result_scan_doc_dont_move) + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_dont_move) // change polygon color to "OK" // polygonView.setFillColor(POLYGON_FILL_COLOR_OK) } else { - userGuidanceHint.text = resources.getString(R.string.result_scan_doc_aspect_ratio) + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_aspect_ratio) } - userGuidanceHint.visibility = View.VISIBLE + binding.userGuidanceHint.visibility = View.VISIBLE } - DetectionResult.ERROR_TOO_DARK -> { - userGuidanceHint.text = resources.getString(R.string.result_scan_doc_poor_light) - userGuidanceHint.visibility = View.VISIBLE + + DetectionStatus.ERROR_TOO_DARK -> { + binding.userGuidanceHint.text = resources.getString(R.string.result_scan_doc_poor_light) + binding.userGuidanceHint.visibility = View.VISIBLE } - else -> userGuidanceHint.visibility = View.GONE + + else -> binding.userGuidanceHint.visibility = View.GONE } } lastUserGuidanceHintTs = System.currentTimeMillis() @@ -310,8 +298,8 @@ class ScanDocumentFragment : Fragment(), ContourDetectorFrameHandler.ResultHandl private fun processPictureTaken(image: ByteArray, imageOrientation: Int) { requireActivity().runOnUiThread { - cameraView.onPause() - progressBar.visibility = View.VISIBLE + binding.camera.viewController.onPause() + binding.scanDocProgressBar.visibility = View.VISIBLE //cameraView.visibility = View.GONE } // Here we get the full image from the camera. @@ -341,14 +329,14 @@ class ScanDocumentFragment : Fragment(), ContourDetectorFrameHandler.ResultHandl false ) } - val detector = scanbotSDK.createContourDetector() + // Run document detection on original image: - detector.detect(originalBitmap) - val operations: MutableList = ArrayList() - operations.add(CropOperation(detector.polygonF!!)) - val documentImage = scanbotSDK.imageProcessor().processBitmap(originalBitmap, operations, false) + val result = contourDetector.detect(originalBitmap)!! + val detectedPolygon = result.polygonF - // val file = saveImage(documentImage) + val documentImage = imageProcessor.processBitmap(originalBitmap, CropOperation(detectedPolygon), false) + + // val file = saveImage(documentImage) // Log.d("SCANNING","File : $file") if (documentImage != null) { onDocScanListener.addScannedDoc(documentImage) @@ -364,10 +352,10 @@ class ScanDocumentFragment : Fragment(), ContourDetectorFrameHandler.ResultHandl //resultView.post { resultView.setImageBitmap(documentImage) } // continue scanning -/* cameraView.postDelayed({ - cameraView.continuousFocus() - cameraView.startPreview() - }, 1000)*/ + /* cameraView.postDelayed({ + cameraView.continuousFocus() + cameraView.startPreview() + }, 1000)*/ } private fun openEditScanFragment() { @@ -378,31 +366,44 @@ class ScanDocumentFragment : Fragment(), ContourDetectorFrameHandler.ResultHandl } private fun setAutoSnapEnabled(enabled: Boolean) { - autoSnappingController.isEnabled = enabled - contourDetectorFrameHandler.isEnabled = enabled - polygonView.visibility = if (enabled) View.VISIBLE else View.GONE + binding.camera.viewController.apply { + autoSnappingEnabled = enabled + isFrameProcessingEnabled = enabled + } + binding.polygonView.visibility = if (enabled) View.VISIBLE else View.GONE /*autoSnappingToggleButton.text = resources.getString(R.string.automatic) + " ${ if (enabled) "ON" else "OFF" }"*/ if (enabled) { - autoSnappingToggleButton.setTextColor(resources.getColor(R.color.primary)) - shutterButton.showAutoButton() + binding.scanDocBtnAutomatic.setTextColor( + ResourcesCompat.getColor( + resources, + R.color.primary, + requireContext().theme + ) + ) + binding.shutterButton.showAutoButton() } else { - autoSnappingToggleButton.setTextColor(resources.getColor(R.color.grey_60)) - shutterButton.showManualButton() - userGuidanceHint.visibility = View.GONE + binding.scanDocBtnAutomatic.setTextColor( + ResourcesCompat.getColor( + resources, + R.color.grey_60, + requireContext().theme + ) + ) + binding.shutterButton.showManualButton() + binding.userGuidanceHint.visibility = View.GONE } } - @RequiresApi(Build.VERSION_CODES.M) override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - //permission is granted - //Nothing to be done + // permission is granted + // Nothing to be done } else { - //permission not granted + // permission not granted for (permission in permissions) { val showRationale = shouldShowRequestPermissionRationale(permission) if (!showRationale) { @@ -435,7 +436,7 @@ class ScanDocumentFragment : Fragment(), ContourDetectorFrameHandler.ResultHandl } private fun onPermissionDenied(message: String) { - //Show Toast instead of snackbar as we are finishing the activity + // Show Toast instead of snackbar as we are finishing the activity Toast.makeText(requireActivity(), message, Toast.LENGTH_LONG).show() requireActivity().finish() } @@ -444,7 +445,7 @@ class ScanDocumentFragment : Fragment(), ContourDetectorFrameHandler.ResultHandl private const val CAMERA_PERMISSION_REQUEST_CODE: Int = 811 @JvmStatic - val ARG_CALLED_FROM = "arg called_From" + val ARG_CALLED_FROM = "arg_called_From" @JvmStatic fun newInstance(calledFrom: String): ScanDocumentFragment { diff --git a/app/src/main/java/com/nmc/android/ui/ScanPagerFragment.java b/app/src/main/java/com/nmc/android/ui/ScanPagerFragment.java index 16b766eb7f6e..f56ca2ee5477 100644 --- a/app/src/main/java/com/nmc/android/ui/ScanPagerFragment.java +++ b/app/src/main/java/com/nmc/android/ui/ScanPagerFragment.java @@ -147,7 +147,7 @@ public void rotate() { binding.editScannedImageView.rotateClockwise(); lastRotationEventTs = System.currentTimeMillis(); executorService.execute(() -> { - Bitmap rotatedBitmap = scanbotSDK.imageProcessor().process(originalBitmap, + Bitmap rotatedBitmap = scanbotSDK.imageProcessor().processBitmap(originalBitmap, new ArrayList<>(Collections.singletonList(new RotateOperation(rotationDegrees))), false); onDocScanListener.replaceScannedDoc(index, rotatedBitmap, false); }); @@ -193,7 +193,7 @@ private void applyFilter(ImageFilterType... imageFilterType) { for (ImageFilterType filters : imageFilterType) { filterOperationList.add(new FilterOperation(filters)); } - previewBitmap = scanbotSDK.imageProcessor().process(originalBitmap, filterOperationList, false); + previewBitmap = scanbotSDK.imageProcessor().processBitmap(originalBitmap, filterOperationList, false); } else { previewBitmap = ScanActivity.originalScannedImages.get(index); } diff --git a/app/src/main/java/com/nmc/android/utils/CheckableThemeUtils.kt b/app/src/main/java/com/nmc/android/utils/CheckableThemeUtils.kt index 7c86042db7cb..a3b8a1149948 100644 --- a/app/src/main/java/com/nmc/android/utils/CheckableThemeUtils.kt +++ b/app/src/main/java/com/nmc/android/utils/CheckableThemeUtils.kt @@ -3,19 +3,33 @@ package com.nmc.android.utils import android.content.res.ColorStateList import androidx.appcompat.widget.AppCompatCheckBox import androidx.appcompat.widget.SwitchCompat -import androidx.core.graphics.drawable.DrawableCompat -import androidx.core.widget.CompoundButtonCompat -import com.owncloud.android.MainApp +import androidx.core.content.res.ResourcesCompat import com.owncloud.android.R object CheckableThemeUtils { @JvmStatic fun tintCheckbox(vararg checkBoxes: AppCompatCheckBox) { for (checkBox in checkBoxes) { - val checkEnabled = MainApp.getAppContext().resources.getColor(R.color.checkbox_checked_enabled) - val checkDisabled = MainApp.getAppContext().resources.getColor(R.color.checkbox_checked_disabled) - val uncheckEnabled = MainApp.getAppContext().resources.getColor(R.color.checkbox_unchecked_enabled) - val uncheckDisabled = MainApp.getAppContext().resources.getColor(R.color.checkbox_unchecked_disabled) + val checkEnabled = ResourcesCompat.getColor( + checkBox.context.resources, + R.color.checkbox_checked_enabled, + checkBox.context.theme + ) + val checkDisabled = ResourcesCompat.getColor( + checkBox.context.resources, + R.color.checkbox_checked_disabled, + checkBox.context.theme + ) + val uncheckEnabled = ResourcesCompat.getColor( + checkBox.context.resources, + R.color.checkbox_unchecked_enabled, + checkBox.context.theme + ) + val uncheckDisabled = ResourcesCompat.getColor( + checkBox.context.resources, + R.color.checkbox_unchecked_disabled, + checkBox.context.theme + ) val states = arrayOf( intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked), @@ -29,8 +43,7 @@ object CheckableThemeUtils { uncheckEnabled, uncheckDisabled ) - val checkColorStateList = ColorStateList(states, colors) - CompoundButtonCompat.setButtonTintList(checkBox, checkColorStateList) + checkBox.buttonTintList = ColorStateList(states, colors) } } @@ -40,47 +53,65 @@ object CheckableThemeUtils { if (colorText) { switchView.setTextColor(color) } - val thumbColorCheckedEnabled = MainApp.getAppContext().resources.getColor(R.color.switch_thumb_checked_enabled) - val thumbColorUncheckedEnabled = - MainApp.getAppContext().resources.getColor(R.color.switch_thumb_unchecked_enabled) - val thumbColorCheckedDisabled = - MainApp.getAppContext().resources.getColor(R.color.switch_thumb_checked_disabled) - val thumbColorUncheckedDisabled = - MainApp.getAppContext().resources.getColor(R.color.switch_thumb_unchecked_disabled) val states = arrayOf( intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked), - intArrayOf(-android.R.attr.state_enabled, android.R.attr.state_checked), intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked), - intArrayOf(-android.R.attr.state_enabled, -android.R.attr.state_checked) + intArrayOf(-android.R.attr.state_enabled) ) + + val thumbColorCheckedEnabled = ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_thumb_checked_enabled, + switchView.context.theme + ) + val thumbColorUncheckedEnabled = + ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_thumb_unchecked_enabled, + switchView.context.theme + ) + val thumbColorDisabled = + ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_thumb_disabled, + switchView.context.theme + ) + val thumbColors = intArrayOf( thumbColorCheckedEnabled, - thumbColorCheckedDisabled, thumbColorUncheckedEnabled, - thumbColorUncheckedDisabled + thumbColorDisabled ) val thumbColorStateList = ColorStateList(states, thumbColors) - val trackColorCheckedEnabled = MainApp.getAppContext().resources.getColor(R.color.switch_track_checked_enabled) + + val trackColorCheckedEnabled = ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_track_checked_enabled, + switchView.context.theme + ) val trackColorUncheckedEnabled = - MainApp.getAppContext().resources.getColor(R.color.switch_track_unchecked_enabled) - val trackColorCheckedDisabled = - MainApp.getAppContext().resources.getColor(R.color.switch_track_checked_disabled) - val trackColorUncheckedDisabled = - MainApp.getAppContext().resources.getColor(R.color.switch_track_unchecked_disabled) + ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_track_unchecked_enabled, + switchView.context.theme + ) + val trackColorDisabled = + ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_track_disabled, + switchView.context.theme + ) val trackColors = intArrayOf( trackColorCheckedEnabled, - trackColorCheckedDisabled, trackColorUncheckedEnabled, - trackColorUncheckedDisabled + trackColorDisabled ) - val trackColorStateList = ColorStateList(states, trackColors) - // setting the thumb color - DrawableCompat.setTintList(switchView.thumbDrawable, thumbColorStateList) + val trackColorStateList = ColorStateList(states, trackColors) - // setting the track color - DrawableCompat.setTintList(switchView.trackDrawable, trackColorStateList) + switchView.thumbTintList = thumbColorStateList + switchView.trackTintList = trackColorStateList } } \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/MainApp.java b/app/src/main/java/com/owncloud/android/MainApp.java index 01117853ebfe..2915f3b49235 100644 --- a/app/src/main/java/com/owncloud/android/MainApp.java +++ b/app/src/main/java/com/owncloud/android/MainApp.java @@ -905,7 +905,7 @@ public static void setAppTheme(DarkMode mode) { */ private void initialiseScanBotSDK() { new ScanbotSDKInitializer() - .withLogging(BuildConfig.DEBUG) + .withLogging(BuildConfig.DEBUG, BuildConfig.DEBUG) .license(this, ScanBotSdkUtils.LICENSE_KEY) .contourDetectorType(ContourDetector.Type.ML_BASED) // ML_BASED is default. Set it to EDGE_BASED to use the edge-based approach .licenceErrorHandler((status, sdkFeature, statusMessage) -> { diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt index 04d1c2615bfa..df4734ac80e4 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FolderPickerActivity.kt @@ -33,7 +33,6 @@ import android.view.Menu import android.view.MenuItem import android.view.View import androidx.activity.OnBackPressedCallback -import androidx.core.content.res.ResourcesCompat import androidx.localbroadcastmanager.content.LocalBroadcastManager import com.nextcloud.client.di.Injectable import com.owncloud.android.R @@ -168,17 +167,15 @@ open class FolderPickerActivity : folderPickerBinding.folderPickerBtnCopy.visibility = View.GONE folderPickerBinding.folderPickerBtnMove.visibility = View.GONE folderPickerBinding.folderPickerBtnChoose.visibility = View.VISIBLE - folderPickerBinding.chooseButtonSpacer.visibility = View.VISIBLE folderPickerBinding.moveOrCopyButtonSpacer.visibility = View.GONE + + // NMC Customization + folderPickerBinding.folderPickerBtnChoose.text = resources.getString(R.string.common_select) } } + // NMC Customization private fun configureDefaultCase() { - // NMC Customization - folderPickerBinding.folderPickerBtnCopy.text = resources.getString(R.string.folder_picker_choose_button_text) - folderPickerBinding.folderPickerBtnMove.visibility = View.GONE - folderPickerBinding.moveOrCopyButtonSpacer.visibility = View.GONE - captionText = themeUtils.getDefaultDisplayNameForRootFolder(this) } @@ -398,7 +395,7 @@ open class FolderPickerActivity : } private fun refreshListOfFilesFragment(fromSearch: Boolean) { - listOfFilesFragment?.listDirectoryFolder(false, fromSearch) + listOfFilesFragment?.listDirectoryFolder(false, fromSearch, mShowOnlyFolder, mHideEncryptedFolder) } fun browseToRoot() { @@ -456,18 +453,23 @@ open class FolderPickerActivity : private fun initControls() { if (this is FilePickerActivity) { + viewThemeUtils.material.colorMaterialButtonPrimaryFilled(filesPickerBinding.folderPickerBtnCancel) filesPickerBinding.folderPickerBtnCancel.setOnClickListener { finish() } } else { + viewThemeUtils.material.colorMaterialButtonText(folderPickerBinding.folderPickerBtnCancel) folderPickerBinding.folderPickerBtnCancel.setOnClickListener { finish() } + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(folderPickerBinding.folderPickerBtnChoose) folderPickerBinding.folderPickerBtnChoose.setOnClickListener { processOperation(null) } + viewThemeUtils.material.colorMaterialButtonPrimaryFilled(folderPickerBinding.folderPickerBtnCopy) folderPickerBinding.folderPickerBtnCopy.setOnClickListener { processOperation( OperationsService.ACTION_COPY_FILE ) } + viewThemeUtils.material.colorMaterialButtonPrimaryTonal(folderPickerBinding.folderPickerBtnMove) folderPickerBinding.folderPickerBtnMove.setOnClickListener { processOperation( OperationsService.ACTION_MOVE_FILE diff --git a/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt index 82ae5bea7a98..a63f9d0127bf 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/NotificationsActivity.kt @@ -79,6 +79,7 @@ class NotificationsActivity : DrawerActivity(), NotificationsContract.View { } setupToolbar() + showHideDefaultToolbarDivider(true) updateActionBarTitleAndHomeButtonByString(getString(R.string.drawer_item_notifications)) setupDrawer(R.id.nav_notifications) diff --git a/app/src/main/res/layout/files_folder_picker.xml b/app/src/main/res/layout/files_folder_picker.xml index fd02a344fe2f..98107b943abe 100644 --- a/app/src/main/res/layout/files_folder_picker.xml +++ b/app/src/main/res/layout/files_folder_picker.xml @@ -50,13 +50,6 @@ android:orientation="horizontal" android:padding="@dimen/standard_padding"> - - - - + - - - - - - @color/grey_80 - @color/grey_70 - @color/grey_60 - @color/grey_70 - @color/grey_60 + @color/grey_70 + @color/grey_60 @color/grey_70 diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 951b6765b8a4..b05b582157a7 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -137,12 +137,10 @@ @color/primary #F399C7 - @color/grey_0 - @color/grey_0 #FFFFFF @color/grey_30 - @color/grey_0 - @color/grey_0 + @color/grey_10 + @color/grey_0 @color/primary diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 398ecd6ce523..032c921bc7ad 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1052,6 +1052,8 @@ Error creating file from template No app available for sending the selected files Tap on a page to zoom in + Choose location + Select Full access Media read-only Photos & videos diff --git a/build.gradle b/build.gradle index d9318f115745..092a5b8f2e71 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ buildscript { exoplayerVersion = "2.19.1" documentScannerVersion = "1.1.1" roomVersion = "2.5.2" - scanbotSdkVersion = "1.89.0" + scanbotSdkVersion = "4.0.0" ciBuild = System.getenv("CI") == "true" }